Class: McpLogs::Recorder
- Inherits:
-
Object
- Object
- McpLogs::Recorder
- 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
- #call(data, &handler) ⇒ Object
-
#initialize(server_name: nil) ⇒ Recorder
constructor
A new instance of Recorder.
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 |