Skip to main content
Glama
yeison-liscano

Simple HTTP MCP Server

간단한 HTTP MCP 서버 구현

이 프로젝트는 HTTP를 통한 Model Context Protocol (MCP)용 경량 서버 구현을 제공합니다. Python 함수를 도구와 프롬프트로 노출하여 JSON-RPC 인터페이스를 통해 원격으로 검색하고 실행할 수 있게 합니다. Starlette 또는 FastAPI 애플리케이션과 함께 사용하도록 설계되었습니다 (데모 참조).

목차

Related MCP server: wazza-mcp-test-server

기능

  • MCP 프로토콜 준수: 도구 및 프롬프트 검색과 실행을 위한 MCP 사양을 구현합니다. 알림은 지원하지 않습니다.

  • 단일 프로토콜 리비전: 무상태(stateless) 2026-07-28 리비전만 사용합니다 — server/discover, 요청별 _meta, 핸드셰이크 없음, 세션 없음. 단일 디스패치 경로는 요청이 이전 리비전을 선언하여 더 약한 처리를 선택할 수 없음을 의미합니다.

  • HTTP 및 STDIO 전송: 통신에 HTTP(POST 요청) 또는 STDIO를 사용합니다.

  • 비동기 지원: 비동기 요청 처리를 위해 Starlette 또는 FastAPI 기반으로 구축되었습니다.

  • 타입 안전: 강력한 데이터 검증 및 직렬화를 위해 Pydantic을 활용합니다.

  • 서버 상태 관리: get_state_key 메서드를 사용하여 lifespan 컨텍스트를 통해 공유 상태에 접근합니다.

  • 요청 접근: 도구와 프롬프트에서 들어오는 요청 객체에 접근합니다.

  • 인증 범위: Starlette의 인증 시스템을 사용한 범위 기반 인증을 지원합니다.

  • 오류 처리: 도구는 예외를 발생시키는 대신 선택적으로 오류 메시지를 반환할 수 있습니다.

  • OAuth 2.1 인증: Bearer 토큰 검증, 보호 리소스 메타데이터(RFC 9728), WWW-Authenticate 오류 응답을 제공하는 선택적 auth_mcp 패키지. pip install http-mcp[auth]로 설치합니다.

서버 아키텍처

이 라이브러리는 전체 애플리케이션 수명주기에 걸쳐 공유 상태를 관리하기 위해 lifespan을 사용하는 단일 MCPServer 클래스를 제공합니다.

MCPServer

MCPServer는 공유 서버 상태를 관리하기 위해 Starlette의 lifespan 시스템과 함께 작동하도록 설계되었습니다.

주요 특징:

  • lifespan 기반: 공유 서버 상태를 초기화하고 관리하기 위해 Starlette의 lifespan 이벤트를 사용합니다.

  • 애플리케이션 수준 상태: 상태는 요청별이 아니라 전체 애플리케이션 수명주기 동안 유지됩니다.

  • 유연성: lifespan 상태에 저장된 모든 사용자 정의 컨텍스트 클래스와 함께 사용할 수 있습니다.

생성자 매개변수:

  • name (str): MCP 서버의 이름

  • version (str): MCP 서버의 버전

  • tools (tuple[Tool, ...]): 노출할 도구의 튜플 (기본값: 빈 튜플)

  • prompts (tuple[Prompt, ...]): 노출할 프롬프트의 튜플 (기본값: 빈 튜플)

  • instructions (str | None): AI 어시스턴트에게 이 서버 사용 방법에 대한 선택적 지침

  • cache_ttl_ms (int): tools/list, prompts/list, server/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-25, 2025-06-18, 2025-03-26에 대한 지원이 initialize, notifications/initialized, ping과 함께 제거되었습니다. 해당 리비전만 사용하는 클라이언트는 더 이상 이 서버와 통신할 수 없습니다. 하나의 리비전만 제공하는 것은 아래 요청 메타데이터 헤더를 신뢰할 수 있게 만드는 이유이기도 합니다. 두 시대가 공존하는 동안 요청은 이전 리비전을 선언하여 헤더 검사를 건너뛸 수 있었으므로, 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를 대체합니다. 지원되는 버전, 기능, 지침, 서버 ID를 한 번의 호출로 보고하며, 사전 요청 없이 응답됩니다:

{
  "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 블록을 포함합니다.

  • initialize, notifications/initialized, ping, logging/setLevel은 존재하지 않으며, 세션 및 SSE 재개 메커니즘도 존재하지 않습니다. 이들은 HTTP 404와 함께 -32601을 반환합니다. JSON-RPC 알림 — id가 없는 notifications/* 메시지 — 은 여전히 202 Accepted와 본문 없음으로 응답됩니다. JSON-RPC는 이에 대한 응답을 금지하기 때문입니다.

  • 필수 요청 헤더. 모든 POST는 MCP-Protocol-VersionMcp-Method를 전송해야 하며, tools/callprompts/get에는 Mcp-Name도 전송해야 합니다. 각각은 해당 본문 값과 일치해야 하며, 그렇지 않으면 요청은 -32020(HeaderMismatch) 및 HTTP 400으로 거부됩니다. 이는 프록시가 한 값으로 라우팅하는 동안 서버가 다른 값으로 처리하는 것을 방지합니다. 일반 ASCII로 표현할 수 없는 값은 =?base64?...?= 봉투를 사용하며, 서버는 비교 전에 이를 디코딩합니다.

  • 알 수 없는 도구와 프롬프트는 -32602를 보고합니다. 이는 도구 및 프롬프트 사양이 규정하는 방식입니다. -32002는 이 리비전에서 폐지되었습니다.

  • Mcp-Session-IdLast-Event-ID는 무시되며, MCP 엔드포인트에 대한 GET/DELETE405 Method Not Allowed를 반환합니다.

다중 왕복 요청(elicitation, sampling, roots) 및 subscriptions/listen은 구현되지 않았습니다. 이 서버는 클라이언트 입력에 의존하는 기능을 노출하지 않으며 listChanged: false를 선언하므로 둘 다 적용되지 않습니다.

캐싱 힌트

tools/list, prompts/list, server/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는 캐싱만 관장하며 도구별 범위 검사를 대체하지 않습니다.

오리진 검증

브라우저는 Origin 헤더를 첨부하며, 이를 통해 서버는 DNS 리바인딩으로 유입된 요청을 거부할 수 있습니다. 이 검사는 기본적으로 꺼져 있어 기존 배포가 계속 작동합니다. 엔드포인트가 브라우저에서 접근 가능한 곳에서는 이 검사를 켜세요:

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은 단독으로는 아무것도 하지 않습니다. 이미 구성된 허용 목록을 강화할 뿐입니다. 로컬에서 실행할 때는 0.0.0.0 대신 127.0.0.1에 바인딩하세요.

도구 매개변수를 헤더로 미러링

도구는 클라이언트가 특정 인자 값을 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, +3, 또는 3이 아니어야 합니다. 숫자 강제 변환은 이들을 동일하다고 간주하겠지만, 원시 헤더 문자열을 기준으로 라우팅하는 중개자는 다른 값을 보게 되며, 이는 미러링이 방지하려는 불일치입니다.

객체 속성의 단순한 체인을 통해 도달할 수 있는 string, integer, boolean 필드만 주석을 달 수 있으며, 두 필드가 동일한 헤더 이름을 주장할 수 없습니다. 충돌은 서버가 생성될 때 거부됩니다. 두 주석 중 하나를 유지하면 다른 하나는 조용히 적용되지 않은 채 남게 되기 때문입니다. 민감한 값에는 주석을 달지 마세요. 헤더 내용은 경로상의 모든 중개자에게 보이기 때문입니다.

도구

도구는 클라이언트가 호출할 수 있는 함수입니다.

기본 도구 예시

  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인 경우 도구는 ToolInvocationError를 발생시키는 대신 오류 세부 정보가 포함된 ErrorMessage 모델을 반환합니다.

인증 범위가 있는 도구

인증 범위를 기반으로 도구 접근을 제한할 수 있습니다:

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 앱에 인증 미들웨어를 설정해야 합니다. Toolscopes 필드는 기본 권한 부여 게이트로, 프레임워크는 호출 전에 권한 범위를 기준으로 도구를 필터링합니다. 위 도구 함수 내부의 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으로 공유하는 변경 가능한 상태를 보호하거나 스레드 안전한 데이터 구조를 사용하세요.

요청 접근

도구에서 들어오는 요청 객체에 접근할 수 있습니다. 요청 객체는 각 도구 호출에 전달되며 헤더, 쿠키 및 기타 요청 데이터(예: 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, ...]): 이 도구에 접근하는 데 필요한 인증 권한 범위입니다 (기본값: 빈 튜플)

Properties:

  • name: 함수 이름 (func.__name__에서 파생)

  • title: 사람이 읽을 수 있는 제목 (함수 이름에서 파생)

  • description: 함수의 docstring

  • input_schema: 입력 매개변수의 JSON 스키마

  • output_schema: 출력의 JSON 스키마

Prompt 클래스

Prompt 클래스는 클라이언트가 호출할 수 있는 프롬프트를 정의하는 데 사용됩니다.

Parameters:

  • func: 호출할 함수입니다. 동기 또는 비동기일 수 있습니다. 함수는 다음 중 하나를 받을 수 있습니다:

    • Arguments[TArguments] 매개변수를 받기

    • 매개변수를 받지 않기

    • 반드시 tuple[PromptMessage, ...]을 반환해야 합니다

  • arguments_type: 인자 유효성 검사를 위한 Pydantic 모델 클래스입니다. 인자가 없는 프롬프트에는 type(None) 또는 NoArguments를 사용하세요.

  • scopes (tuple[str, ...]): 이 프롬프트에 접근하는 데 필요한 인증 권한 범위입니다 (기본값: 빈 튜플)

속성:

  • name: 함수 이름 (func.__name__에서 파생)

  • title: 사람이 읽을 수 있는 제목 (함수 이름에서 파생)

  • description: 함수의 docstring

  • arguments: 프롬프트의 인자를 정의하는 PromptArgument 객체의 튜플

Arguments 클래스

Arguments 클래스는 도구 및 프롬프트 함수에 전달되어 입력, 요청 및 상태에 대한 접근을 제공합니다.

Parameters

  • request: Starlette의 Request 객체

  • inputs: 유효성 검사를 마친 입력/인자 데이터 (유형은 Tool/Prompt 정의에 따라 다릅니다)

Methods

  • get_state_key(key: str, _object_type: type[TKey]) -> TKey: lifespan 상태에서 값을 접근합니다. 키가 존재하지 않으면 ServerError를 발생시킵니다.

NoArguments 클래스

인자가 없는 도구나 프롬프트를 정의할 때 type(None)보다 더 명확한 대안으로 사용할 수 있는 빈 Pydantic 모델입니다.

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)

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)

  • resource_metadata 매개변수가 있는 401/403 응답의 VVWWW-Authenticate 헤더

  • 보안 헤더 (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()를 통한 범위 기반 필터링입니다. 일치하지 않는 범위를 가진 도구와 프롬프트는 목록에서 숨겨지고 호출이 차단됩니다. 요청-헤더 검증은 동일한 범위 검사를 통해 도구 스키마를 해석하므로, 도구에서 숨겨진 호출자는 불일치 메시지를 통해서도 해당 도구의 x-mcp-header 인자를 알 수 없습니다.

  • 입력 검증 — JSON-RPC 메시지는 Pydantic으로 검증됩니다. 요청 본문은 4MB로 제한되며, 읽는 동안에도 강제됩니다. 크기가 큰 Content-Length는 본문을 읽기도 전에 거부되고, 스트림 중간에서 본문이 제한을 초과하면 해당 시점에서 버퍼링을 중단합니다. Content-Type은 엄격히 확인됩니다 (application/json만 허용하며, 미디어 유형 매개변수는 무시).

  • 오류 처리 — 오류 메시지에 표시되는 도구와 프롬프트 이름은 100자로 잘립니다. Pydantic 검증 오류는 응답에 포함되기 전에 삭제됩니다.

  • 응답 헤더 — 모든 응답에 X-Content-Type-Options: nosniff, Cache-Control: no-store가 포함됩니다. auth_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)

  • 헤더 주입 — 모든 매개변수 값(realm, resource_metadata, scope, error, error_description)은 정리됩니다: CR/LF 문자가 제거되고 백슬래시와 큰따옴표는 RFC 7230 quoted-string 규칙에 따라 이스케이프됩니다.

  • 정보 유출 — 오류 응답은 일반 메시지("Authentication required")를 사용합니다. 원래의 AuthenticationError 세부 정보는 폐기됩니다. 오류 코드(401의 invalid_token)는 내부 상태를 유출하지 않고 RFC 6750을 따릅니다.

STDIO 전송

  • 메시지 크기 — HTTP 전송과 동일하게 4MB로 제한됩니다.

  • 로깅 — 로그 폭주를 방지하기 위해 디버그 로그의 메시지는 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

License

이 프로젝트는 MIT 라이선스에 따라 라이선스가 부여됩니다. 자세한 내용은 LICENSE 파일을 참조하세요.

Install Server
A
license - permissive license
B
quality
A
maintenance

Maintenance

Maintainers
Response time
4wRelease cycle
13Releases (12mo)
Commit activity
Issues opened vs closed

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • MCP server for secureFlows: token-free URL builders and integration-linting tools for AI agents.

  • MCP server exposing the Backtest360 engine API as tools for AI agents.

  • MCP server for Clipkit — gives AI agents a video toolbox via the Clipkit schema.

View all MCP Connectors

Latest Blog Posts

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