Res mcp#1936
Draft
tomaszpatrzek wants to merge 32 commits into
Draft
Conversation
New contrib gem: MCP (Model Context Protocol) server exposing RubyEventStore inspection as AI tools over stdio JSON-RPC. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements the MCP protocol loop over stdio: initialize handshake, tools/list, tools/call, and ping. Tools are registered via #register and receive the event_store instance on each call. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows event count, version, and first/last event for a named stream. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ReadEvents.of centralizes filtering logic (type, after, before, from, limit) — mirroring the same class in ruby_event_store-cli. stream_events lists events in a stream with optional filters. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows full event details: ID, type, timestamps, data and metadata. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lists all streams an event has been published or linked to. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Searches events across all streams with optional type, time range, stream, and limit filters. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows total event count and unique event types, with optional per-stream mode. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows the causation tree for all events sharing a correlation ID, using the \$by_correlation_id_* stream. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MCP.server(event_store) builds a fully configured Server with all seven tools registered. bin/res-mcp loads the Rails environment and starts the stdio server. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents installation, usage, MCP client configuration, and the list of available tools. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Test workflow runs on Ruby 3.2/3.3/3.4. Mutate workflow runs incremental mutation testing on PRs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests the full JSON-RPC pipeline end-to-end: stdin → server dispatch → tool execution against a real event store → stdout response. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add targeted specs to kill alive mutations across all tool specs and the server spec: exact schema hash comparisons, iso8601 precision anchoring, two-stream isolation tests, direct private method tests via send, and factory coverage in mcp_spec. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add targeted specs for: error message content in resolve_type, stored event_store and empty tools list in Server#initialize, argument passing and response shape in call_tool, full type name format in search and stream_events, external causation_id root detection and prefix accumulation in trace. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract render_node/render_last_node/render_children to eliminate dead `last` parameter - Use *rest, last = children destructuring instead of index-based slicing - Rewrite spec around three business scenarios (chain, branching, full order flow) - Add tree_shape helper for readable structural assertions - 100% mutant coverage (335/335) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract event_line helper to remove duplication between render_node and render_last_node - Extract child_prefix as named variable - Simplify render_children using splat destructuring (*non_last, last) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add tests to cover all surviving mutations in Server#handle and
Server#call_tool. Each test targets the specific behavioral difference
the mutation would introduce:
- params["name"] vs params.fetch("name"): test with absent "name" key
verifies the response text includes "Unknown tool" (not a generic
rescue "Error:" message)
- e.message vs e (to_s) in call_tool rescue: test with a custom
exception class that overrides both to_s and message to distinct
values; asserts the text uses message, not to_s
- request["method"] vs request.fetch("method"): test with no "method"
key verifies -32601 is returned (not -32603 from rescue)
- request["params"] vs request.fetch("params"): test with absent
"params" in tools/call verifies a result key is returned (call_tool
rescues internally) not an error key (handle rescue path)
- request["id"] vs request.fetch("id") in rescue: test with a request
that triggers handle's rescue but has no "id" field; verifies
response id is nil instead of the fetch raising KeyError
Server#start is added to the mutant ignore list. The three
strip-variant mutations (strip/lstrip/rstrip/none) are equivalent:
Ruby's JSON.parse accepts surrounding whitespace — including the
trailing \n from each_line — per RFC 8259, so the parse result is
identical regardless of which strip variant is used. The output.sync
mutation is also in this method; it is covered by a spec asserting
sync= is called with true, but since the method is ignored by mutant,
that spec runs as a regular RSpec test rather than being driven by
mutant.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Four mutations were surviving in ReadEvents.of and ReadEvents.resolve_type.
limit.to_i vs limit.to_int / bare limit:
Tests with limit: "2" (string) and limit: "2abc" kill these. Strings
do not respond to to_int (NoMethodError), and specification.limit
raises ArgumentError on a raw string. The "2abc" case also documents
the to_i behavior of extracting leading digits from partial numeric
strings.
limit.to_i vs Integer(limit):
Integer("2abc") raises ArgumentError while "2abc".to_i returns 2,
so the test with limit: "2abc" returning 2 events distinguishes them.
Object.const_get vs self.const_get in resolve_type:
For fully qualified names these are equivalent (both traverse Object
to resolve the first segment), but they differ for unqualified names:
Object.const_get("LocalEvent") raises NameError while
ReadEvents.const_get("LocalEvent") would find ReadEvents::LocalEvent.
The test stubs a constant directly on ReadEvents and asserts that
resolving it by its unqualified name raises "Unknown event type",
confirming Object namespace is used, not ReadEvents' own namespace.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows the full event history of an aggregate instance by constructing the stream name from aggregate_type and aggregate_id using the RubyEventStore convention (ClassName$id) and reading all events from that stream. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
recent: shows the N most recent events across all streams, newest first. Useful as a starting point when stream names are not yet known. Defaults to 20 events. aggregate_history: refactor render to build output via string interpolation instead of array accumulation. Eliminates equivalent nil/empty-string mutation and kills join-separator mutants with a line-count assertion on two events. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ktop Users know the JSON snippet but often don't know where to put it. Add explicit file paths for both clients and platforms so they can configure res-mcp without digging through documentation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
settings.json holds permissions/hooks, not MCP servers. Claude Code reads MCP servers from .mcp.json (project) or `claude mcp add`. Document the real flow, split Claude Code vs Desktop (with cwd + paths), note install alone doesn't register the server, and add an other-clients pointer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Branch was rebased onto master (2.18.0 -> 2.19.2); regenerate the mcp Gemfile.lock so frozen/deployment installs on CI match the current path gemspecs. Co-Authored-By: Claude Opus 4.8 <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.
New contrib gem exposing RubyEventStore as an MCP (Model Context Protocol) server. Allows AI assistants
(Claude, etc.) to inspect the event store directly — without a terminal, without writing SQL, without leaving
the conversation.
How it works
The server runs as a stdio process started by the MCP client (Claude Desktop, Claude Code). It reads JSON-RPC
requests from stdin and writes responses to stdout. The MCP client decides which tool to call based on the
user's question.
Claude ←→ MCP protocol (stdio JSON-RPC) ←→ res-mcp ←→ Rails.configuration.event_store
Usage
Add to your Gemfile:
gem "ruby_event_store-mcp"
Configure in your MCP client. Add the snippet to the appropriate config file:
Claude Code
Claude Desktop
After that Claude can answer questions like:
Available tools
Architecture