mcpspan 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +4 -2
- data/lib/mcpspan/collector.rb +2 -1
- data/lib/mcpspan/definitions.rb +6 -0
- data/lib/mcpspan/event.rb +3 -2
- data/lib/mcpspan/instrumentation.rb +4 -0
- data/lib/mcpspan/repeats.rb +50 -0
- data/lib/mcpspan/version.rb +1 -1
- data/lib/mcpspan.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a62e8c7de0a3fe8feed4683fa2a3aaa3b307c317f69d3f42091e62dbf32f05a7
|
|
4
|
+
data.tar.gz: 719d417a432bb75fdb3437d1e043c63d95754ed056d047fff42f23a28e3ac5c2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c595aa5e4e7416e2e0ea941ab5d032d9acf349c582731fa49a5c93859863a3210e4984aeef330c57a644ad9b8ba95c786ab21c2e7f3cfe67bb9e5938adc0f2fc
|
|
7
|
+
data.tar.gz: '078ec63e4be0c64dbe694a2f6fca21a9a7bda88f48ace0e9ee4a371ee1c7c9e77150d4dd52637012d62fb0cbedc7a93ba0613ee4dde7429c139f7b51039ef752'
|
data/README.md
CHANGED
|
@@ -149,8 +149,10 @@ mode, not in debug.
|
|
|
149
149
|
|
|
150
150
|
What is collected: the tool name, how long it took, whether it succeeded, the
|
|
151
151
|
error type and a truncated message when it did not, how large the answer was
|
|
152
|
-
in bytes (its size only, never its content),
|
|
153
|
-
|
|
152
|
+
in bytes (its size only, never its content), whether it repeated the previous
|
|
153
|
+
call's arguments to the same tool in its session (compared in your process;
|
|
154
|
+
the arguments, or any digest of them, never leave it), which client called,
|
|
155
|
+
and the SDK version. For a resource or a prompt, the same, under the name it was
|
|
154
156
|
registered with: never the address a client read, only its template or, for
|
|
155
157
|
an address the server does not have, its scheme.
|
|
156
158
|
|
data/lib/mcpspan/collector.rb
CHANGED
|
@@ -20,7 +20,7 @@ module McpSpan
|
|
|
20
20
|
|
|
21
21
|
# What an integration knows about one call as it starts. A kind of nil is a tool call.
|
|
22
22
|
Call = Struct.new(:tool_name, :parameters, :client_name, :client_version, :server_version, :session_id, :started,
|
|
23
|
-
:timestamp, :kind, keyword_init: true,)
|
|
23
|
+
:timestamp, :kind, :repeated, keyword_init: true,)
|
|
24
24
|
|
|
25
25
|
@lock = Mutex.new
|
|
26
26
|
@reporter = nil
|
|
@@ -141,6 +141,7 @@ module McpSpan
|
|
|
141
141
|
response_bytes: response_bytes(response),
|
|
142
142
|
# A tool the server has, refused arguments included: often the schema is why.
|
|
143
143
|
definition_hash: call.kind.nil? && source != Source::UNKNOWN_TOOL ? Definitions.of(call.tool_name) : nil,
|
|
144
|
+
repeated: call.kind.nil? && call.repeated ? true : nil,
|
|
144
145
|
timestamp: call.timestamp.strftime("%Y-%m-%dT%H:%M:%S.%LZ"),
|
|
145
146
|
session_id: call.session_id,
|
|
146
147
|
parameters: call.parameters,
|
data/lib/mcpspan/definitions.rb
CHANGED
|
@@ -48,6 +48,12 @@ module McpSpan
|
|
|
48
48
|
nil
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
# A value as canonical JSON (contract, 3.8), read the way it was sent: through JSON, so symbols and strings
|
|
52
|
+
# alike. Raises on what cannot be written so.
|
|
53
|
+
def canonical_text(value)
|
|
54
|
+
canonical(JSON.parse(JSON.generate(value)))
|
|
55
|
+
end
|
|
56
|
+
|
|
51
57
|
private
|
|
52
58
|
|
|
53
59
|
# Sorted keys, no whitespace, minimal escaping: the same text in every SDK.
|
data/lib/mcpspan/event.rb
CHANGED
|
@@ -6,8 +6,8 @@ module McpSpan
|
|
|
6
6
|
# One call of a tool, a resource or a prompt, in the shape the ingest API takes. Parameter values are never in it.
|
|
7
7
|
Event = Struct.new(
|
|
8
8
|
:id, :kind, :tool_name, :duration_ms, :success, :error_source, :error_type, :error_message,
|
|
9
|
-
:client_type, :client_name, :client_version, :server_version, :response_bytes, :definition_hash, :
|
|
10
|
-
:session_id, :parameters,
|
|
9
|
+
:client_type, :client_name, :client_version, :server_version, :response_bytes, :definition_hash, :repeated,
|
|
10
|
+
:timestamp, :session_id, :parameters,
|
|
11
11
|
keyword_init: true,
|
|
12
12
|
) do
|
|
13
13
|
# The event as the API takes it, leaving absent fields out rather than sending them as null.
|
|
@@ -27,6 +27,7 @@ module McpSpan
|
|
|
27
27
|
serverVersion: server_version,
|
|
28
28
|
responseBytes: response_bytes,
|
|
29
29
|
definitionHash: definition_hash,
|
|
30
|
+
repeated: repeated,
|
|
30
31
|
timestamp: timestamp,
|
|
31
32
|
sdkVersion: VERSION,
|
|
32
33
|
sessionId: session_id,
|
|
@@ -177,6 +177,10 @@ module McpSpan
|
|
|
177
177
|
server_version: Instrumentation.server_version(self),
|
|
178
178
|
**Instrumentation.client(envelope, session, self),
|
|
179
179
|
)
|
|
180
|
+
# Compared once, as the request arrives, before any validation (contract, 3.9).
|
|
181
|
+
if call&.session_id && !Repeats.continues_earlier_call?(request)
|
|
182
|
+
call.repeated = Repeats.note(call.session_id, name, request[:arguments])
|
|
183
|
+
end
|
|
180
184
|
[call, tool]
|
|
181
185
|
rescue *INTERNAL
|
|
182
186
|
nil
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module McpSpan
|
|
6
|
+
# Whether a call repeats the previous call to the same tool in the same session (contract, 3.9): an agent stuck in a
|
|
7
|
+
# loop. Only the answer leaves the process. Kept here is a SHA-256 of the canonical arguments of the latest call per
|
|
8
|
+
# session and tool, never sent: a digest of a short identifier or an enumerated value is found by trying every one.
|
|
9
|
+
module Repeats
|
|
10
|
+
# Session and tool pairs kept, the oldest forgotten first.
|
|
11
|
+
MAX_KEPT = 10_000
|
|
12
|
+
|
|
13
|
+
# Insertion-ordered: a pair noted again is moved to the end, so the first is always the longest unused.
|
|
14
|
+
@latest = {}
|
|
15
|
+
@lock = Mutex.new
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
# Notes a call's arguments, as the client sent them, and says whether they are the previous call's to the same
|
|
19
|
+
# tool in the same session. Arguments that cannot be written down are never a repeat.
|
|
20
|
+
def note(session_id, tool_name, arguments)
|
|
21
|
+
digest = Digest::SHA256.digest(Definitions.canonical_text(arguments || {}))
|
|
22
|
+
key = [session_id, tool_name.to_s]
|
|
23
|
+
@lock.synchronize do
|
|
24
|
+
previous = @latest.delete(key)
|
|
25
|
+
@latest[key] = digest
|
|
26
|
+
@latest.shift if @latest.size > MAX_KEPT
|
|
27
|
+
previous == digest
|
|
28
|
+
end
|
|
29
|
+
rescue StandardError
|
|
30
|
+
false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Whether a call answers an interim result's question (2026-07-28): it then continues that call, and is neither
|
|
34
|
+
# compared nor kept.
|
|
35
|
+
def continues_earlier_call?(request)
|
|
36
|
+
return false unless request.is_a?(Hash)
|
|
37
|
+
|
|
38
|
+
%i[inputResponses requestState].any? do |name|
|
|
39
|
+
value = request[name] || request[name.to_s]
|
|
40
|
+
!value.nil? && value != ""
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# For tests: forgets every call.
|
|
45
|
+
def forget
|
|
46
|
+
@lock.synchronize { @latest.clear }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/mcpspan/version.rb
CHANGED
data/lib/mcpspan.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "mcpspan/transport"
|
|
|
7
7
|
require_relative "mcpspan/reporter"
|
|
8
8
|
require_relative "mcpspan/collector"
|
|
9
9
|
require_relative "mcpspan/definitions"
|
|
10
|
+
require_relative "mcpspan/repeats"
|
|
10
11
|
require_relative "mcpspan/instrumentation"
|
|
11
12
|
require_relative "mcpspan/primitives"
|
|
12
13
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mcpspan
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kacper Zatoń
|
|
@@ -24,6 +24,7 @@ files:
|
|
|
24
24
|
- lib/mcpspan/event.rb
|
|
25
25
|
- lib/mcpspan/instrumentation.rb
|
|
26
26
|
- lib/mcpspan/primitives.rb
|
|
27
|
+
- lib/mcpspan/repeats.rb
|
|
27
28
|
- lib/mcpspan/reporter.rb
|
|
28
29
|
- lib/mcpspan/text.rb
|
|
29
30
|
- lib/mcpspan/transport.rb
|