Skip to content

Follow an MCP request

This walkthrough is for platform operators who need to explain an allowed and a denied tool call. Use an existing demo server, active agent, grant and enrolled adapter. See Client Connections for enrollment and Teams and Access for policy setup.

What each view answers

View Question it answers
MCPSnoop at the client boundary What JSON-RPC request and response did the client actually exchange?
Tempo trace Which instrumented hops and gateway stages ran, and where was time spent?
Gateway/tool metrics How many calls were allowed or denied, and how does latency change over time?
Correlated logs What completed or failed for this trace ID?
Read-only kubectl Which pod, container, configuration revision and rollout handled the call?

The path is client → local adapter → Traefik → gateway sidecar → MCP server. The platform API handles session enrollment separately. Gateway audit delivery runs asynchronously: its span can end after the client's response. A trace ID links timing and metadata; it does not retrieve request-body snapshots at every hop. MCPSnoop sees the exchange at the point where you insert it.

Prepare the connection

Replace the four values below with your demo's existing identifiers. Do not create or change grants just to make a denied call succeed.

export DEMO_SERVER=workspace-assistant
export DEMO_NAMESPACE=mcp-team-acme
export DEMO_AGENT=agt_REPLACE_WITH_ACTIVE_AGENT
export DEMO_RUNTIME_URL=https://mcp.mcpruntime.org/workspace-assistant/mcp

mcp-runtime adapter proxy --help
mcp-runtime adapter proxy \
  --runtime-url "$DEMO_RUNTIME_URL" \
  --platform-url https://platform.mcpruntime.org \
  --server "$DEMO_SERVER" \
  --namespace "$DEMO_NAMESPACE" \
  --agent "$DEMO_AGENT" \
  --auto-refresh \
  --listen 127.0.0.1:8765

Keep this process running. An OAuth-enabled server also needs the local MCP client's OAuth flow and bearer header, as described in Client Connections. The adapter keeps the enrolled certificate on its upstream TLS connection.

In another terminal, start MCPSnoop in front of the local HTTP adapter (upstream CLI reviewed 2026-10-13):

mcpsnoop http --target http://127.0.0.1:8765/mcp --listen 127.0.0.1:7000

Point your Streamable HTTP MCP client at http://127.0.0.1:7000/mcp. Let the client perform initialization and maintain any negotiated MCP transport session. An MCP transport session ID is separate from the platform's MCPAgentSession. Keep captures local and use artificial demo arguments: captures contain payloads and can expose credentials. Inspect the actual client exchange rather than replacing its TLS connection between Traefik and the gateway.

Compare one allowed and one denied call

For the shipped Go example, a read-only grant can allow add while withholding create_task. First call add with a=2, b=3, then ask the client to call create_task without changing that grant. Confirm the policy and advertised tools in your deployment; clients may refuse to invoke a tool omitted from tools/list. In that case use a protocol-capable test client that can issue the explicit request on its initialized connection. See the hosted acceptance workflow for reproducible governed call tests.

Observe the JSON-RPC frame and HTTP status in MCPSnoop. Find the corresponding gateway response's X-Trace-ID header in the HTTP exchange. Runtime derives this header from its active span; an upstream correlation header cannot replace it. Copy that ID into Grafana Explore's Tempo trace-ID search, or match it to the completion log's trace_id. When no valid span context exists, the header is absent. An ID alone does not guarantee a retained trace: unsampled requests still have an ID, and export failures or retention expiry can make Tempo lookup empty. A request with propagated W3C context shares the ingress trace; without incoming context, an instrumented hop creates a new root. Do not assume a MCPSnoop-exported span automatically shares the same trace until its propagation is verified.

For an allowed call, expect the gateway's inspect, policy, auth, authz and upstream stage spans. A denial ends at the rejecting stage and has no upstream request. Inside-server MCP spans require an instrumented server: the shipped Go example supports them, but an arbitrary Buddy or other server does not gain SDK spans merely from OTEL environment variables. External OAuth providers and optional Nginx likewise need their own instrumentation. Inspect Services and Observability for deployment prerequisites.

The gateway completion record contains event=gateway.request, service, namespace, server, tool, policy_version, method, status, decision, reason, duration_ms, trace_id and span_id. Tool names come from the configured inventory and are length-bounded; unrecognized names use __other__. The policy_version is the decision's configured version, not a hash proving identical ConfigMap contents. HTTP services emit event=http.request with a registered route template in operation; unmatched routes do not log their raw URL. Healthy probes are quiet; failed probes remain visible. A gateway HTTP success can still contain an MCP tool result with isError=true; inspect the instrumented server span and MCPSnoop result to distinguish these cases.

Logs and metrics in Grafana

Open the reference platform's authenticated Grafana at https://platform.mcpruntime.org/grafana. In Loki Explore select the demo namespace and container, then filter by the trace ID:

{namespace="mcp-team-acme", container="mcp-gateway"} |= "trace_id=REPLACE_TRACE_ID"

Completion records can be parsed using | logfmt (the standard Go logger's leading timestamp is not an application field). The TraceID derived-field link opens Tempo. From Tempo, Logs for this span searches collected namespaces by the same trace ID within a short time window. The service metrics links open request-rate and latency queries; the gateway tool-outcomes link applies to gateway root spans with namespace/server attributes. The linked metrics are aggregate observations around the request, not a per-request metric lookup. Platform service HTTP histograms also attach sampled trace exemplars. Inspect the server container in the same namespace for its SDK completion record; its span ID differs while the trace ID matches.

In Prometheus Explore, use:

sum by (tool, decision) (increase(mcp_gateway_tool_call_total{namespace="mcp-team-acme",server="workspace-assistant"}[5m]))
histogram_quantile(0.95, sum by (le, tool) (rate(mcp_gateway_tool_call_duration_seconds_bucket{namespace="mcp-team-acme",server="workspace-assistant",decision="allow"}[5m])))

Two calls demonstrate counters, not a meaningful p95. These series count inspected tool calls and HTTP/policy outcomes. Tool labels use the bounded configured inventory; unknown names collapse to __other__. Sampled histogram exemplars can link a measurement to Tempo when OpenMetrics scraping and Prometheus exemplar storage are enabled. Scrape and log ingestion take time; refresh after the next scrape rather than assuming zero means a lost request.

Retention and sampling limits

The bundled stack keeps data for a limited time: Tempo compacts blocks after 24 hours (k8s/16-tempo.yaml) and Loki retains logs for 168 hours (k8s/17-loki.yaml); Prometheus retention is set by its own flags. A trace ID copied after those windows will not resolve, although it may still be present in a client capture. The Traefik ingress tracer samples every request (--tracing.sampleRate=1); other services follow their OpenTelemetry SDK configuration, and an unsampled request has a trace ID and log lines but no retained trace or histogram exemplar. Grafana's Tempo-to-Loki link searches from 5 seconds before the span start to 5 seconds after its end, and its Tempo-to-Prometheus links use a two-minute window around the span.

Confirm Kubernetes state without changing it

Use an explicit kubeconfig for the intended cluster. Set DEMO_KUBECONFIG to your existing read-only diagnostic credential file, then select the pod that handled the trace (if replicas exist, inspect logs for all candidate pods).

export DEMO_KUBECONFIG=/path/to/explicit-kubeconfig
kubectl --kubeconfig "$DEMO_KUBECONFIG" -n "$DEMO_NAMESPACE" get pods -l "app=$DEMO_SERVER" -o wide
export DEMO_POD=REPLACE_WITH_POD_NAME
export DEMO_TRACE_ID=REPLACE_WITH_32_HEX_TRACE_ID
kubectl --kubeconfig "$DEMO_KUBECONFIG" -n "$DEMO_NAMESPACE" get pod "$DEMO_POD" -o jsonpath='{range .spec.containers[*]}{.name}{" "}{.image}{"\n"}{end}'
kubectl --kubeconfig "$DEMO_KUBECONFIG" -n "$DEMO_NAMESPACE" logs "$DEMO_POD" -c mcp-gateway --since=10m --timestamps | rg --fixed-strings "$DEMO_TRACE_ID"
kubectl --kubeconfig "$DEMO_KUBECONFIG" -n "$DEMO_NAMESPACE" logs "$DEMO_POD" -c "$DEMO_SERVER" --since=10m --timestamps | rg --fixed-strings "$DEMO_TRACE_ID"
kubectl --kubeconfig "$DEMO_KUBECONFIG" -n "$DEMO_NAMESPACE" get events --field-selector "involvedObject.name=$DEMO_POD" --sort-by=.lastTimestamp

For direct metric inspection, port-forward the gateway's metrics port in one terminal, then query from another (the reconciled default is 9103):

kubectl --kubeconfig "$DEMO_KUBECONFIG" -n "$DEMO_NAMESPACE" port-forward "pod/$DEMO_POD" 19103:9103
curl --fail --silent --show-error http://127.0.0.1:19103/metrics | rg '^mcp_gateway_tool_call_'

Events describe pod lifecycle and scheduling, not individual MCP requests. No event archive is introduced here. If a span is missing, verify the deployed images, Traefik v3 tracing configuration, OTLP collector reachability, sampling and the server's SDK support before claiming full end-to-end coverage.

Explain the platform API path separately

Enrollment is a platform operation before the MCP call. Follow its response X-Trace-ID through mcp_request_total / mcp_request_duration_seconds and event=http.request records for the registered API route. Shared bootstrap services use the same service identifier in logs and HTTP metrics, including an explicit OTEL_SERVICE_NAME override. Queries must use the configured name. Internal HTTP client spans connect downstream services when W3C propagation is supported. An enrollment trace and a later tool-call trace normally have different IDs; the grant/agent-session relationship explains why the tool call is authorized, not why the two independent HTTP operations share a trace.

Correlation provisioning follows Grafana's Tempo provisioning documentation. The Grafana data sources remain behind the existing administrative access boundary.