feat(mcp): implement MCP server with 4 read-only tools - #9
Merged
Merged
Conversation
Replaces the eventmesh-mcp placeholder (which exited 2 with "not
implemented yet") with a working stdio MCP server.
Protocol layer is hand-rolled JSON-RPC 2.0 rather than an SDK, keeping
the module at exactly one dependency (pgx) so the release image stays
~20MB distroless. Supports initialize (with version negotiation),
notifications/initialized, ping, tools/list and tools/call.
Tools, all read-only so an agent triaging an incident cannot mutate the
store:
describe_data — event_types catalog; the entry point for an agent
that does not know the domain yet
query_events — filter by entity / type / window / tags / severity;
raw payload is opt-in, since returning it by default
would undo the point of structuring at write time
get_entity_state — current merged state for one entity
summarize_window — precomputed ai_summary across a window plus a
severity tally; performs no LLM call, which is what
makes it a millisecond read
Read queries live in internal/db/reads.go, separate from the write path,
every one bounded by an explicit LIMIT (capped at 500) so an agent cannot
stall the pool. Timestamps are normalised to UTC on read — pgx returns
timestamptz in the connection's local zone, which would otherwise make
identical data serialise differently per deployment.
Domain-level failures (unknown entity, inverted time window, unparseable
timestamp) return isError content the model can read and retry from,
rather than JSON-RPC errors, which are reserved for malformed requests.
Adds 20 tests covering the handshake, version negotiation, notification
silence, tool dispatch, argument validation and the JSON-RPC error codes.
Verified end-to-end against Postgres 16 with all four tools.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Replaces the
eventmesh-mcpplaceholder — which exited with code 2 and printed "not implemented yet" — with a working stdio MCP server.Tools
All four are read-only. An agent triaging an incident has no path to mutate the store, and that is structural rather than a prompt convention.
describe_dataquery_eventsget_entity_stateentity_idsummarize_windowsinceDesign decisions
Hand-rolled JSON-RPC 2.0 instead of an SDK. MCP is a small, stable method set. Implementing it directly keeps the module at exactly one dependency (pgx), which is what preserves the ~20MB distroless image.
internal/mcp/protocol.goholds the whole wire format in one readable file.summarize_windowperforms no LLM call. Summaries were computed once by the enrichment worker at write time. That is what makes it a millisecond read rather than a multi-second inference, and why an agent can afford to call it in a loop.Raw payload is opt-in on
query_events. Returning it by default would undo the entire point of structuring at write time — agents would be back to parsing arbitrary JSON in context.Domain failures return
isErrorcontent, not JSON-RPC errors. "No such entity" is a real answer the model needs to read and act on. JSON-RPC errors are reserved for malformed requests, per spec.Timestamps normalised to UTC on read. pgx returns
timestamptzin the connection's local timezone, so identical data would otherwise serialise differently depending on where the server runs. These tools promise deterministic reads.Every read bounded by an explicit
LIMIT, capped at 500. An agent cannot ask for the whole table and stall the pool.Layout
internal/mcp/protocol.go— JSON-RPC 2.0 + MCP message typesinternal/mcp/server.go— read loop, dispatch, version negotiationinternal/mcp/tools.go— the four tools, schemas and argument validationinternal/db/reads.go— read queries, kept separate from the write pathinternal/domain/reads.go— read-side projectionscmd/eventmesh-mcp/main.go— wiring, signal handling, stderr-only loggingTesting
20 new tests covering the initialize handshake, protocol version negotiation, notification silence, tool dispatch, argument validation (including rejection of unknown fields) and all JSON-RPC error codes. Full suite green,
go vetclean.Verified end-to-end against Postgres 16: real handshake,
tools/list, and all four tools returning real rows.Docs
README updated in all four places that described MCP as a future milestone, plus a new MCP server section with Claude Desktop and Claude Code setup instructions.
🤖 Generated with Claude Code