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

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.message, 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