Skip to content

feat(mcp): implement MCP server with 4 read-only tools - #9

Merged
HarshSingh21 merged 1 commit into
mainfrom
feat/mcp-server
Aug 14, 2026
Merged

HarshSingh21 merged 1 commit into
mainfrom
feat/mcp-server

Conversation

@HarshSingh21

Copy link
Copy Markdown
Owner

Replaces the eventmesh-mcp placeholder — 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.

Tool Purpose Required args
describe_data Event-type catalog. The entry point for an agent that doesn't know the domain yet. —
query_events Filter by entity / type / time window / tags / severity. —
get_entity_state Current merged state for one entity. entity_id
summarize_window Precomputed summaries across a window + severity tally. since

Design 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.go holds the whole wire format in one readable file.

summarize_window performs 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 isError content, 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 timestamptz in 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 types
  • internal/mcp/server.go — read loop, dispatch, version negotiation
  • internal/mcp/tools.go — the four tools, schemas and argument validation
  • internal/db/reads.go — read queries, kept separate from the write path
  • internal/domain/reads.go — read-side projections
  • cmd/eventmesh-mcp/main.go — wiring, signal handling, stderr-only logging

Testing

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 vet clean.

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

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>
@HarshSingh21
HarshSingh21 merged commit b649e07 into main Aug 14, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant