feat(server): read-only MCP server on /mcp exposing the read API (JEF-471)#119
Merged
thejefflarson merged 1 commit intoJul 22, 2026
Conversation
…-471) Mount an in-process Model Context Protocol server on the axum app at /mcp (streamable-HTTP, official `rmcp` SDK nested as a tower service — its axum integration is mature, so no hand-rolled JSON-RPC). Nine READ-ONLY tools wrap watcher's existing read queries: search_traces, get_trace, query_logs, list_services, service_map, query_metrics, metric_series, list_alerts, alert_events. Each returns the typed `api` structs as JSON. The nine read handlers' query bodies are refactored in api.rs into shared `query_*` functions that both the HTTP handler and the MCP tool call, so every limit clamp and default time window is shared by construction — the MCP surface adds no unbounded scan. No write/mutate tools; /mcp touches no ingest/reconcile path. /mcp is gated behind WATCHER_MCP_ENABLED (default OFF) and mounted outside the browser edge-auth path — it gets its own auth in JEF-472, so it must not ship exposed. Records ADR 0018. Tests: an end-to-end smoke test enables /mcp, serves the real app on an ephemeral port, and drives it with the rmcp streamable-HTTP client — lists the tools and calls list_services + query_logs, asserting JSON shape. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JbrmfzHsTMMzPaSrUkZgWo
thejefflarson
force-pushed
the
thejefflarson/jef-471-mcp-server-expose-watchers-read-api-as-read-only-mcp-tools
branch
from
July 22, 2026 05:40
e7ea1f4 to
77e21ea
Compare
thejefflarson
deleted the
thejefflarson/jef-471-mcp-server-expose-watchers-read-api-as-read-only-mcp-tools
branch
July 22, 2026 05:46
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes JEF-471.
What
Mounts an in-process Model Context Protocol server on the axum app at
/mcp(streamable-HTTP transport) so an MCP client (MCP Inspector, Claude Code, …) can query watcher's telemetry against the same Postgres the UI uses. Single binary, same origin.Nine READ-ONLY tools, each a thin wrapper over an existing read query:
search_traces,get_trace,query_logs,list_services,service_map,query_metrics,metric_series,list_alerts,alert_events. Tools return the exactapiresponse structs as JSON.DECISION NEEDED — ratify (documented in ADR 0018)
rmcpvs hand-rolled JSON-RPC: used the official Rust SDKrmcp(transport-streamable-http-server). ItsStreamableHttpServiceimplementstower::Service, so it nests straight into the existing axum router withnest_service("/mcp", …)— no second listener, no bespoke protocol code. Its axum integration is mature, so the hand-rolled fallback wasn't needed. The client half is a dev-dependency only (used by the smoke test); the shipped binary carries only the server transport.pub async fn query_*(pool, …)functions. Both the HTTP handler and the MCP tool call the same function, so every limit clamp + default time window is shared by construction — the MCP surface can't introduce an unbounded scan the HTTP surface doesn't already permit. HTTP behavior is unchanged (all existing smoke tests pass)./mcpis gated behindWATCHER_MCP_ENABLED(default OFF) and mounted outside the browser-cookie edge auth (ADR 0013) — an MCP client is not a browser. It carries no auth of its own yet; JEF-472 adds it. The transport's default loopback-only Host allow-list (a DNS-rebinding guard for browser-reached localhost servers) is disabled, since watcher's MCP is a server-to-server endpoint reached through a public tunnel host where that list would reject every legitimate client. Flag default + ADR make the "don't enable before auth" ordering explicit.Scope / swarm overlap (JEF-473 runs concurrently)
lib.rs::app()change is localized to conditionallynest_service("/mcp", …)outside the/apirouter (so it never rides the browser-cookie middleware JEF-473 adds).main.rschange is just the MCP flag-status log block.Cargo.tomladdsrmcp+schemars(different lines than JEF-473'sjsonwebtoken).api.rsis touched by this PR alone.tests/smoke.rs— appended one test (shared file).Tests
mcp_lists_tools_and_calls_read_queries: enables/mcp, serves the real app on an ephemeral port, drives it with the officialrmcpstreamable-HTTP client, lists the tools (asserts exactly the nine read tools) and callslist_services+query_logs, asserting the JSON shape. It would fail without the mount/tools.cd server:cargo fmt --check✓,cargo clippy --all-targets✓ (no warnings),cargo build --locked✓,cargo test --locked→ 49 passed (against local Postgres).🤖 Generated with Claude Code
https://claude.ai/code/session_01JbrmfzHsTMMzPaSrUkZgWo