{"id":37073997,"url":"https://github.com/yeison-liscano/http_mcp","last_synced_at":"2026-04-06T22:01:37.027Z","repository":{"id":309137221,"uuid":"1035260525","full_name":"yeison-liscano/http_mcp","owner":"yeison-liscano","description":"MCP - HTTP server implementation ","archived":false,"fork":false,"pushed_at":"2026-03-20T23:07:14.000Z","size":1075,"stargazers_count":0,"open_issues_count":5,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-27T07:51:13.610Z","etag":null,"topics":["http","mcp"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yeison-liscano.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-08-10T02:19:32.000Z","updated_at":"2026-03-20T23:07:18.000Z","dependencies_parsed_at":"2025-11-29T23:02:10.613Z","dependency_job_id":null,"html_url":"https://github.com/yeison-liscano/http_mcp","commit_stats":null,"previous_names":["yeison-liscano/http_mcp"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/yeison-liscano/http_mcp","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeison-liscano%2Fhttp_mcp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeison-liscano%2Fhttp_mcp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeison-liscano%2Fhttp_mcp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeison-liscano%2Fhttp_mcp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yeison-liscano","download_url":"https://codeload.github.com/yeison-liscano/http_mcp/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeison-liscano%2Fhttp_mcp/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31491097,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-06T17:22:55.647Z","status":"ssl_error","status_checked_at":"2026-04-06T17:22:54.741Z","response_time":112,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["http","mcp"],"created_at":"2026-01-14T08:42:55.681Z","updated_at":"2026-04-06T22:01:37.021Z","avatar_url":"https://github.com/yeison-liscano.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Simple HTTP MCP Server Implementation\n\nThis project provides a lightweight server implementation for the Model Context\nProtocol (MCP) over HTTP. It allows you to expose Python functions as tools and\nprompts that can be discovered and executed remotely via a JSON-RPC interface.\nIt is intended to be used with a Starlette or FastAPI application (see\n[demo](https://github.com/yeison-liscano/demo_http_mcp)).\n\n## Table of Contents\n\n- [Features](#features)\n- [Installation](#installation)\n- [Server Architecture](#server-architecture)\n- [Tools](#tools)\n  - [Basic Tool Example](#basic-tool-example)\n  - [Tools Without Arguments](#tools-without-arguments)\n  - [Tools with Error Handling](#tools-with-error-handling)\n  - [Tools with Authorization Scopes](#tools-with-authorization-scopes)\n- [Server State Management](#server-state-management)\n- [Request Access](#request-access)\n- [Prompts](#prompts)\n  - [Basic Prompt Example](#basic-prompt-example)\n  - [Prompts Without Arguments](#prompts-without-arguments)\n  - [Prompts with Lifespan State](#prompts-with-lifespan-state)\n  - [Prompts with Authorization Scopes](#prompts-with-authorization-scopes)\n- [STDIO Transport](#stdio-transport)\n- [Authentication and Authorization](#authentication-and-authorization)\n- [OAuth 2.1 Authorization (auth_mcp)](#oauth-21-authorization-auth_mcp)\n- [API Reference](#api-reference)\n- [Security Surfaces by Endpoint](#security-surfaces-by-endpoint)\n- [License](#license)\n\n## Features\n\n- **MCP Protocol Compliant**: Implements the MCP specification for tool and\n  prompts discovery and execution. No support for notifications.\n- **HTTP and STDIO Transport**: Uses HTTP (POST requests) or STDIO for\n  communication.\n- **Async Support**: Built on `Starlette` or `FastAPI` for asynchronous request\n  handling.\n- **Type-Safe**: Leverages `Pydantic` for robust data validation and\n  serialization.\n- **Server State Management**: Access shared state through the lifespan context\n  using the `get_state_key` method.\n- **Request Access**: Access the incoming request object from your tools and\n  prompts.\n- **Authorization Scopes**: Support for scope-based authorization using\n  Starlette's authentication system.\n- **Error Handling**: Tools can optionally return error messages instead of\n  raising exceptions.\n- **OAuth 2.1 Authorization**: Optional `auth_mcp` package with Bearer token\n  validation, Protected Resource Metadata (RFC 9728), and `WWW-Authenticate`\n  error responses. Install with `pip install http-mcp[auth]`.\n\n## Server Architecture\n\nThe library provides a single `MCPServer` class that uses lifespan to manage\nshared state across the entire application lifecycle.\n\n### MCPServer\n\nThe `MCPServer` is designed to work with Starlette's lifespan system for\nmanaging shared server state.\n\n**Key Characteristics:**\n\n- **Lifespan Based**: Uses Starlette's lifespan events to initialize and manage\n  shared server state\n- **Application-Level State**: State persists across the entire application\n  lifecycle, not per-request\n- **Flexible**: Can be used with any custom context class stored in the lifespan\n  state\n\n**Constructor Parameters:**\n\n- `name` (str): The name of your MCP server\n- `version` (str): The version of your MCP server\n- `tools` (tuple[Tool, ...]): Tuple of tools to expose (default: empty tuple)\n- `prompts` (tuple[Prompt, ...]): Tuple of prompts to expose (default: empty\n  tuple)\n- `instructions` (str | None): Optional instructions for AI assistants on how to\n  use this server\n\n**Example Usage:**\n\n```python\nimport contextlib\nfrom collections.abc import AsyncIterator\nfrom typing import TypedDict\nfrom dataclasses import dataclass, field\nfrom starlette.applications import Starlette\nfrom http_mcp.server import MCPServer\n\n@dataclass\nclass Context:\n    call_count: int = 0\n    user_preferences: dict = field(default_factory=dict)\n\nclass State(TypedDict):\n    context: Context\n\n@contextlib.asynccontextmanager\nasync def lifespan(_app: Starlette) -\u003e AsyncIterator[State]:\n    yield {\"context\": Context()}\n\nmcp_server = MCPServer(\n    name=\"my-server\",\n    version=\"1.0.0\",\n    tools=my_tools,\n    prompts=my_prompts,\n    instructions=\"Optional instructions for AI assistants on how to use this server\"\n)\n\napp = Starlette(lifespan=lifespan)\napp.mount(\"/mcp\", mcp_server.app)\n```\n\n## Tools\n\nTools are the functions that can be called by the client.\n\n### Basic Tool Example\n\n1. **Define the arguments and output for the tools:**\n\n```python\n# app/tools/models.py\nfrom pydantic import BaseModel, Field\n\nclass GreetInput(BaseModel):\n    question: str = Field(description=\"The question to answer\")\n\nclass GreetOutput(BaseModel):\n    answer: str = Field(description=\"The answer to the question\")\n\n# Note: the description on Field will be passed when listing the tools.\n# Having a description is optional, but it's recommended to provide one.\n```\n\n2. **Define the tools:**\n\n```python\n# app/tools/tools.py\nfrom http_mcp.types import Arguments\n\nfrom app.tools.models import GreetInput, GreetOutput\n\ndef greet(args: Arguments[GreetInput]) -\u003e GreetOutput:\n    return GreetOutput(answer=f\"Hello, {args.inputs.question}!\")\n\n```\n\n```python\n# app/tools/__init__.py\n\nfrom http_mcp.types import Tool\nfrom app.tools.models import GreetInput, GreetOutput\nfrom app.tools.tools import greet\n\nTOOLS = (\n    Tool(\n        func=greet,\n        inputs=GreetInput,\n        output=GreetOutput,\n    ),\n)\n\n__all__ = [\"TOOLS\"]\n\n```\n\n3. **Instantiate the server:**\n\n```python\n# app/main.py\nfrom starlette.applications import Starlette\nfrom http_mcp.server import MCPServer\nfrom app.tools import TOOLS\n\nmcp_server = MCPServer(tools=TOOLS, name=\"test\", version=\"1.0.0\")\n\napp = Starlette()\napp.mount(\n    \"/mcp\",\n    mcp_server.app,\n)\n```\n\n### Tools Without Arguments\n\nYou can define tools that don't require any input arguments:\n\n```python\nfrom datetime import UTC, datetime\nfrom pydantic import BaseModel, Field\nfrom http_mcp.types import Tool\n\nclass GetTimeOutput(BaseModel):\n    time: str = Field(description=\"The current time\")\n\nasync def get_time() -\u003e GetTimeOutput:\n    \"\"\"Get the current time.\"\"\"\n    return GetTimeOutput(time=datetime.now(UTC).strftime(\"%H:%M:%S\"))\n\nTOOLS = (\n    Tool(\n        func=get_time,\n        inputs=type(None),  # No arguments required\n        output=GetTimeOutput,\n    ),\n)\n```\n\nAlternatively, you can use the `NoArguments` class for better clarity:\n\n```python\nfrom http_mcp.types import Arguments, NoArguments, Tool\n\nclass SimpleOutput(BaseModel):\n    success: bool = Field(description=\"Whether the operation was successful\")\n\ndef simple_tool(args: Arguments[NoArguments]) -\u003e SimpleOutput:\n    \"\"\"A simple tool with no arguments.\"\"\"\n    # You can still access request and state\n    context = args.get_state_key(\"context\", Context)\n    return SimpleOutput(success=True)\n\nTOOLS = (\n    Tool(\n        func=simple_tool,\n        inputs=NoArguments,\n        output=SimpleOutput,\n    ),\n)\n```\n\n### Tools with Error Handling\n\nTools can optionally return error messages instead of raising exceptions:\n\n```python\nfrom pydantic import BaseModel, Field\nfrom http_mcp.types import Arguments, Tool\nfrom http_mcp.exceptions import ToolInvocationError\n\nclass RiskyToolInput(BaseModel):\n    value: int = Field(description=\"An integer value\")\n\nclass RiskyToolOutput(BaseModel):\n    result: str = Field(description=\"The result of the operation\")\n\ndef risky_tool(args: Arguments[RiskyToolInput]) -\u003e RiskyToolOutput:\n    \"\"\"A tool that might fail.\"\"\"\n    if args.inputs.value \u003c 0:\n        raise ToolInvocationError(\"risky_tool\", \"Value must be positive\")\n    return RiskyToolOutput(result=f\"Success: {args.inputs.value}\")\n\nTOOLS = (\n    Tool(\n        func=risky_tool,\n        inputs=RiskyToolInput,\n        output=RiskyToolOutput,\n        return_error_message=True,  # Return ErrorMessage instead of raising\n    ),\n)\n```\n\nWhen `return_error_message=True`, the tool will return an `ErrorMessage` model\nwith the error details instead of raising a `ToolInvocationError`.\n\n### Tools with Authorization Scopes\n\nYou can restrict tool access based on authentication scopes:\n\n```python\nfrom http_mcp.exceptions import ToolInvocationError\nfrom http_mcp.types import Arguments, NoArguments, Tool\nfrom starlette.authentication import has_required_scope\n\nclass SecureOutput(BaseModel):\n    message: str = Field(description=\"A secure message\")\n\ndef private_tool(args: Arguments[NoArguments]) -\u003e SecureOutput:\n    \"\"\"A tool that requires authentication.\"\"\"\n    if not has_required_scope(args.request, (\"private\",)):\n        raise ToolInvocationError(\"private_tool\", \"Insufficient scope\")\n    return SecureOutput(message=\"This is private data\")\n\ndef admin_tool(args: Arguments[NoArguments]) -\u003e SecureOutput:\n    \"\"\"A tool that requires admin or superuser scope.\"\"\"\n    if not has_required_scope(args.request, (\"admin\", \"superuser\")):\n        raise ToolInvocationError(\"admin_tool\", \"Insufficient scope\")\n    return SecureOutput(message=\"This is admin data\")\n\nTOOLS = (\n    Tool(\n        func=private_tool,\n        inputs=NoArguments,\n        output=SecureOutput,\n        scopes=(\"private\",),  # Only accessible with 'private' scope\n    ),\n    Tool(\n        func=admin_tool,\n        inputs=NoArguments,\n        output=SecureOutput,\n        scopes=(\"admin\", \"superuser\"),  # Accessible with either scope\n    ),\n)\n```\n\nNote: You need to set up authentication middleware in your Starlette app for\nscopes to work properly. The `scopes` field on `Tool` is the primary\nauthorization gate — the framework filters tools by scope before invocation. The\n`raise ToolInvocationError(...)` calls inside the tool functions above are\noptional defense-in-depth checks that return a proper error response to the\nclient instead of silently failing.\n\n## Server State Management\n\nThe server uses Starlette's lifespan system to manage shared state across the\nentire application lifecycle. State is initialized when the application starts\nand persists until it shuts down. Context is accessed through the\n`get_state_key` method on the `Arguments` object.\n\nThis is useful for sharing resources like database connection pools, HTTP\nclients, caches, or any application state across tools.\n\n### Database Connection Pool\n\nThe most common pattern — initialize a connection pool at startup, share it\nacross all tools, and close it on shutdown:\n\n```python\n# app/context.py\nfrom dataclasses import dataclass\nimport asyncpg\n\n@dataclass\nclass AppContext:\n    db: asyncpg.Pool\n```\n\n```python\n# app/main.py\nimport contextlib\nimport os\nfrom collections.abc import AsyncIterator\nfrom typing import TypedDict\nimport asyncpg\nfrom starlette.applications import Starlette\nfrom http_mcp.server import MCPServer\nfrom app.context import AppContext\n\nclass State(TypedDict):\n    ctx: AppContext\n\n@contextlib.asynccontextmanager\nasync def lifespan(_app: Starlette) -\u003e AsyncIterator[State]:\n    pool = await asyncpg.create_pool(os.environ[\"DATABASE_URL\"])\n    yield {\"ctx\": AppContext(db=pool)}\n    await pool.close()\n\nmcp_server = MCPServer(tools=TOOLS, name=\"my-server\", version=\"1.0.0\")\n\napp = Starlette(lifespan=lifespan)\napp.mount(\"/mcp\", mcp_server.app)\n```\n\n```python\n# app/tools.py\nfrom pydantic import BaseModel, Field\nfrom http_mcp.types import Arguments\nfrom app.context import AppContext\n\nclass GetUserInput(BaseModel):\n    user_id: int = Field(description=\"The user ID to look up\")\n\nclass GetUserOutput(BaseModel):\n    name: str = Field(description=\"The user's name\")\n    email: str = Field(description=\"The user's email\")\n\nasync def get_user(args: Arguments[GetUserInput]) -\u003e GetUserOutput:\n    \"\"\"Look up a user by ID.\"\"\"\n    ctx = args.get_state_key(\"ctx\", AppContext)\n    row = await ctx.db.fetchrow(\n        \"SELECT name, email FROM users WHERE id = $1\",\n        args.inputs.user_id,\n    )\n    return GetUserOutput(name=row[\"name\"], email=row[\"email\"])\n```\n\n### Shared HTTP Client\n\nShare a single `httpx.AsyncClient` across tools to reuse connections and\nconfigure base URLs, headers, or timeouts once:\n\n```python\n# app/context.py\nfrom dataclasses import dataclass\nimport httpx\n\n@dataclass\nclass AppContext:\n    http_client: httpx.AsyncClient\n```\n\n```python\n# app/main.py\nimport contextlib\nfrom collections.abc import AsyncIterator\nfrom typing import TypedDict\nimport httpx\nfrom starlette.applications import Starlette\nfrom http_mcp.server import MCPServer\nfrom app.context import AppContext\n\nclass State(TypedDict):\n    ctx: AppContext\n\n@contextlib.asynccontextmanager\nasync def lifespan(_app: Starlette) -\u003e AsyncIterator[State]:\n    async with httpx.AsyncClient(\n        base_url=\"https://api.example.com\",\n        headers={\"Authorization\": \"Bearer \u003ctoken\u003e\"},\n    ) as client:\n        yield {\"ctx\": AppContext(http_client=client)}\n\nmcp_server = MCPServer(tools=TOOLS, name=\"my-server\", version=\"1.0.0\")\n\napp = Starlette(lifespan=lifespan)\napp.mount(\"/mcp\", mcp_server.app)\n```\n\n```python\n# app/tools.py\nfrom pydantic import BaseModel, Field\nfrom http_mcp.types import Arguments\nfrom app.context import AppContext\n\nclass SearchInput(BaseModel):\n    query: str = Field(description=\"The search query\")\n\nclass SearchOutput(BaseModel):\n    results: list[str] = Field(description=\"Search result titles\")\n\nasync def search(args: Arguments[SearchInput]) -\u003e SearchOutput:\n    \"\"\"Search via an external API.\"\"\"\n    ctx = args.get_state_key(\"ctx\", AppContext)\n    resp = await ctx.http_client.get(\"/search\", params={\"q\": args.inputs.query})\n    resp.raise_for_status()\n    return SearchOutput(results=[r[\"title\"] for r in resp.json()[\"items\"]])\n```\n\n### In-Memory Cache\n\nShare mutable state like caches or counters across tool invocations within the\nsame server lifecycle:\n\n```python\n# app/context.py\nfrom dataclasses import dataclass, field\n\n@dataclass\nclass AppContext:\n    cache: dict[str, str] = field(default_factory=dict)\n    request_count: int = 0\n```\n\n```python\n# app/tools.py\nfrom pydantic import BaseModel, Field\nfrom http_mcp.types import Arguments\nfrom app.context import AppContext\n\nclass LookupInput(BaseModel):\n    key: str = Field(description=\"The cache key to look up\")\n\nclass LookupOutput(BaseModel):\n    value: str | None = Field(description=\"The cached value, or null if not found\")\n    total_requests: int = Field(description=\"Total requests served\")\n\nasync def lookup(args: Arguments[LookupInput]) -\u003e LookupOutput:\n    \"\"\"Look up a value in the cache.\"\"\"\n    ctx = args.get_state_key(\"ctx\", AppContext)\n    ctx.request_count += 1\n    return LookupOutput(\n        value=ctx.cache.get(args.inputs.key),\n        total_requests=ctx.request_count,\n    )\n```\n\nAll tools sharing the same `AppContext` instance see each other's writes\nimmediately, since the lifespan yields a single shared object.\n\nNote: Plain `dict` and `int` are not thread-safe. If your tools run concurrently\n(e.g., sync tools dispatched via threads), protect shared mutable state with an\n`asyncio.Lock` or use thread-safe data structures.\n\n## Request Access\n\nYou can access the incoming request object from your tools. The request object\nis passed to each tool call and can be used to access headers, cookies, and\nother request data (e.g. request.state, request.scope).\n\n```python\nfrom pydantic import BaseModel, Field\nfrom http_mcp.types import Arguments\n\nclass MyToolArguments(BaseModel):\n    question: str = Field(description=\"The question to answer\")\n\nclass MyToolOutput(BaseModel):\n    answer: str = Field(description=\"The answer to the question\")\n\n\nasync def my_tool(args: Arguments[MyToolArguments]) -\u003e MyToolOutput:\n    # Access the request\n    auth_header = args.request.headers.get(\"Authorization\")\n    ...\n\n    return MyToolOutput(answer=f\"Hello, {args.inputs.question}!\")\n\n# Use MCPServer:\nfrom http_mcp.server import MCPServer\n\nmcp_server = MCPServer(\n    name=\"my-server\",\n    version=\"1.0.0\",\n    tools=(my_tool,),\n)\n```\n\n## Prompts\n\nYou can add interactive templates that are invoked by user choice. Prompts now\nsupport lifespan state access, similar to tools.\n\n### Basic Prompt Example\n\n1. **Define the arguments for the prompts:**\n\n```python\nfrom pydantic import BaseModel, Field\n\nfrom http_mcp._mcp_types.content import TextContent\nfrom http_mcp._mcp_types.prompts import PromptMessage\nfrom http_mcp.types import Arguments, Prompt\n\n\nclass GetAdvice(BaseModel):\n    topic: str = Field(description=\"The topic to get advice on\")\n    include_actionable_steps: bool = Field(\n        description=\"Whether to include actionable steps in the advice\", default=False\n    )\n\n\ndef get_advice(args: Arguments[GetAdvice]) -\u003e tuple[PromptMessage, ...]:\n    \"\"\"Get advice on a topic.\"\"\"\n    template = \"\"\"\n    You are a helpful assistant that can give advice on {topic}.\n    \"\"\"\n    if args.inputs.include_actionable_steps:\n        template += \"\"\"\n        The advice should include actionable steps.\n        \"\"\"\n    return (\n        PromptMessage(\n            role=\"user\",\n            content=TextContent(\n                text=template.format(topic=args.inputs.topic)\n            ),\n        ),\n    )\n\n\nPROMPTS = (\n    Prompt(\n        func=get_advice,\n        arguments_type=GetAdvice,\n    ),\n)\n```\n\n2. **Instantiate the server:**\n\n```python\nfrom starlette.applications import Starlette\n\nfrom app.prompts import PROMPTS\nfrom http_mcp.server import MCPServer\n\napp = Starlette()\nmcp_server = MCPServer(tools=(), prompts=PROMPTS, name=\"test\", version=\"1.0.0\")\n\napp.mount(\n    \"/mcp\",\n    mcp_server.app,\n)\n```\n\n### Prompts Without Arguments\n\nYou can define prompts that don't require any input arguments:\n\n```python\nfrom http_mcp.types import Prompt, PromptMessage, TextContent\n\ndef help_prompt() -\u003e tuple[PromptMessage, ...]:\n    \"\"\"Use this prompt to get general help.\"\"\"\n    return (\n        PromptMessage(\n            role=\"user\",\n            content=TextContent(\n                text=\"You are a helpful assistant. Help the user with their task.\"\n            ),\n        ),\n    )\n\nPROMPTS = (\n    Prompt(\n        func=help_prompt,\n        arguments_type=type(None),  # No arguments required\n    ),\n)\n```\n\nAlternatively, you can use the `NoArguments` class:\n\n```python\nfrom http_mcp.types import Arguments, NoArguments, Prompt, PromptMessage, TextContent\n\ndef help_prompt_with_context(args: Arguments[NoArguments]) -\u003e tuple[PromptMessage, ...]:\n    \"\"\"Use this prompt to get help with access to context.\"\"\"\n    # You can still access request and state\n    context = args.get_state_key(\"context\", Context)\n    return (\n        PromptMessage(\n            role=\"user\",\n            content=TextContent(text=\"You are a helpful assistant.\"),\n        ),\n    )\n\nPROMPTS = (\n    Prompt(\n        func=help_prompt_with_context,\n        arguments_type=NoArguments,\n    ),\n)\n```\n\n### Prompts with Lifespan State\n\n```python\nfrom pydantic import BaseModel, Field\nfrom http_mcp.types import Arguments, Prompt, PromptMessage, TextContent\nfrom app.context import Context\n\nclass GetAdvice(BaseModel):\n    topic: str = Field(description=\"The topic to get advice on\")\n\ndef get_advice_with_context(args: Arguments[GetAdvice]) -\u003e tuple[PromptMessage, ...]:\n    \"\"\"Get advice on a topic with context awareness.\"\"\"\n    # Access the context from lifespan state\n    context = args.get_state_key(\"context\", Context)\n    called_tools = context.get_called_tools()\n    template = \"\"\"\n    You are a helpful assistant that can give advice on {topic}.\n    Previously called tools: {tools}\n    \"\"\"\n\n    return (\n        PromptMessage(\n            role=\"user\",\n            content=TextContent(\n                text=template.format(\n                    topic=args.inputs.topic,\n                    tools=\", \".join(called_tools) if called_tools else \"none\"\n                )\n            )\n        ),\n    )\n\nPROMPTS_WITH_CONTEXT = (\n    Prompt(\n        func=get_advice_with_context,\n        arguments_type=GetAdvice,\n    ),\n)\n```\n\n### Prompts with Authorization Scopes\n\nYou can restrict prompt access based on authentication scopes:\n\n```python\nfrom http_mcp.types import Arguments, NoArguments, Prompt, PromptMessage, TextContent\n\ndef private_prompt(args: Arguments[NoArguments]) -\u003e tuple[PromptMessage, ...]:\n    \"\"\"Private prompt that is only accessible to authenticated users.\"\"\"\n    return (\n        PromptMessage(\n            role=\"user\",\n            content=TextContent(text=\"This is a private prompt.\"),\n        ),\n    )\n\ndef admin_prompt(args: Arguments[NoArguments]) -\u003e tuple[PromptMessage, ...]:\n    \"\"\"Admin prompt accessible to users with admin or superuser scope.\"\"\"\n    return (\n        PromptMessage(\n            role=\"user\",\n            content=TextContent(text=\"This is an admin prompt.\"),\n        ),\n    )\n\nPROMPTS = (\n    Prompt(\n        func=private_prompt,\n        arguments_type=NoArguments,\n        scopes=(\"private\",),  # Only accessible with 'private' scope\n    ),\n    Prompt(\n        func=admin_prompt,\n        arguments_type=NoArguments,\n        scopes=(\"admin\", \"superuser\"),  # Accessible with either scope\n    ),\n)\n```\n\nNote: You need to set up authentication middleware in your Starlette app for\nscopes to work properly.\n\n## STDIO Transport\n\nIn addition to HTTP transport, the server supports STDIO transport for\ncommunication. This is useful for command-line applications and integrations\nthat communicate through standard input/output.\n\n### Using STDIO Transport\n\n```python\nimport asyncio\nimport os\nfrom http_mcp.server import MCPServer\nfrom app.tools import TOOLS\nfrom app.prompts import PROMPTS\n\nmcp_server = MCPServer(\n    tools=TOOLS,\n    prompts=PROMPTS,\n    name=\"test\",\n    version=\"1.0.0\"\n)\n\n# Run the server with STDIO transport\nasync def main() -\u003e None:\n    request_headers = {\n        \"Authorization\": f\"Bearer {os.getenv('MCP_TOKEN', '')}\",\n        \"X-Custom-Header\": \"value\",\n    }\n    await mcp_server.serve_stdio(request_headers)\n\nasyncio.run(main())\n```\n\nThe `request_headers` parameter allows you to pass headers that will be included\nin the request context, enabling authentication and other header-based features\neven when using STDIO transport.\n\n## Authentication and Authorization\n\nThe library integrates with Starlette's authentication system to provide\nscope-based authorization for tools and prompts.\n\n### Setting Up Authentication Middleware\n\n```python\nimport contextlib\nfrom collections.abc import AsyncIterator\nfrom typing import TypedDict\nfrom starlette.applications import Starlette\nfrom starlette.authentication import (\n    AuthCredentials,\n    AuthenticationBackend,\n    BaseUser,\n    SimpleUser,\n)\nfrom starlette.middleware import Middleware\nfrom starlette.middleware.authentication import AuthenticationMiddleware\nfrom starlette.requests import HTTPConnection\n\nfrom http_mcp.server import MCPServer\nfrom app.context import Context\nfrom app.tools import TOOLS\nfrom app.prompts import PROMPTS\n\n\nclass BasicAuthBackend(AuthenticationBackend):\n    def __init__(self, granted_scopes: tuple[str, ...] = (\"authenticated\",)) -\u003e None:\n        self.granted_scopes = granted_scopes\n        super().__init__()\n\n    async def authenticate(\n        self, conn: HTTPConnection\n    ) -\u003e tuple[AuthCredentials, BaseUser] | None:\n        # Implement your authentication logic here\n        # For example, check Bearer token, API key, etc.\n        auth_header = conn.headers.get(\"Authorization\")\n        if not auth_header:\n            return None\n\n        # Validate token and return credentials with scopes\n        return AuthCredentials(self.granted_scopes), SimpleUser(\"username\")\n\n\nclass State(TypedDict):\n    context: Context\n\n\n@contextlib.asynccontextmanager\nasync def lifespan(_app: Starlette) -\u003e AsyncIterator[State]:\n    yield {\"context\": Context()}\n\n\nmcp_server = MCPServer(\n    tools=TOOLS,\n    prompts=PROMPTS,\n    name=\"test\",\n    version=\"1.0.0\"\n)\n\napp = Starlette(\n    lifespan=lifespan,\n    middleware=[\n        Middleware(\n            AuthenticationMiddleware,\n            backend=BasicAuthBackend(granted_scopes=(\"private\", \"admin\")),\n        ),\n    ],\n)\napp.mount(\"/mcp\", mcp_server.app)\n```\n\n### How Scopes Work\n\n1. **Authentication Middleware**: The middleware authenticates each request and\n   assigns scopes to the user through `AuthCredentials`.\n\n1. **Tool/Prompt Scopes**: When defining tools or prompts, you can specify\n   required scopes using the `scopes` parameter.\n\n1. **Access Control**: The server automatically filters tools and prompts based\n   on the user's granted scopes. Tools and prompts without the required scopes\n   are not visible in listings and cannot be invoked.\n\n1. **Multiple Scopes**: If you specify multiple scopes (e.g.,\n   `scopes=(\"admin\", \"superuser\")`), the user needs at least one of those scopes\n   to access the tool or prompt.\n\n## API Reference\n\n### Tool Class\n\nThe `Tool` class is used to define tools that can be invoked by clients.\n\n**Parameters:**\n\n- `func`: The function to be invoked. Can be sync or async. The function can\n  either:\n  - Accept an `Arguments[TInputs]` parameter\n  - Accept no parameters\n- `inputs`: The Pydantic model class for input validation. Use `type(None)` or\n  `NoArguments` for tools without inputs\n- `output`: The Pydantic model class for output validation\n- `return_error_message` (bool): If `True`, tool errors return `ErrorMessage`\n  instead of raising exceptions (default: `False`)\n- `scopes` (tuple[str, ...]): Required authentication scopes for accessing this\n  tool (default: empty tuple)\n\n**Properties:**\n\n- `name`: The function name (derived from `func.__name__`)\n- `title`: A human-readable title (derived from the function name)\n- `description`: The function's docstring\n- `input_schema`: JSON schema for the input parameters\n- `output_schema`: JSON schema for the output\n\n### Prompt Class\n\nThe `Prompt` class is used to define prompts that can be invoked by clients.\n\n**Parameters:**\n\n- `func`: The function to be invoked. Can be sync or async. The function can\n  either:\n  - Accept an `Arguments[TArguments]` parameter\n  - Accept no parameters\n  - Must return `tuple[PromptMessage, ...]`\n- `arguments_type`: The Pydantic model class for argument validation. Use\n  `type(None)` or `NoArguments` for prompts without arguments\n- `scopes` (tuple[str, ...]): Required authentication scopes for accessing this\n  prompt (default: empty tuple)\n\n**Properties:**\n\n- `name`: The function name (derived from `func.__name__`)\n- `title`: A human-readable title (derived from the function name)\n- `description`: The function's docstring\n- `arguments`: Tuple of `PromptArgument` objects defining the prompt's arguments\n\n### Arguments Class\n\nThe `Arguments` class is passed to tool and prompt functions to provide access\nto inputs, request, and state.\n\n**Parameters:**\n\n- `request`: The Starlette `Request` object\n- `inputs`: The validated input/argument data (type depends on the Tool/Prompt\n  definition)\n\n**Methods:**\n\n- `get_state_key(key: str, _object_type: type[TKey]) -\u003e TKey`: Access a value\n  from the lifespan state. Raises `ServerError` if the key doesn't exist.\n\n### NoArguments Class\n\nAn empty Pydantic model that can be used as a clearer alternative to\n`type(None)` when defining tools or prompts without arguments.\n\n```python\nfrom http_mcp.types import NoArguments\n\n# Use this instead of type(None)\nTool(func=my_func, inputs=NoArguments, output=MyOutput)\n```\n\n## OAuth 2.1 Authorization (auth_mcp)\n\nThe `auth_mcp` package adds standards-compliant OAuth 2.1 authorization to your\nMCP server. Install with the `auth` extra:\n\n```bash\npip install http-mcp[auth]\n```\n\n### Quick Start\n\n```python\nfrom http_mcp.server import MCPServer\nfrom auth_mcp.resource_server import (\n    ProtectedMCPAppConfig,\n    TokenInfo,\n    TokenValidator,\n    create_protected_mcp_app,\n)\n\n\nclass MyTokenValidator(TokenValidator):\n    async def validate_token(\n        self, token: str, resource: str | None = None\n    ) -\u003e TokenInfo | None:\n        # Validate against your authorization server\n        ...\n\n\nmcp_server = MCPServer(name=\"my-server\", version=\"1.0.0\", tools=MY_TOOLS)\n\nconfig = ProtectedMCPAppConfig(\n    mcp_server=mcp_server,\n    token_validator=MyTokenValidator(),\n    resource_uri=\"https://mcp.example.com\",\n    authorization_servers=(\"https://auth.example.com\",),\n)\n\napp = create_protected_mcp_app(config)\n```\n\nThis gives you:\n\n- Bearer token validation on all MCP endpoints (secure by default)\n- `/.well-known/oauth-protected-resource` discovery endpoint (RFC 9728)\n- `WWW-Authenticate` headers on 401/403 with `resource_metadata` parameter\n- Security headers (HSTS, nosniff, no-store)\n- Optional CORS configuration via `CORSConfig`\n\nFor full documentation, best practices, and security surface details, see\n[auth_mcp README](src/auth_mcp/README.md).\n\n## Security Surfaces by Endpoint\n\n### `POST /mcp` — MCP JSON-RPC Endpoint\n\n- **Authentication** — When using `auth_mcp`, Bearer tokens are extracted from\n  the `Authorization` header and validated via `TokenValidator`. Tokens\n  exceeding 2048 characters or containing characters outside the RFC 6750\n  `b64token` pattern are rejected before reaching the validator. Without\n  `auth_mcp`, authentication is handled by Starlette's\n  `AuthenticationMiddleware`.\n- **Authorization** — Scope-based filtering via Starlette's\n  `has_required_scope()`. Tools and prompts without matching scopes are hidden\n  from listings and blocked on invocation.\n- **Input validation** — JSON-RPC messages validated by Pydantic. Request body\n  capped at 4 MB. Content-Type strictly checked (`application/json` only, media\n  type parameters ignored).\n- **Error handling** — Tool and prompt names truncated to 100 characters in\n  error messages. Pydantic validation errors sanitized before inclusion in\n  responses.\n- **Response headers** — `X-Content-Type-Options: nosniff`,\n  `Cache-Control: no-store` on all responses. `auth_mcp` additionally adds\n  `Strict-Transport-Security: max-age=31536000; includeSubDomains`.\n\n### `GET /.well-known/oauth-protected-resource` — Discovery Endpoint (auth_mcp)\n\n- **Authentication** — Subject to the same auth middleware as `/mcp`. When\n  `require_authentication=True` (default), requires a valid token. Set to\n  `False` if clients need to discover the authorization server before\n  authenticating.\n- **Input validation** — Only `GET` allowed; other methods return\n  `405 Method Not Allowed`.\n- **Output** — Serialized once at startup from a frozen\n  `ProtectedResourceMetadata` model. URI fields validated as HTTP/HTTPS URLs via\n  Pydantic's `AnyHttpUrl`.\n\n### `WWW-Authenticate` Response Header (auth_mcp)\n\n- **Header injection** — All parameter values (`realm`, `resource_metadata`,\n  `scope`, `error`, `error_description`) are sanitized: CR/LF characters\n  stripped, backslash and double-quote escaped per RFC 7230 quoted-string rules.\n- **Information disclosure** — Error responses use generic messages\n  (`\"Authentication required\"`). The original `AuthenticationError` details are\n  discarded. Error codes (`invalid_token` on 401) follow RFC 6750 without\n  leaking internal state.\n\n### STDIO Transport\n\n- **Message size** — Capped at 4 MB, matching HTTP transport.\n- **Logging** — Messages truncated to 500 characters in debug logs to prevent\n  log flooding. Token values are never logged.\n- **Headers** — Request headers are converted to proper ASGI\n  `list[tuple[bytes, bytes]]` format.\n\n## Installation\n\nRequires **Python 3.12+** (uses PEP 695 type parameter syntax).\n\nInstall the package using pip or uv:\n\n```bash\npip install http-mcp\n```\n\nWith OAuth 2.1 authorization support:\n\n```bash\npip install http-mcp[auth]\n```\n\nor\n\n```bash\nuv add http-mcp\n```\n\n## License\n\nThis project is licensed under the MIT License. See the LICENSE file for\ndetails.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyeison-liscano%2Fhttp_mcp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyeison-liscano%2Fhttp_mcp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyeison-liscano%2Fhttp_mcp/lists"}