mcpspan 0.1.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.
package/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.
package/README.md ADDED
@@ -0,0 +1,292 @@
1
+ # mcpspan
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 handler breaking or your
9
+ tool politely saying no.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ npm install mcpspan
15
+ ```
16
+
17
+ Needs Node 18 or newer. No dependencies.
18
+
19
+ ## Use
20
+
21
+ One line, anywhere before the server starts:
22
+
23
+ ```ts
24
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
25
+ import { instrument } from 'mcpspan';
26
+
27
+ const server = new McpServer({ name: 'flights', version: '1.0.0' });
28
+
29
+ instrument(server, {
30
+ apiKey: process.env.MCPSPAN_API_KEY,
31
+ endpoint: 'http://localhost:6271', // your mcpspan installation
32
+ });
33
+
34
+ // Register tools exactly as you would without this.
35
+ server.registerTool('search_flights', { inputSchema }, async (params) => {
36
+ return { content: [{ type: 'text', text: 'Found 3 flights' }] };
37
+ });
38
+ ```
39
+
40
+ Every tool on the server is measured, whether it was registered before that
41
+ line or after. Nothing about how you write them changes, and the wrapper hands
42
+ back whatever your handler returned, exceptions included.
43
+
44
+ ### On v2 of the MCP SDK
45
+
46
+ Both major versions of the official TypeScript SDK are supported, v1
47
+ (`@modelcontextprotocol/sdk`) and v2 (`@modelcontextprotocol/server`), and
48
+ both protocol revisions v2 speaks, 2025-11-25 and 2026-07-28.
49
+
50
+ v2 usually builds the server for you: `createMcpHandler` builds a fresh one
51
+ for every HTTP request, and `serveStdio` builds one when the client connects.
52
+ Configure mcpspan once when the process starts, and instrument each server
53
+ the SDK builds:
54
+
55
+ ```ts
56
+ import * as mcp from '@modelcontextprotocol/server';
57
+ import { configure, instrument } from 'mcpspan';
58
+
59
+ // Once, as the process starts, so the dashboard sees the server come up.
60
+ configure({ apiKey: process.env.MCPSPAN_API_KEY, endpoint: process.env.MCPSPAN_ENDPOINT });
61
+
62
+ export const handler = mcp.createMcpHandler(() => {
63
+ const server = new mcp.McpServer(
64
+ { name: 'flights', version: '1.0.0' },
65
+ { capabilities: { tools: {} } },
66
+ );
67
+
68
+ instrument(server);
69
+
70
+ server.registerTool('search_flights', {}, async () => {
71
+ return { content: [{ type: 'text', text: 'Found 3 flights' }] };
72
+ });
73
+
74
+ return server;
75
+ });
76
+ ```
77
+
78
+ Instrumenting a fresh server costs a few property lookups, and passing the
79
+ same options to `instrument` on every request is also fine: settings that
80
+ have not changed are left alone rather than started over.
81
+
82
+ ### Sessions and clients
83
+
84
+ Calls are grouped into sessions when there is a connection to group them by:
85
+ a stdio process, or an HTTP transport that hands out session IDs. A stateless
86
+ HTTP endpoint, and every endpoint on the 2026-07-28 protocol, which dropped
87
+ sessions, records calls without one.
88
+
89
+ The client is read from the call itself on 2026-07-28, where each request
90
+ names its client, and from the handshake on 2025-11-25. A stateless 2025-11-25
91
+ HTTP endpoint builds a new server for each request, which never saw the
92
+ handshake, so its calls are recorded with an unknown client rather than a
93
+ guessed one.
94
+
95
+ A tool that asks the client for more before it can finish, such as a
96
+ confirmation, is one call however many round trips that takes. The interim
97
+ answer asking for input is not counted; the one that ends the call is. Where
98
+ the transport cannot put the question to the client and the SDK answers with
99
+ an error instead, that is recorded as the tool's failure, since it is what the
100
+ agent saw.
101
+
102
+ ### Without a key
103
+
104
+ If `apiKey` is missing, nothing is collected and nothing is sent. Wrapped
105
+ handlers return before reading the clock, so an SDK nobody configured costs
106
+ what an SDK nobody installed costs. That makes it safe to leave in place in
107
+ tests, in CI, and in a fork somebody is only reading.
108
+
109
+ ### One tool at a time
110
+
111
+ If your server is not an `McpServer`, or you want to pick tools by hand:
112
+
113
+ ```ts
114
+ import { configure, track } from 'mcpspan';
115
+
116
+ configure({ apiKey: process.env.MCPSPAN_API_KEY, endpoint: process.env.MCPSPAN_ENDPOINT });
117
+
118
+ const search = track('search_flights', async (params: { destination: string }) => {
119
+ return { content: [{ type: 'text', text: `Flights to ${params.destination}` }] };
120
+ });
121
+ ```
122
+
123
+ `track` returns a function with the same signature as the one you gave it.
124
+
125
+ ### Leaving a tool out
126
+
127
+ ```ts
128
+ import { exclude } from 'mcpspan';
129
+
130
+ server.registerTool('health_check', {}, exclude(async () => {
131
+ return { content: [{ type: 'text', text: 'ok' }] };
132
+ }));
133
+ ```
134
+
135
+ For tools called by machinery rather than by an agent. A health check polled
136
+ every few seconds outnumbers everything a person does and drags the whole
137
+ server's error rate and response time towards its own.
138
+
139
+ It takes no tool name on purpose: a name written twice can drift during a
140
+ rename, and the exclusion would quietly stop applying.
141
+
142
+ ### Resources and prompts
143
+
144
+ Reads of your resources and gets of your prompts are measured too, with
145
+ nothing to add: each is one event, in the same session and from the same
146
+ client as the tool calls around it, and the dashboard shows them in a card of
147
+ their own and in each session's timeline. Listings are not recorded.
148
+
149
+ A resource at a fixed address is named by that address. One read through a
150
+ template is named by the template, `trips://{id}`, never by the address the
151
+ client asked for, which can carry a user's data; the template's variables are
152
+ its parameters, by name only. A read of an address the server has nothing for
153
+ is named by its scheme alone, `db://`. A prompt is named by its name, and its
154
+ arguments are its parameters, as a tool's are.
155
+
156
+ ### Versions
157
+
158
+ Every call carries the version of the server that answered it, so the
159
+ dashboard marks where each release began and compares it with the one before.
160
+ There is nothing to add: it is the version the server gives itself,
161
+ `new McpServer({ name: 'flights', version: '1.4.0' })`. To record a commit or
162
+ a deploy instead, set `serverVersion` (or `MCPSPAN_SERVER_VERSION`). The
163
+ client's version is recorded beside its name.
164
+
165
+ ### Shutting down
166
+
167
+ Queued events are delivered when the process winds down, so most servers need
168
+ nothing here. If yours has its own shutdown path and you want to be explicit:
169
+
170
+ ```ts
171
+ import { shutdown } from 'mcpspan';
172
+
173
+ await shutdown();
174
+ ```
175
+
176
+ A process killed outright by a signal is the exception - nothing runs after
177
+ that - and the last few seconds of calls go with it.
178
+
179
+ ## Two kinds of failure
180
+
181
+ MCP asks tools to report their own errors inside the result, with `isError`
182
+ set, so the model can see what went wrong. A thrown exception is the deviation
183
+ from that, and usually means the handler broke.
184
+
185
+ Both are recorded, and each event says which happened. The distinction is the
186
+ useful one: "no flights found" is a tool working as written, while a
187
+ `TypeError` is something to fix. A library watching only for exceptions would
188
+ report a correctly written server as having no errors at all.
189
+
190
+ ### And two that never reach your handler
191
+
192
+ With `instrument`, calls the server refuses on its own are recorded too:
193
+ arguments its schema rejects, and names it has no enabled tool for. The first
194
+ reaches the model as an ordinary error result, the second as an error result
195
+ on v1 of the MCP SDK and a protocol error on v2. Bad arguments are the
196
+ commonest way an agent fails, so leaving them out would make a server look
197
+ healthier than it is to the agents using it.
198
+
199
+ A refused call carries no message, because the server's validation text can
200
+ quote back what the agent sent. With `captureParameterNames` on, it carries the
201
+ names and types of the arguments instead, which is what shows the agent wrote
202
+ `dest` where the schema says `destination`. Tools passed through `exclude` stay
203
+ out of this as well.
204
+
205
+ ## Privacy
206
+
207
+ **Parameter values never leave your process.** Not by default, not in any
208
+ mode, not in debug.
209
+
210
+ What is collected: the tool name, how long it took, whether it succeeded, the
211
+ error type and a truncated message when it did not, which client called, and
212
+ the SDK version. For a resource or a prompt, the same, under the name it was
213
+ registered with: never the address a client read, only its template or, for
214
+ an address the server does not have, its scheme.
215
+
216
+ Optionally, parameter *names and types*:
217
+
218
+ ```ts
219
+ instrument(server, {
220
+ apiKey: process.env.MCPSPAN_API_KEY,
221
+ captureParameterNames: true,
222
+ });
223
+ ```
224
+
225
+ That records `{ destination: 'string', passengers: 'number' }`. Knowing
226
+ `search_flights` is always called with `destination` and never with
227
+ `departureDate` tells you your tool description is not landing. Knowing which
228
+ destination tells you nothing you needed, and puts your users' data somewhere
229
+ it does not belong.
230
+
231
+ Types stay coarse and carry no length, because the distance between "a 34
232
+ character string" and "a credit card number" is shorter than it looks. Nested
233
+ objects are named but not opened.
234
+
235
+ ## Self-hosting
236
+
237
+ Point it at your own installation:
238
+
239
+ ```ts
240
+ instrument(server, {
241
+ apiKey: process.env.MCPSPAN_API_KEY,
242
+ endpoint: 'https://mcpspan.example.com',
243
+ });
244
+ ```
245
+
246
+ Or set `MCPSPAN_ENDPOINT`. Both `apiKey` and `endpoint` fall back to
247
+ `MCPSPAN_API_KEY` and `MCPSPAN_ENDPOINT`, so a server can be instrumented with
248
+ no configuration in code at all.
249
+
250
+ There is no default: events go only where you point them. With a key and no
251
+ endpoint, nothing is collected, and the SDK says so once on standard error.
252
+
253
+ When it starts with a key, the SDK sends one empty batch to say it is there.
254
+ That is how the dashboard's Status page tells a server nobody has used yet
255
+ from one pointed at the wrong address, and how a wrong key is reported when
256
+ your server starts rather than at its first tool call. It is sent once, in the
257
+ background, and never retried.
258
+
259
+ ## It will not break your server
260
+
261
+ That is the first rule, and everything below follows from it.
262
+
263
+ - Delivery happens in the background. A tool call returns without waiting on
264
+ the network.
265
+ - A failure to send is never raised into your code. Retryable failures wait
266
+ and try again with a widening gap; a refused key switches collection off
267
+ rather than buffering events forever.
268
+ - The queue is bounded. An unreachable endpoint cannot grow it until your
269
+ process runs out of memory.
270
+ - Diagnostics go to stderr, never stdout - on a stdio transport, stdout
271
+ carries the MCP protocol itself.
272
+ - `configure` and `instrument` never throw. A mistyped option falls back to
273
+ its default rather than stopping your server from starting.
274
+
275
+ ## Options
276
+
277
+ | Option | Default | What it does |
278
+ |---|---|---|
279
+ | `apiKey` | `MCPSPAN_API_KEY` | Identifies your server. Without it, nothing is collected. |
280
+ | `endpoint` | `MCPSPAN_ENDPOINT`; none | Your mcpspan installation. Nothing is collected without it. |
281
+ | `captureParameterNames` | `false` | Records parameter names and types, never values. |
282
+ | `serverVersion` | `MCPSPAN_SERVER_VERSION`, then the server's own | The version to record calls under: a release, a tag, a commit. |
283
+ | `debug` | `false` | Writes delivery diagnostics to stderr. |
284
+ | `onDiagnostic` | - | Receives diagnostics instead of stderr. Implies `debug`. |
285
+ | `flushOnExit` | `true` | Delivers what is queued as the process ends. |
286
+ | `flushIntervalMs` | `5000` | How long a partly filled batch waits. |
287
+ | `maxBatchSize` | `100` | Events per request. Reaching it sends early. |
288
+ | `maxQueueSize` | `10000` | Events held while delivery is failing. |
289
+
290
+ ## Licence
291
+
292
+ MIT.