mcpspan 0.3.0 → 0.5.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 +29 -2
- data/lib/mcpspan/argument_checks.rb +119 -0
- data/lib/mcpspan/collector.rb +18 -4
- data/lib/mcpspan/definitions.rb +21 -2
- data/lib/mcpspan/event.rb +4 -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 +5 -2
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 809dac003b7d4f882b23c28d717c060123f019b3dd833a9df61bc868ba7c6445
|
|
4
|
+
data.tar.gz: 03b8898e5e333f995e9f10b9512bf35d658356e6264eeffd62a723bbdaa55aec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 98b5fa6edb27bddcb8d9c06ba77a6f4e5715ef0923339a15a49bc329dbc521826887cc34b9474b0c3b2efa2bffdba7e334cb015e51d57956cc2acab919a91d84
|
|
7
|
+
data.tar.gz: ec137d06a3c7b5b08f3e55fe6ae930cc35cb0860a07e4fcfbde699e807e52b529ddd476e40b9a93f82ce82a7800ef78372b28868e95cfe87e770efebab4d6a90
|
data/README.md
CHANGED
|
@@ -142,6 +142,14 @@ refused call carries no message, because validation text can quote back what
|
|
|
142
142
|
the agent sent. Whether a call reached your tool is observed directly, not
|
|
143
143
|
read from the server's wording.
|
|
144
144
|
|
|
145
|
+
For a refusal of arguments, the SDK also says which ones. It checks what the
|
|
146
|
+
agent sent against the input schema your server listed, in your process, and
|
|
147
|
+
records the top-level arguments that did not match, by the names the schema
|
|
148
|
+
declares, so the tool's page can show that 29 refusals were all `passengers`.
|
|
149
|
+
Values are never sent, and neither is a name the agent made up. It works from
|
|
150
|
+
the latest `tools/list` your server answered in the same process; a refusal
|
|
151
|
+
before any listing names nothing.
|
|
152
|
+
|
|
145
153
|
## Privacy
|
|
146
154
|
|
|
147
155
|
**Parameter values never leave your process.** Not by default, not in any
|
|
@@ -149,8 +157,14 @@ mode, not in debug.
|
|
|
149
157
|
|
|
150
158
|
What is collected: the tool name, how long it took, whether it succeeded, the
|
|
151
159
|
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
|
-
|
|
160
|
+
in bytes (its size only, never its content), whether it repeated the previous
|
|
161
|
+
call's arguments to the same tool in its session (compared in your process;
|
|
162
|
+
the arguments, or any digest of them, never leave it), a fingerprint of the
|
|
163
|
+
tool's definition as your server lists it (its name, title, description and
|
|
164
|
+
input schema, hashed, so the dashboard can mark when you changed it), for a call your
|
|
165
|
+
server refused for its arguments the names of those that did not match the
|
|
166
|
+
tool's schema (never what was sent), which
|
|
167
|
+
client called, and the SDK version. For a resource or a prompt, the same, under the name it was
|
|
154
168
|
registered with: never the address a client read, only its template or, for
|
|
155
169
|
an address the server does not have, its scheme.
|
|
156
170
|
|
|
@@ -166,6 +180,18 @@ called with `destination` and never with `departure_date` tells you your tool
|
|
|
166
180
|
description is not landing. Knowing which destination tells you nothing you
|
|
167
181
|
needed, and puts your users' data somewhere it does not belong.
|
|
168
182
|
|
|
183
|
+
Error messages are sent, cut short, because they are usually what says why a
|
|
184
|
+
call failed. If your tools can fail with text you would not send anywhere, as
|
|
185
|
+
a tool that runs commands or reads files might quote a path or a token, turn
|
|
186
|
+
them off:
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
McpSpan.instrument(server, capture_error_messages: false)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Every failure is still recorded, with where it came from and the exception's
|
|
193
|
+
class. Only the text is left out.
|
|
194
|
+
|
|
169
195
|
## Self-hosting
|
|
170
196
|
|
|
171
197
|
```ruby
|
|
@@ -207,6 +233,7 @@ Keywords of `McpSpan.instrument` and `McpSpan.configure`.
|
|
|
207
233
|
| `api_key` | `MCPSPAN_API_KEY` | Identifies your server. Without it, nothing is collected. |
|
|
208
234
|
| `endpoint` | `MCPSPAN_ENDPOINT`; none | Your mcpspan installation. Nothing is collected without it. |
|
|
209
235
|
| `capture_parameter_names` | `false` | Records parameter names and types, never values. |
|
|
236
|
+
| `capture_error_messages` | `true` | Sends the text of a failure, cut short. Off sends that it failed and how, without the text. |
|
|
210
237
|
| `server_version` | `MCPSPAN_SERVER_VERSION`, then the server's own | The version to record calls under: a release, a tag, a commit. |
|
|
211
238
|
| `debug` | `false` | Writes delivery diagnostics to standard error. |
|
|
212
239
|
| `on_diagnostic` | - | Receives diagnostics instead. Implies `debug`. |
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module McpSpan
|
|
6
|
+
# Which top-level arguments of a refused call did not match the tool's input schema (contract, 3.10). The server's
|
|
7
|
+
# own refusal is not read: each validation library words it differently, and some quote the value the agent sent.
|
|
8
|
+
# The arguments are checked here instead, against the schema the server listed, by a small set of rules that never
|
|
9
|
+
# fail what they do not understand. Only names the schema declares come out, so nothing the client made up, and no
|
|
10
|
+
# value, is sent.
|
|
11
|
+
module ArgumentChecks
|
|
12
|
+
# Names sent at most, per call.
|
|
13
|
+
MAX_NAMES = 20
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
# The declared names whose arguments fail the schema, sorted, at most twenty. Both are read the way they were
|
|
17
|
+
# sent, through JSON, so symbols and strings alike. Never raises.
|
|
18
|
+
def invalid(schema, arguments)
|
|
19
|
+
rules = plain(schema)
|
|
20
|
+
return [] unless rules.is_a?(Hash)
|
|
21
|
+
|
|
22
|
+
values = arguments.nil? ? {} : plain(arguments)
|
|
23
|
+
return [] unless values.is_a?(Hash)
|
|
24
|
+
|
|
25
|
+
names = []
|
|
26
|
+
required = rules["required"]
|
|
27
|
+
names.concat(required.select { |name| name.is_a?(String) && !values.key?(name) }) if required.is_a?(Array)
|
|
28
|
+
properties = rules["properties"]
|
|
29
|
+
if properties.is_a?(Hash)
|
|
30
|
+
properties.each do |name, rule|
|
|
31
|
+
names << name if values.key?(name) && !matches?(rule, values[name])
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
names.uniq.sort.first(MAX_NAMES)
|
|
35
|
+
rescue StandardError
|
|
36
|
+
[]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def plain(value)
|
|
42
|
+
JSON.parse(JSON.generate(value))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Whether a value passes a schema under the checks the contract lists, and only those.
|
|
46
|
+
def matches?(schema, value)
|
|
47
|
+
return false if schema == false
|
|
48
|
+
return true unless schema.is_a?(Hash)
|
|
49
|
+
|
|
50
|
+
type = schema["type"]
|
|
51
|
+
return false if type.is_a?(String) && !type?(type, value)
|
|
52
|
+
return false if type.is_a?(Array) && type.all?(String) && type.none? { |name| type?(name, value) }
|
|
53
|
+
|
|
54
|
+
allowed = schema["enum"]
|
|
55
|
+
if allowed.is_a?(Array)
|
|
56
|
+
sent = Definitions.canonical_text(value)
|
|
57
|
+
return false if allowed.none? { |option| Definitions.canonical_text(option) == sent }
|
|
58
|
+
end
|
|
59
|
+
if schema.key?("const") && Definitions.canonical_text(schema["const"]) != Definitions.canonical_text(value)
|
|
60
|
+
return false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
within?(schema, value)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The bounds on numbers, lengths and counts, and what objects and arrays hold.
|
|
67
|
+
def within?(schema, value)
|
|
68
|
+
bound = ->(name) { number?(schema[name]) ? schema[name] : nil }
|
|
69
|
+
if number?(value)
|
|
70
|
+
return false if bound.call("minimum")&.then { |minimum| value < minimum }
|
|
71
|
+
return false if bound.call("maximum")&.then { |maximum| value > maximum }
|
|
72
|
+
return false if bound.call("exclusiveMinimum")&.then { |above| value <= above }
|
|
73
|
+
return false if bound.call("exclusiveMaximum")&.then { |below| value >= below }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
case value
|
|
77
|
+
when String
|
|
78
|
+
# A Ruby string's length is in characters, which in UTF-8 are code points.
|
|
79
|
+
return false if bound.call("minLength")&.then { |shortest| value.length < shortest }
|
|
80
|
+
return false if bound.call("maxLength")&.then { |longest| value.length > longest }
|
|
81
|
+
when Array
|
|
82
|
+
return false if bound.call("minItems")&.then { |fewest| value.length < fewest }
|
|
83
|
+
return false if bound.call("maxItems")&.then { |most| value.length > most }
|
|
84
|
+
|
|
85
|
+
each = schema["items"]
|
|
86
|
+
checked = each.is_a?(Hash) || [true, false].include?(each)
|
|
87
|
+
return false if checked && !value.all? { |item| matches?(each, item) }
|
|
88
|
+
when Hash
|
|
89
|
+
required = schema["required"]
|
|
90
|
+
return false if required.is_a?(Array) && required.any? { |name| name.is_a?(String) && !value.key?(name) }
|
|
91
|
+
|
|
92
|
+
properties = schema["properties"]
|
|
93
|
+
if properties.is_a?(Hash) && properties.any? { |name, rule| value.key?(name) && !matches?(rule, value[name]) }
|
|
94
|
+
return false
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
true
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def type?(type, value)
|
|
101
|
+
case type
|
|
102
|
+
when "string" then value.is_a?(String)
|
|
103
|
+
when "number" then number?(value)
|
|
104
|
+
when "integer" then number?(value) && value == value.truncate
|
|
105
|
+
when "boolean" then [true, false].include?(value)
|
|
106
|
+
when "object" then value.is_a?(Hash)
|
|
107
|
+
when "array" then value.is_a?(Array)
|
|
108
|
+
when "null" then value.nil?
|
|
109
|
+
# A type this list does not know is not checked.
|
|
110
|
+
else true
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def number?(value)
|
|
115
|
+
value.is_a?(Integer) || (value.is_a?(Float) && value.finite?)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
data/lib/mcpspan/collector.rb
CHANGED
|
@@ -14,13 +14,14 @@ module McpSpan
|
|
|
14
14
|
MAX_EVENTS_PER_REQUEST = 1_000
|
|
15
15
|
|
|
16
16
|
SETTINGS = %i[
|
|
17
|
-
api_key endpoint capture_parameter_names server_version debug on_diagnostic
|
|
18
|
-
max_batch_size max_queue_size
|
|
17
|
+
api_key endpoint capture_parameter_names capture_error_messages server_version debug on_diagnostic
|
|
18
|
+
flush_on_exit flush_interval max_batch_size max_queue_size
|
|
19
19
|
].freeze
|
|
20
20
|
|
|
21
21
|
# What an integration knows about one call as it starts. A kind of nil is a tool call.
|
|
22
|
+
# `arguments` are kept as sent, by reference, only to tell which a refusal was over (contract, 3.10).
|
|
22
23
|
Call = Struct.new(:tool_name, :parameters, :client_name, :client_version, :server_version, :session_id, :started,
|
|
23
|
-
:timestamp, :kind, keyword_init: true,)
|
|
24
|
+
:timestamp, :kind, :repeated, :arguments, keyword_init: true,)
|
|
24
25
|
|
|
25
26
|
@lock = Mutex.new
|
|
26
27
|
@reporter = nil
|
|
@@ -101,6 +102,7 @@ module McpSpan
|
|
|
101
102
|
started: started,
|
|
102
103
|
timestamp: timestamp,
|
|
103
104
|
kind: kind,
|
|
105
|
+
arguments: arguments,
|
|
104
106
|
)
|
|
105
107
|
end
|
|
106
108
|
|
|
@@ -123,8 +125,11 @@ module McpSpan
|
|
|
123
125
|
def record(call, success:, source: nil, type: nil, message: nil, response: nil)
|
|
124
126
|
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - call.started) * 1000.0
|
|
125
127
|
reporter = @reporter
|
|
126
|
-
|
|
128
|
+
settings = @settings
|
|
129
|
+
return if reporter.nil? || settings.nil?
|
|
127
130
|
|
|
131
|
+
# The text of a failure, unless the developer chose to send none (contract, 5).
|
|
132
|
+
message = nil unless settings[:capture_error_messages]
|
|
128
133
|
reporter.record(Event.new(
|
|
129
134
|
id: SecureRandom.uuid,
|
|
130
135
|
kind: call.kind,
|
|
@@ -141,6 +146,8 @@ module McpSpan
|
|
|
141
146
|
response_bytes: response_bytes(response),
|
|
142
147
|
# A tool the server has, refused arguments included: often the schema is why.
|
|
143
148
|
definition_hash: call.kind.nil? && source != Source::UNKNOWN_TOOL ? Definitions.of(call.tool_name) : nil,
|
|
149
|
+
repeated: call.kind.nil? && call.repeated ? true : nil,
|
|
150
|
+
invalid_arguments: call.kind.nil? && source == Source::ARGUMENTS ? refused_names(call) : nil,
|
|
144
151
|
timestamp: call.timestamp.strftime("%Y-%m-%dT%H:%M:%S.%LZ"),
|
|
145
152
|
session_id: call.session_id,
|
|
146
153
|
parameters: call.parameters,
|
|
@@ -149,6 +156,12 @@ module McpSpan
|
|
|
149
156
|
nil
|
|
150
157
|
end
|
|
151
158
|
|
|
159
|
+
# Which declared arguments a refusal was over (contract, 3.10), or nil when none were found.
|
|
160
|
+
def refused_names(call)
|
|
161
|
+
names = ArgumentChecks.invalid(Definitions.schema_of(call.tool_name), call.arguments)
|
|
162
|
+
names.empty? ? nil : names.map { |name| Text.truncate(name, Text::MAX_NAME) }
|
|
163
|
+
end
|
|
164
|
+
|
|
152
165
|
# For tests: forgets that the missing endpoint was already mentioned.
|
|
153
166
|
def forget_no_endpoint_notice
|
|
154
167
|
@lock.synchronize { @said_no_endpoint = false }
|
|
@@ -211,6 +224,7 @@ module McpSpan
|
|
|
211
224
|
api_key: first_set(settings[:api_key], ENV.fetch("MCPSPAN_API_KEY", nil)),
|
|
212
225
|
endpoint: first_set(settings[:endpoint], ENV.fetch("MCPSPAN_ENDPOINT", nil)),
|
|
213
226
|
capture_parameter_names: settings[:capture_parameter_names] ? true : false,
|
|
227
|
+
capture_error_messages: settings.fetch(:capture_error_messages, true) ? true : false,
|
|
214
228
|
server_version: first_set(settings[:server_version], ENV.fetch("MCPSPAN_SERVER_VERSION", nil)),
|
|
215
229
|
debug: debug,
|
|
216
230
|
on_diagnostic: on_diagnostic,
|
data/lib/mcpspan/definitions.rb
CHANGED
|
@@ -14,6 +14,8 @@ module McpSpan
|
|
|
14
14
|
"\t" => "\\t", }.freeze
|
|
15
15
|
|
|
16
16
|
@listed = {}
|
|
17
|
+
# Each tool's input schema as last listed, to tell which arguments a refusal was over (contract, 3.10).
|
|
18
|
+
@schemas = {}
|
|
17
19
|
@lock = Mutex.new
|
|
18
20
|
|
|
19
21
|
class << self
|
|
@@ -22,13 +24,21 @@ module McpSpan
|
|
|
22
24
|
@lock.synchronize { @listed[tool_name] }
|
|
23
25
|
end
|
|
24
26
|
|
|
27
|
+
# The latest input schema listed for a tool, or nil when no listing in this process named it.
|
|
28
|
+
def schema_of(tool_name)
|
|
29
|
+
@lock.synchronize { @schemas[tool_name] }
|
|
30
|
+
end
|
|
31
|
+
|
|
25
32
|
# Notes every tool in a listing, in whatever key style the gem keeps them. Never raises.
|
|
26
33
|
def note(tools)
|
|
27
34
|
JSON.parse(JSON.generate(Array(tools))).each do |tool|
|
|
28
35
|
next unless tool.is_a?(Hash) && tool["name"].is_a?(String)
|
|
29
36
|
|
|
30
37
|
fingerprint = hash(tool)
|
|
31
|
-
@lock.synchronize
|
|
38
|
+
@lock.synchronize do
|
|
39
|
+
@listed[tool["name"]] = fingerprint if fingerprint
|
|
40
|
+
@schemas[tool["name"]] = tool["inputSchema"]
|
|
41
|
+
end
|
|
32
42
|
end
|
|
33
43
|
rescue StandardError
|
|
34
44
|
nil
|
|
@@ -36,7 +46,10 @@ module McpSpan
|
|
|
36
46
|
|
|
37
47
|
# For tests: forgets every listing.
|
|
38
48
|
def forget
|
|
39
|
-
@lock.synchronize
|
|
49
|
+
@lock.synchronize do
|
|
50
|
+
@listed.clear
|
|
51
|
+
@schemas.clear
|
|
52
|
+
end
|
|
40
53
|
end
|
|
41
54
|
|
|
42
55
|
# The first 16 hex characters of the SHA-256 of the tool's name, title, description and input schema, as
|
|
@@ -48,6 +61,12 @@ module McpSpan
|
|
|
48
61
|
nil
|
|
49
62
|
end
|
|
50
63
|
|
|
64
|
+
# A value as canonical JSON (contract, 3.8), read the way it was sent: through JSON, so symbols and strings
|
|
65
|
+
# alike. Raises on what cannot be written so.
|
|
66
|
+
def canonical_text(value)
|
|
67
|
+
canonical(JSON.parse(JSON.generate(value)))
|
|
68
|
+
end
|
|
69
|
+
|
|
51
70
|
private
|
|
52
71
|
|
|
53
72
|
# 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
|
+
:invalid_arguments, :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,8 @@ module McpSpan
|
|
|
27
27
|
serverVersion: server_version,
|
|
28
28
|
responseBytes: response_bytes,
|
|
29
29
|
definitionHash: definition_hash,
|
|
30
|
+
repeated: repeated,
|
|
31
|
+
invalidArguments: invalid_arguments,
|
|
30
32
|
timestamp: timestamp,
|
|
31
33
|
sdkVersion: VERSION,
|
|
32
34
|
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,8 @@ 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/argument_checks"
|
|
11
|
+
require_relative "mcpspan/repeats"
|
|
10
12
|
require_relative "mcpspan/instrumentation"
|
|
11
13
|
require_relative "mcpspan/primitives"
|
|
12
14
|
|
|
@@ -18,8 +20,9 @@ require_relative "mcpspan/primitives"
|
|
|
18
20
|
# Without an API key nothing is collected and nothing is sent. Parameter values never leave the process.
|
|
19
21
|
#
|
|
20
22
|
# Settings: +api_key+ (else +MCPSPAN_API_KEY+), +endpoint+, your mcpspan installation (else +MCPSPAN_ENDPOINT+; no
|
|
21
|
-
# default, and nothing is collected without it), +capture_parameter_names+, +
|
|
22
|
-
# +flush_on_exit+, +flush_interval+ (seconds), +max_batch_size+, +max_queue_size+.
|
|
23
|
+
# default, and nothing is collected without it), +capture_parameter_names+, +capture_error_messages+ (on unless
|
|
24
|
+
# false), +debug+, +on_diagnostic+, +flush_on_exit+, +flush_interval+ (seconds), +max_batch_size+, +max_queue_size+.
|
|
25
|
+
# Nothing here raises over a setting.
|
|
23
26
|
module McpSpan
|
|
24
27
|
@excluded_names = Set.new
|
|
25
28
|
|
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.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kacper Zatoń
|
|
@@ -19,11 +19,13 @@ files:
|
|
|
19
19
|
- LICENSE
|
|
20
20
|
- README.md
|
|
21
21
|
- lib/mcpspan.rb
|
|
22
|
+
- lib/mcpspan/argument_checks.rb
|
|
22
23
|
- lib/mcpspan/collector.rb
|
|
23
24
|
- lib/mcpspan/definitions.rb
|
|
24
25
|
- lib/mcpspan/event.rb
|
|
25
26
|
- lib/mcpspan/instrumentation.rb
|
|
26
27
|
- lib/mcpspan/primitives.rb
|
|
28
|
+
- lib/mcpspan/repeats.rb
|
|
27
29
|
- lib/mcpspan/reporter.rb
|
|
28
30
|
- lib/mcpspan/text.rb
|
|
29
31
|
- lib/mcpspan/transport.rb
|