Skip to content
 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

34 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mcp-utils-msgspec

A synchronous Python utility package for building Model Context Protocol (MCP) servers with msgspec.

Tests PyPI - Version

This package targets MCP protocol revision 2026-07-28. It implements the modern stateless protocol: there is no initialize handshake, implicit MCP session, or long-lived HTTP GET stream.

Features

  • Required server/discover support
  • Per-request protocol version and client capability metadata
  • Tools, prompts, resources, simple {name} resource templates, and completions
  • Multi-round-trip input requests and retry metadata
  • Required resultType, cache hints, and server identity metadata
  • Synchronous, framework-independent request handling
  • Optional validation of standard Streamable HTTP mirror headers
  • msgspec models for the supported server surface

The core returns either one JSON response or no response for a JSON-RPC notification. It does not implement optional request-scoped SSE streams or subscriptions/listen, the tasks extension, or optional x-mcp-header tool parameter annotations.

Installation

pip install mcp-utils-msgspec

For development:

pip install -e '.[dev]'

Python 3.10 or newer and msgspec 0.18 or newer are required. Flask and Gunicorn are optional and only needed for the HTTP example.

Define a server

from mcp_utils.core import MCPServer
from mcp_utils.schema import GetPromptResult, Message, Role, TextContent

mcp = MCPServer(
    name="weather",
    version="1.0.0",
    instructions="Use get_weather for current conditions.",
)


@mcp.tool()
def get_weather(city: str) -> dict[str, str]:
    """Return the current conditions for a city."""
    return {"city": city, "conditions": "sunny"}


@mcp.prompt()
def weather_report(city: str) -> GetPromptResult:
    """Create a prompt asking for a weather report."""
    return GetPromptResult(
        messages=[
            Message(
                role=Role.USER,
                content=TextContent(text=f"Report the weather in {city}."),
            )
        ]
    )

Dictionary and other JSON-compatible tool return values are emitted as both structuredContent and serialized text. A string return value is emitted as text. A tool may also return CallToolResult directly.

List and resource results default to ttlMs=0 and cacheScope="private". Servers with globally identical, safely shareable results can opt into caching:

mcp = MCPServer(
    name="weather",
    version="1.0.0",
    cache_ttl_ms=300_000,
    cache_scope="public",
)

Handle a request

Every modern request includes its protocol version and client capabilities in params._meta:

message = {
    "jsonrpc": "2.0",
    "id": "tools-1",
    "method": "tools/list",
    "params": {
        "_meta": {
            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
            "io.modelcontextprotocol/clientCapabilities": {},
            "io.modelcontextprotocol/clientInfo": {
                "name": "example-client",
                "version": "1.0.0",
            },
        }
    },
}

response = mcp.handle_message(message)

The server rejects an unsupported version with error -32022 and includes its supported versions. It uses -32021 when a multi-round-trip result requires a client capability that the request did not declare. initialize, ping, and notifications/initialized are not modern protocol methods.

Streamable HTTP with Flask

Revision 2026-07-28 uses one POST per JSON-RPC message. The GET stream and Mcp-Session-Id header were removed. Pass the HTTP headers to handle_message to validate the required body/header mirrors:

from flask import Flask, jsonify, request
import msgspec

from mcp_utils.core import MCPServer
from mcp_utils.schema import MCPErrorResponse

app = Flask(__name__)
mcp = MCPServer("example", "1.0.0")
allowed_origins = {
    "http://127.0.0.1:6274",
    "http://localhost:6274",
}


@app.post("/mcp")
def mcp_route():
    origin = request.headers.get("Origin")
    if origin is not None and origin not in allowed_origins:
        return "", 403

    response = mcp.handle_message(
        request.get_json(),
        http_headers=request.headers,
    )
    if response is None:
        return "", 202

    status = 200
    if isinstance(response, MCPErrorResponse):
        status = response.http_status_code
    return jsonify(msgspec.to_builtins(response)), status

For HTTP requests, clients must send:

  • MCP-Protocol-Version, matching the version in params._meta
  • Mcp-Method, matching the JSON-RPC method
  • Mcp-Name for tools/call, prompts/get, and resources/read
  • Accept: application/json, text/event-stream

The application remains responsible for authentication and its allowed-origin policy. Bind local development servers to 127.0.0.1, not 0.0.0.0.

See examples/flask_app.py for a complete local example.

Stateful tools

MCP no longer has protocol-level sessions. A stateful tool should return an opaque handle from a creation tool and require that handle as an ordinary argument on later calls. See examples/python_session_flask.py for this pattern.

Multi-round-trip input

A tool, prompt, or resource that needs elicitation, sampling, or roots can return InputRequiredResult. To inspect the client's inputResponses and the echoed requestState when it retries, declare the reserved keyword-only _mcp_request parameter. This parameter receives the decoded MCPRequest and is omitted from advertised argument schemas:

from mcp_utils.schema import InputRequiredResult, MCPRequest


@mcp.tool()
def confirm_action(
    action: str,
    *,
    _mcp_request: MCPRequest,
) -> dict[str, object] | InputRequiredResult:
    responses = _mcp_request.params.get("inputResponses")
    if not isinstance(responses, dict):
        return InputRequiredResult(
            inputRequests={
                "confirmation": {
                    "method": "elicitation/create",
                    "params": {
                        "mode": "form",
                        "message": f"Confirm {action}?",
                        "requestedSchema": {"type": "object"},
                    },
                }
            },
            requestState="opaque-application-state",
        )
    return {"confirmed": responses["confirmation"]}

The server returns -32021 if an input request needs a capability missing from that request's io.modelcontextprotocol/clientCapabilities metadata.

Testing with MCP Inspector

The current Inspector understands modern 2026-07-28 servers and requires Node 22.19 or newer:

npx @modelcontextprotocol/inspector \
  --server-url http://127.0.0.1:9000/mcp \
  --transport http

The CLI can list tools without opening the web interface:

npx @modelcontextprotocol/inspector --cli \
  http://127.0.0.1:9000/mcp \
  --transport http \
  --method tools/list

Protocol scope

The package advertises only capabilities backed by current registrations. It does not advertise optional subscriptions, roots, sampling, or logging. Roots, sampling, and logging are deprecated in protocol revision 2026-07-28.

Protocol references:

Related projects

License

MIT

About

Python utilities to add an MCP server to Flask

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages