Skip to main content

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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mcp_utils_msgspec-3.0.1-py3-none-any.whl (16.6 kB view details)

Uploaded Python 3

File details

Details for the file mcp_utils_msgspec-3.0.1-py3-none-any.whl.

File metadata

File hashes

Hashes for mcp_utils_msgspec-3.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 2c114963e377021906e88c64913ee6fa54a8a09ae5476f31cd6b38447b549456
MD5 855dfe388c353c4d5e640c1d60fa0036
BLAKE2b-256 5777ec4c0127523fa1dcffa526e18fbf0e9c303e07f9fc8c5973ee2ca28cd064

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

3.0.1 This release

1 file

3.0.0

1 file

2.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page