SDK reference¶
Everything exported from mcpobs. Generated from the source, so it cannot drift
from what the package actually does.
mcpobs — the optional onboarding helper for MCP Observability.
Everything here is additive and replaceable. A customer who prefers to
configure vanilla OpenTelemetry and send OTLP directly loses only the precise
failure category -- their failures report the coarse tool_error instead --
and nothing else breaks.
(No internal spec citation here on purpose: this docstring is published as the customer-facing SDK reference, where a reference the reader cannot resolve is worse than no reference.)
from mcp.server import MCPServer
from mcpobs import instrument
mcp = MCPServer("my-server", version="1.0.0")
instrument(mcp) # that is the whole integration
FailureKind
¶
TOOL_ERROR
class-attribute
instance-attribute
¶
The tool ran and reported failure. The server behaved correctly.
SERVER_EXCEPTION
class-attribute
instance-attribute
¶
The handler raised. Our bug, or the tool's.
UNKNOWN_TOOL
class-attribute
instance-attribute
¶
The client called a tool that does not exist. Often a client-side bug.
INVALID_ARGUMENTS
class-attribute
instance-attribute
¶
Arguments failed schema validation. Usually a model or client problem.
UNCLASSIFIED
class-attribute
instance-attribute
¶
Matched nothing. A rising count means the SDK moved -- alert on it.
FailureClassifier
¶
Maps an errored CallToolResult to a failure kind.
Pure and side-effect free so it can be unit tested without a server.
classify
¶
Return a FailureKind for the first text block of an errored result.
ORDER MATTERS. A validation failure is wrapped by the SDK and therefore ALSO carries the "Error executing tool " prefix:
Error executing tool echo_fast: 1 validation error for ...
so the validation check must come first, or every schema violation is misreported as a server exception.
first_text
staticmethod
¶
First text block of a result, or '' if there is none.
Handles BOTH shapes. By the time middleware sees a result it is the sealed wire form -- a plain dict with camelCase keys:
{"content": [{"type": "text", "text": "..."}],
"isError": True, "resultType": "complete", "_meta": {...}}
The CallToolResult model only appears if this is called earlier in the
chain or from a test. Supporting both costs four lines and avoids a
classifier that silently returns "" in production.
Deliberately tolerant: this runs inside a customer's request path and must never raise, whatever shape the result turns out to be.
error_detail
¶
Truncated error text for a FAILING result. Never called otherwise.
The caller checks is_error first; this method does not, deliberately,
so that the "errors only" rule lives at one obvious call site rather
than being implied by a helper's internals.
mrtr_state
staticmethod
¶
Short, stable hash of a requestState blob. Never the blob itself.
outgoing_state
classmethod
¶
Hash of the requestState this round EMITS (round N).
incoming_state
classmethod
¶
Hash of the requestState this round RECEIVES (round N+1).
result_type
staticmethod
¶
The 2026-07-28 resultType: "complete" or "input_required".
Present in the wire form, which means MRTR interim results ARE observable from middleware -- closing the gap D11 recorded, where the SDK's own span carries no resultType attribute.
PayloadCapture
¶
Renders tool arguments and results into bounded, redacted previews.
request
¶
The JSON-RPC request, as (preview, original size).
THE ACTUAL WIRE MESSAGE, not just arguments. This is an MCP
observability product: the useful artefact is the protocol message you
can compare against the spec or paste into a bug report, and the first
version threw away everything except the arguments.
What that discarded, all of it debugging-relevant:
* _meta.io.modelcontextprotocol/clientInfo -- WHICH CLIENT called.
The SDK sets no client attribute on the span, so this is the only
place client identity appears at all, and V2 6.1 asks for exactly
that ("which clients are calling which tools").
* _meta clientCapabilities -- what the client claimed to support,
the first thing to check on a capability error.
* _meta protocolVersion and traceparent.
* name, so a prompt or resource call is self-describing.
response
¶
The JSON-RPC response, as (preview, original size).
Also the whole message, so resultType, isError and especially
structuredContent -- the typed result, previously invisible -- survive.
Works for EVERY method. The first version looked for content blocks
and therefore returned nothing for prompts/get (which returns
messages) or resources/read (which returns contents).
render
¶
Redact + truncate an arbitrary value, as (preview, original size).
Public because mcpobs.http needs exactly the same treatment for HTTP
bodies, and a second copy of the redaction rules is how the two drift
apart until one of them leaks.
HttpBodyCapture
¶
Hooks that record downstream HTTP detail onto the client span.
ObservedSubscriptionBus
¶
Wraps a SubscriptionBus so every published event becomes a span.
A WRAPPER, NOT A PATCH, because the customer already hands the bus to
MCPServer(subscriptions=...) -- so there is an explicit seam and no reason
to monkey-patch a class. It also means a customer with their own bus
implementation gets this for free:
bus = ObservedSubscriptionBus(InMemorySubscriptionBus())
mcp = MCPServer("srv", subscriptions=bus)
WHAT IT FIXES
A subscriptions/listen span covers the whole stream and is exported
when the stream ends, so events delivered on it were invisible --
throughput, delivery gaps and starvation all unanswerable. Each event is
now its own span, exported immediately, so a stream that has stopped
carrying events is visible as an absence of recent spans rather than as
nothing at all.
instrument
¶
instrument(
server,
capture_error_detail=True,
capture_payloads=False,
transport=None,
session_endpoint=None,
session_headers=None,
)
Attach failure classification to an MCPServer.
capture_error_detail (default True) also records the error text from
FAILING tool results, truncated to 512 characters. Without it an operator
cannot see why a call failed -- the SDK leaves status_message empty. It
never reads successful results and never populates the payload columns.
Pass False to send only the failure category.
capture_payloads (default False) additionally records tool arguments and
results, truncated and redacted. It is OFF by default because it is every
argument and every result of every call, not just failures -- see
mcpobs/payload.py for what the redaction does and does not catch.
Appends to the server's middleware chain, so it runs inside the SDK's built-in OpenTelemetry middleware and can annotate the span the SDK already opened. Does not create spans and does not wrap the protocol.
transport names the transport this server runs on ("stdio",
"streamable-http", "sse"). Leave it unset and it is detected from
server.run(...), which is where the SDK itself names it. Pass it when your
server never calls run() -- for example when you build the ASGI app and
drive uvicorn yourself.
session_endpoint is your own service that mints short-lived tokens for a
server running on an END USER's machine. Configure it HERE, in
your server, rather than asking your users to add environment variables to
their MCP client config -- they should not have to paste observability
settings into Claude Desktop to use your product.
session_headers authenticates that call. Pass a CALLABLE when the
credential refreshes, which it usually does:
instrument(
mcp,
session_endpoint="https://acme.com/mcpobs-session",
session_headers=lambda: {"authorization": f"Bearer {current_token()}"},
)
A dict is read once at startup; a callable is read on every fetch. With a refreshing user token the difference is telemetry that works for an hour and then stops with no error anywhere.
Idempotent: calling it twice attaches one middleware.
instrument_downstream
¶
Instrument every installed library. Returns {name: outcome}.
A REPORT, not None. The customer needs to be able to see what this touched -- a call that patches an unknown set of libraries and says nothing is not something anyone should be comfortable putting in a production server.
Never raises. Each instrumentor is attempted independently, so one package with a version conflict cannot stop the others, and none of them can take down the customer's startup. An observability library that prevents a server from booting has done more damage than the telemetry was worth.
instrument_httpx
¶
Attach body capture to the OTel httpx instrumentation.
Deliberately separate from instrument(server): that instruments the
customer's MCP server, this instruments their OUTBOUND HTTP client. They are
different subjects, and conflating them would surprise someone who asked for
only the first.
Returns False when httpx instrumentation is not installed, rather than raising -- it is an optional dependency and its absence is not an error.
instrument_asgi
¶
Wrap an ASGI app so every HTTP request becomes a server span.
Returns the wrapped app; the original is unchanged. Use it as:
app = mcp.streamable_http_app()
app = instrument_asgi(app)
uvicorn.run(app, host="0.0.0.0", port=8000)
This is what makes 401 and 403 visible: those responses never reach an MCP method, so the span the ASGI layer produces is the ONLY record that the request happened at all.
Returns the app unwrapped if the ASGI instrumentation is not installed. An observability helper must not stop a server from serving because an optional dependency is missing.
instrument_progress
¶
Emit a span for every ctx.report_progress() call.
Patches the SDK's Context.report_progress, which is the only seam: the
call goes straight to the session and produces no telemetry of its own.
Idempotent, and returns False if the SDK is not importable.
The customer's call is made FIRST and its result returned unchanged. If the notification fails, that failure reaches them exactly as it would have -- instrumentation must not change the semantics of the thing it observes.
available
¶
Names of the instrumentations installed in this process.
Separate from instrument_downstream so a customer can ask what WOULD be
turned on before turning it on. "It silently did nothing" and "it silently
did more than I expected" are both worth being able to check.