Class: RailsMarkup::McpServer

Inherits:
Object
  • Object
show all
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

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

#startObject



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