MCP Server Observability¶
The StockTrim MCP Server is observability-agnostic. We don't ship a built-in tracing layer or prescribe a logging format — operators choose their own stack and plug it in via standard hooks.
This page documents what the server does emit, what it doesn't, and how to wire in OpenTelemetry, structured logs, and HTTP-level tracing.
What the server ships¶
- Structured app logs via structlog for
lifecycle events (startup, shutdown, auth errors, lifespan failures). Tunable
via
LOG_LEVELandLOG_FORMATenv vars. - Native OpenTelemetry instrumentation via FastMCP 3.x. The server emits
per-tool spans with MCP semantic conventions (
mcp.tool.name,mcp.tool.duration,mcp.tool.success) automatically — no decorator, no config in the codebase. ErrorLoggingTransport(instocktrim_public_api_client, the underlying client library) logs API parse errors to capture real-API-vs-spec divergences. This is library-author concern, not operator concern, and is separate from the observability story below.- Response caching via FastMCP's
ResponseCachingMiddlewareis wired inserver.pywith an in-memory store. Read tools cache for 5 minutes; resources cache for 60 seconds; mutating tools (everycreate_*/delete_*/set_*/configure_*/etc. surface) are excluded so cached entries are never returned for state changes. Operators can swap the in-memory backend for Redis/disk by overriding the middleware (see Caching below).
What the server does NOT ship¶
- No bespoke tracing decorators (
@observe_tool,@observe_servicewere removed in #147 — see the v3 modernization tracking issue). - No prescribed log format for tool invocations.
- No prescribed metrics backend.
The intent: keep the package thin and let each operator wire in their preferred stack — Datadog, Honeycomb, Grafana, plain logs, or nothing.
Operator setup¶
App-level structured logs¶
Set environment variables before starting the server:
| Variable | Values | Default |
|---|---|---|
LOG_LEVEL |
DEBUG, INFO, WARNING, ERROR, CRITICAL |
INFO |
LOG_FORMAT |
console (human-readable), json (machine) |
console |
Example:
Use json in production for log aggregators (Datadog, Splunk, Loki, etc.).
Tool-boundary tracing (OpenTelemetry)¶
FastMCP 3.x emits OTel spans natively. Operators provide an exporter via standard env vars — no code changes.
Install the OTel SDK with an OTLP exporter:
pip install 'fastmcp[telemetry]'
# or, equivalently:
pip install opentelemetry-sdk opentelemetry-exporter-otlp
Set the standard OTel env vars before starting the server:
export OTEL_EXPORTER_OTLP_ENDPOINT="https://otel-collector.your-org.com:4317"
export OTEL_SERVICE_NAME="stocktrim-mcp-server"
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production"
uvx stocktrim-mcp-server
Each tool call is a span with attributes for the tool name, duration, success flag, and any FastMCP-attached context. Errors are recorded as span events.
For richer attributes (parameter values, custom tags), add a FastMCP middleware — see below.
Tool-boundary logging (FastMCP middleware)¶
Operators who want structured tool-call logs (rather than OTel traces) drop in
a middleware at server construction time. FastMCP ships
LoggingMiddleware and operators
can write their own:
from fastmcp.server.middleware import LoggingMiddleware
from stocktrim_mcp_server.server import mcp
mcp.add_middleware(LoggingMiddleware(
log_args=True,
log_results=False,
log_errors=True,
))
For Datadog/Sentry/etc., write a small middleware that calls the SDK directly
in before_call / after_call hooks.
HTTP-level tracing (outbound StockTrim API calls)¶
The MCP server's tool calls eventually translate into HTTP requests against
StockTrim. To trace those at the wire level, instrument httpx:
Then enable instrumentation before starting the server (e.g., in a wrapper
script or via OTEL_PYTHON_DISABLED_INSTRUMENTATIONS/auto-instrumentation):
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
HTTPXClientInstrumentor().instrument()
You'll get spans for every API call, attached to the parent tool span.
Caching¶
The server ships with FastMCP's ResponseCachingMiddleware enabled by default,
backed by an in-memory store:
| Surface | TTL | Notes |
|---|---|---|
call_tool |
5 min | Mutating tools (create_*, delete_*, etc.) excluded |
read_resource |
60 sec | Resources are for discovery — favor freshness |
list_tools/etc. |
5 min | FastMCP defaults |
Staleness window: a successful mutation invalidates downstream reads only when the next read miss exceeds the TTL. For most workflows the 5-minute window is acceptable — pair-programmer agents typically don't mutate and re-read the same entity within seconds. If your workload does, see "Tightening cache freshness" below.
Swapping the cache backend¶
To use Redis (multi-process or persistent across restarts):
from key_value.aio.stores.redis import RedisStore
from fastmcp.server.middleware.caching import ResponseCachingMiddleware
from stocktrim_mcp_server.server import mcp
# Replace the default in-memory middleware before mcp.run()
mcp.middleware = [m for m in mcp.middleware if not isinstance(m, ResponseCachingMiddleware)]
mcp.add_middleware(ResponseCachingMiddleware(
cache_storage=RedisStore(host="redis.internal", port=6379),
# …existing call_tool_settings / read_resource_settings…
))
Tightening cache freshness¶
Three options, ordered cheapest to most invasive:
- Lower TTLs — pass smaller
ttlvalues viaCallToolSettings(ttl=60)etc. - Add tools to
excluded_tools— any tool you list there is never cached. - Disable response caching entirely — remove the middleware after
constructing
mcp(see snippet above; just don't re-add it).
Why this design¶
The MCP server is distributed via PyPI as stocktrim-mcp-server. Different
operators run it in different environments — solo dev workstations, hosted
infrastructure, CI agents. Prescribing a tracing backend or a structured-logging
format would force everyone to live with one team's choices.
By relying on:
- FastMCP's native OTel (an open standard, swappable backend),
- FastMCP's middleware system (composable, operator-owned),
- httpx's instrumentation surface (already covered by the OTel ecosystem),
…the server stays small, consumers stay flexible, and the open standards do the heavy lifting.