Skip to content

MCP (AI Tools)

The Model Context Protocol server, registries, and transports. Most applications drive MCP through app.mount_mcp(...), @app.mcp_tool, and @app.mcp_prompt; these names are for callers that assemble or serve the registry themselves.

MCPContext

Per-invocation context for an MCP tool call.

Usage::

@app.mcp_tool(description="Look up a user by id")
async def get_user(user_id: int, ctx: MCPContext) -> dict:
    await ctx.log("info", f"looking up {user_id}")
    await ctx.report_progress(1, 2)
    return {"id": user_id}

cancelled property

cancelled: bool

Whether the client has sent notifications/cancelled for this call.

state property

state: Any

Scratch space shared by everything resolving this one call.

The same object a handler declaring request: Request reaches through request.state, so a dependency and the handler holding this context read and write one store rather than two that could disagree. Scoped to the call: a later tools/call starts clean.

Usage::

@app.mcp_tool(description="Look something up")
async def lookup(ctx: MCPContext) -> dict:
    ctx.state.started = time.monotonic()
    return {"ok": True}

session_id property

session_id: str | None

The dispatching connection's id, or None on the stateless path.

Unique across processes, so it stays a safe key for per-client state under a multi-worker server. It identifies a connection, not a client: a reconnecting client gets a new one, and under HTTP without a shared session_backend a client that lands on another worker does too.

client_info property

client_info: dict[str, Any]

The client's implementation block from initialize, or empty.

client_capabilities property

client_capabilities: dict[str, Any]

The capabilities the client advertised, or empty off a stateful transport.

is_background_task property

is_background_task: bool

Whether this call is running as a task rather than inline.

request_meta property

request_meta: dict[str, Any]

The _meta the client sent with this request, or an empty mapping.

The protocol reserves _meta for metadata it does not define - a progress token, a trace id, an extension's own block - so a handler that needs to read or relay what the client attached finds it here.

client_id property

client_id: str | None

The authenticated caller's id, or None when the call is unauthenticated.

The subject of the principal the transport established - for a client-credentials token that is the registered MCP client. client_info is what the client said it was at initialize; this is what it proved.

request_id property

request_id: Any

The JSON-RPC id of the call being served, or None for a notification.

origin_request_id property

origin_request_id: Any

The id of the tools/call that created this task, or None inline.

A background task outlives the request that started it, so its own request_id is not the one the client is correlating against.

task_id property

task_id: str | None

The id of the task this call is running as, or None when inline.

The same handle the client polls with tasks/get, so a handler can record it against whatever it writes.

transport property

transport: str | None

The transport serving this call - "stdio", "http" or "sse".

None off a transport. A handler that needs to know whether a server-initiated request can reach the client should ask client_supports(...) instead; this is for logging and diagnostics.

lifespan_context property

lifespan_context: Any

The application state established at startup.

The same app.state an HTTP handler reaches, so a connection pool or a client opened in a lifespan hook is reached the same way through either door. None off a server.

result_meta property

result_meta: dict[str, Any]

Scratch _meta sent back on this call's result.

The protocol reserves _meta for metadata it does not define, so this is where a handler puts what its client agreed to read - a cost, a trace id, an extension's own block. Mutated in place::

ctx.result_meta["io.example/trace"] = trace_id

It belongs to one call: the next one starts empty. A handler that never touches it sends no _meta at all.

client_supports

client_supports(capability: str) -> bool

Return whether the client advertised capability (dotted for nested).

ctx.client_supports("sampling") and ctx.client_supports("sampling.tools") both work; the same lookup the server-initiated requests gate on.

debug async

debug(message: Any, logger: str | None = None) -> None

Send a debug-level log message to the client.

info async

info(message: Any, logger: str | None = None) -> None

Send an info-level log message to the client.

warning async

warning(message: Any, logger: str | None = None) -> None

Send a warning-level log message to the client.

error async

error(message: Any, logger: str | None = None) -> None

Send an error-level log message to the client.

log async

log(level: str, message: Any, logger: str | None = None) -> None

Send a log message to the MCP client (notifications/message).

Dropped when no notification channel is wired, or when level is below the client's logging/setLevel minimum.

report_progress async

report_progress(progress: float, total: float | None = None, message: str | None = None) -> None

Report progress to the MCP client (notifications/progress).

Dropped when no notification channel is wired, or when the client did not send a progressToken with the call (progress is only reported on request).

read_resource async

read_resource(uri: str) -> dict[str, Any]

Read one of this server's registered resources by URI.

Goes through the same handler resources/read serves, so the resource's declared scopes are enforced against the calling principal exactly as they would be for a direct client read - a tool cannot reach a resource its caller could not have read itself.

get_prompt async

get_prompt(name: str, arguments: dict[str, Any] | None = None) -> dict[str, Any]

Render one of this server's registered prompts by name.

Goes through the same handler prompts/get serves, including its scope check, for the same reason read_resource does.

list_resources

list_resources() -> list[dict[str, Any]]

List this server's registered resources, as resources/list reports them.

list_prompts

list_prompts() -> list[dict[str, Any]]

List this server's registered prompts, as prompts/list reports them.

send_notification async

send_notification(method: str, params: dict[str, Any] | None = None) -> None

Send a JSON-RPC notification to the client.

Inert when no notification channel is wired, matching log and report_progress.

sample async

sample(messages: list[dict[str, Any]], *, max_tokens: int, model_preferences: dict[str, Any] | None = None, system_prompt: str | None = None, temperature: float | None = None, stop_sequences: list[str] | None = None, tools: list[dict[str, Any]] | None = None, tool_choice: dict[str, Any] | None = None, metadata: dict[str, Any] | None = None, include_context: str | None = None) -> dict[str, Any]

Ask the client's LLM to sample a completion (sampling/createMessage).

Returns the client's result (its chosen model, role, and content). Requires a bidirectional transport and a client that advertised the sampling capability; tools / tool_choice additionally require sampling.tools.

include_context asks the client to attach context from MCP servers to the prompt - "none", "thisServer", or "allServers". The client MAY ignore the request, so it is a hint rather than a guarantee.

sample_with_tools async

sample_with_tools(messages: list[dict[str, Any]], *, tools: list[str], max_tokens: int, max_tool_rounds: int = 5, model_preferences: dict[str, Any] | None = None, system_prompt: str | None = None, temperature: float | None = None, stop_sequences: list[str] | None = None, tool_choice: dict[str, Any] | None = None, metadata: dict[str, Any] | None = None) -> SamplingRun

Sample with tools, executing the ones the model asks for, until it answers.

tools names tools of this server the model may drive. Each request the model makes runs through the same path tools/call serves - declared scopes, call hooks, timeout and error shaping included - and its result is fed back as the next message. Returns a SamplingRun carrying the answer, the transcript, and every tool call made.

max_tool_rounds caps how many times tools are executed. On the round after the cap the model is asked to answer without tools, so a run ends with an answer rather than an unanswered request; a client that ignores that instruction ends the run where it stands.

Requires a client that advertised sampling.tools.

elicit async

elicit(message: str, *, requested_schema: dict[str, Any] | None = None, url: str | None = None, elicitation_id: str | None = None) -> dict[str, Any]

Ask the client to gather input from its user (elicitation/create).

Form mode passes a requested_schema (the JSON Schema of the fields to collect); URL mode passes a url the client opens instead. Returns the client's response (its action and any collected content). Requires a bidirectional transport and a client that advertised elicitation.

URL mode also carries the elicitationId the spec requires, which names the interaction in a later notifications/elicitation/complete. One is minted per call; pass elicitation_id to use an identifier the URL flow already knows, so the completion can be correlated with whatever happens out of band.

roots async

roots() -> list[dict[str, Any]]

List the client's exposed filesystem roots (roots/list).

Returns the client's roots array. Requires a bidirectional transport and a client that advertised the roots capability.

hide async

hide(*names: str) -> None

Hide tools, prompts or resources from this connection's listings.

Names a tool or prompt by name and a resource by URI. The change belongs to the connection that made the call - another client's listings are untouched - and this connection is told its lists changed so it fetches them again.

Hiding is not enforcement. A hidden primitive is still callable, exactly as with a mount_mcp(tool_filter=...) policy: what a caller may invoke is decided by its declared scopes, so a hidden name cannot be mistaken for a permission boundary.

unhide async

unhide(*names: str) -> None

Show primitives hidden earlier on this connection.

reset_visibility async

reset_visibility() -> None

Show everything this connection had hidden.

MCPSession

Lifecycle state for one stateful MCP connection.

Holds whether the connection has initialized and the client's advertised capabilities / implementation info, recorded from the initialize request.

public_id property

public_id: str

A globally unique identity for this connection, safe to key state on.

connection_id alone restarts at 1 in every worker process, so it is an ownership key for this process's registries and nothing more. This is what application code is handed. Composed on access rather than stored: a stateless HTTP POST builds a session per request, and a string it never reads would be pure per-request cost.

record_initialize

record_initialize(params: dict[str, Any]) -> None

Record the client's advertised capabilities and info from initialize.

record_request_meta

record_request_meta(meta: dict[str, Any] | None) -> None

Record the client identity a modern request carries in its _meta.

The modern revision has no initialize: a client states who it is and what it supports on every request. Recording it here keeps one place - client_info / client_capabilities - answering for both eras, so nothing downstream has to know which handshake produced them. Absent keys leave the previous values alone rather than clearing them, since a session may be persistent across requests.

supports

supports(capability: str) -> bool

Return whether the client advertised the named top-level capability.

MCPServer

Bases: TasksMixin, InvocationMixin

Serve a Veloce app's MCP tools over JSON-RPC 2.0.

Build once with the app; the registry is assembled eagerly so a registration-time safety violation (missing description, duplicate name) surfaces before any client connects.

set_notifier staticmethod

set_notifier(notifier: Callable[[dict[str, Any]], Awaitable[None]]) -> None

Wire the current context's outbound one-way notification sink.

Sets the per-request _notifier_var; the stdio transport calls this once in its serve task, while the Streamable HTTP transport sets the var per request so concurrent calls never cross notifications.

set_requester staticmethod

set_requester(requester: Callable[[str, dict[str, Any]], Awaitable[dict[str, Any]]]) -> None

Wire the current context's server->client request issuer.

Sets the per-context _requester_var; a bidirectional transport (the stdio loop) calls this once in its serve task so a tool's MCPContext.sample / elicit / roots reaches the client. A one-way transport never calls it, leaving those methods to raise.

current_request_id staticmethod

current_request_id() -> Any

The JSON-RPC id of the request being dispatched, or None for a notification.

send_to_current_connection async staticmethod

send_to_current_connection(message: dict[str, Any]) -> None

Send one server-initiated message down the dispatching connection.

current_session staticmethod

current_session() -> MCPSession | None

Return the session of the connection currently dispatching, or None.

Set per dispatch by handle_message when a stateful transport supplies a session; None on the stateless HTTP path or off-dispatch.

register_connection

register_connection(session: MCPSession, sink: Callable[[dict[str, Any]], Awaitable[None]]) -> object | None

Record an open stateful connection so it can receive resource updates.

Returns an opaque token a transport passes back to unregister_connection to drop exactly this stream, so concurrent streams on one session are tracked independently. A no-op returning None when subscriptions are disabled, so a transport may call this unconditionally.

unregister_connection

unregister_connection(token: object | None) -> None

Drop the connection named by its token (a no-op when token is None).

Any subscriptions/listen streams the connection held go with it: the transport is gone, so there is nowhere to send a graceful close, and a stream left registered would keep a dead session reachable by fan-out.

evict_session

evict_session(session: MCPSession) -> None

Reclaim everything an evicted session owns: its connection and tasks.

Called when a session's transport drops it (idle TTL on HTTP). Beyond unregistering the subscription connection, this cancels and drops the session's tasks - including a never-settling one TTL eviction would leave in place - so an abandoned session cannot pin a task for the process lifetime.

notify_resource_updated async

notify_resource_updated(uri: str) -> None

Tell subscribed clients a resource changed (notifications/resources/updated).

Call this from the app when a resource's data changes; the server fans the notification out to every connection subscribed to uri. A no-op when subscriptions are disabled or no connection subscribed to uri.

notify_resources_list_changed async

notify_resources_list_changed() -> None

Tell clients the resource list changed (notifications/resources/list_changed).

Call this from the app when the set of available resources changes; the server fans the notification out to every open connection. A no-op when subscriptions are disabled.

notify_tools_list_changed async

notify_tools_list_changed() -> None

Tell listening clients the tool list changed.

Reaches only the subscriptions/listen streams that asked for toolsListChanged; the spec forbids sending a type a client did not request. A no-op when nothing is listening.

notify_prompts_list_changed async

notify_prompts_list_changed() -> None

Tell listening clients the prompt list changed.

Reaches only the streams that asked for promptsListChanged.

handle_message async

handle_message(message: dict[str, Any], session: MCPSession | None = None) -> dict[str, Any] | None

Dispatch one decoded JSON-RPC request; return the response object.

Returns None for a notification (a request with no id), which carries no response per JSON-RPC 2.0 Sec. 4.1.

A stateful transport (the serial stdio loop) passes its session so the server records the client's advertised capabilities from initialize and enforces the lifecycle ordering: before initialize completes the only requests answered are initialize and ping. The stateless HTTP transport passes none, leaving its fast path unaffected.

close_listen_stream async

close_listen_stream(session: MCPSession, subscription_id: Any) -> None

End one open stream, answering its long-lived request as it closes.

The response is what tells the client the subscription ended cleanly, as opposed to a transport that simply dropped.

connection_is_stateful

connection_is_stateful() -> bool

Whether the connection being answered persists beyond this request.

A stateful connection has an outbound channel and can carry per-connection state; a stateless request has neither, and nothing it is told survives the response.

MCPTool dataclass

Bases: MCPDescriptor

One registered MCP tool.

MCPResource dataclass

Bases: MCPDescriptor

One registered MCP resource (a read-only route addressed by URI).

MCPPrompt dataclass

Bases: MCPDescriptor

One registered MCP prompt template.

MCPTask dataclass

Bases: MCPDescriptor

One in-flight or settled task created from a task-augmented tool call.

name (inherited) is the task id the client polls by. The task records its lifecycle status, the tool it runs, its created / last-updated timestamps and time-to-live, and - once it settles - the tools/call result the client retrieves with tasks/result.

describe

describe(*, modern: bool = False) -> dict[str, Any]

Shape this task into the MCP Task object the task methods return.

The extension renamed the two duration fields, so a modern client is sent ttlMs / pollIntervalMs and a handshake client the names its revision defined. Everything else is common to both.

touch

touch() -> None

Record that the task changed without altering its status.

is_terminal

is_terminal() -> bool

Return whether the task has settled (no further transition happens).

settle

settle(status: str, result: dict[str, Any], message: str | None = None) -> None

Move the task to a terminal status carrying its final result.

A no-op once the task is already terminal: this guards against a racing tasks/cancel and the natural completion of _run_task both settling the same task, so the first terminal status (e.g. cancelled) is never overwritten by the second.

ToolRegistry dataclass

Bases: Registry[MCPTool]

Name -> MCPTool, plus the shared JSON Schema component registry.

schemas holds Pydantic-model components shared across tool input schemas (mirroring OpenAPI components.schemas); a tool input schema references them by $ref.

register

register(item: T) -> None

Add item, rejecting a key already present with a primitive-specific error.

get

get(name: str) -> T | None

Return the primitive registered under name, or None.

add

add(tool: MCPTool) -> None

Register tool, rejecting a name already taken by the same version.

Two registrations sharing a name and declaring different versions are both kept: the higher one is listed and answers a call naming no version, and either answers a call naming its own.

resolve

resolve(name: str, version: str | None) -> MCPTool | None

Return the tool name registered under version, or the listed one.

ResourceRegistry dataclass

Bases: Registry[MCPResource]

URI -> MCPResource, plus the shared JSON Schema component registry.

register

register(item: T) -> None

Add item, rejecting a key already present with a primitive-specific error.

get

get(name: str) -> T | None

Return the primitive registered under name, or None.

add

add(resource: MCPResource) -> None

Register resource, rejecting a URI already taken.

statics

statics() -> list[MCPResource]

Return the concrete-URI resources (for resources/list).

templates

templates() -> list[MCPResource]

Return the URI-template resources (for resources/templates/list).

match

match(uri: str) -> tuple[MCPResource, dict[str, str]] | None

Resolve a concrete URI to its resource and extracted path parameters.

A static resource matches by exact URI (no parameters); a template resource matches by its compiled pattern, yielding the path-parameter values to invoke the route with. Static resources are tried first so a concrete URI never falls through to a template that would also match it.

PromptRegistry dataclass

Bases: Registry[MCPPrompt]

Name -> MCPPrompt, plus the shared JSON Schema component registry.

register

register(item: T) -> None

Add item, rejecting a key already present with a primitive-specific error.

get

get(name: str) -> T | None

Return the primitive registered under name, or None.

add

add(prompt: MCPPrompt) -> None

Register prompt, rejecting a name already taken.

TaskRegistry dataclass

Bases: Registry[MCPTask]

Task id -> MCPTask, the in-memory store of created tasks.

register

register(item: T) -> None

Add item, rejecting a key already present with a primitive-specific error.

get

get(name: str) -> T | None

Return the primitive registered under name, or None.

evict_expired

evict_expired() -> None

Drop tasks whose time-to-live has elapsed so a stale task does not leak.

Only a settled task is evicted on expiry; a still-working task is left in place even past its ttl so its eventual result is never discarded out from under a client that is still polling.

owned_by

owned_by(owner_key: int | None) -> list[MCPTask]

Return the tasks created by the given connection.

Used to reclaim a session's tasks when it is evicted so a never-settling task does not pin memory for the process lifetime after its owner is gone.

drop

drop(task: MCPTask) -> None

Remove a task from the store, settled or not (used on owner eviction).

TasksCapability

Bases: _ServerCapability

The tasks/get|result|list|cancel methods and the tasks advertisement.

Advertised only when at least one tool opts into task support, so a server whose tools all run synchronously stays inert and a client never probes an empty capability.

extensions

extensions() -> dict[str, Any] | None

Advertise the tasks extension when any tool opts into task execution.

SubscriptionsCapability

Bases: _ServerCapability

The resources/subscribe / resources/unsubscribe methods, opt-in.

Folded into the resource area but kept a separate capability so the base ResourcesCapability stays unchanged when subscriptions are off. It contributes no initialize entry of its own — the resource advertisement (the subscribe/listChanged sub-capability flags) lives on ResourcesCapability, which reads the same opt-in flag — so advertise returns None.

CompletionsCapability

Bases: _ServerCapability

The completion/complete method, advertised when a completer exists.

Completion is opt-in: the capability is advertised only when at least one prompt or resource argument carries a registered completer, so a server with none stays inert and a client never probes an empty capability.

CompletionResult dataclass

An explicit completion response: candidate values plus optional totals.

Return this from a completer to declare the full match total and whether more values exist beyond those returned; return a bare sequence of strings instead to let the capability derive both from the values it received.

Usage::

@app.mcp_completer(prompt="greet", argument="name")
async def complete_name(value: str) -> CompletionResult:
    matches = await lookup_names(prefix=value)
    return CompletionResult(matches[:100], total=len(matches))

ToolFilter module-attribute

ToolFilter = Callable[['MCPTool', Any], 'bool | Awaitable[bool]']

build_registry

build_registry(app: Any) -> ToolRegistry

Assemble the tool registry from explicit tools plus exposed routes.

build_resource_registry

build_resource_registry(app: Any) -> ResourceRegistry

Assemble the resource registry from routes flagged expose_as_mcp_resource.

Mirrors the tool registry walk: every route is visited (including those hidden from the OpenAPI schema), WebSocket routes are skipped, and a multi-verb route is deduplicated by RouteInfo identity so it is exposed once with its full verb set.

build_prompt_registry

build_prompt_registry(app: Any) -> PromptRegistry

Assemble the prompt registry from @app.mcp_prompt registrations.

Composing a surface from more than one source: tools mounted from a sub-application, served from an upstream MCP server, or derived from a tool this application already registers.

add_mcp_proxy async

add_mcp_proxy(app: Any, namespace: str, request: Callable[[str, dict[str, Any]], Awaitable[dict[str, Any]]], *, scopes: Sequence[str] | None = None, tags: Sequence[str] | None = None) -> list[str]

Discover an upstream MCP server's tools and serve them from app.

namespace prefixes every discovered tool name, so two upstreams offering a tool of the same name stay distinct and a local tool is never shadowed. Returns the local names registered, in the order the upstream listed them.

scopes requires them of a caller before any of these tools is invoked or listed, the same check a locally registered tool's scopes= performs - a gateway is where that matters most, since the upstream cannot see who is asking. tags labels them for a mount_mcp(tool_filter=...) policy.

Call this before mount_mcp, which builds the registry.

derive_tool

derive_tool(tool: MCPTool, *, name: str | None = None, description: str | None = None, arguments: dict[str, ArgTransform] | None = None) -> MCPTool

Return a new tool that calls tool's handler through a narrower surface.

arguments maps an argument of the original to how it should appear. An argument not mentioned is published unchanged. Naming one the original does not have is refused, since it would silently do nothing.

ArgTransform dataclass

How one argument of a derived tool differs from the original's.

name publishes the argument under a different name; the value is translated back before the handler runs. description and schema reshape what the agent is told. default supplies a value when the caller omits one. hide removes the argument from the schema entirely, in which case default is what the handler receives - a hidden argument with no default would leave the handler short of something it requires, so that combination is refused.

has_default property

has_default: bool

Whether this transform supplies a value when the caller omits one.

What a sample_with_tools run reports back to the handler that started it.

SamplingRun dataclass

The outcome of a sample_with_tools loop.

content is the final assistant content, messages the whole transcript including that answer as its closing turn - so extending it for another run carries the reply along - and tool_calls every tool the model drove, in the order it drove them.

text property

text: str

Return the final answer's text blocks, joined by newlines.

SampledToolCall dataclass

One tool the model asked for during a run, and what it answered.

The content a tool, resource, or prompt may return beyond plain text.

ContentBlock

Base MCP content block: its type payload plus optional annotations.

to_payload

to_payload() -> dict[str, Any]

Render this block as its MCP content-block dict, merging annotations.

TextContent

Bases: ContentBlock

A text content block carrying a plain-text value.

to_payload

to_payload() -> dict[str, Any]

Render this block as its MCP content-block dict, merging annotations.

ImageContent

Bases: ContentBlock

An image content block carrying base64 bytes and their media type.

to_payload

to_payload() -> dict[str, Any]

Render this block as its MCP content-block dict, merging annotations.

AudioContent

Bases: ContentBlock

An audio content block carrying base64 bytes and their media type.

to_payload

to_payload() -> dict[str, Any]

Render this block as its MCP content-block dict, merging annotations.

EmbeddedResource

Bases: ContentBlock

A resource block inlining a resource's contents directly in the result.

Carries the resource-contents entry (the uri/mimeType plus a text or base64 blob value) the client would otherwise fetch, so an agent reads the data without a follow-up resources/read.

to_payload

to_payload() -> dict[str, Any]

Render this block as its MCP content-block dict, merging annotations.

Bases: ContentBlock

A resource_link block referencing a resource by URI rather than inlining it.

A tool whose result points an agent at a server resource emits this in place of the bytes: the client follows the uri with a resources/read. The name is required by the spec; title, description, and mime_type are optional hints and are omitted when unset.

to_payload

to_payload() -> dict[str, Any]

Render this block as its MCP content-block dict, merging annotations.

Icon dataclass

One MCP icon: a source URI plus optional media type and rendered sizes.

to_payload

to_payload() -> dict[str, Any]

Render this icon as its MCP icon dict, omitting unset optionals.

Transports, and the store that lets HTTP sessions outlive one worker.

register_http_transport

register_http_transport(app: Any, server: MCPServer, path: str = '/mcp', auth: MCPAuth | None = None, allowed_origins: frozenset[str] | None = None, exclude_middleware: Sequence[str] | None = None, sessions: bool = False, resumable: bool = False, session_backend: SessionBackend | None = None) -> None

Mount the Streamable HTTP transport for server at path on app.

When auth is given the endpoint becomes an OAuth 2.1 resource server: each request is authenticated before dispatch, and the RFC 9728 protected-resource metadata is served so a client can discover the authorization server. allowed_origins enables Origin validation (DNS-rebinding defense). exclude_middleware names app middleware the transport routes opt out of - typically an app-wide auth middleware the transport's own auth replaces.

sessions opts into Mcp-Session-Id lifecycle: the server assigns a session id on the initialize result, requires it on every later request (HTTP 400 if missing, 404 once terminated), and accepts a DELETE to terminate it. The default keeps the stateless behavior with no per-request session bookkeeping. session_backend shares those sessions between workers: without one a session lives in the process that minted it, so a request reaching another worker is answered 404 and the client starts over.

resumable opts into SSE resumability: each streamed event carrying a payload gets an id encoding its originating stream, the events are kept in a bounded SSEEventStore, and a GET carrying Last-Event-ID replays only that stream's missed events. The default keeps no event ids or history and answers a GET 405.

StdioTransport

Drive an MCPServer over a line-delimited JSON byte stream.

Satisfies BidirectionalTransport: send writes one outbound JSON-RPC message line (wired as the server's notification sink) and request issues a server->client request, awaiting the client's correlated reply read by the serve loop.

serve async

serve() -> None

Read, dispatch, and reply line-by-line until the input closes.

A blank line is skipped; an unparseable line yields a JSON-RPC parse error; a notification (no response) writes nothing; a reply to a pending server->client request resolves it instead of dispatching. The loop ends when read_line returns None (EOF).

Framing is newline-delimited per the MCP stdio transport spec ("messages are delimited by newlines, and MUST NOT contain embedded newlines"). This is deliberate and correct: the MCP stdio transport does NOT use LSP-style Content-Length: header framing - that belongs to the Language Server Protocol, not MCP. One JSON line in, one JSON line out. Do not "fix" this into header framing.

send async

send(message: dict[str, Any]) -> None

Write one server-initiated JSON-RPC message line to the client.

request async

request(method: str, params: dict[str, Any]) -> dict[str, Any]

Issue a server->client request and await the client's correlated reply.

Sends the JSON-RPC request and awaits the future the serve loop settles when the correlated reply arrives. Returns the reply's result; an error reply raises MCPRequestError.

This does not read the stream itself. It used to, which made the serve loop and the calling handler two readers of one blocking stream - so it had to be refused from a task-augmented call, where both are live at once. The loop is now the sole reader, so there is nothing to refuse and a task-augmented tool may sample, elicit and list roots like any other.

serve_stdio async

serve_stdio(server: MCPServer) -> None

Serve server over the real process stdin / stdout.

Blocking stdin reads are offloaded to the default thread executor so the event loop stays responsive; stdout writes are flushed per line so a client reading the pipe sees each response immediately.

SessionBackend

Bases: Protocol

Where session records live when more than one worker serves a client.

The methods are async because a shared backend is I/O - a round trip to Redis or a database - and a blocking call would stall the worker's event loop.

Usage::

class RedisSessions:
    def __init__(self, client):
        self._client = client

    async def read(self, session_id):
        raw = await self._client.get(f"mcp:{session_id}")
        return None if raw is None else SessionRecord(**json.loads(raw))

    async def write(self, session_id, record, ttl):
        await self._client.set(
            f"mcp:{session_id}", json.dumps(asdict(record)), ex=int(ttl)
        )

    async def delete(self, session_id):
        await self._client.delete(f"mcp:{session_id}")

app.mount_mcp(transport="http", sessions=True, session_backend=RedisSessions(redis))

read async

read(session_id: str) -> SessionRecord | None

Return the record for session_id, or None if it is not live.

write async

write(session_id: str, record: SessionRecord, ttl: float) -> None

Store record under session_id, expiring it after ttl idle seconds.

delete async

delete(session_id: str) -> None

Drop session_id, whether or not it was live.

SessionRecord dataclass

What a session id means, independently of the worker serving it.

This is the whole of what a shared backend stores: the lifecycle flag and the identity the client declared. Everything else a session owns is bound to one worker's connection and is rebuilt there.

Authorization: validating a bearer token on the resource server, and issuing one from an authorization server of your own.

MCPAuth dataclass

OAuth 2.1 Resource Server configuration for the MCP HTTP transport.

Usage::

app.mount_mcp(transport="http", auth=MCPAuth(
    verify=my_token_verifier,            # str token -> Principal | None
    required_scopes=["mcp:tools"],
    resource_server_url="https://api.example.com/mcp",
    authorization_servers=["https://auth.example.com"],
))

metadata

metadata() -> dict[str, object]

Build the RFC 9728 protected-resource metadata document.

MCPAuthorizationServer dataclass

An OAuth 2.1 authorization server for MCP clients.

Usage::

from veloce.contrib.mcp import (
    MCPAuth, MCPAuthorizationServer, register_authorization_server,
)

def authenticate(request):
    user = request.session.get("user")
    if user is None:
        return RedirectResponse(f"/login?next={request.url}")
    return Principal(subject=user, scopes={"mcp:tools"})

authorization = MCPAuthorizationServer(
    issuer="https://api.example.com",
    authenticate=authenticate,
    scopes_supported=["mcp:tools"],
)
register_authorization_server(app, authorization)

app.mount_mcp(transport="http", auth=MCPAuth(
    verify=authorization.verifier(),
    resource_server_url="https://api.example.com/mcp",
    authorization_servers=["https://api.example.com"],
))

metadata

metadata() -> dict[str, Any]

Build the RFC 8414 authorization server metadata document.

verifier

verifier() -> Callable[[str], Awaitable[Principal | None]]

Return the token verifier to hand MCPAuth(verify=...).

Resolves an opaque token to its Principal, refusing one that has expired or was minted for a different resource.

register_authorization_server

register_authorization_server(app: Any, server: MCPAuthorizationServer, prefix: str = '', exclude_middleware: Sequence[str] | None = None) -> None

Mount server's OAuth endpoints on app.

Registers the RFC 8414 metadata document, /authorize, /token, and - when dynamic registration is on - /register. prefix mounts them under a path segment; the metadata document advertises whatever the issuer says, so the prefix and the issuer must agree.

AuthorizationStore

Bases: Protocol

Where issued clients, codes and tokens live.

Codes and tokens are keyed by digest, never by the credential, so a store that leaks yields nothing usable. take_code is single-use by contract: it must return a code at most once, so a replayed code finds nothing.

save_client async

save_client(client: OAuthClient) -> None

Record a newly registered client.

get_client async

get_client(client_id: str) -> OAuthClient | None

Return the registered client, or None.

save_code async

save_code(code_digest: str, code: AuthorizationCode) -> None

Record an issued authorization code under its digest.

take_code async

take_code(code_digest: str) -> AuthorizationCode | None

Return and remove the code, so a second redemption finds nothing.

save_token async

save_token(token_digest: str, token: AccessToken) -> None

Record an issued access token under its digest.

get_token async

get_token(token_digest: str) -> AccessToken | None

Return the token recorded under token_digest, or None.

delete_token async

delete_token(token_digest: str) -> None

Drop a token, whether or not it was present.

take_refresh async

take_refresh(refresh_digest: str) -> tuple[str, AccessToken] | None

Return and remove the (token_digest, token) a refresh token replaces.

InMemoryAuthorizationStore

A single-process store, for development and for tests.

Everything is lost on restart, and nothing is shared between workers. A deployment that survives either needs its own AuthorizationStore.

family_of_spent_refresh async

family_of_spent_refresh(refresh_digest: str) -> str | None

Return the family a already-spent refresh token belonged to.

revoke_family async

revoke_family(family_id: str) -> int

Drop every token descended from one authorization. Returns the count.

OAuthClient dataclass

A registered client: who may redirect where, and how it authenticates.

is_public property

is_public: bool

Whether this client has no secret and relies on PKCE.

AuthorizationCode dataclass

One issued code, bound to everything it was issued for.

AccessToken dataclass

One issued token: who it is for, what it may do, and until when.

Errors. A handler raising one of these surfaces it to the client as the JSON-RPC error carrying its code; anything else a tool raises is reported in-band as an isError result.

MCPError

Bases: Exception

Base for any MCP failure that maps to a JSON-RPC error object.

to_error

to_error(msg_id: Any) -> dict[str, Any]

Render this error as a JSON-RPC 2.0 error response.

InvalidRequestError

Bases: MCPError

A malformed JSON-RPC request object.

to_error

to_error(msg_id: Any) -> dict[str, Any]

Render this error as a JSON-RPC 2.0 error response.

MethodNotFoundError

Bases: MCPError

A request named a method this server does not implement.

to_error

to_error(msg_id: Any) -> dict[str, Any]

Render this error as a JSON-RPC 2.0 error response.

InvalidParamsError

Bases: MCPError

A call's params are missing, mistyped, or fail validation.

to_error

to_error(msg_id: Any) -> dict[str, Any]

Render this error as a JSON-RPC 2.0 error response.

InternalError

Bases: MCPError

An unexpected server-side failure handling the request.

to_error

to_error(msg_id: Any) -> dict[str, Any]

Render this error as a JSON-RPC 2.0 error response.

ResourceNotFoundError

Bases: MCPError

A resources/read named a URI the server cannot resolve.

to_error

to_error(msg_id: Any) -> dict[str, Any]

Render this error as a JSON-RPC 2.0 error response.

AuthorizationError

Bases: MCPError

The principal lacks a required scope — reported as a forbidden error.

Carries the required scopes so the HTTP transport can surface them in an RFC 6750 WWW-Authenticate insufficient-scope challenge.

to_error

to_error(msg_id: Any) -> dict[str, Any]

Render this error as a JSON-RPC 2.0 error response.

MCPCapabilityError

Bases: InvalidRequestError, _InBandError

A server->client request needs a client capability the client did not advertise.

Raised by MCPContext.sample / elicit / roots (and their sub-capabilities) when the connected client did not advertise the matching capability in initialize, so the request cannot be issued.

In-band on the tool path: the caller's own request was well formed, and what failed is something the tool tried to do while running it. The model can act on that - carry on without the sampled text, ask its user directly - which it cannot do with a JSON-RPC error. Resources and prompts have no in-band channel, so the same failure travels there as the -32600 its base carries.

to_error

to_error(msg_id: Any) -> dict[str, Any]

Render this error as a JSON-RPC 2.0 error response.

OriginNotAllowedError

Bases: InvalidRequestError

A request carries an Origin header outside the configured allowlist.

Per the MCP transport's DNS-rebinding defense the server MUST reject a present, disallowed Origin with HTTP 403 (a missing Origin, a non-browser client, is allowed).

to_error

to_error(msg_id: Any) -> dict[str, Any]

Render this error as a JSON-RPC 2.0 error response.

ProtocolVersionError

Bases: InvalidRequestError

The HTTP MCP-Protocol-Version header names a version this server rejects.

Per the MCP 2025-06-18 Streamable HTTP transport the server MUST answer a request carrying an invalid or unsupported protocol version with HTTP 400.

to_error

to_error(msg_id: Any) -> dict[str, Any]

Render this error as a JSON-RPC 2.0 error response.

SessionRequiredError

Bases: InvalidRequestError

A post-initialization request omitted the required Mcp-Session-Id header.

Per the MCP 2025-06-18 Streamable HTTP transport, once the server has assigned a session id the client MUST echo it on every later request; a request missing it is answered HTTP 400.

to_error

to_error(msg_id: Any) -> dict[str, Any]

Render this error as a JSON-RPC 2.0 error response.

SessionNotFoundError

Bases: MCPError

A request named an Mcp-Session-Id the server no longer recognizes.

Per the MCP 2025-06-18 Streamable HTTP transport a terminated (or never issued) session is answered HTTP 404, signalling the client to start a new session.

to_error

to_error(msg_id: Any) -> dict[str, Any]

Render this error as a JSON-RPC 2.0 error response.

MCPRequestError

Bases: Exception

A server->client request failed (the client replied with an error or closed).

JSON_SCHEMA_DIALECT module-attribute

JSON_SCHEMA_DIALECT = 'https://json-schema.org/draft/2020-12/schema'