Class: McpLogs::Recorder

Inherits:
Object
  • Object
show all
Defined in:
app/models/mcp_logs/recorder.rb

Overview

The body of the lambda a host hands to MCP::Configuration#around_request.

The gem calls it as around_request.call(data, &block), where data is a hash the server keeps writing into for the duration of the block: tool_name, tool_arguments, client and error all land during the call, not before it. So the row is inserted with what is known up front (the method) and completed with everything else once the block returns.

Load-bearing requirement on the host: build a fresh MCP::Server per request. In mcp 1.0.0 the data hash above is not per-request — it is @instrumentation_data, a plain instance variable on the MCP::Server object, reassigned to {} at the top of every instrument_call (lib/mcp/instrumentation.rb). If one server instance serves two concurrent requests, request B's reassignment can land while A is still mid-handler, so A's later tool_name/tool_arguments/error writes end up in B's hash instead of A's. This recorder only holds the reference it was handed and trusts it describes the request in progress; it has no way to detect or correct for a shared server, so the host must not memoize or share one.

Constant Summary collapse

ERROR_MESSAGE_BYTES =

error_message goes into a plain text column, not jsonb, and has no upstream size bound the way arguments/response do via Payload: exception messages routinely echo attribute values or an entire failing SQL statement. This is a size bound only, not redaction — filtering free-text exception messages is out of scope here.

2_000
STRING_LIMIT =

Cap for every string column a client can influence: tool_name, session_id, client_name, client_version, protocol_version, user_agent.

These are not validated input. mcp 1.0.0 reads tool_name straight from params.name and hands it to this hook before looking the tool up, so an unknown tool records whatever the client sent; the rest are raw request headers. Uncapped, one looping client writes megabyte rows for the whole retention window and puts one

255 is the migration's limit:, and PostgreSQL's varchar(n) counts characters, so this counts characters too and a value cut here always fits. It is far above every legitimate value (a tool name is an identifier, a session id a UUID, a protocol version a date), which is why the cut is silent: unlike a capped payload there is nothing an operator would want back, so it gets no *_truncated flag.

255

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(server_name: nil) ⇒ Recorder

Returns a new instance of Recorder.



51
52
53
# File 'app/models/mcp_logs/recorder.rb', line 51

def initialize(server_name: nil)
  @server_name = server_name
end

Class Method Details

.around_request(server_name: nil) ⇒ Object



46
47
48
49
# File 'app/models/mcp_logs/recorder.rb', line 46

def self.around_request(server_name: nil)
  recorder = new(server_name: server_name)
  ->(data, &handler) { recorder.call(data, &handler) }
end

Instance Method Details

#call(data, &handler) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'app/models/mcp_logs/recorder.rb', line 55

def call(data, &handler)
  return handler.call unless McpLogs.enabled?

  record  = start(data)
  started = clock

  begin
    response = handler.call
  rescue Exception => exception # rubocop:disable Lint/RescueException
    # Re-raised immediately and unchanged; the rescue exists only so a
    # failing request still gets a completed row rather than a stuck one.
    finish(record, data, started: started, exception: exception)
    raise
  end

  finish(record, data, started: started, response: response)
  response
end