Class: RailsMarkup::McpServer
- Inherits:
-
Object
- Object
- RailsMarkup::McpServer
- Defined in:
- lib/rails_markup/mcp_server.rb
Overview
MCP (Model Context Protocol) server speaking JSON-RPC 2.0 over stdio.
Exposes five focused tools for AI agents to read and act on browser annotations.
Each tool accepts an optional environment param ("development"|"production")
to route to the correct backend (default: "development").
Configuration (via .mcp.json env vars, set by bin/markup configure):
RAILS_MARKUP_DEV_URL — local Rails server URL (auto-detected on install)
RAILS_MARKUP_PROD_URL — production URL
RAILS_MARKUP_PROD_TOKEN — production API token
RAILS_MARKUP_MOUNT_PATH — engine mount path (default: /admin/annotations)
Defined Under Namespace
Classes: TargetError, ToolError
Constant Summary collapse
- ENV_SCHEMA =
{ environment: { type: "string", enum: %w[development production], description: "Target environment (default: development)" } }.freeze
- TOOLS =
[ { name: "rails_markup_read", description: "Read pending annotations, sessions, one session, or one annotation without changing state.", inputSchema: { type: "object", properties: { resource: { type: "string", enum: %w[pending sessions session annotation], description: "Resource to read; session requires sessionId and annotation requires annotationId." }, **ENV_SCHEMA, sessionId: { type: "string", description: "Session ID, required only when resource is session; filters pending when supplied." }, annotationId: { type: "string", description: "Annotation ID, required only when resource is annotation." } }, required: ["resource"], additionalProperties: false }, annotations: { readOnlyHint: true, destructiveHint: false } }, { name: "rails_markup_watch", description: "In development, wait for newly created annotations and return a bounded batch without changing state.", inputSchema: { type: "object", properties: { sessionId: { type: "string", description: "Optional session ID to filter" }, timeoutSeconds: { type: "number", description: "Max seconds to wait (default: 120, max: 300)" }, batchWindowSeconds: { type: "number", description: "Seconds to wait after first annotation before returning batch (default: 10, max: 60)" } }, required: [], additionalProperties: false }, annotations: { readOnlyHint: true, destructiveHint: false } }, { name: "rails_markup_transition", description: "Acknowledge or resolve one annotation; summary is used only when resolving.", inputSchema: { type: "object", properties: { action: { type: "string", enum: %w[acknowledge resolve], description: "State transition to apply." }, annotationId: { type: "string", description: "The annotation ID" }, summary: { type: "string", description: "Optional resolution summary; valid only for resolve." }, **ENV_SCHEMA }, required: %w[action annotationId], additionalProperties: false }, annotations: { readOnlyHint: false, destructiveHint: false } }, { name: "rails_markup_reply", description: "Add a message to one annotation's discussion thread.", inputSchema: { type: "object", properties: { annotationId: { type: "string", description: "The annotation ID" }, message: { type: "string", description: "Reply message" }, **ENV_SCHEMA }, required: %w[annotationId message], additionalProperties: false }, annotations: { readOnlyHint: false, destructiveHint: false } }, { name: "rails_markup_dismiss", description: "Destructively dismiss one annotation with an explicit reason.", inputSchema: { type: "object", properties: { annotationId: { type: "string", description: "The annotation ID" }, reason: { type: "string", description: "Reason for dismissal" }, **ENV_SCHEMA }, required: %w[annotationId reason], additionalProperties: false }, annotations: { readOnlyHint: false, destructiveHint: true } } ].freeze
- TOOL_ARGUMENTS =
{ "rails_markup_read" => %w[resource environment sessionId annotationId], "rails_markup_watch" => %w[sessionId timeoutSeconds batchWindowSeconds], "rails_markup_transition" => %w[action annotationId summary environment], "rails_markup_reply" => %w[annotationId message environment], "rails_markup_dismiss" => %w[annotationId reason environment] }.freeze
- LEGACY_ALIASES =
Legacy tool names → canonical handler + trusted injected args. Removed after v1.3.0.
{ "rails_markup_sessions" => { handler: "rails_markup_read", inject: { "resource" => "sessions" } }, "rails_markup_list_sessions" => { handler: "rails_markup_read", inject: { "resource" => "sessions" } }, "rails_markup_session" => { handler: "rails_markup_read", inject: { "resource" => "session" } }, "rails_markup_get_session" => { handler: "rails_markup_read", inject: { "resource" => "session" } }, "rails_markup_pending" => { handler: "rails_markup_read", inject: { "resource" => "pending" } }, "rails_markup_get_pending" => { handler: "rails_markup_read", inject: { "resource" => "pending" } }, "rails_markup_get_all_pending" => { handler: "rails_markup_read", inject: { "resource" => "pending" } }, "rails_markup_fetch_production" => { handler: "rails_markup_read", inject: { "resource" => "pending", "environment" => "production" } }, "rails_markup_watch_annotations" => { handler: "rails_markup_watch" }, "rails_markup_acknowledge" => { handler: "rails_markup_transition", inject: { "action" => "acknowledge" } }, "rails_markup_resolve" => { handler: "rails_markup_transition", inject: { "action" => "resolve" } }, "rails_markup_resolve_production" => { handler: "rails_markup_transition", inject: { "action" => "resolve", "environment" => "production" } }, "rails_markup_reply_production" => { handler: "rails_markup_reply", inject: { "environment" => "production" } }, "rails_markup_dismiss_production" => { handler: "rails_markup_dismiss", inject: { "environment" => "production" } } }.freeze
Instance Method Summary collapse
-
#initialize(store:, input: $stdin, output: $stdout, dir: Dir.pwd) ⇒ McpServer
constructor
A new instance of McpServer.
- #start ⇒ Object
Constructor Details
#initialize(store:, input: $stdin, output: $stdout, dir: Dir.pwd) ⇒ McpServer
Returns a new instance of McpServer.
148 149 150 151 152 153 |
# File 'lib/rails_markup/mcp_server.rb', line 148 def initialize(store:, input: $stdin, output: $stdout, dir: Dir.pwd) @store = store @input = input @output = output @mcp_config = McpConfig.new(dir: dir) end |
Instance Method Details
#start ⇒ Object
155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/rails_markup/mcp_server.rb', line 155 def start @input.each_line do |line| line = line.strip next if line.empty? request = JSON.parse(line) response = handle_request(request) write_response(response) if response rescue JSON::ParserError write_response(error_response(nil, -32700, "Parse error")) end end |