main
rb 77 lines 3.03 KB
Raw
1 # frozen_string_literal: true
2
3 module Mcp
4 # Protocol-level dispatcher: maps a single JSON-RPC message to a result.
5 # Knows nothing about HTTP — that's the controller's job.
6 class Server
7 PROTOCOL_VERSION = "2025-06-18"
8 SUPPORTED_VERSIONS = %w[2025-06-18 2025-03-26 2024-11-05].freeze
9
10 def initialize(current_user:)
11 @current_user = current_user
12 end
13
14 # message: a parsed JSON-RPC object. Returns a response Hash, or nil for
15 # notifications (no id) which must not produce a reply.
16 def handle(message)
17 id = message["id"]
18 method = message["method"]
19 params = message["params"] || {}
20
21 case method
22 when "initialize" then result(id, initialize_result(params))
23 when "ping" then result(id, {})
24 when "tools/list" then result(id, { "tools" => Tools.specs })
25 when "tools/call" then call_tool(id, params)
26 when "resources/list" then result(id, { "resources" => [] })
27 when "prompts/list" then result(id, { "prompts" => [] })
28 when %r{\Anotifications/} then nil # initialized, cancelled, etc.
29 else error(id, -32601, "Method not found: #{method}")
30 end
31 rescue => e
32 Rails.logger.error("[mcp] #{e.class}: #{e.message}")
33 error(id, -32603, "Internal error")
34 end
35
36 private
37
38 def initialize_result(params)
39 requested = params["protocolVersion"]
40 version = SUPPORTED_VERSIONS.include?(requested) ? requested : PROTOCOL_VERSION
41 {
42 "protocolVersion" => version,
43 "capabilities" => { "tools" => { "listChanged" => false } },
44 "serverInfo" => { "name" => "sigit", "title" => "siGit Code", "version" => "0.1.0" },
45 "instructions" => "Tools for siGit-hosted git repositories: list repos, " \
46 "read files, search code, and open or comment on pull requests and issues. " \
47 "Also web_search, for finding pages on the public web."
48 }
49 end
50
51 def call_tool(id, params)
52 tool = Tools.find(params["name"])
53 return error(id, -32602, "Unknown tool: #{params['name']}") unless tool
54
55 output = tool.call(params["arguments"] || {}, current_user: @current_user)
56 result(id, { "content" => [ text_block(output) ], "isError" => false })
57 rescue Mcp::ToolError => e
58 # Tool failures are reported in-band (isError: true) so the model can see
59 # and recover from them, not as JSON-RPC protocol errors.
60 result(id, { "content" => [ text_block(e.message) ], "isError" => true })
61 end
62
63 def text_block(value)
64 text = value.is_a?(String) ? value : JSON.pretty_generate(value)
65 { "type" => "text", "text" => text }
66 end
67
68 def result(id, value)
69 return nil if id.nil? # was a notification
70 { "jsonrpc" => "2.0", "id" => id, "result" => value }
71 end
72
73 def error(id, code, message)
74 { "jsonrpc" => "2.0", "id" => id, "error" => { "code" => code, "message" => message } }
75 end
76 end
77 end