Metadata-Version: 2.4
Name: mcp-utils-msgspec
Version: 3.0.0
Summary: Synchronous utilities for Model Context Protocol (MCP) integration
Author: Fulfil.IO Inc, Pioreactor Inc.
License-Expression: MIT
Project-URL: Homepage, https://github.com/pioreactor/mcp-utils
Project-URL: Repository, https://github.com/pioreactor/mcp-utils.git
Keywords: mcp,model-context-protocol,ai,llm
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: msgspec>=0.18.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0.0; extra == "dev"
Dynamic: license-file

# mcp-utils-msgspec

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

![Tests](https://github.com/pioreactor/mcp-utils/actions/workflows/test.yml/badge.svg) ![PyPI - Version](https://img.shields.io/pypi/v/mcp-utils-msgspec)

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

```bash
pip install mcp-utils-msgspec
```

For development:

```bash
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

```python
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:

```python
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`:

```python
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:

```python
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](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](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:

```python
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:

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

The CLI can list tools without opening the web interface:

```bash
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:

- [MCP `2026-07-28` key changes](https://modelcontextprotocol.io/specification/2026-07-28/changelog)
- [Versioning and compatibility](https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning)
- [Streamable HTTP](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http)
- [`server/discover`](https://modelcontextprotocol.io/specification/2026-07-28/server/discover)

## Related projects

- [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk)
- [Original Pydantic-based mcp-utils](https://github.com/fulfilio/mcp-utils)

## License

MIT
