mcpspan 0.1.0__tar.gz

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.
Files changed (39) hide show
  1. mcpspan-0.1.0/.gitignore +59 -0
  2. mcpspan-0.1.0/LICENSE +21 -0
  3. mcpspan-0.1.0/PKG-INFO +309 -0
  4. mcpspan-0.1.0/README.md +287 -0
  5. mcpspan-0.1.0/pyproject.toml +94 -0
  6. mcpspan-0.1.0/src/mcpspan/__init__.py +34 -0
  7. mcpspan-0.1.0/src/mcpspan/_call.py +49 -0
  8. mcpspan-0.1.0/src/mcpspan/_client.py +148 -0
  9. mcpspan-0.1.0/src/mcpspan/_config.py +276 -0
  10. mcpspan-0.1.0/src/mcpspan/_failure.py +110 -0
  11. mcpspan-0.1.0/src/mcpspan/_fastmcp.py +297 -0
  12. mcpspan-0.1.0/src/mcpspan/_instrument.py +82 -0
  13. mcpspan-0.1.0/src/mcpspan/_marks.py +51 -0
  14. mcpspan-0.1.0/src/mcpspan/_official.py +240 -0
  15. mcpspan-0.1.0/src/mcpspan/_parameters.py +58 -0
  16. mcpspan-0.1.0/src/mcpspan/_primitives.py +363 -0
  17. mcpspan-0.1.0/src/mcpspan/_queue.py +63 -0
  18. mcpspan-0.1.0/src/mcpspan/_reporter.py +290 -0
  19. mcpspan-0.1.0/src/mcpspan/_session.py +89 -0
  20. mcpspan-0.1.0/src/mcpspan/_track.py +390 -0
  21. mcpspan-0.1.0/src/mcpspan/_transport.py +152 -0
  22. mcpspan-0.1.0/src/mcpspan/_types.py +54 -0
  23. mcpspan-0.1.0/src/mcpspan/_version.py +6 -0
  24. mcpspan-0.1.0/src/mcpspan/py.typed +0 -0
  25. mcpspan-0.1.0/tests/__init__.py +0 -0
  26. mcpspan-0.1.0/tests/conftest.py +28 -0
  27. mcpspan-0.1.0/tests/test_client.py +82 -0
  28. mcpspan-0.1.0/tests/test_config.py +142 -0
  29. mcpspan-0.1.0/tests/test_failure.py +70 -0
  30. mcpspan-0.1.0/tests/test_fastmcp.py +265 -0
  31. mcpspan-0.1.0/tests/test_official.py +411 -0
  32. mcpspan-0.1.0/tests/test_parameters.py +45 -0
  33. mcpspan-0.1.0/tests/test_queue.py +45 -0
  34. mcpspan-0.1.0/tests/test_readme.py +48 -0
  35. mcpspan-0.1.0/tests/test_reporter.py +222 -0
  36. mcpspan-0.1.0/tests/test_session.py +48 -0
  37. mcpspan-0.1.0/tests/test_track.py +257 -0
  38. mcpspan-0.1.0/tests/test_transport.py +127 -0
  39. mcpspan-0.1.0/uv.lock +2489 -0
@@ -0,0 +1,59 @@
1
+ node_modules/
2
+ dist/
3
+ .next/
4
+ next-env.d.ts
5
+ *.tsbuildinfo
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+ *.log
10
+
11
+ # Database dumps. Somebody's data, and often a lot of it.
12
+ backups/
13
+ .DS_Store
14
+
15
+
16
+ # Python
17
+ .venv/
18
+ __pycache__/
19
+ .pytest_cache/
20
+ .mypy_cache/
21
+ .ruff_cache/
22
+
23
+ # Go conformance adapters, built before the suite runs
24
+ conformance/adapters/go-*/adapter
25
+
26
+ # .NET build output
27
+ bin/
28
+ obj/
29
+ conformance/adapters/dotnet/out/
30
+
31
+ # Throwaway twins for comparing SDKs against the dashboard
32
+ conformance/.criterion/
33
+
34
+ # Gradle
35
+ .gradle/
36
+ packages/mcpspan-jvm/**/build/
37
+ conformance/adapters/java/build/
38
+ conformance/adapters/kotlin/build/
39
+
40
+ # Cargo
41
+ packages/mcpspan-rust/target/
42
+ conformance/adapters/rust/target/
43
+
44
+ # Bundler
45
+ packages/mcpspan-ruby/Gemfile.lock
46
+ packages/mcpspan-ruby/vendor/
47
+ packages/mcpspan-ruby/*.gem
48
+ conformance/adapters/ruby/Gemfile.lock
49
+ conformance/adapters/ruby/vendor/
50
+ conformance/adapters/ruby/.bundle/
51
+
52
+ # Composer
53
+ packages/mcpspan-php/vendor/
54
+ packages/mcpspan-php/composer.lock
55
+ packages/mcpspan-php/.php-cs-fixer.cache
56
+ packages/mcpspan-php/.phpunit.cache/
57
+ conformance/adapters/php-*/vendor/
58
+ conformance/adapters/php-*/composer.lock
59
+ packages/mcpspan-php/.phpunit.result.cache
mcpspan-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kacper Zatoń
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
mcpspan-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,309 @@
1
+ Metadata-Version: 2.5
2
+ Name: mcpspan
3
+ Version: 0.1.0
4
+ Summary: Self-hosted analytics for MCP servers: which tools, resources and prompts get used, by which client, how fast, and why they fail.
5
+ Project-URL: Homepage, https://github.com/mcpspan/mcpspan
6
+ Project-URL: Documentation, https://github.com/mcpspan/mcpspan/tree/main/packages/mcpspan-python#readme
7
+ Project-URL: Repository, https://github.com/mcpspan/mcpspan
8
+ Project-URL: Issues, https://github.com/mcpspan/mcpspan/issues
9
+ Author: Kacper Zatoń
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: ai-agents,analytics,mcp,mcp-server,metrics,model-context-protocol,monitoring,observability,telemetry,tool-calls
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Topic :: System :: Monitoring
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+
23
+ # mcpspan for Python
24
+
25
+ Analytics for MCP servers. Find out which of your tools get called, by which
26
+ client, how long they take, and which ones fail.
27
+
28
+ Your own logs tell you a tool ran. This tells you whether it was Claude,
29
+ Cursor, or something you have not heard of, how that call compares to the
30
+ other nine hundred, and whether the failures are your tool breaking or your
31
+ tool politely saying no.
32
+
33
+ ## Install
34
+
35
+ ```sh
36
+ pip install mcpspan
37
+ ```
38
+
39
+ Needs Python 3.10 or newer. No dependencies. Works with the official MCP SDK,
40
+ `mcp` 1.30+ (`FastMCP`) and 2.2+ (`MCPServer`), and with FastMCP 4+ (the
41
+ `fastmcp` package).
42
+
43
+ ## Use
44
+
45
+ One line, anywhere before the server starts:
46
+
47
+ ```python
48
+ import os
49
+
50
+ import mcpspan
51
+ from mcp.server.fastmcp import FastMCP
52
+
53
+ mcp = FastMCP("flights")
54
+
55
+
56
+ @mcp.tool()
57
+ def search_flights(destination: str) -> str:
58
+ return f"Found 3 flights to {destination}"
59
+
60
+
61
+ mcpspan.instrument(
62
+ mcp,
63
+ api_key=os.environ.get("MCPSPAN_API_KEY"),
64
+ endpoint="http://localhost:6271", # your mcpspan installation
65
+ )
66
+
67
+ if __name__ == "__main__":
68
+ mcp.run()
69
+ ```
70
+
71
+ Every tool on the server is measured, whether it was registered before that
72
+ line or after. Nothing about how you write tools changes: the schema the
73
+ server builds from your function is untouched, and your tool returns and
74
+ raises exactly what it did before.
75
+
76
+ The same line works on v2 of the MCP SDK and on FastMCP:
77
+
78
+ ```python
79
+ from mcp.server.mcpserver import MCPServer
80
+
81
+ mcp = MCPServer("flights")
82
+ mcpspan.instrument(mcp)
83
+ ```
84
+
85
+ ```python
86
+ from fastmcp import FastMCP
87
+
88
+ mcp = FastMCP("flights")
89
+ mcpspan.instrument(mcp)
90
+ ```
91
+
92
+ With no `api_key` given, it is read from `MCPSPAN_API_KEY`.
93
+
94
+ ### Sessions and clients
95
+
96
+ Calls are grouped into sessions when there is a connection to group them by:
97
+ a stdio process, or an HTTP transport that hands out session IDs. A stateless
98
+ HTTP endpoint, and every endpoint on the 2026-07-28 protocol, which dropped
99
+ sessions, records calls without one.
100
+
101
+ The client is read from the call itself on 2026-07-28, where each request
102
+ names its client, and from the handshake on 2025-11-25. A stateless
103
+ 2025-11-25 HTTP endpoint has no handshake to read, so its calls are recorded
104
+ with an unknown client rather than a guessed one.
105
+
106
+ A tool that asks the client for more before it can finish is one call however
107
+ many round trips that takes. The interim answer asking for input is not
108
+ counted; the one that ends the call is.
109
+
110
+ ### Without a key
111
+
112
+ If there is no key, nothing is collected and nothing is sent, and no thread is
113
+ started. Wrapped tools return before reading the clock. That makes it safe to
114
+ leave in place in tests, in CI, and in a fork somebody is only reading.
115
+
116
+ ### One tool at a time
117
+
118
+ If your server is not one of the above, or you want to pick tools by hand:
119
+
120
+ ```python
121
+ import mcpspan
122
+
123
+ mcpspan.configure() # reads MCPSPAN_API_KEY and MCPSPAN_ENDPOINT
124
+
125
+
126
+ @mcpspan.track("search_flights")
127
+ async def search_flights(destination: str) -> str:
128
+ return f"Flights to {destination}"
129
+ ```
130
+
131
+ `track` keeps the function's signature, name and docstring, so a server
132
+ builds the same schema from it. A function tracked by hand and then
133
+ registered on an instrumented server is counted once.
134
+
135
+ ### Leaving a tool out
136
+
137
+ ```python
138
+ @mcp.tool()
139
+ @mcpspan.exclude
140
+ def health_check() -> str:
141
+ return "ok"
142
+ ```
143
+
144
+ For tools called by machinery rather than by an agent. A health check polled
145
+ every few seconds outnumbers everything a person does and drags the whole
146
+ server's error rate and response time towards its own. Put `exclude` below
147
+ the server's decorator, so the server registers the marked function.
148
+
149
+ It takes no tool name on purpose: a name written twice can drift during a
150
+ rename, and the exclusion would quietly stop applying.
151
+
152
+ ### Resources and prompts
153
+
154
+ Reads of your resources and gets of your prompts are measured too, with
155
+ nothing to add: each is one event, in the same session and from the same
156
+ client as the tool calls around it, and the dashboard shows them in a card of
157
+ their own and in each session's timeline. Listings are not recorded.
158
+
159
+ A resource at a fixed address is named by that address. One read through a
160
+ template is named by the template, `trips://{id}`, never by the address the
161
+ client asked for, which can carry a user's data; the template's variables are
162
+ its parameters, by name only. A read of an address the server has nothing for
163
+ is named by its scheme alone, `db://`. A prompt is named by its name, and its
164
+ arguments are its parameters, as a tool's are.
165
+
166
+ On the official MCP SDK and on FastMCP alike.
167
+
168
+ ### Versions
169
+
170
+ Every call carries the version of the server that answered it, so the
171
+ dashboard marks where each release began and compares it with the one before.
172
+ There is nothing to add: it is the version the server gives itself,
173
+ `MCPServer("flights", version="1.4.0")` on v2 of the MCP SDK or
174
+ `FastMCP("flights", version="1.4.0")`. v1's `FastMCP` takes no version, so
175
+ there, or to record a commit or a deploy instead, set `server_version` (or
176
+ `MCPSPAN_SERVER_VERSION`). The client's version is recorded beside its name.
177
+
178
+ ### Shutting down
179
+
180
+ Queued events are delivered as the interpreter exits, so most servers need
181
+ nothing here. If yours has its own shutdown path and you want to be explicit:
182
+
183
+ ```python
184
+ mcpspan.shutdown()
185
+ ```
186
+
187
+ It blocks for as long as that last delivery takes, a few seconds at most. A
188
+ process killed outright (`kill -9`, a container stopped without notice) runs
189
+ nothing after that, and the last few seconds of calls go with it.
190
+
191
+ Forked workers, as under gunicorn, keep collecting: each starts its own
192
+ delivery on its first call.
193
+
194
+ ## Two kinds of failure
195
+
196
+ MCP asks tools to report their own errors inside the result, with `isError`
197
+ set, so the model can see what went wrong. A raised exception is the deviation
198
+ from that, and usually means the tool broke.
199
+
200
+ Both are recorded, and each event says which happened, with the exception's
201
+ class name for the second: "no flights found" is a tool working as written,
202
+ while a `KeyError` is something to fix. On FastMCP, a `ToolError` you raise
203
+ on purpose is recorded as `ToolError`; anything else your tool raises is
204
+ recorded as itself, not as the error FastMCP wraps it in.
205
+
206
+ ### And two that never reach your tool
207
+
208
+ Calls the server refuses on its own are recorded too: arguments that fail
209
+ validation, and names it has no tool for. Both reach the model as error
210
+ results. Bad arguments are the commonest way an agent fails, so leaving them
211
+ out would make a server look healthier than it is to the agents using it.
212
+
213
+ A refused call carries no message, because the validation text can quote back
214
+ what the agent sent. With `capture_parameter_names` on, it carries the names
215
+ and types of the arguments instead, which is what shows the agent wrote
216
+ `dest` where the schema says `destination`. Tools passed through `exclude`
217
+ stay out of this as well.
218
+
219
+ ## Privacy
220
+
221
+ **Parameter values never leave your process.** Not by default, not in any
222
+ mode, not in debug.
223
+
224
+ What is collected: the tool name, how long it took, whether it succeeded, the
225
+ error type and a truncated message when it did not, which client called, and
226
+ the SDK version. For a resource or a prompt, the same, under the name it was
227
+ registered with: never the address a client read, only its template or, for
228
+ an address the server does not have, its scheme.
229
+
230
+ Optionally, parameter *names and types*:
231
+
232
+ ```python
233
+ mcpspan.instrument(mcp, capture_parameter_names=True)
234
+ ```
235
+
236
+ That records `{"destination": "string", "passengers": "number"}`, in JSON's
237
+ vocabulary, as the client sent them. Knowing `search_flights` is always
238
+ called with `destination` and never with `departure_date` tells you your
239
+ tool description is not landing. Knowing which destination tells you nothing
240
+ you needed, and puts your users' data somewhere it does not belong.
241
+
242
+ ## Self-hosting
243
+
244
+ Point it at your own installation:
245
+
246
+ ```python
247
+ mcpspan.instrument(mcp, endpoint="https://mcpspan.example.com")
248
+ ```
249
+
250
+ Or set `MCPSPAN_ENDPOINT`. There is no default: events go only where you point
251
+ them. With a key and no endpoint, nothing is collected, and the SDK says so
252
+ once on standard error.
253
+
254
+ When it starts with a key, the SDK sends one empty batch to say it is there.
255
+ That is how the dashboard's Status page tells a server nobody has used yet
256
+ from one pointed at the wrong address, and how a wrong key is reported when
257
+ your server starts rather than at its first tool call.
258
+
259
+ ## It will not break your server
260
+
261
+ - Delivery happens on a background thread of its own, whether your server is
262
+ synchronous, on asyncio or on trio. A tool call returns without waiting on
263
+ the network, and the thread never keeps a process alive.
264
+ - A failure to send is never raised into your code. Retryable failures wait
265
+ and try again with a widening gap; a refused key switches collection off
266
+ and says so once on standard error.
267
+ - The queue is bounded. An unreachable endpoint cannot grow it until your
268
+ process runs out of memory.
269
+ - Nothing is ever written to standard output, which carries the MCP protocol
270
+ on a stdio server. Diagnostics go to standard error.
271
+ - `configure`, `instrument` and `shutdown` never raise. A mistyped option
272
+ falls back to its default rather than stopping your server from starting.
273
+
274
+ ## Options
275
+
276
+ Given to `instrument` or `configure`.
277
+
278
+ | Option | Default | What it does |
279
+ |---|---|---|
280
+ | `api_key` | `MCPSPAN_API_KEY` | Identifies your server. Without it, nothing is collected. |
281
+ | `endpoint` | `MCPSPAN_ENDPOINT`; none | Your mcpspan installation. Nothing is collected without it. |
282
+ | `capture_parameter_names` | `False` | Records parameter names and types, never values. |
283
+ | `server_version` | `MCPSPAN_SERVER_VERSION`, then the server's own | The version to record calls under: a release, a tag, a commit. |
284
+ | `debug` | `False` | Writes delivery diagnostics to standard error. |
285
+ | `on_diagnostic` | - | Receives diagnostics instead. Implies `debug`. |
286
+ | `flush_on_exit` | `True` | Delivers what is queued as the interpreter exits. |
287
+ | `flush_interval` | `5.0` | Seconds a partly filled batch waits. |
288
+ | `max_batch_size` | `100` | Events per request. Reaching it sends early. |
289
+ | `max_queue_size` | `10000` | Events held while delivery is failing. |
290
+
291
+ Calling `configure` or `instrument` again with the same settings changes
292
+ nothing, so a server built per request can pass them every time.
293
+
294
+ ## Developing
295
+
296
+ ```sh
297
+ uv sync --group mcp2 # or mcp1, or fastmcp: the MCP SDK the tests run against
298
+ uv run --group mcp2 pytest
299
+ uv run --group mcp2 mypy
300
+ uv run ruff check && uv run ruff format --check
301
+ ```
302
+
303
+ The SDK follows [the contract every mcpspan SDK
304
+ follows](../../docs/sdk-contract.md), checked by the suite in
305
+ [`conformance/`](../../conformance/README.md).
306
+
307
+ ## Licence
308
+
309
+ MIT.
@@ -0,0 +1,287 @@
1
+ # mcpspan for Python
2
+
3
+ Analytics for MCP servers. Find out which of your tools get called, by which
4
+ client, how long they take, and which ones fail.
5
+
6
+ Your own logs tell you a tool ran. This tells you whether it was Claude,
7
+ Cursor, or something you have not heard of, how that call compares to the
8
+ other nine hundred, and whether the failures are your tool breaking or your
9
+ tool politely saying no.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ pip install mcpspan
15
+ ```
16
+
17
+ Needs Python 3.10 or newer. No dependencies. Works with the official MCP SDK,
18
+ `mcp` 1.30+ (`FastMCP`) and 2.2+ (`MCPServer`), and with FastMCP 4+ (the
19
+ `fastmcp` package).
20
+
21
+ ## Use
22
+
23
+ One line, anywhere before the server starts:
24
+
25
+ ```python
26
+ import os
27
+
28
+ import mcpspan
29
+ from mcp.server.fastmcp import FastMCP
30
+
31
+ mcp = FastMCP("flights")
32
+
33
+
34
+ @mcp.tool()
35
+ def search_flights(destination: str) -> str:
36
+ return f"Found 3 flights to {destination}"
37
+
38
+
39
+ mcpspan.instrument(
40
+ mcp,
41
+ api_key=os.environ.get("MCPSPAN_API_KEY"),
42
+ endpoint="http://localhost:6271", # your mcpspan installation
43
+ )
44
+
45
+ if __name__ == "__main__":
46
+ mcp.run()
47
+ ```
48
+
49
+ Every tool on the server is measured, whether it was registered before that
50
+ line or after. Nothing about how you write tools changes: the schema the
51
+ server builds from your function is untouched, and your tool returns and
52
+ raises exactly what it did before.
53
+
54
+ The same line works on v2 of the MCP SDK and on FastMCP:
55
+
56
+ ```python
57
+ from mcp.server.mcpserver import MCPServer
58
+
59
+ mcp = MCPServer("flights")
60
+ mcpspan.instrument(mcp)
61
+ ```
62
+
63
+ ```python
64
+ from fastmcp import FastMCP
65
+
66
+ mcp = FastMCP("flights")
67
+ mcpspan.instrument(mcp)
68
+ ```
69
+
70
+ With no `api_key` given, it is read from `MCPSPAN_API_KEY`.
71
+
72
+ ### Sessions and clients
73
+
74
+ Calls are grouped into sessions when there is a connection to group them by:
75
+ a stdio process, or an HTTP transport that hands out session IDs. A stateless
76
+ HTTP endpoint, and every endpoint on the 2026-07-28 protocol, which dropped
77
+ sessions, records calls without one.
78
+
79
+ The client is read from the call itself on 2026-07-28, where each request
80
+ names its client, and from the handshake on 2025-11-25. A stateless
81
+ 2025-11-25 HTTP endpoint has no handshake to read, so its calls are recorded
82
+ with an unknown client rather than a guessed one.
83
+
84
+ A tool that asks the client for more before it can finish is one call however
85
+ many round trips that takes. The interim answer asking for input is not
86
+ counted; the one that ends the call is.
87
+
88
+ ### Without a key
89
+
90
+ If there is no key, nothing is collected and nothing is sent, and no thread is
91
+ started. Wrapped tools return before reading the clock. That makes it safe to
92
+ leave in place in tests, in CI, and in a fork somebody is only reading.
93
+
94
+ ### One tool at a time
95
+
96
+ If your server is not one of the above, or you want to pick tools by hand:
97
+
98
+ ```python
99
+ import mcpspan
100
+
101
+ mcpspan.configure() # reads MCPSPAN_API_KEY and MCPSPAN_ENDPOINT
102
+
103
+
104
+ @mcpspan.track("search_flights")
105
+ async def search_flights(destination: str) -> str:
106
+ return f"Flights to {destination}"
107
+ ```
108
+
109
+ `track` keeps the function's signature, name and docstring, so a server
110
+ builds the same schema from it. A function tracked by hand and then
111
+ registered on an instrumented server is counted once.
112
+
113
+ ### Leaving a tool out
114
+
115
+ ```python
116
+ @mcp.tool()
117
+ @mcpspan.exclude
118
+ def health_check() -> str:
119
+ return "ok"
120
+ ```
121
+
122
+ For tools called by machinery rather than by an agent. A health check polled
123
+ every few seconds outnumbers everything a person does and drags the whole
124
+ server's error rate and response time towards its own. Put `exclude` below
125
+ the server's decorator, so the server registers the marked function.
126
+
127
+ It takes no tool name on purpose: a name written twice can drift during a
128
+ rename, and the exclusion would quietly stop applying.
129
+
130
+ ### Resources and prompts
131
+
132
+ Reads of your resources and gets of your prompts are measured too, with
133
+ nothing to add: each is one event, in the same session and from the same
134
+ client as the tool calls around it, and the dashboard shows them in a card of
135
+ their own and in each session's timeline. Listings are not recorded.
136
+
137
+ A resource at a fixed address is named by that address. One read through a
138
+ template is named by the template, `trips://{id}`, never by the address the
139
+ client asked for, which can carry a user's data; the template's variables are
140
+ its parameters, by name only. A read of an address the server has nothing for
141
+ is named by its scheme alone, `db://`. A prompt is named by its name, and its
142
+ arguments are its parameters, as a tool's are.
143
+
144
+ On the official MCP SDK and on FastMCP alike.
145
+
146
+ ### Versions
147
+
148
+ Every call carries the version of the server that answered it, so the
149
+ dashboard marks where each release began and compares it with the one before.
150
+ There is nothing to add: it is the version the server gives itself,
151
+ `MCPServer("flights", version="1.4.0")` on v2 of the MCP SDK or
152
+ `FastMCP("flights", version="1.4.0")`. v1's `FastMCP` takes no version, so
153
+ there, or to record a commit or a deploy instead, set `server_version` (or
154
+ `MCPSPAN_SERVER_VERSION`). The client's version is recorded beside its name.
155
+
156
+ ### Shutting down
157
+
158
+ Queued events are delivered as the interpreter exits, so most servers need
159
+ nothing here. If yours has its own shutdown path and you want to be explicit:
160
+
161
+ ```python
162
+ mcpspan.shutdown()
163
+ ```
164
+
165
+ It blocks for as long as that last delivery takes, a few seconds at most. A
166
+ process killed outright (`kill -9`, a container stopped without notice) runs
167
+ nothing after that, and the last few seconds of calls go with it.
168
+
169
+ Forked workers, as under gunicorn, keep collecting: each starts its own
170
+ delivery on its first call.
171
+
172
+ ## Two kinds of failure
173
+
174
+ MCP asks tools to report their own errors inside the result, with `isError`
175
+ set, so the model can see what went wrong. A raised exception is the deviation
176
+ from that, and usually means the tool broke.
177
+
178
+ Both are recorded, and each event says which happened, with the exception's
179
+ class name for the second: "no flights found" is a tool working as written,
180
+ while a `KeyError` is something to fix. On FastMCP, a `ToolError` you raise
181
+ on purpose is recorded as `ToolError`; anything else your tool raises is
182
+ recorded as itself, not as the error FastMCP wraps it in.
183
+
184
+ ### And two that never reach your tool
185
+
186
+ Calls the server refuses on its own are recorded too: arguments that fail
187
+ validation, and names it has no tool for. Both reach the model as error
188
+ results. Bad arguments are the commonest way an agent fails, so leaving them
189
+ out would make a server look healthier than it is to the agents using it.
190
+
191
+ A refused call carries no message, because the validation text can quote back
192
+ what the agent sent. With `capture_parameter_names` on, it carries the names
193
+ and types of the arguments instead, which is what shows the agent wrote
194
+ `dest` where the schema says `destination`. Tools passed through `exclude`
195
+ stay out of this as well.
196
+
197
+ ## Privacy
198
+
199
+ **Parameter values never leave your process.** Not by default, not in any
200
+ mode, not in debug.
201
+
202
+ What is collected: the tool name, how long it took, whether it succeeded, the
203
+ error type and a truncated message when it did not, which client called, and
204
+ the SDK version. For a resource or a prompt, the same, under the name it was
205
+ registered with: never the address a client read, only its template or, for
206
+ an address the server does not have, its scheme.
207
+
208
+ Optionally, parameter *names and types*:
209
+
210
+ ```python
211
+ mcpspan.instrument(mcp, capture_parameter_names=True)
212
+ ```
213
+
214
+ That records `{"destination": "string", "passengers": "number"}`, in JSON's
215
+ vocabulary, as the client sent them. Knowing `search_flights` is always
216
+ called with `destination` and never with `departure_date` tells you your
217
+ tool description is not landing. Knowing which destination tells you nothing
218
+ you needed, and puts your users' data somewhere it does not belong.
219
+
220
+ ## Self-hosting
221
+
222
+ Point it at your own installation:
223
+
224
+ ```python
225
+ mcpspan.instrument(mcp, endpoint="https://mcpspan.example.com")
226
+ ```
227
+
228
+ Or set `MCPSPAN_ENDPOINT`. There is no default: events go only where you point
229
+ them. With a key and no endpoint, nothing is collected, and the SDK says so
230
+ once on standard error.
231
+
232
+ When it starts with a key, the SDK sends one empty batch to say it is there.
233
+ That is how the dashboard's Status page tells a server nobody has used yet
234
+ from one pointed at the wrong address, and how a wrong key is reported when
235
+ your server starts rather than at its first tool call.
236
+
237
+ ## It will not break your server
238
+
239
+ - Delivery happens on a background thread of its own, whether your server is
240
+ synchronous, on asyncio or on trio. A tool call returns without waiting on
241
+ the network, and the thread never keeps a process alive.
242
+ - A failure to send is never raised into your code. Retryable failures wait
243
+ and try again with a widening gap; a refused key switches collection off
244
+ and says so once on standard error.
245
+ - The queue is bounded. An unreachable endpoint cannot grow it until your
246
+ process runs out of memory.
247
+ - Nothing is ever written to standard output, which carries the MCP protocol
248
+ on a stdio server. Diagnostics go to standard error.
249
+ - `configure`, `instrument` and `shutdown` never raise. A mistyped option
250
+ falls back to its default rather than stopping your server from starting.
251
+
252
+ ## Options
253
+
254
+ Given to `instrument` or `configure`.
255
+
256
+ | Option | Default | What it does |
257
+ |---|---|---|
258
+ | `api_key` | `MCPSPAN_API_KEY` | Identifies your server. Without it, nothing is collected. |
259
+ | `endpoint` | `MCPSPAN_ENDPOINT`; none | Your mcpspan installation. Nothing is collected without it. |
260
+ | `capture_parameter_names` | `False` | Records parameter names and types, never values. |
261
+ | `server_version` | `MCPSPAN_SERVER_VERSION`, then the server's own | The version to record calls under: a release, a tag, a commit. |
262
+ | `debug` | `False` | Writes delivery diagnostics to standard error. |
263
+ | `on_diagnostic` | - | Receives diagnostics instead. Implies `debug`. |
264
+ | `flush_on_exit` | `True` | Delivers what is queued as the interpreter exits. |
265
+ | `flush_interval` | `5.0` | Seconds a partly filled batch waits. |
266
+ | `max_batch_size` | `100` | Events per request. Reaching it sends early. |
267
+ | `max_queue_size` | `10000` | Events held while delivery is failing. |
268
+
269
+ Calling `configure` or `instrument` again with the same settings changes
270
+ nothing, so a server built per request can pass them every time.
271
+
272
+ ## Developing
273
+
274
+ ```sh
275
+ uv sync --group mcp2 # or mcp1, or fastmcp: the MCP SDK the tests run against
276
+ uv run --group mcp2 pytest
277
+ uv run --group mcp2 mypy
278
+ uv run ruff check && uv run ruff format --check
279
+ ```
280
+
281
+ The SDK follows [the contract every mcpspan SDK
282
+ follows](../../docs/sdk-contract.md), checked by the suite in
283
+ [`conformance/`](../../conformance/README.md).
284
+
285
+ ## Licence
286
+
287
+ MIT.