Skip to main content
Glama
yeison-liscano

Simple HTTP MCP Server

简单 HTTP MCP 服务器实现

本项目为模型上下文协议(MCP)提供基于 HTTP 的轻量级服务器实现。它允许你将 Python 函数作为工具(tools)和提示(prompts)暴露出来,供远程通过 JSON-RPC 接口发现和执行。它旨在与 Starlette 或 FastAPI 应用配合使用(参见 demo)。

目录

Related MCP server: remote-mcp

功能特性

  • 符合 MCP 协议:实现了 MCP 规范中工具和提示的发现与执行。不支持通知(notifications)。

  • 单一协议修订版:仅使用无状态的 2026-07-28 修订版——server/discover、按请求携带的 _meta、无握手、无会话。单一分发路径意味着请求无法通过声明较旧的修订版来选择更弱的处理方式。

  • HTTP 和 STDIO 传输:使用 HTTP(POST 请求)或 STDIO 进行通信。

  • 异步支持:基于 StarletteFastAPI 构建,用于异步请求处理。

  • 类型安全:利用 Pydantic 进行稳健的数据验证和序列化。

  • 服务器状态管理:通过生命周期上下文使用 get_state_key 方法访问共享状态。

  • 请求访问:从你的工具和提示中访问传入的请求对象。

  • 授权范围:支持基于 Starlette 认证系统的基于范围的授权。

  • 错误处理:工具可以选择返回错误消息而不是抛出异常。

  • OAuth 2.1 授权:可选的 auth_mcp 包,支持 Bearer 令牌验证、受保护资源元数据(RFC 9728)以及 WWW-Authenticate 错误响应。使用 pip install http-mcp[auth] 安装。

服务器架构

该库提供了一个单一的 MCPServer 类,使用生命周期(lifespan)在整个应用生命周期内管理共享状态。

MCPServer

MCPServer 设计用于与 Starlette 的生命周期系统配合,以管理共享的服务器状态。

关键特性:

  • 基于生命周期:使用 Starlette 的生命周期事件来初始化和管理共享的服务器状态

  • 应用级状态:状态在整个应用生命周期内持续存在,而非按请求存在

  • 灵活:可与存储在生命周期状态中的任何自定义上下文类一起使用

构造函数参数:

  • name(str):你的 MCP 服务器的名称

  • version(str):你的 MCP 服务器的版本

  • tools(tuple[Tool, ...]):要暴露的工具元组(默认:空元组)

  • prompts(tuple[Prompt, ...]):要暴露的提示元组(默认:空元组)

  • instructions(str | None):可选的、供 AI 助手了解如何使用此服务器的说明

  • cache_ttl_ms(int):以毫秒为单位的新鲜度提示,随 tools/listprompts/listserver/discover 结果一起发送(默认:300000)。使用 0 告诉客户端永远不要缓存。参见 缓存提示

  • cache_scope("public" | "private" | None):共享缓存是否可以在不同授权上下文之间复用这些结果。省略时自动推导。参见 缓存提示

  • allowed_origins(tuple[str, ...]):HTTP 传输接受的来源(默认:空,表示检查被禁用)。参见 来源验证

  • require_origin(bool):当设置了 allowed_origins 时,是否拒绝完全未携带 Origin 头的请求(默认:False)。参见 来源验证

用法示例:

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)

协议版本

服务器仅实现一个协议修订版 2026-07-28,每个请求都走同一条路径。没有版本协商,也没有请求可以选入的第二套规则。

0.17.0 中的破坏性变更。 对基于会话的修订版 2025-11-252025-06-182025-03-26 的支持已被移除,同时移除的还有 initializenotifications/initializedping。只讲这些修订版的客户端将无法再与此服务器通信。只服务一个修订版也正是使下面的请求元数据头变得可信的原因:当两个时代并存时,请求可以通过声明较旧的版本来跳过头部检查,因此基于 Mcp-Method 路由的中间层可能与基于请求体行动的服务器失去同步。

0.18.0 中的破坏性变更。 ServerInterface.get_tool_input_schema 现在接收 Request,因此授权范围在分发之前得到执行;接口的实现必须更新,而 MCPServer 用户不受影响。镜像的 Mcp-Param-* 值按文本而非数值进行比较,因此对于 "replicas": 3,读取为 3.0 的头部现在会得到 -32020。每个 notifications/* 方法都返回 404,因为该修订版未定义任何通知。

2026-07-28 请求形态

该修订版没有会话概念。实际上:

  • 无握手。 每个请求都在 _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": {}
        }
      }
    }

    protocolVersionclientCapabilities 是必需的;省略任一字段都会得到 -32602 和 HTTP 400。任何其他版本都会得到 -32022,其 data.supported 列出此服务器所讲的唯一修订版。

  • server/discover 取代 initialize 用于能力发现。 它在一个调用中报告支持的版本、能力、说明和服务器身份,并且无需任何先前的请求即可应答:

    {
      "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"
    }
  • 每个结果都携带 resultType: "complete" 以及一个指明服务器的 _meta 块。

  • initializenotifications/initializedpinglogging/setLevel 不存在,会话和 SSE 恢复机制也不存在。它们返回 -32601 和 HTTP 404。JSON-RPC 通知——即没有 idnotifications/* 消息——仍然得到 202 Accepted 且无响应体,因为 JSON-RPC 禁止对它们进行响应。

  • 必需的请求头。 每个 POST 必须发送 MCP-Protocol-VersionMcp-Method,在 tools/callprompts/get 上还必须发送 Mcp-Name。每个都必须与对应的请求体值匹配,否则请求将以 -32020HeaderMismatch)和 HTTP 400 被拒绝——这可以防止代理按一个值路由而服务器按另一个值行动。无法以纯 ASCII 表达的值使用 =?base64?...?= 信封格式,服务器在比较前会先解码。

  • 未知的工具和提示报告 -32602,这正是工具和提示规范所规定的。-32002 已被此修订版淘汰。

  • Mcp-Session-IdLast-Event-ID 被忽略,对 MCP 端点的 GET/DELETE 返回 405 Method Not Allowed

多轮往返请求(elicitation、sampling、roots)和 subscriptions/listen 未实现:此服务器不暴露任何依赖客户端输入的功能,并声明 listChanged: false,因此两者都不适用于它。

缓存提示

tools/listprompts/listserver/discover 结果携带 ttlMscacheScope,以便客户端避免重新获取未更改的列表:

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
)

工具和提示在构造 MCPServer 时即已固定,因此 ttlMs 实际上限制的是客户端可能错过重新部署的时间,而非数据稳定的时间。将其设置为 0 以要求客户端永不缓存。

当你省略 cache_scope 时会自动推导:如果任何工具或提示受范围限制,则为 "private"——此时列表因调用者而异,因此共享缓存不得在不同授权上下文之间复用——否则为 "public"。如果你的部署环境更了解情况,可以覆盖它。请注意,cacheScope 仅管理缓存;它绝不能替代逐工具的 scope 检查。

来源验证

浏览器会附加 Origin 头,这正是服务器能够拒绝通过 DNS 重绑定(DNS rebinding)偷偷带入的请求的原因。该检查默认关闭,以便现有部署继续工作;在端点可从浏览器访问的地方请将其打开:

mcp_server = MCPServer(
    name="my-server",
    version="1.0.0",
    tools=my_tools,
    allowed_origins=("https://app.example.com",),
)

请求的 Origin 存在但不在列表中时,会得到 403 Forbidden。完全没有 Origin 的请求——普通的非浏览器客户端——默认不受影响,因为浏览器在 POST 时总会发送 Origin,而重绑定威胁模型不涵盖非浏览器客户端。

如果端点只应服务浏览器流量,请添加 require_origin 以同样拒绝省略该头的请求,这使允许列表从建议性变为强制性:

mcp_server = MCPServer(
    name="my-server",
    version="1.0.0",
    tools=my_tools,
    allowed_origins=("https://app.example.com",),
    require_origin=True,
)

require_origin 本身不起作用——它只是收紧已经配置的允许列表。在本地运行时,还应绑定到 127.0.0.1 而不是 0.0.0.0

将工具参数镜像到请求头

工具可以要求客户端将特定参数值复制到 Mcp-Param-* 头中,以便代理无需解析请求体即可据此进行路由或限流。用 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")

符合规范的客户端随后会在调用时发送 Mcp-Param-Region: us-west1,服务器会将其与请求体进行核对——如果头部缺失、与参数矛盾,或在参数不存在时发送,则请求以 -32020 被拒绝。未被任何注解声明的 Mcp-Param-* 头会被忽略,因为中间层应原样转发未识别的头部。

比较是文本性的,针对 JSON 写入该值的方式:对于 "replicas": 3,头部必须恰好读取 3,而不是 3.0+33。数值强制转换会将这些视为相等,而基于原始头部字符串路由的中间层看到的却是别的东西,这正是镜像机制要防止的失步。

只有通过普通对象属性链可达的 stringintegerboolean 字段可以被注解,且不能有两个字段声明同一个头部名称——在构造服务器时会拒绝冲突,因为保留两个注解中的一个会让另一个静默地得不到执行。不要注解敏感值:头部内容对路径上的每个中间层都是可见的。

工具

工具是客户端可以调用的函数。

基本工具示例

  1. 定义工具的参数和输出:

# 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.
  1. 定义工具:

# 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"]
  1. 实例化服务器:

# 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,
)

无参数工具

你可以定义不需要任何输入参数的工具:

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,
    ),
)

或者,你可以使用 NoArguments 类以获得更好的清晰度:

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,
    ),
)

带错误处理的工具

工具可以选择返回错误消息而不是抛出异常:

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
    ),
)

return_error_message=True 时,工具将返回一个包含错误详情的 ErrorMessage 模型,而不是抛出 ToolInvocationError

带授权范围的工具

你可以基于认证范围限制工具访问:

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
    ),
)

注意:您需要在 Starlette 应用中设置认证中间件,作用域才能正常工作。Tool 上的 scopes 字段是主要的授权门禁——框架在调用前会按作用域过滤工具。上面工具函数内部的 raise ToolInvocationError(...) 调用是可选的纵深防御检查,用于向客户端返回正确的错误响应,而不是静默失败。

服务器状态管理

服务器使用 Starlette 的 lifespan 系统来管理整个应用生命周期中的共享状态。状态在应用启动时初始化,并持续到应用关闭。通过 Arguments 对象上的 get_state_key 方法访问上下文。

这对于跨工具共享数据库连接池、HTTP 客户端、缓存或任何应用状态等资源非常有用。

数据库连接池

最常见的模式——在启动时初始化连接池,在所有工具之间共享,并在关闭时释放:

# 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"])

共享 HTTP 客户端

在工具之间共享单个 httpx.AsyncClient,以复用连接并一次性配置基础 URL、请求头或超时:

# 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"]])

内存缓存

在同一服务器生命周期内的工具调用之间共享可变状态(如缓存或计数器):

# 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,
    )

所有共享同一 AppContext 实例的工具都能立即看到彼此的写入,因为 lifespan 产生的是一个共享的单一对象。

注意:普通的 dictint 不是线程安全的。如果您的工具并发运行(例如,通过线程分发的同步工具),请使用 asyncio.Lock 保护共享的可变状态,或使用线程安全的数据结构。

请求访问

您可以从工具中访问传入的请求对象。请求对象会传递给每次工具调用,可用于访问请求头、Cookie 和其他请求数据(例如 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,),
)

提示词

您可以添加由用户选择调用的交互式模板。提示词现在支持访问 lifespan 状态,与工具类似。

基本提示词示例

  1. 定义提示词的参数:

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,
    ),
)
  1. 实例化服务器:

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,
)

无参数提示词

您可以定义不需要任何输入参数的提示词:

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
    ),
)

或者,您可以使用 NoArguments 类:

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,
    ),
)

带 Lifespan 状态的提示词

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,
    ),
)

带授权作用域的提示词

您可以根据认证作用域限制提示词的访问:

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
    ),
)

注意:您需要在 Starlette 应用中设置认证中间件,作用域才能正常工作。

STDIO 传输

除了 HTTP 传输之外,服务器还支持 STDIO 传输进行通信。这对于通过标准输入/输出进行通信的命令行应用程序和集成非常有用。

使用 STDIO 传输

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())

request_headers 参数允许您传递将包含在请求上下文中的请求头,即使在使用 STDIO 传输时也能启用认证和其他基于请求头的功能。

认证与授权

该库与 Starlette 的认证系统集成,为工具和提示词提供基于作用域的授权。

设置认证中间件

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)

作用域如何工作

  1. 认证中间件:中间件对每个请求进行认证,并通过 AuthCredentials 为用户分配作用域。

  2. 工具/提示词作用域:在定义工具或提示词时,您可以使用 scopes 参数指定所需的作用域。

  3. 访问控制:服务器根据用户被授予的作用域自动过滤工具和提示词。不具备所需作用域的工具和提示词在列表中不可见,也无法被调用。

  4. 多个作用域:如果您指定了多个作用域(例如 scopes=("admin", "superuser")),用户只需拥有其中一个作用域即可访问该工具或提示词。

API 参考

Tool 类

Tool 类用于定义可被客户端调用的工具。

参数:

  • func:要调用的函数。可以是同步或异步的。该函数可以:

    • 接受一个 Arguments[TInputs] 参数

    • 不接受任何参数

  • inputs:用于输入验证的 Pydantic 模型类。对于无输入的工具,使用 type(None)NoArguments

  • output:用于输出验证的 Pydantic 模型类

  • return_error_message(bool):如果为 True,工具错误返回 ErrorMessage 而不是抛出异常(默认值:False

  • scopes(tuple[str, ...]):访问此工具所需的认证作用域(默认值:空元组)

属性:

  • name:函数名称(从 func.__name__ 派生)

  • title:人类可读的标题(从函数名称派生)

  • description:函数的文档字符串

  • input_schema:输入参数的 JSON schema

  • output_schema:输出的 JSON schema

Prompt 类

Prompt 类用于定义可被客户端调用的提示词。

参数:

  • func:要调用的函数。可以是同步或异步的。该函数可以:

    • 接受一个 Arguments[TArguments] 参数

    • 不接受任何参数

    • 必须返回 tuple[PromptMessage, ...]

  • arguments_type:用于参数验证的 Pydantic 模型类。对于无参数的提示词,使用 type(None)NoArguments

  • scopes(tuple[str, ...]):访问此提示词所需的认证作用域(默认值:空元组)

属性:

  • name:函数名称(从 func.__name__ 派生)

  • title:人类可读的标题(从函数名称派生)

  • description:函数的文档字符串

  • arguments:定义提示词参数的 PromptArgument 对象元组

Arguments 类

Arguments 类被传递给工具和提示词函数,以提供对输入、请求和状态的访问。

参数:

  • request:Starlette Request 对象

  • inputs:经过验证的输入/参数数据(类型取决于 Tool/Prompt 的定义)

方法:

  • get_state_key(key: str, _object_type: type[TKey]) -> TKey:从 lifespan 状态中访问一个值。如果键不存在,则抛出 ServerError

NoArguments 类

一个空的 Pydantic 模型,在定义无参数的工具或提示词时,可作为 type(None) 的更清晰的替代方案。

from http_mcp.types import NoArguments

# Use this instead of type(None)
Tool(func=my_func, inputs=NoArguments, output=MyOutput)

OAuth 2.1 授权(auth_mcp)

auth_mcp 包为您的 MCP 服务器添加了符合标准的 OAuth 2.1 授权。使用 auth 附加项安装:

pip install http-mcp[auth]

快速开始

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)

这将为您提供:

  • 对所有 MCP 端点的 Bearer 令牌验证(默认安全)

  • /.well-known/oauth-protected-resource 发现端点(RFC 9728)

  • 401/403 响应上的 WWW-Authenticate 请求头,带有 resource_metadata 参数

  • 安全请求头(HSTS、nosniff、no-store)

  • 通过 middlewares 参数支持可选的自定义中间件

有关完整文档、最佳实践和安全面详情,请参阅 auth_mcp README

各端点的安全面

POST /mcp — MCP JSON-RPC 端点

  • 认证 — 使用 auth_mcp 时,Bearer 令牌从 Authorization 请求头中提取,并通过 TokenValidator 验证。超过 2048 个字符或包含 RFC 6750 b64token 模式之外字符的令牌,会在到达验证器之前被拒绝。不使用 auth_mcp 时,认证由 Starlette 的 AuthenticationMiddleware 处理。

  • 授权 — 通过 Starlette 的 has_required_scope() 进行基于作用域的过滤。没有匹配作用域的工具和提示词在列表中隐藏,并在调用时被阻止。请求头验证通过相同的作用域检查来解析工具 schema,因此即使从不匹配消息中,被隐藏工具的调用者也无法得知其 x-mcp-header 参数。

  • 输入验证 — JSON-RPC 消息由 Pydantic 验证。请求体上限为 4 MB,在读取时强制执行:过大的 Content-Length 在读取请求体之前就被拒绝,而在流式读取过程中超出上限的请求体会在该点停止缓冲。Content-Type 被严格检查(仅允许 application/json,忽略媒体类型参数)。

  • 错误处理 — 工具和提示词名称在错误消息中被截断为 100 个字符。Pydantic 验证错误在包含到响应中之前会被清理。

  • 响应请求头 — 所有响应上带有 X-Content-Type-Options: nosniffCache-Control: no-storeauth_mcp 额外添加 Strict-Transport-Security: max-age=31536000; includeSubDomains

GET /.well-known/oauth-protected-resource — 发现端点(auth_mcp)

  • 认证 — 与 /mcp 受相同的认证中间件约束。当 require_authentication=True(默认值)时,需要有效的令牌。如果客户端需要在认证之前发现授权服务器,则将其设置为 False

  • 输入验证 — 仅允许 GET;其他方法返回 405 Method Not Allowed

  • 输出 — 在启动时从冻结的 ProtectedResourceMetadata 模型序列化一次。URI 字段通过 Pydantic 的 AnyHttpUrl 验证为 HTTP/HTTPS URL。

WWW-Authenticate 响应请求头(auth_mcp)

  • 请求头注入 — 所有参数值(realmresource_metadatascopeerrorerror_description)都会被清理:去除 CR/LF 字符,反斜杠和双引号按照 RFC 7230 带引号字符串规则进行转义。

  • 信息泄露 — 错误响应使用通用消息("Authentication required")。原始的 AuthenticationError 详细信息会被丢弃。错误代码(401 上的 invalid_token)遵循 RFC 6750,不会泄露内部状态。

STDIO 传输

  • 消息大小 — 上限为 4 MB,与 HTTP 传输一致。

  • 日志记录 — 调试日志中的消息被截断为 500 个字符,以防止日志泛滥。令牌值永远不会被记录。

  • 请求头 — 请求头被转换为正确的 ASGI list[tuple[bytes, bytes]] 格式。

安装

需要 Python 3.12+(使用 PEP 695 类型参数语法)。

使用 pip 或 uv 安装该包:

pip install http-mcp

带 OAuth 2.1 授权支持:

pip install http-mcp[auth]

uv add http-mcp

许可证

本项目根据 MIT 许可证授权。详情请参阅 LICENSE 文件。

Available Tools

4 tools
get_called_toolsGet Called ToolsA
Idempotent

Get the list of called tools.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
called_toolsYesThe list of called tools

TDQS

A3.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description doesn't contradict annotations and adds some context by specifying 'list of called tools' (implying retrieval of historical tool usage data). However, annotations already provide rich behavioral information: readOnlyHint=false (potentially confusing for a 'get' operation), openWorldHint=true, idempotentHint=true, destructiveHint=false. The description doesn't add significant behavioral details beyond what annotations already cover, but it doesn't contradict them either.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, clear sentence with zero wasted words. It's front-loaded with the essential information and perfectly sized for a simple tool. Every word earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

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, rich annotations, output schema exists), the description is reasonably complete. The annotations cover safety and behavioral traits, and the output schema will document return values. The description could be more specific about what 'called tools' means in context, but for a simple retrieval tool, it's mostly adequate.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

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 already fully documents the lack of inputs. The description doesn't need to explain parameters, and it correctly doesn't mention any. The baseline for 0 parameters is 4, as the description appropriately focuses on the tool's purpose rather than nonexistent parameters.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose3/5

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 verb ('Get') and resource ('list of called tools'), making the purpose understandable. However, it doesn't distinguish this tool from its siblings (get_time, get_weather, tool_that_access_request) - all are 'get' operations but for different data. The description is adequate but lacks sibling differentiation.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

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 comparison with sibling tools. The agent must infer usage from the tool name alone, which offers minimal guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_timeGet TimeA
Idempotent

Get the current time.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
timeYesThe current time

TDQS

A3.5/5.0
Behavior3/5

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. The description adds minimal context about what 'current time' means, but doesn't elaborate on format, timezone, or other behavioral details beyond the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description 'Get the current time' is a single, efficient sentence that front-loads the core purpose with zero wasted words. It's appropriately sized for a simple tool with no parameters.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

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 that presumably handles return values), the description is reasonably complete. However, it could slightly improve by hinting at the output format (e.g., timestamp vs. string) since sibling tools suggest varied contexts.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

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 doesn't need to add parameter information, so it appropriately avoids redundancy. A baseline of 4 is justified since no parameters exist to explain.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description 'Get the current time' clearly states the verb ('Get') and resource ('current time'), making the purpose immediately understandable. However, it doesn't distinguish this tool from potential sibling tools like 'get_called_tools' or 'get_weather', 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.

Usage Guidelines2/5

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 are sibling tools like 'get_weather' that might serve related time/weather queries, but the description doesn't mention any context, prerequisites, or exclusions for usage.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_weatherGet WeatherB
Idempotent

Get the current weather in a given location.

ParametersJSON Schema
NameRequiredDescriptionDefault
locationYesThe location to get the weather for
unitNoThe unit of temperaturecelsius

Output Schema

ParametersJSON Schema
NameRequiredDescription
weatherYesThe weather in the given location

TDQS

B3.4/5.0
Behavior3/5

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. It adds minimal context by implying real-time data retrieval, but doesn't disclose additional traits like rate limits, error handling, or authentication needs, which would elevate the score.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that front-loads the core purpose without unnecessary details. Every word earns its place, making it highly concise and well-structured for quick understanding.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's low complexity (2 parameters, 100% schema coverage, annotations provided, and an output schema exists), the description is reasonably complete. It states what the tool does, though it could benefit from slight enhancements like mentioning the output includes current conditions, but the output schema likely covers return values, reducing the burden.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema fully documents the parameters (location and unit). The description mentions 'location' but adds no extra meaning beyond what the schema provides, such as format examples or usage nuances, meeting the baseline for high coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

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 it specifies the scope ('in a given location'). However, it doesn't distinguish this tool from potential siblings like 'get_forecast' or 'get_historical_weather', which would require explicit differentiation for a score of 5.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

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 lacks any mention of prerequisites, exclusions, or comparisons with sibling tools (e.g., 'get_time' or 'get_called_tools'), leaving the agent without context for tool selection.

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 RequestC
Idempotent

Access the request.

ParametersJSON Schema
NameRequiredDescriptionDefault
usernameYesThe username of the user

Output Schema

ParametersJSON Schema
NameRequiredDescription
messageYesThe message to the user

TDQS

C2.6/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations provide significant behavioral information: readOnlyHint=false (implies mutation), openWorldHint=true (handles unknown inputs), idempotentHint=true (safe to retry), and destructiveHint=false (non-destructive). The description adds no behavioral context beyond these annotations—it doesn't explain what 'access' entails operationally, potential side effects, or any constraints like rate limits. However, it doesn't contradict the annotations, so it meets the lower bar with annotations present.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely concise at just three words ('Access the request.'), with no wasted language or unnecessary elaboration. It is front-loaded and efficiently communicates the core idea, though this brevity contributes to its vagueness in other dimensions.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's moderate complexity (1 parameter, annotations provide behavioral hints, output schema exists), the description is minimally adequate but incomplete. It lacks context on what the tool actually does, usage scenarios, or output expectations. The presence of an output schema means return values needn't be explained, but the description should still clarify purpose and guidelines better to be fully helpful.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 100% description coverage, with the 'username' parameter fully documented in the schema. The description adds no parameter semantics beyond what the schema provides—it doesn't explain why the username is needed, how it relates to the request, or any contextual details about parameter usage. With high schema coverage, the baseline score of 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose2/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description 'Access the request' is a tautology that essentially restates the tool name 'tool_that_access_request' without adding meaningful specificity. It doesn't clarify what type of request is being accessed, what resource is involved, or what 'access' means in this context (read, modify, submit?). While it includes a verb ('access') and resource ('request'), it remains vague about the actual purpose.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines1/5

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 are sibling tools like 'get_called_tools', 'get_time', and 'get_weather', but the description doesn't explain how this tool differs from them or in what context it should be selected. No prerequisites, exclusions, or comparative context are mentioned.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 4 tool updatesv1.0.0
    • First observedget_called_tools
    • First observedget_time
    • First observedget_weather
    • First observedtool_that_access_request

TDQS

B3/5.0

Scored across 4 tools

Disambiguation5/5

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.

Naming Consistency3/5

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.

Tool Count4/5

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.

Completeness2/5

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

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    B
    quality
    D
    maintenance
    A Model Context Protocol (MCP) server based on OpenRPC, providing JSON-RPC function invocation and method discovery services.
    2
    1
    Apache 2.0
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables building and running MCP servers over streamable HTTP, exposing tools to AI assistants like Cursor, with examples of mounting multiple servers in FastAPI.
    -
  • F
    license
    Not graded
    quality
    D
    maintenance
    Framework for building and running MCP servers as HTTP services. Define tools as pure Python functions, wire up with two lines, run with one command.
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    A lightweight HTTP-based MCP server built with Bun, enabling tool discovery and execution via JSON-RPC 2.0 over HTTP.
    5 npm
    MIT