Module: Mcpeye::OfficialServerCapture
- Defined in:
- lib/mcpeye/tracker.rb
Overview
Prepended to an official ‘MCP::Server`’s SINGLETON class so its ‘call_tool` is captured at the dispatch layer — the Ruby analog of the TS/Python protocol-level hook. The official gem invokes `call_tool` directly (mcp 0.20 server.rb), running required-arg + schema validation there, so wrapping it (not the tool classes) also captures the validation failures the gem returns BEFORE a tool’s own ‘.call` —exactly the failed asks mcpeye exists to surface.
Prepended PER SERVER (this instance’s singleton), never to the global ‘MCP::Tool` subclasses, so a tool class shared across servers is never bound to one tracker. Fail-open: the host’s return value is always passed through unchanged, and the injected ‘mcpeyeIntent` is stripped from the arguments BEFORE `super`, so the gem never splats it into the host tool’s keyword signature (which would raise ‘unknown keyword: :mcpeyeIntent` and break the call).
Class Method Summary collapse
-
.classify(response) ⇒ Object
The official gem’s call_tool returns EITHER a Hash (‘isError:`, the common 0.2x case) OR a Tool::Response object.
-
.text_of_content(content) ⇒ Object
Join the text parts of an MCP content array (Hash or struct items).
Instance Method Summary collapse
Class Method Details
.classify(response) ⇒ Object
The official gem’s call_tool returns EITHER a Hash (‘isError:`, the common 0.2x case) OR a Tool::Response object. Classify both into [is_error, error_text, result_payload]. Fail-open: unknown shapes -> success.
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/mcpeye/tracker.rb', line 117 def self.classify(response) if response.respond_to?(:error?) payload = if response.respond_to?(:structured_content) && response.structured_content response.structured_content elsif response.respond_to?(:content) { "content" => response.content } else response end [!!response.error?, text_of_content(response.respond_to?(:content) ? response.content : nil), payload] elsif response.is_a?(Hash) flag = response["isError"] flag = response[:isError] if flag.nil? [!!flag, text_of_content(response["content"] || response[:content]), response] else [false, nil, response] end rescue StandardError [false, nil, response] end |
.text_of_content(content) ⇒ Object
Join the text parts of an MCP content array (Hash or struct items).
140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/mcpeye/tracker.rb', line 140 def self.text_of_content(content) return nil unless content.is_a?(Array) parts = content.filter_map do |item| if item.is_a?(Hash) item[:text] || item["text"] elsif item.respond_to?(:text) item.text end end parts.empty? ? nil : parts.join("\n") rescue StandardError nil end |
Instance Method Details
#call_tool(request, **kwargs) ⇒ Object
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/mcpeye/tracker.rb', line 62 def call_tool(request, **kwargs) tracker = @__mcpeye_tracker return super if tracker.nil? name = (request[:name] || request["name"]).to_s raw_args = request[:arguments] || request["arguments"] args = raw_args.is_a?(Hash) ? raw_args : {} # Strip the injected intent in place (request[:arguments] is the same object), # so the gem validates/dispatches WITHOUT mcpeyeIntent. intent = nil begin v = args.delete(Mcpeye::Intent::INTENT_PARAM_NAME) v = args.delete(Mcpeye::Intent::INTENT_PARAM_NAME.to_sym) if v.nil? intent = v if v.is_a?(String) && !v.strip.empty? rescue StandardError intent = nil end # Reserved capability tool: never dispatch to the host; answer locally. if name == Mcpeye::RequestCapability::TOOL_NAME && tracker.capture_missing_capabilities? return tracker.answer_request_capability_official(args) end started = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) begin response = super rescue StandardError => e dur = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - started begin tracker.record(name, args, is_error: true, error_message: e., intent: intent, duration_ms: dur) rescue StandardError nil end raise end begin dur = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - started is_error, error_text, result_payload = Mcpeye::OfficialServerCapture.classify(response) if is_error tracker.record(name, args, is_error: true, error_message: error_text, intent: intent, duration_ms: dur) else tracker.record(name, args, result: result_payload, intent: intent, duration_ms: dur) end rescue StandardError nil end response end |