Simple HTTP MCP Server
This server acts as a lightweight HTTP/STDIO gateway that exposes Python functions as discoverable and executable tools under the Model Context Protocol (MCP). It enables remote tool execution via JSON-RPC interface with both stateful and stateless contexts.
Key Capabilities:
Remote Tool Execution: Execute Python functions remotely through HTTP POST requests or STDIO
Tool Discovery: Discover available tools and their specifications via the MCP protocol
Dual Context Support: Maintain state across tool calls (stateful) or access incoming request data like headers and cookies (stateless)
Type-Safe Validation: Uses Pydantic for robust input/output schema validation and serialization
Asynchronous Handling: Provides async request processing via Starlette or FastAPI
Example Tools:
get_weather: Retrieve weather data for specified locations with temperature unit optionsget_time: Fetch current timetool_that_access_request: Access request details like username from headersget_called_tools: Retrieve list of previously called tools using stateful context
Provides a framework for building HTTP-based MCP servers that can be integrated into FastAPI applications, enabling the exposure of Python functions as discoverable tools and prompts
Uses Pydantic models for type-safe data validation and serialization of tool inputs, outputs, and server configurations
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Simple HTTP MCP Servergreet me with 'Hello, how can I help you today?'"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Simple HTTP MCP Server Implementation
This project provides a lightweight server implementation for the Model Context Protocol (MCP) over HTTP. It allows you to expose Python functions as tools and prompts that can be discovered and executed remotely via a JSON-RPC interface. It is intended to be used with a Starlette or FastAPI application (see demo).
Table of Contents
Related MCP server: wazza-mcp-test-server
Features
MCP Protocol Compliant: Implements the MCP specification for tool and prompts discovery and execution. No support for notifications.
One Protocol Revision: Speaks the stateless
2026-07-28revision only —server/discover, per-request_meta, no handshake, no session. A single dispatch path means a request cannot select weaker handling by declaring an older revision.HTTP and STDIO Transport: Uses HTTP (POST requests) or STDIO for communication.
Async Support: Built on
StarletteorFastAPIfor asynchronous request handling.Type-Safe: Leverages
Pydanticfor robust data validation and serialization.Server State Management: Access shared state through the lifespan context using the
get_state_keymethod.Request Access: Access the incoming request object from your tools and prompts.
Authorization Scopes: Support for scope-based authorization using Starlette's authentication system.
Error Handling: Tools can optionally return error messages instead of raising exceptions.
OAuth 2.1 Authorization: Optional
auth_mcppackage with Bearer token validation, Protected Resource Metadata (RFC 9728), andWWW-Authenticateerror responses. Install withpip install http-mcp[auth].
Server Architecture
The library provides a single MCPServer class that uses lifespan to manage
shared state across the entire application lifecycle.
MCPServer
The MCPServer is designed to work with Starlette's lifespan system for
managing shared server state.
Key Characteristics:
Lifespan Based: Uses Starlette's lifespan events to initialize and manage shared server state
Application-Level State: State persists across the entire application lifecycle, not per-request
Flexible: Can be used with any custom context class stored in the lifespan state
Constructor Parameters:
name(str): The name of your MCP serverversion(str): The version of your MCP servertools(tuple[Tool, ...]): Tuple of tools to expose (default: empty tuple)prompts(tuple[Prompt, ...]): Tuple of prompts to expose (default: empty tuple)instructions(str | None): Optional instructions for AI assistants on how to use this servercache_ttl_ms(int): Freshness hint in milliseconds sent withtools/list,prompts/list, andserver/discoverresults (default:300000). Use0to tell clients never to cache. See Caching Hints.cache_scope("public" | "private" | None): Whether shared caches may reuse those results across authorization contexts. Derived automatically when omitted. See Caching Hints.allowed_origins(tuple[str, ...]): Origins the HTTP transport accepts (default: empty, meaning the check is disabled). See Origin Validation.require_origin(bool): Whether a request carrying noOriginheader at all is refused whenallowed_originsis set (default:False). See Origin Validation.
Example Usage:
import contextlib
from collections.abc import AsyncIterator
from typing import TypedDict
from dataclasses import dataclass, field
from starlette.applications import Starlette
from http_mcp.server import MCPServer
@dataclass
class Context:
call_count: int = 0
user_preferences: dict = field(default_factory=dict)
class State(TypedDict):
context: Context
@contextlib.asynccontextmanager
async def lifespan(_app: Starlette) -> AsyncIterator[State]:
yield {"context": Context()}
mcp_server = MCPServer(
name="my-server",
version="1.0.0",
tools=my_tools,
prompts=my_prompts,
instructions="Optional instructions for AI assistants on how to use this server"
)
app = Starlette(lifespan=lifespan)
app.mount("/mcp", mcp_server.app)Protocol Version
The server implements exactly one protocol revision, 2026-07-28, and every
request travels the same path. There is no version negotiation and no second set
of rules a request can select into.
Breaking change in 0.17.0. Support for the session-based revisions
2025-11-25,2025-06-18, and2025-03-26was removed, along withinitialize,notifications/initialized, andping. A client that speaks only those revisions can no longer talk to this server. Serving one revision is also what makes the request-metadata headers below trustworthy: while two eras coexisted, a request could skip the header checks by declaring the older one, so an intermediary routing onMcp-Methodcould be desynchronised from the server acting on the body.
Breaking change in 0.18.0.
ServerInterface.get_tool_input_schemanow takes theRequest, so authorization scopes are honoured before dispatch; implementations of the interface must update, whileMCPServerusers are unaffected. MirroredMcp-Param-*values are compared textually rather than numerically, so a header reading3.0for"replicas": 3now gets-32020. Everynotifications/*method returns 404, the revision defining none.
The 2026-07-28 Request Shape
The revision has no session concept. In practice:
No handshake. Every request restates its protocol version and the client's capabilities in
_meta:{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_weather", "arguments": { "location": "Seattle, WA" }, "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" }, "io.modelcontextprotocol/clientCapabilities": {} } } }protocolVersionandclientCapabilitiesare required; omitting either gets a-32602and HTTP 400. Any other version gets a-32022whosedata.supportedlists the one revision this server speaks.server/discoverreplacesinitializefor capability discovery. It reports the supported version, capabilities, instructions, and server identity in one call, and is answered without any prior request:{ "resultType": "complete", "supportedVersions": ["2026-07-28"], "capabilities": { "tools": { "listChanged": false }, "prompts": { "listChanged": false } }, "_meta": { "io.modelcontextprotocol/serverInfo": { "name": "my-server", "version": "1.0.0" } }, "ttlMs": 300000, "cacheScope": "public" }Every result carries
resultType: "complete"and a_metablock naming the server.initialize,notifications/initialized,ping, andlogging/setLeveldo not exist, along with the session and SSE resumption machinery. They return-32601with HTTP 404. JSON-RPC notifications — anotifications/*message with noid— still get202 Acceptedand no body, because JSON-RPC forbids responding to them.Required request headers. Every POST must send
MCP-Protocol-VersionandMcp-Method, plusMcp-Nameontools/callandprompts/get. Each must match the corresponding body value, or the request is rejected with-32020(HeaderMismatch) and HTTP 400 — this stops a proxy routing on one value while the server acts on another. Values that cannot be expressed as plain ASCII use the=?base64?...?=envelope, which the server decodes before comparing.Unknown tools and prompts report
-32602, which is what the tools and prompts specs prescribe.-32002was retired by this revision.Mcp-Session-IdandLast-Event-IDare ignored, andGET/DELETEon the MCP endpoint return405 Method Not Allowed.
Multi round-trip requests (elicitation, sampling, roots) and
subscriptions/listen are not implemented: this server exposes no
client-input-dependent features and declares listChanged: false, so neither
applies to it.
Caching Hints
tools/list, prompts/list, and server/discover results carry ttlMs and
cacheScope so clients can avoid re-fetching a list that has not changed:
mcp_server = MCPServer(
name="my-server",
version="1.0.0",
tools=my_tools,
cache_ttl_ms=300_000, # clients may treat the list as fresh for 5 minutes
cache_scope="public", # shared caches may serve it to any caller
)Tools and prompts are fixed when MCPServer is constructed, so ttlMs really
bounds how long a client may miss a redeploy rather than how long the data is
stable. Set it to 0 to ask clients never to cache.
cache_scope is derived when you omit it: "private" if any tool or prompt is
scope-restricted — the list then varies per caller, so a shared cache must not
reuse it across authorization contexts — and "public" otherwise. Override it
if your deployment knows better. Note that cacheScope governs caching only; it
is never a substitute for the per-tool scope checks.
Origin Validation
Browsers attach an Origin header, which is what lets a server refuse requests
smuggled in by DNS rebinding. The check is off by default so existing
deployments keep working; turn it on wherever the endpoint is reachable from a
browser:
mcp_server = MCPServer(
name="my-server",
version="1.0.0",
tools=my_tools,
allowed_origins=("https://app.example.com",),
)A request whose Origin is present and not on the list gets 403 Forbidden.
Requests with no Origin at all — ordinary non-browser clients — are unaffected
by default, because browsers always send Origin on a POST and the rebinding
threat model does not cover clients that are not browsers.
If the endpoint should only ever serve browser traffic, add require_origin to
refuse a request that omits the header too, which makes the allowlist mandatory
rather than advisory:
mcp_server = MCPServer(
name="my-server",
version="1.0.0",
tools=my_tools,
allowed_origins=("https://app.example.com",),
require_origin=True,
)require_origin does nothing on its own — it only tightens an allowlist that is
already configured. When running locally, also bind to 127.0.0.1 rather than
0.0.0.0.
Mirroring Tool Parameters into Headers
A tool can ask clients to copy specific argument values into Mcp-Param-*
headers, so proxies can route or rate-limit on them without parsing the body.
Annotate the field with x-mcp-header:
from pydantic import BaseModel, Field
class ExecuteSQLInput(BaseModel):
region: str = Field(
description="The region to execute the query in",
json_schema_extra={"x-mcp-header": "Region"},
)
query: str = Field(description="The SQL query to execute")A conforming client then sends Mcp-Param-Region: us-west1 alongside the call,
and the server verifies it against the body — rejecting the request with
-32020 if the header is missing, contradicts the argument, or is sent when the
argument is absent. Mcp-Param-* headers that no annotation claims are ignored,
as intermediaries are expected to forward unrecognised ones untouched.
The comparison is textual, against the value as JSON writes it: for
"replicas": 3 the header must read exactly 3, not 3.0, +3, or 3.
Numeric coercion would call those equal while an intermediary routing on the raw
header string saw something else, which is the desync the mirroring exists to
prevent.
Only string, integer, and boolean fields reachable through a plain chain
of object properties can be annotated, and no two fields may claim the same
header name — a collision is refused when the server is constructed, because
keeping one of the two annotations would leave the other silently unenforced. Do
not annotate sensitive values: header contents are visible to every intermediary
on the path.
Tools
Tools are the functions that can be called by the client.
Basic Tool Example
Define the arguments and output for the tools:
# app/tools/models.py
from pydantic import BaseModel, Field
class GreetInput(BaseModel):
question: str = Field(description="The question to answer")
class GreetOutput(BaseModel):
answer: str = Field(description="The answer to the question")
# Note: the description on Field will be passed when listing the tools.
# Having a description is optional, but it's recommended to provide one.Define the tools:
# app/tools/tools.py
from http_mcp.types import Arguments
from app.tools.models import GreetInput, GreetOutput
def greet(args: Arguments[GreetInput]) -> GreetOutput:
return GreetOutput(answer=f"Hello, {args.inputs.question}!")
# app/tools/__init__.py
from http_mcp.types import Tool
from app.tools.models import GreetInput, GreetOutput
from app.tools.tools import greet
TOOLS = (
Tool(
func=greet,
inputs=GreetInput,
output=GreetOutput,
),
)
__all__ = ["TOOLS"]
Instantiate the server:
# app/main.py
from starlette.applications import Starlette
from http_mcp.server import MCPServer
from app.tools import TOOLS
mcp_server = MCPServer(tools=TOOLS, name="test", version="1.0.0")
app = Starlette()
app.mount(
"/mcp",
mcp_server.app,
)Tools Without Arguments
You can define tools that don't require any input arguments:
from datetime import UTC, datetime
from pydantic import BaseModel, Field
from http_mcp.types import Tool
class GetTimeOutput(BaseModel):
time: str = Field(description="The current time")
async def get_time() -> GetTimeOutput:
"""Get the current time."""
return GetTimeOutput(time=datetime.now(UTC).strftime("%H:%M:%S"))
TOOLS = (
Tool(
func=get_time,
inputs=type(None), # No arguments required
output=GetTimeOutput,
),
)Alternatively, you can use the NoArguments class for better clarity:
from http_mcp.types import Arguments, NoArguments, Tool
class SimpleOutput(BaseModel):
success: bool = Field(description="Whether the operation was successful")
def simple_tool(args: Arguments[NoArguments]) -> SimpleOutput:
"""A simple tool with no arguments."""
# You can still access request and state
context = args.get_state_key("context", Context)
return SimpleOutput(success=True)
TOOLS = (
Tool(
func=simple_tool,
inputs=NoArguments,
output=SimpleOutput,
),
)Tools with Error Handling
Tools can optionally return error messages instead of raising exceptions:
from pydantic import BaseModel, Field
from http_mcp.types import Arguments, Tool
from http_mcp.exceptions import ToolInvocationError
class RiskyToolInput(BaseModel):
value: int = Field(description="An integer value")
class RiskyToolOutput(BaseModel):
result: str = Field(description="The result of the operation")
def risky_tool(args: Arguments[RiskyToolInput]) -> RiskyToolOutput:
"""A tool that might fail."""
if args.inputs.value < 0:
raise ToolInvocationError("risky_tool", "Value must be positive")
return RiskyToolOutput(result=f"Success: {args.inputs.value}")
TOOLS = (
Tool(
func=risky_tool,
inputs=RiskyToolInput,
output=RiskyToolOutput,
return_error_message=True, # Return ErrorMessage instead of raising
),
)When return_error_message=True, the tool will return an ErrorMessage model
with the error details instead of raising a ToolInvocationError.
Tools with Authorization Scopes
You can restrict tool access based on authentication scopes:
from http_mcp.exceptions import ToolInvocationError
from http_mcp.types import Arguments, NoArguments, Tool
from starlette.authentication import has_required_scope
class SecureOutput(BaseModel):
message: str = Field(description="A secure message")
def private_tool(args: Arguments[NoArguments]) -> SecureOutput:
"""A tool that requires authentication."""
if not has_required_scope(args.request, ("private",)):
raise ToolInvocationError("private_tool", "Insufficient scope")
return SecureOutput(message="This is private data")
def admin_tool(args: Arguments[NoArguments]) -> SecureOutput:
"""A tool that requires admin or superuser scope."""
if not has_required_scope(args.request, ("admin", "superuser")):
raise ToolInvocationError("admin_tool", "Insufficient scope")
return SecureOutput(message="This is admin data")
TOOLS = (
Tool(
func=private_tool,
inputs=NoArguments,
output=SecureOutput,
scopes=("private",), # Only accessible with 'private' scope
),
Tool(
func=admin_tool,
inputs=NoArguments,
output=SecureOutput,
scopes=("admin", "superuser"), # Accessible with either scope
),
)Note: You need to set up authentication middleware in your Starlette app for
scopes to work properly. The scopes field on Tool is the primary
authorization gate — the framework filters tools by scope before invocation. The
raise ToolInvocationError(...) calls inside the tool functions above are
optional defense-in-depth checks that return a proper error response to the
client instead of silently failing.
Server State Management
The server uses Starlette's lifespan system to manage shared state across the
entire application lifecycle. State is initialized when the application starts
and persists until it shuts down. Context is accessed through the
get_state_key method on the Arguments object.
This is useful for sharing resources like database connection pools, HTTP clients, caches, or any application state across tools.
Database Connection Pool
The most common pattern — initialize a connection pool at startup, share it across all tools, and close it on shutdown:
# app/context.py
from dataclasses import dataclass
import asyncpg
@dataclass
class AppContext:
db: asyncpg.Pool# app/main.py
import contextlib
import os
from collections.abc import AsyncIterator
from typing import TypedDict
import asyncpg
from starlette.applications import Starlette
from http_mcp.server import MCPServer
from app.context import AppContext
class State(TypedDict):
ctx: AppContext
@contextlib.asynccontextmanager
async def lifespan(_app: Starlette) -> AsyncIterator[State]:
pool = await asyncpg.create_pool(os.environ["DATABASE_URL"])
yield {"ctx": AppContext(db=pool)}
await pool.close()
mcp_server = MCPServer(tools=TOOLS, name="my-server", version="1.0.0")
app = Starlette(lifespan=lifespan)
app.mount("/mcp", mcp_server.app)# app/tools.py
from pydantic import BaseModel, Field
from http_mcp.types import Arguments
from app.context import AppContext
class GetUserInput(BaseModel):
user_id: int = Field(description="The user ID to look up")
class GetUserOutput(BaseModel):
name: str = Field(description="The user's name")
email: str = Field(description="The user's email")
async def get_user(args: Arguments[GetUserInput]) -> GetUserOutput:
"""Look up a user by ID."""
ctx = args.get_state_key("ctx", AppContext)
row = await ctx.db.fetchrow(
"SELECT name, email FROM users WHERE id = $1",
args.inputs.user_id,
)
return GetUserOutput(name=row["name"], email=row["email"])Shared HTTP Client
Share a single httpx.AsyncClient across tools to reuse connections and
configure base URLs, headers, or timeouts once:
# app/context.py
from dataclasses import dataclass
import httpx
@dataclass
class AppContext:
http_client: httpx.AsyncClient# app/main.py
import contextlib
from collections.abc import AsyncIterator
from typing import TypedDict
import httpx
from starlette.applications import Starlette
from http_mcp.server import MCPServer
from app.context import AppContext
class State(TypedDict):
ctx: AppContext
@contextlib.asynccontextmanager
async def lifespan(_app: Starlette) -> AsyncIterator[State]:
async with httpx.AsyncClient(
base_url="https://api.example.com",
headers={"Authorization": "Bearer <token>"},
) as client:
yield {"ctx": AppContext(http_client=client)}
mcp_server = MCPServer(tools=TOOLS, name="my-server", version="1.0.0")
app = Starlette(lifespan=lifespan)
app.mount("/mcp", mcp_server.app)# app/tools.py
from pydantic import BaseModel, Field
from http_mcp.types import Arguments
from app.context import AppContext
class SearchInput(BaseModel):
query: str = Field(description="The search query")
class SearchOutput(BaseModel):
results: list[str] = Field(description="Search result titles")
async def search(args: Arguments[SearchInput]) -> SearchOutput:
"""Search via an external API."""
ctx = args.get_state_key("ctx", AppContext)
resp = await ctx.http_client.get("/search", params={"q": args.inputs.query})
resp.raise_for_status()
return SearchOutput(results=[r["title"] for r in resp.json()["items"]])In-Memory Cache
Share mutable state like caches or counters across tool invocations within the same server lifecycle:
# app/context.py
from dataclasses import dataclass, field
@dataclass
class AppContext:
cache: dict[str, str] = field(default_factory=dict)
request_count: int = 0# app/tools.py
from pydantic import BaseModel, Field
from http_mcp.types import Arguments
from app.context import AppContext
class LookupInput(BaseModel):
key: str = Field(description="The cache key to look up")
class LookupOutput(BaseModel):
value: str | None = Field(description="The cached value, or null if not found")
total_requests: int = Field(description="Total requests served")
async def lookup(args: Arguments[LookupInput]) -> LookupOutput:
"""Look up a value in the cache."""
ctx = args.get_state_key("ctx", AppContext)
ctx.request_count += 1
return LookupOutput(
value=ctx.cache.get(args.inputs.key),
total_requests=ctx.request_count,
)All tools sharing the same AppContext instance see each other's writes
immediately, since the lifespan yields a single shared object.
Note: Plain dict and int are not thread-safe. If your tools run concurrently
(e.g., sync tools dispatched via threads), protect shared mutable state with an
asyncio.Lock or use thread-safe data structures.
Request Access
You can access the incoming request object from your tools. The request object is passed to each tool call and can be used to access headers, cookies, and other request data (e.g. request.state, request.scope).
from pydantic import BaseModel, Field
from http_mcp.types import Arguments
class MyToolArguments(BaseModel):
question: str = Field(description="The question to answer")
class MyToolOutput(BaseModel):
answer: str = Field(description="The answer to the question")
async def my_tool(args: Arguments[MyToolArguments]) -> MyToolOutput:
# Access the request
auth_header = args.request.headers.get("Authorization")
...
return MyToolOutput(answer=f"Hello, {args.inputs.question}!")
# Use MCPServer:
from http_mcp.server import MCPServer
mcp_server = MCPServer(
name="my-server",
version="1.0.0",
tools=(my_tool,),
)Prompts
You can add interactive templates that are invoked by user choice. Prompts now support lifespan state access, similar to tools.
Basic Prompt Example
Define the arguments for the prompts:
from pydantic import BaseModel, Field
from http_mcp.types import Arguments, Prompt, PromptMessage, TextContent
class GetAdvice(BaseModel):
topic: str = Field(description="The topic to get advice on")
include_actionable_steps: bool = Field(
description="Whether to include actionable steps in the advice", default=False
)
def get_advice(args: Arguments[GetAdvice]) -> tuple[PromptMessage, ...]:
"""Get advice on a topic."""
template = """
You are a helpful assistant that can give advice on {topic}.
"""
if args.inputs.include_actionable_steps:
template += """
The advice should include actionable steps.
"""
return (
PromptMessage(
role="user",
content=TextContent(
text=template.format(topic=args.inputs.topic)
),
),
)
PROMPTS = (
Prompt(
func=get_advice,
arguments_type=GetAdvice,
),
)Instantiate the server:
from starlette.applications import Starlette
from app.prompts import PROMPTS
from http_mcp.server import MCPServer
app = Starlette()
mcp_server = MCPServer(tools=(), prompts=PROMPTS, name="test", version="1.0.0")
app.mount(
"/mcp",
mcp_server.app,
)Prompts Without Arguments
You can define prompts that don't require any input arguments:
from http_mcp.types import Prompt, PromptMessage, TextContent
def help_prompt() -> tuple[PromptMessage, ...]:
"""Use this prompt to get general help."""
return (
PromptMessage(
role="user",
content=TextContent(
text="You are a helpful assistant. Help the user with their task."
),
),
)
PROMPTS = (
Prompt(
func=help_prompt,
arguments_type=type(None), # No arguments required
),
)Alternatively, you can use the NoArguments class:
from http_mcp.types import Arguments, NoArguments, Prompt, PromptMessage, TextContent
def help_prompt_with_context(args: Arguments[NoArguments]) -> tuple[PromptMessage, ...]:
"""Use this prompt to get help with access to context."""
# You can still access request and state
context = args.get_state_key("context", Context)
return (
PromptMessage(
role="user",
content=TextContent(text="You are a helpful assistant."),
),
)
PROMPTS = (
Prompt(
func=help_prompt_with_context,
arguments_type=NoArguments,
),
)Prompts with Lifespan State
from pydantic import BaseModel, Field
from http_mcp.types import Arguments, Prompt, PromptMessage, TextContent
from app.context import Context
class GetAdvice(BaseModel):
topic: str = Field(description="The topic to get advice on")
def get_advice_with_context(args: Arguments[GetAdvice]) -> tuple[PromptMessage, ...]:
"""Get advice on a topic with context awareness."""
# Access the context from lifespan state
context = args.get_state_key("context", Context)
called_tools = context.get_called_tools()
template = """
You are a helpful assistant that can give advice on {topic}.
Previously called tools: {tools}
"""
return (
PromptMessage(
role="user",
content=TextContent(
text=template.format(
topic=args.inputs.topic,
tools=", ".join(called_tools) if called_tools else "none"
)
)
),
)
PROMPTS_WITH_CONTEXT = (
Prompt(
func=get_advice_with_context,
arguments_type=GetAdvice,
),
)Prompts with Authorization Scopes
You can restrict prompt access based on authentication scopes:
from http_mcp.types import Arguments, NoArguments, Prompt, PromptMessage, TextContent
def private_prompt(args: Arguments[NoArguments]) -> tuple[PromptMessage, ...]:
"""Private prompt that is only accessible to authenticated users."""
return (
PromptMessage(
role="user",
content=TextContent(text="This is a private prompt."),
),
)
def admin_prompt(args: Arguments[NoArguments]) -> tuple[PromptMessage, ...]:
"""Admin prompt accessible to users with admin or superuser scope."""
return (
PromptMessage(
role="user",
content=TextContent(text="This is an admin prompt."),
),
)
PROMPTS = (
Prompt(
func=private_prompt,
arguments_type=NoArguments,
scopes=("private",), # Only accessible with 'private' scope
),
Prompt(
func=admin_prompt,
arguments_type=NoArguments,
scopes=("admin", "superuser"), # Accessible with either scope
),
)Note: You need to set up authentication middleware in your Starlette app for scopes to work properly.
STDIO Transport
In addition to HTTP transport, the server supports STDIO transport for communication. This is useful for command-line applications and integrations that communicate through standard input/output.
Using STDIO Transport
import asyncio
import os
from http_mcp.server import MCPServer
from app.tools import TOOLS
from app.prompts import PROMPTS
mcp_server = MCPServer(
tools=TOOLS,
prompts=PROMPTS,
name="test",
version="1.0.0"
)
# Run the server with STDIO transport
async def main() -> None:
request_headers = {
"Authorization": f"Bearer {os.getenv('MCP_TOKEN', '')}",
"X-Custom-Header": "value",
}
await mcp_server.serve_stdio(request_headers)
asyncio.run(main())The request_headers parameter allows you to pass headers that will be included
in the request context, enabling authentication and other header-based features
even when using STDIO transport.
Authentication and Authorization
The library integrates with Starlette's authentication system to provide scope-based authorization for tools and prompts.
Setting Up Authentication Middleware
import contextlib
from collections.abc import AsyncIterator
from typing import TypedDict
from starlette.applications import Starlette
from starlette.authentication import (
AuthCredentials,
AuthenticationBackend,
BaseUser,
SimpleUser,
)
from starlette.middleware import Middleware
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.requests import HTTPConnection
from http_mcp.server import MCPServer
from app.context import Context
from app.tools import TOOLS
from app.prompts import PROMPTS
class BasicAuthBackend(AuthenticationBackend):
def __init__(self, granted_scopes: tuple[str, ...] = ("authenticated",)) -> None:
self.granted_scopes = granted_scopes
super().__init__()
async def authenticate(
self, conn: HTTPConnection
) -> tuple[AuthCredentials, BaseUser] | None:
# Implement your authentication logic here
# For example, check Bearer token, API key, etc.
auth_header = conn.headers.get("Authorization")
if not auth_header:
return None
# Validate token and return credentials with scopes
return AuthCredentials(self.granted_scopes), SimpleUser("username")
class State(TypedDict):
context: Context
@contextlib.asynccontextmanager
async def lifespan(_app: Starlette) -> AsyncIterator[State]:
yield {"context": Context()}
mcp_server = MCPServer(
tools=TOOLS,
prompts=PROMPTS,
name="test",
version="1.0.0"
)
app = Starlette(
lifespan=lifespan,
middleware=[
Middleware(
AuthenticationMiddleware,
backend=BasicAuthBackend(granted_scopes=("private", "admin")),
),
],
)
app.mount("/mcp", mcp_server.app)How Scopes Work
Authentication Middleware: The middleware authenticates each request and assigns scopes to the user through
AuthCredentials.Tool/Prompt Scopes: When defining tools or prompts, you can specify required scopes using the
scopesparameter.Access Control: The server automatically filters tools and prompts based on the user's granted scopes. Tools and prompts without the required scopes are not visible in listings and cannot be invoked.
Multiple Scopes: If you specify multiple scopes (e.g.,
scopes=("admin", "superuser")), the user needs at least one of those scopes to access the tool or prompt.
API Reference
Tool Class
The Tool class is used to define tools that can be invoked by clients.
Parameters:
func: The function to be invoked. Can be sync or async. The function can either:Accept an
Arguments[TInputs]parameterAccept no parameters
inputs: The Pydantic model class for input validation. Usetype(None)orNoArgumentsfor tools without inputsoutput: The Pydantic model class for output validationreturn_error_message(bool): IfTrue, tool errors returnErrorMessageinstead of raising exceptions (default:False)scopes(tuple[str, ...]): Required authentication scopes for accessing this tool (default: empty tuple)
Properties:
name: The function name (derived fromfunc.__name__)title: A human-readable title (derived from the function name)description: The function's docstringinput_schema: JSON schema for the input parametersoutput_schema: JSON schema for the output
Prompt Class
The Prompt class is used to define prompts that can be invoked by clients.
Parameters:
func: The function to be invoked. Can be sync or async. The function can either:Accept an
Arguments[TArguments]parameterAccept no parameters
Must return
tuple[PromptMessage, ...]
arguments_type: The Pydantic model class for argument validation. Usetype(None)orNoArgumentsfor prompts without argumentsscopes(tuple[str, ...]): Required authentication scopes for accessing this prompt (default: empty tuple)
Properties:
name: The function name (derived fromfunc.__name__)title: A human-readable title (derived from the function name)description: The function's docstringarguments: Tuple ofPromptArgumentobjects defining the prompt's arguments
Arguments Class
The Arguments class is passed to tool and prompt functions to provide access
to inputs, request, and state.
Parameters:
request: The StarletteRequestobjectinputs: The validated input/argument data (type depends on the Tool/Prompt definition)
Methods:
get_state_key(key: str, _object_type: type[TKey]) -> TKey: Access a value from the lifespan state. RaisesServerErrorif the key doesn't exist.
NoArguments Class
An empty Pydantic model that can be used as a clearer alternative to
type(None) when defining tools or prompts without arguments.
from http_mcp.types import NoArguments
# Use this instead of type(None)
Tool(func=my_func, inputs=NoArguments, output=MyOutput)OAuth 2.1 Authorization (auth_mcp)
The auth_mcp package adds standards-compliant OAuth 2.1 authorization to your
MCP server. Install with the auth extra:
pip install http-mcp[auth]Quick Start
from http_mcp.server import MCPServer
from auth_mcp.resource_server import (
ProtectedMCPAppConfig,
TokenInfo,
TokenValidator,
create_protected_mcp_app,
)
from auth_mcp.types import ProtectedResourceMetadata
class MyTokenValidator(TokenValidator):
async def validate_token(
self, token: str, resource: str | None = None
) -> TokenInfo | None:
# Validate against your authorization server
...
mcp_server = MCPServer(name="my-server", version="1.0.0", tools=MY_TOOLS)
config = ProtectedMCPAppConfig(
mcp_server=mcp_server,
token_validator=MyTokenValidator(),
resource_endpoint=ProtectedResourceMetadata(
resource="https://mcp.example.com",
authorization_servers=("https://auth.example.com",),
),
)
app = create_protected_mcp_app(config)This gives you:
Bearer token validation on all MCP endpoints (secure by default)
/.well-known/oauth-protected-resourcediscovery endpoint (RFC 9728)WWW-Authenticateheaders on 401/403 withresource_metadataparameterSecurity headers (HSTS, nosniff, no-store)
Optional custom middleware via the
middlewaresparameter
For full documentation, best practices, and security surface details, see auth_mcp README.
Security Surfaces by Endpoint
POST /mcp — MCP JSON-RPC Endpoint
Authentication — When using
auth_mcp, Bearer tokens are extracted from theAuthorizationheader and validated viaTokenValidator. Tokens exceeding 2048 characters or containing characters outside the RFC 6750b64tokenpattern are rejected before reaching the validator. Withoutauth_mcp, authentication is handled by Starlette'sAuthenticationMiddleware.Authorization — Scope-based filtering via Starlette's
has_required_scope(). Tools and prompts without matching scopes are hidden from listings and blocked on invocation. Request-header validation resolves tool schemas through the same scope check, so a caller a tool is hidden from cannot learn itsx-mcp-headerarguments from a mismatch message either.Input validation — JSON-RPC messages validated by Pydantic. Request body capped at 4 MB, enforced while reading: an oversized
Content-Lengthis refused before the body is read at all, and a body that outgrows the cap mid-stream stops being buffered at that point. Content-Type strictly checked (application/jsononly, media type parameters ignored).Error handling — Tool and prompt names truncated to 100 characters in error messages. Pydantic validation errors sanitized before inclusion in responses.
Response headers —
X-Content-Type-Options: nosniff,Cache-Control: no-storeon all responses.auth_mcpadditionally addsStrict-Transport-Security: max-age=31536000; includeSubDomains.
GET /.well-known/oauth-protected-resource — Discovery Endpoint (auth_mcp)
Authentication — Subject to the same auth middleware as
/mcp. Whenrequire_authentication=True(default), requires a valid token. Set toFalseif clients need to discover the authorization server before authenticating.Input validation — Only
GETallowed; other methods return405 Method Not Allowed.Output — Serialized once at startup from a frozen
ProtectedResourceMetadatamodel. URI fields validated as HTTP/HTTPS URLs via Pydantic'sAnyHttpUrl.
WWW-Authenticate Response Header (auth_mcp)
Header injection — All parameter values (
realm,resource_metadata,scope,error,error_description) are sanitized: CR/LF characters stripped, backslash and double-quote escaped per RFC 7230 quoted-string rules.Information disclosure — Error responses use generic messages (
"Authentication required"). The originalAuthenticationErrordetails are discarded. Error codes (invalid_tokenon 401) follow RFC 6750 without leaking internal state.
STDIO Transport
Message size — Capped at 4 MB, matching HTTP transport.
Logging — Messages truncated to 500 characters in debug logs to prevent log flooding. Token values are never logged.
Headers — Request headers are converted to proper ASGI
list[tuple[bytes, bytes]]format.
Installation
Requires Python 3.12+ (uses PEP 695 type parameter syntax).
Install the package using pip or uv:
pip install http-mcpWith OAuth 2.1 authorization support:
pip install http-mcp[auth]or
uv add http-mcpLicense
This project is licensed under the MIT License. See the LICENSE file for details.
Available Tools
4 toolsget_called_toolsGet Called ToolsAIdempotent
Get the list of called tools.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| called_tools | Yes | The list of called tools |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The description doesn't add behavioral details beyond what annotations provide, but annotations are comprehensive (readOnlyHint=false, openWorldHint=true, idempotentHint=true, destructiveHint=false). Since annotations cover key behavioral traits, the description doesn't need to compensate, and there's no contradiction with annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single, efficient sentence with no wasted words. It's appropriately sized and front-loaded, making it easy to understand at a glance.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given 0 parameters, comprehensive annotations, and an output schema, the description is complete enough for its purpose. It could be slightly improved by clarifying what 'called tools' means, but the structured data compensates well.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
There are 0 parameters, and schema description coverage is 100%, so no parameter documentation is needed. The description doesn't mention parameters, which is appropriate, earning a baseline score for this scenario.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description 'Get the list of called tools' clearly states the action (get) and resource (called tools), but it's somewhat vague about what 'called tools' means in this context. It doesn't differentiate from sibling tools like 'get_time' or 'get_weather' beyond the resource name.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides no guidance on when to use this tool versus alternatives. There's no mention of context, prerequisites, or comparisons with sibling tools like 'tool_that_access_request' that might serve similar purposes.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_timeGet TimeAIdempotent
Get the current time.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| time | Yes | The current time |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already provide key behavioral hints (readOnlyHint=false, openWorldHint=true, idempotentHint=true, destructiveHint=false), so the description doesn't need to repeat these. However, it adds no additional context about rate limits, authentication needs, or specific behavioral traits beyond the basic action, resulting in a baseline score.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is extremely concise and front-loaded with a single, clear sentence that directly states the tool's purpose. There is no wasted language or unnecessary elaboration.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's simplicity (0 parameters, annotations covering key behaviors, and an output schema present), the description is complete enough for basic understanding. However, it lacks any usage context or differentiation from siblings, which slightly reduces completeness.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
With 0 parameters and 100% schema description coverage, the schema fully documents the lack of inputs. The description implicitly confirms this by not mentioning any parameters, which is appropriate. A baseline of 4 is given as no parameters are present.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's purpose with a specific verb ('Get') and resource ('current time'), making it immediately understandable. However, it doesn't distinguish itself from potential sibling tools like 'get_weather' or 'get_called_tools' beyond the resource difference, which prevents a perfect score.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides no guidance on when to use this tool versus alternatives. It doesn't mention any context, prerequisites, or exclusions, leaving the agent to infer usage based on the tool name alone.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_weatherGet WeatherBIdempotent
Get the current weather in a given location.
| Name | Required | Description | Default |
|---|---|---|---|
| location | Yes | The location to get the weather for | |
| unit | No | The unit of temperature | celsius |
Output Schema
| Name | Required | Description |
|---|---|---|
| weather | Yes | The weather in the given location |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already provide key behavioral hints (readOnlyHint=false, openWorldHint=true, idempotentHint=true, destructiveHint=false), covering safety and idempotency. The description adds minimal context beyond this, stating it retrieves 'current' weather, which implies real-time data but doesn't elaborate on rate limits, authentication needs, or data freshness.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single, efficient sentence that directly states the tool's function without unnecessary words. It's appropriately sized and front-loaded, making it easy for an agent to parse quickly.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's low complexity, rich annotations, and the presence of an output schema, the description is reasonably complete. It covers the core purpose adequately, though it lacks usage guidelines and deeper behavioral context, which are partially mitigated by the structured data.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
With 100% schema description coverage, the input schema fully documents both parameters (location and unit). The description adds no additional semantic context beyond implying location is required, which is already clear from the schema. Baseline 3 is appropriate when schema handles parameter documentation.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's purpose with a specific verb ('Get') and resource ('current weather'), and specifies the scope ('in a given location'). However, it doesn't explicitly differentiate from potential weather-related siblings, though none are listed among the provided sibling tools.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides no guidance on when to use this tool versus alternatives. It doesn't mention any prerequisites, constraints, or scenarios where other tools might be more appropriate, leaving the agent with minimal context for decision-making.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
tool_that_access_requestTool That Access RequestCIdempotent
Access the request.
| Name | Required | Description | Default |
|---|---|---|---|
| username | Yes | The username of the user |
Output Schema
| Name | Required | Description |
|---|---|---|
| message | Yes | The message to the user |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations provide substantial behavioral information (readOnlyHint=false, openWorldHint=true, idempotentHint=true, destructiveHint=false), so the description's burden is lower. The description adds no behavioral context beyond what annotations already declare - it doesn't mention what type of access occurs, whether authentication is needed, rate limits, or what happens when accessing requests. However, it doesn't contradict annotations either, so it meets the minimum baseline for descriptions with good annotation coverage.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
While technically concise with only three words, this represents under-specification rather than effective brevity. The description fails to provide necessary information about the tool's purpose and usage. Every sentence should earn its place, but this single sentence doesn't provide enough value to justify its existence as a helpful description.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given that this tool has one required parameter, annotations covering key behavioral aspects, and an output schema exists, the description is incomplete. While the output schema means the description doesn't need to explain return values, the description fails to explain what 'access the request' means in practical terms, what kind of requests are involved, or provide any operational context. For a tool with parameter requirements and behavioral implications, this description leaves too many questions unanswered.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% with the username parameter fully documented in the schema. The description adds no parameter information whatsoever - it doesn't explain why username is required, what relationship it has to 'accessing the request', or provide any context beyond what's already in the structured schema. This meets the baseline score of 3 when schema coverage is high and description adds no parameter semantics.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description 'Access the request' is essentially a tautology that restates the tool name 'tool_that_access_request' without adding meaningful clarification. It doesn't specify what type of request is being accessed, what 'access' entails (e.g., retrieve, modify, approve), or what resource is involved. While it distinguishes from unrelated siblings like get_weather, it fails to provide specific verb+resource information needed for clear purpose understanding.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides absolutely no guidance on when to use this tool versus alternatives. There's no mention of context, prerequisites, or comparison with sibling tools like get_called_tools, get_time, or get_weather. The agent receives no information about appropriate use cases or when this tool would be preferred over other options.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
TDQS
Each tool has a clearly distinct purpose: get_called_tools retrieves internal tool usage history, get_time provides current time, get_weather fetches weather data for a location, and tool_that_access_request handles request access. There is no overlap in functionality, making tool selection unambiguous.
Three tools follow a consistent 'get_*' verb_noun pattern (get_called_tools, get_time, get_weather), but tool_that_access_request deviates with a noun_verb structure and lacks the 'get' prefix. This mixed convention reduces predictability, though the names remain readable.
With 4 tools, the count is reasonable for a simple HTTP server, avoiding bloat. However, the scope feels slightly thin as it lacks common HTTP operations like making requests or handling responses, which might be expected for such a server.
The tool set is severely incomplete for an HTTP server domain. It includes utility functions (time, weather) and internal tracking (called tools, request access) but lacks core HTTP operations such as send_request, get_response, or manage_connections, leaving obvious gaps that will hinder agent workflows.
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Remote MCP server exposing SMI Aware tools, resources, and skills over Streamable HTTP.
Nifty's MCP server — exposes tasks, projects, messages, and files as tools for AI agents.
MCP server for progressive tool usage at any scale (see https://klavis.ai)
Model Context Protocol server for the Apideck Unified API. Connect any MCP-compatible agent framework to 100+ accounting systems, HRIS platforms, file storage providers, and more through one integration. More information https://www.apideck.com/mcp-server
Related MCP Servers
- AlicenseBqualityDmaintenanceA Model Context Protocol (MCP) server based on OpenRPC, providing JSON-RPC function invocation and method discovery services.21Apache 2.0
- FlicenseNot gradedqualityBmaintenanceA simple HTTP server to validate MCP infrastructure for the Wazza MCP client, exposing endpoints for tool discovery and calling.
- FlicenseNot gradedqualityDmaintenanceEnables building and running MCP servers over streamable HTTP, exposing tools to AI assistants like Cursor, with examples of mounting multiple servers in FastAPI.
Appeared in Searches
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/yeison-liscano/http_mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server