opentel-mcp 0.15.0 → 0.17.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/CHANGELOG.md +180 -0
- package/README.md +300 -36
- package/package.json +3 -2
- package/src/attributes.js +23 -0
- package/src/config.js +86 -2
- package/src/cost/extractor.js +4 -13
- package/src/error-recording/types.d.ts +8 -6
- package/src/fingerprint/normalize/message.js +4 -1
- package/src/fingerprint/normalize/secrets.js +93 -0
- package/src/flush-on-exit.js +215 -0
- package/src/index.d.ts +138 -3
- package/src/index.js +2 -0
- package/src/instrument.js +335 -24
- package/src/metrics.js +29 -0
- package/src/result-meta.js +34 -0
- package/src/sdk/detect.js +9 -2
- package/src/staleness.js +278 -0
- package/src/unactionable.js +129 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,185 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.17.0
|
|
4
|
+
|
|
5
|
+
Staleness detection: a read tool that returns data older than its own
|
|
6
|
+
freshness window now shows up, even though the call succeeded. Suggested by
|
|
7
|
+
the r/mcp community. Opt-in per tool; tools that don't report freshness
|
|
8
|
+
are untouched. No new metric label keys.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Staleness detection (ADR 027, `docs/adr/027-staleness-detection.md`).**
|
|
13
|
+
A tool reports freshness in its result's `_meta` under
|
|
14
|
+
`FRESHNESS_META_KEY` (`'io.github.thirumalaiboobathi.opentel-mcp/freshness'`,
|
|
15
|
+
a reverse-DNS key per the MCP spec): `{ asOf, maxAgeMs }` or
|
|
16
|
+
`{ asOf, freshUntil }` (both allowed; the earlier deadline wins).
|
|
17
|
+
`asOf`/`freshUntil` are epoch milliseconds or ISO 8601 strings with a
|
|
18
|
+
timezone. Every successful `tools/call` carrying it gets:
|
|
19
|
+
- span attributes `mcp.tool.freshness.verdict` (`fresh` | `stale` |
|
|
20
|
+
`unknown`), `.data_age_ms`, `.max_age_ms`, `.limit_source`
|
|
21
|
+
(`max_age_ms` | `fresh_until` | `config`) and, for `unknown`,
|
|
22
|
+
`.unknown_reason` (`invalid_as_of` | `future_as_of` | `missing_window`
|
|
23
|
+
| `invalid_window`);
|
|
24
|
+
- `+1` on the new `mcp.tool.stale_responses` counter (label:
|
|
25
|
+
`gen_ai.tool.name` only) when stale.
|
|
26
|
+
- **Stale is not an error:** span status stays `OK`, no `error.type`,
|
|
27
|
+
never a silent failure, ToolOutcome stays `SUCCESS`.
|
|
28
|
+
- Responses without the key get no verdict and no attributes.
|
|
29
|
+
`isError: true` responses are skipped.
|
|
30
|
+
- Malformed input (epoch seconds, timezone-less ISO strings, negative
|
|
31
|
+
windows, `freshUntil` before `asOf`, `asOf` more than 5 s in the
|
|
32
|
+
future, throwing getters…) never throws: it yields `unknown` with a
|
|
33
|
+
reason and at most one `diag.warn` per tool.
|
|
34
|
+
- Only `asOf`, `maxAgeMs` and `freshUntil` under that one key are read;
|
|
35
|
+
content and other `_meta` fields are never read for this, and raw
|
|
36
|
+
values never reach spans, metrics or logs (canary-tested).
|
|
37
|
+
- **`staleness` option.** `staleness.tools[toolName].maxAgeMs` overrides a
|
|
38
|
+
tool's own window (config can't supply `asOf`); `staleness.enabled:
|
|
39
|
+
false` turns the feature off. On by default, which is a no-op for tools
|
|
40
|
+
that don't report freshness.
|
|
41
|
+
- **Types:** `FRESHNESS_META_KEY`, `FreshnessMeta` (autocomplete for the
|
|
42
|
+
`_meta` value), `FreshnessTimestamp`, `FreshnessVerdict`, and
|
|
43
|
+
`InstrumentOptions.staleness`.
|
|
44
|
+
|
|
45
|
+
### Security
|
|
46
|
+
|
|
47
|
+
- **Credentials are scrubbed from recorded exceptions, in every mode.**
|
|
48
|
+
Bearer and Basic tokens, AWS access key ids, `sk-`/`sk-proj-`/`sk-ant-`,
|
|
49
|
+
Stripe, `ghp_`/`github_pat_`, Slack `xox…` and Google `AIza…` keys, JWTs,
|
|
50
|
+
passwords in URLs (`https://user:<password>@host`), and the values of keys like
|
|
51
|
+
`token=`, `api_key=`, `access_token=`, `password=`, `client_secret` are
|
|
52
|
+
replaced with `<SECRET>`. Ordinary words and ids (`token expired`, UUIDs,
|
|
53
|
+
commit hashes) are left alone.
|
|
54
|
+
- **Behavior change: the default `errorRecording.mode: 'full'` is no
|
|
55
|
+
longer raw and uncapped.** It still records the error's type, message and
|
|
56
|
+
stack, keeping ids, paths, URLs and line numbers, but with credentials
|
|
57
|
+
scrubbed as above, the message capped at 4096 characters and the stack at
|
|
58
|
+
16384 (a ` …[truncated by opentel-mcp]` marker shows where).
|
|
59
|
+
`exception.type` is capped at 128 characters, as in `'normalized'` mode.
|
|
60
|
+
Ordinary errors are recorded exactly as before. There is no switch back
|
|
61
|
+
to unscrubbed output. To record *less*, use `'normalized'`, `'none'`, or
|
|
62
|
+
a `redactor`.
|
|
63
|
+
- **Fingerprints change only for errors that contain a credential.** The
|
|
64
|
+
same patterns now run first in `'normalized'` mode and in fingerprinting,
|
|
65
|
+
so two errors that differ only by a leaked token now share one
|
|
66
|
+
fingerprint (before, each token value made its own). Expect a one-time
|
|
67
|
+
regrouping of those errors only.
|
|
68
|
+
- A thrown value whose properties throw when read (e.g. a throwing
|
|
69
|
+
`message` getter) now still marks the span `ERROR`; before, the span was
|
|
70
|
+
left with no status.
|
|
71
|
+
- New test: one canary test runs every feature at once (fingerprinting,
|
|
72
|
+
cost and budget, thrash, schema drift, unactionable errors,
|
|
73
|
+
resources/prompts coverage, staleness) on SDK v1 and v2, `Server` and
|
|
74
|
+
`McpServer`, and checks that no tool argument, result content, `_meta`
|
|
75
|
+
value, resource URI, prompt argument or request `_meta` reaches any span,
|
|
76
|
+
metric or diag log. Nothing leaked; no code change was needed.
|
|
77
|
+
- New `SECURITY.md` at the repository root: private vulnerability reporting
|
|
78
|
+
and what the library does and doesn't record.
|
|
79
|
+
|
|
80
|
+
### Changed
|
|
81
|
+
|
|
82
|
+
- The cost extractor and staleness detection now read `result._meta`
|
|
83
|
+
through one shared accessor (`src/result-meta.js`). Extraction behavior is
|
|
84
|
+
unchanged.
|
|
85
|
+
- Dev-mode metrics (`setupNodeSdk: true`) print 13 instruments, up from 12.
|
|
86
|
+
- README examples and `examples/hello-mcpserver` use `registerTool()`
|
|
87
|
+
instead of the deprecated `server.tool()`.
|
|
88
|
+
- README's configuration table now lists `unactionableErrors`, `coverage`
|
|
89
|
+
and `staleness`; the misplaced `unactionableErrors` doc comment in
|
|
90
|
+
`index.d.ts` is back on its own option.
|
|
91
|
+
- Test: the flush-on-exit "capped at 1000 ms" check now allows up to 5 s
|
|
92
|
+
(half the uncapped 10 s), so a loaded CI runner can't fail it.
|
|
93
|
+
|
|
94
|
+
## 0.16.0
|
|
95
|
+
|
|
96
|
+
Finishes what 0.15 started. Telemetry now survives the way MCP clients
|
|
97
|
+
actually stop servers (`flushOnExit`), and two new signals land: errors
|
|
98
|
+
your agent can't act on, and opt-in resources/prompts coverage. Plus a fix
|
|
99
|
+
for SDK v2 servers that declare `capabilities.tools`.
|
|
100
|
+
`setupNodeSdk: false` (production) behavior is unchanged apart from the
|
|
101
|
+
new span attributes on `isError: true` results and the v2 fix; resources/
|
|
102
|
+
prompts coverage is off unless you opt in. No new metric label keys.
|
|
103
|
+
|
|
104
|
+
### Added
|
|
105
|
+
|
|
106
|
+
- **`flushOnExit` (ADR 024, `docs/adr/024-flush-on-exit.md`).** Under
|
|
107
|
+
`setupNodeSdk: true`, buffered spans and dev-mode metrics are now
|
|
108
|
+
flushed automatically when the process stops: on `beforeExit` (stdin
|
|
109
|
+
closed, loop drained), `SIGTERM` and `SIGINT`. MCP clients built on the
|
|
110
|
+
SDK's stdio transport stop servers with exactly that sequence (stdin
|
|
111
|
+
close → SIGTERM → SIGKILL, 2 s apart); the old `process.on('beforeExit')`
|
|
112
|
+
snippet only covered the first step. On by default with
|
|
113
|
+
`setupNodeSdk: true`; `flushOnExit: false` opts out;
|
|
114
|
+
`flushOnExit: { timeoutMs }` lowers the 1000 ms cap.
|
|
115
|
+
- One listener set per process, however many servers are instrumented,
|
|
116
|
+
and it's removed again once every server has shut down.
|
|
117
|
+
- After flushing, a signal is re-raised when no other listener for it
|
|
118
|
+
exists, so the process still dies by that signal. If your code has its
|
|
119
|
+
own listener, yours decides. `beforeExit` never changes the exit code.
|
|
120
|
+
- **No effect at all with `setupNodeSdk: false`** (production
|
|
121
|
+
behavior unchanged): passing it there logs a one-time `diag.warn`.
|
|
122
|
+
- Not covered: `process.exit()` (call `await server.shutdown()` first)
|
|
123
|
+
and `SIGKILL`.
|
|
124
|
+
|
|
125
|
+
- **Unactionable tool errors (ADR 025, `docs/adr/025-unactionable-errors.md`).**
|
|
126
|
+
Every `isError: true` span now carries `mcp.failure.unactionable`
|
|
127
|
+
(boolean) and `mcp.failure.content_length_bucket` (`empty` | `tiny` |
|
|
128
|
+
`short` | `medium` | `long`). Unactionable means no content, or under
|
|
129
|
+
10 characters of trimmed text with no image/audio/resource item: an
|
|
130
|
+
error that gives the agent nothing to act on. Computed only from text
|
|
131
|
+
lengths and non-text item counts, never from the text; a canary-string
|
|
132
|
+
test checks every exported span and metric. Span-only (no metric label,
|
|
133
|
+
`METRIC_SAFE_ATTRIBUTES` unchanged); fingerprints and categories
|
|
134
|
+
unchanged. Configure with `unactionableErrors: { enabled, minTextLength }`
|
|
135
|
+
or `OTEL_MCP_UNACTIONABLE_ERRORS_ENABLED` /
|
|
136
|
+
`OTEL_MCP_UNACTIONABLE_ERRORS_MIN_TEXT_LENGTH`.
|
|
137
|
+
|
|
138
|
+
- **Resources and prompts coverage, opt-in (ADR 026,
|
|
139
|
+
`docs/adr/026-resources-prompts-coverage.md`).**
|
|
140
|
+
`coverage: { resources: true, prompts: true }` also traces
|
|
141
|
+
`resources/read`, `resources/list`, `resources/templates/list`,
|
|
142
|
+
`prompts/get` and `prompts/list`, on SDK v1 and v2, low-level `Server`
|
|
143
|
+
and `McpServer`. Both are off by default; nothing changes unless you opt in.
|
|
144
|
+
- Spans are named by method (`prompts/get <name>` for prompts/get) and
|
|
145
|
+
carry `mcp.method.name` (+ `gen_ai.prompt.name` on prompts/get, capped
|
|
146
|
+
at 128 characters). `gen_ai.tool.name` is absent.
|
|
147
|
+
- Never captured: resource URIs, prompt arguments, result content, or
|
|
148
|
+
the exception message of a failing request (SDK resource errors embed
|
|
149
|
+
the URI). Failures get status `ERROR`, `error.type` and the usual
|
|
150
|
+
fingerprint/category/channel.
|
|
151
|
+
- New histogram `mcp.server.operation.duration`, labeled by
|
|
152
|
+
`mcp.method.name` + `error.type` only. No new label keys;
|
|
153
|
+
`mcp.method.name` stays a fixed set of constants (now 7 values).
|
|
154
|
+
- A handler already registered when `instrumentMcpServer()` runs is
|
|
155
|
+
skipped with one `diag.warn`, never a throw. That includes SDK v2
|
|
156
|
+
`McpServer` declaring `capabilities.resources/prompts` in its
|
|
157
|
+
constructor.
|
|
158
|
+
|
|
159
|
+
### Changed
|
|
160
|
+
|
|
161
|
+
- `server.shutdown()` (under `setupNodeSdk: true`) is now idempotent:
|
|
162
|
+
every call returns the same promise, so an explicit `shutdown()` and
|
|
163
|
+
`flushOnExit` never shut the providers down twice.
|
|
164
|
+
- The README quickstart and both examples no longer need the
|
|
165
|
+
`process.on('beforeExit', ...)` block; it's been removed.
|
|
166
|
+
|
|
167
|
+
### Fixed
|
|
168
|
+
|
|
169
|
+
- **SDK v2: a `McpServer` constructed with `capabilities: { tools: {} }`
|
|
170
|
+
can now be instrumented.** v2's `McpServer` installs its own
|
|
171
|
+
`tools/list`/`tools/call` dispatchers in its constructor when that
|
|
172
|
+
capability is declared, so `instrumentMcpServer()` threw
|
|
173
|
+
`INSTRUMENT_FIRST_ERROR` on every such server, whatever the call order.
|
|
174
|
+
It now recognizes McpServer's own constructor-installed handlers (v2
|
|
175
|
+
McpServer, installer ran, no tool registered yet, not yet connected),
|
|
176
|
+
takes them down with the public `removeRequestHandler()`, and lets
|
|
177
|
+
McpServer re-install them through the instrumented `setRequestHandler`,
|
|
178
|
+
so they're wrapped like any lazily-installed handler. Genuine misorders
|
|
179
|
+
still throw: a tool registered before instrumenting (v1 or v2, with or
|
|
180
|
+
without declared capabilities) or a low-level `Server` handler
|
|
181
|
+
registered first. Present since v2 support shipped (SDK 2.0.0).
|
|
182
|
+
|
|
3
183
|
## 0.15.0
|
|
4
184
|
|
|
5
185
|
"Make it visible" — no instrumentation changes, no `tools/call` hot-path
|
package/README.md
CHANGED
|
@@ -77,40 +77,39 @@ import { z } from 'zod';
|
|
|
77
77
|
|
|
78
78
|
const server = new McpServer({ name: 'my-server', version: '1.0.0' });
|
|
79
79
|
|
|
80
|
-
// Wraps every tool registered below. Must run BEFORE server.
|
|
80
|
+
// Wraps every tool registered below. Must run BEFORE server.registerTool() —
|
|
81
81
|
// see "Ordering constraint" below for why.
|
|
82
|
-
|
|
82
|
+
instrumentMcpServer(server, {
|
|
83
83
|
serviceName: 'my-mcp-server', // shows up on your traces
|
|
84
84
|
setupNodeSdk: true, // dev mode: prints traces to your terminal
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
// A normal tool, registered exactly as usual.
|
|
88
|
-
server.
|
|
88
|
+
server.registerTool('echo', { inputSchema: { text: z.string() } }, async ({ text }) => ({
|
|
89
89
|
content: [{ type: 'text', text: `you said: ${text}` }],
|
|
90
90
|
}));
|
|
91
91
|
|
|
92
92
|
const transport = new StdioServerTransport();
|
|
93
93
|
await server.connect(transport);
|
|
94
|
-
|
|
95
|
-
// Only needed to see metrics (below) immediately when piping ONE request
|
|
96
|
-
// in and exiting right after, like the "See it working" example does —
|
|
97
|
-
// a real, long-running server sees the same lines on their own, every 5
|
|
98
|
-
// seconds, with no extra code.
|
|
99
|
-
let flushed = false;
|
|
100
|
-
process.on('beforeExit', () => {
|
|
101
|
-
if (flushed) return;
|
|
102
|
-
flushed = true;
|
|
103
|
-
instrumented.shutdown();
|
|
104
|
-
});
|
|
105
94
|
```
|
|
106
95
|
|
|
107
96
|
That's it. Every tool call now emits a trace. Wire an exporter to see them
|
|
108
|
-
(next section).
|
|
97
|
+
(next section). When the server stops — your MCP client closing stdin, or
|
|
98
|
+
sending SIGTERM/SIGINT — anything still buffered is flushed first,
|
|
99
|
+
automatically (see "Flushing on exit" below).
|
|
109
100
|
|
|
110
101
|
## See it working
|
|
111
102
|
|
|
112
|
-
|
|
113
|
-
|
|
103
|
+
Save the snippet as `server.js`. On its own it just waits: an MCP server
|
|
104
|
+
reads requests from stdin and does nothing until a client sends one. To
|
|
105
|
+
try it without a client, pipe in a single tool call:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"echo","arguments":{"text":"hi"}}}' | node server.js
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The JSON-RPC response goes to stdout, and this prints to stderr — a real,
|
|
112
|
+
captured run (full dump: `examples/hello-mcpserver/README.md`):
|
|
114
113
|
|
|
115
114
|
```
|
|
116
115
|
name: 'tools/call echo'
|
|
@@ -127,10 +126,12 @@ attributes: {
|
|
|
127
126
|
No dashboard needed — `setupNodeSdk: true`'s dev exporter printed this
|
|
128
127
|
directly. Point it at a real backend later; see "Two modes" below.
|
|
129
128
|
|
|
130
|
-
**Metrics print too (v0.15.0+)** —
|
|
131
|
-
60-second export interval), a compact one-line-per-metric summary follows
|
|
129
|
+
**Metrics print too (v0.15.0+)** — a compact one-line-per-metric summary
|
|
132
130
|
on stderr, reusing the exact `mcp.tool.*` instruments documented in
|
|
133
|
-
"Metrics" below
|
|
131
|
+
"Metrics" below. A long-running server prints it every 5 seconds (not the
|
|
132
|
+
default 60-second export interval). The one-shot piped run above prints it
|
|
133
|
+
once as it exits, because the server flushes when stdin closes (v0.16.0,
|
|
134
|
+
see "Flushing on exit"):
|
|
134
135
|
|
|
135
136
|
```
|
|
136
137
|
[opentel-mcp metrics] mcp.tool.calls{gen_ai.tool.name=echo,mcp.method.name=tools/call} = 1
|
|
@@ -299,7 +300,7 @@ problem" above) — exactly like the quickstart's `echo` tool, but with
|
|
|
299
300
|
`isError: true` added and no `throw` involved:
|
|
300
301
|
|
|
301
302
|
```js
|
|
302
|
-
server.
|
|
303
|
+
server.registerTool('fetch_weather', { inputSchema: { city: z.string() } }, async ({ city }) => ({
|
|
303
304
|
isError: true,
|
|
304
305
|
content: [{ type: 'text', text: `couldn't reach the weather service for ${city}` }],
|
|
305
306
|
}));
|
|
@@ -315,6 +316,60 @@ error.type = tool_error
|
|
|
315
316
|
|
|
316
317
|
Verified in `test/instrument.test.js`'s "tool-level failure" tests.
|
|
317
318
|
|
|
319
|
+
#### Errors your agent can't act on (v0.16.0)
|
|
320
|
+
|
|
321
|
+
An `isError: true` result with no content, or just `"Error"`, tells the
|
|
322
|
+
model nothing about what went wrong or what to do next — a common cause
|
|
323
|
+
of retries and thrash. Every `isError: true` span also carries:
|
|
324
|
+
|
|
325
|
+
| Attribute | Values |
|
|
326
|
+
| --- | --- |
|
|
327
|
+
| `mcp.failure.unactionable` | `true` when there's no content, or fewer than 10 characters of text (whitespace-trimmed) and no image/audio/resource item |
|
|
328
|
+
| `mcp.failure.content_length_bucket` | `empty` (0) · `tiny` (1–9) · `short` (< 80) · `medium` (< 500) · `long` |
|
|
329
|
+
|
|
330
|
+
Both are computed **only from lengths and item counts — never from the
|
|
331
|
+
text itself** (proved by a canary-string test in
|
|
332
|
+
`test/unactionable.test.js`), on spans only, never as metric labels. They
|
|
333
|
+
don't change fingerprints or categories. Configure with
|
|
334
|
+
`unactionableErrors: { enabled, minTextLength }` (defaults `true`, `10`).
|
|
335
|
+
Design: [ADR 025](../../docs/adr/025-unactionable-errors.md).
|
|
336
|
+
|
|
337
|
+
### Resources and prompts (opt-in, v0.16.0)
|
|
338
|
+
|
|
339
|
+
Tools are traced by default. `resources/*` and `prompts/*` requests can be
|
|
340
|
+
traced too, if you turn them on:
|
|
341
|
+
|
|
342
|
+
```js
|
|
343
|
+
instrumentMcpServer(server, {
|
|
344
|
+
coverage: { resources: true, prompts: true }, // either or both
|
|
345
|
+
});
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
| Method | Span name | Attributes |
|
|
349
|
+
| --- | --- | --- |
|
|
350
|
+
| `resources/read`, `resources/list`, `resources/templates/list` | the method, e.g. `resources/read` | `mcp.method.name` |
|
|
351
|
+
| `prompts/get` | `prompts/get <prompt name>` | `mcp.method.name`, `gen_ai.prompt.name` |
|
|
352
|
+
| `prompts/list` | `prompts/list` | `mcp.method.name` |
|
|
353
|
+
|
|
354
|
+
- **Never captured:** resource URIs, prompt arguments, result content. A
|
|
355
|
+
failing request gets status `ERROR`, `error.type`, and the usual
|
|
356
|
+
fingerprint/category/channel (`mcp.failure.*`), but **not** the exception
|
|
357
|
+
message, because SDK resource errors embed the URI in it.
|
|
358
|
+
- `gen_ai.tool.name` is absent (not empty) on these spans, so they never
|
|
359
|
+
show up in per-tool views or tool metrics.
|
|
360
|
+
- Durations go to a separate histogram, `mcp.server.operation.duration`,
|
|
361
|
+
labeled only by `mcp.method.name` (one of the five methods above) and
|
|
362
|
+
`error.type`. No thrash detection or cost tracking applies.
|
|
363
|
+
- **Ordering:** instrument before registering resources/prompts, same as
|
|
364
|
+
tools. If a handler for one of these methods already exists, that method
|
|
365
|
+
is skipped with a single `diag.warn`. It never throws, so turning this on
|
|
366
|
+
can't break an existing server. On an SDK v2 `McpServer`, declaring
|
|
367
|
+
`capabilities: { resources: {}, prompts: {} }` in the constructor
|
|
368
|
+
registers those handlers immediately (and so skips them); leave them out,
|
|
369
|
+
since registering a resource or prompt declares the capability for you.
|
|
370
|
+
|
|
371
|
+
Design: [ADR 026](../../docs/adr/026-resources-prompts-coverage.md).
|
|
372
|
+
|
|
318
373
|
### Span attributes
|
|
319
374
|
|
|
320
375
|
Every span follows the OpenTelemetry MCP semantic conventions (see
|
|
@@ -344,11 +399,12 @@ The four base `mcp.tool.*` call/duration metrics below, via
|
|
|
344
399
|
registered — **except under `setupNodeSdk: true`, which registers one for
|
|
345
400
|
you (v0.15.0+); see "Dev-mode metrics" below.** Set `enableMetrics: false`
|
|
346
401
|
to opt out even when one is (dev-mode included); tracing is unaffected
|
|
347
|
-
either way. Source of truth: `src/metrics.js`.
|
|
348
|
-
across the library in total — this section's four, plus two
|
|
349
|
-
metrics ("Cost & Token Attribution" below), five Agent Thrash
|
|
350
|
-
metrics ("Agent Thrash Detection" below),
|
|
351
|
-
("Tool schema drift detection" below).
|
|
402
|
+
either way. Source of truth: `src/metrics.js`. Thirteen `mcp.tool.*`
|
|
403
|
+
metrics exist across the library in total — this section's four, plus two
|
|
404
|
+
cost/token metrics ("Cost & Token Attribution" below), five Agent Thrash
|
|
405
|
+
Detection metrics ("Agent Thrash Detection" below), one schema-drift metric
|
|
406
|
+
("Tool schema drift detection" below), and `mcp.tool.stale_responses`
|
|
407
|
+
("Staleness detection" below).
|
|
352
408
|
|
|
353
409
|
| Metric | Type | Unit | Attributes | Emitted when |
|
|
354
410
|
|---|---|---|---|---|
|
|
@@ -449,8 +505,30 @@ What it does, under `setupNodeSdk: true` and `enableMetrics` (default
|
|
|
449
505
|
already registered, exactly as it does today.
|
|
450
506
|
- Extends `server.shutdown()` (already attached under `setupNodeSdk: true`)
|
|
451
507
|
to also flush and shut down the `MeterProvider` it registered, alongside
|
|
452
|
-
the existing `TracerProvider` shutdown
|
|
453
|
-
|
|
508
|
+
the existing `TracerProvider` shutdown. As of v0.16.0 this runs
|
|
509
|
+
automatically when the process stops — see "Flushing on exit" below.
|
|
510
|
+
|
|
511
|
+
### Flushing on exit (`flushOnExit`, v0.16.0)
|
|
512
|
+
|
|
513
|
+
Under `setupNodeSdk: true`, buffered spans and dev-mode metrics are
|
|
514
|
+
flushed automatically when the process stops. It's on by default; pass
|
|
515
|
+
`flushOnExit: false` to turn it off. MCP clients built on the SDK's stdio
|
|
516
|
+
transport stop a server by closing stdin, then sending SIGTERM 2 seconds
|
|
517
|
+
later, then SIGKILL; this covers the first two:
|
|
518
|
+
|
|
519
|
+
| Event | What happens |
|
|
520
|
+
| --- | --- |
|
|
521
|
+
| stdin closes and nothing else keeps the process alive | flushed on `beforeExit`; the process exits with its own exit code |
|
|
522
|
+
| `SIGTERM` / `SIGINT` | flushed, then the same signal is re-raised so the process still dies by it, unless your code has its own listener for that signal, in which case yours decides |
|
|
523
|
+
| `process.exit()` | **not covered**: call `await server.shutdown()` before exiting |
|
|
524
|
+
| `SIGKILL` | can't be intercepted |
|
|
525
|
+
|
|
526
|
+
The flush waits at most 1000 ms (`flushOnExit: { timeoutMs }` can only
|
|
527
|
+
lower that), so a stuck exporter can't keep a stopping server alive.
|
|
528
|
+
`server.shutdown()` is safe to call yourself as well: it runs once, however
|
|
529
|
+
many times it's called. With `setupNodeSdk: false` this option does
|
|
530
|
+
nothing — your application owns its providers and its shutdown. Design:
|
|
531
|
+
[ADR 024](../../docs/adr/024-flush-on-exit.md).
|
|
454
532
|
|
|
455
533
|
`@opentelemetry/sdk-metrics` is a real (non-dev) dependency of opentel-mcp
|
|
456
534
|
as of this feature — not an optional peer, not a dynamic import with a
|
|
@@ -502,6 +580,181 @@ See `dashboards/grafana-mcp-health.json` and `dashboards/dev/` in the repo
|
|
|
502
580
|
for a full worked example (dashboard JSON + a script that exercises this
|
|
503
581
|
exact setup end to end).
|
|
504
582
|
|
|
583
|
+
## Staleness detection (v0.17.0)
|
|
584
|
+
|
|
585
|
+
A read tool can return data older than it should be: a quote from four
|
|
586
|
+
minutes ago, yesterday's bars after today's session opened. The call
|
|
587
|
+
succeeds, the span is green, and the agent acts on stale data with full
|
|
588
|
+
confidence. opentel-mcp can flag that, if the tool says how fresh its data
|
|
589
|
+
is. The tool is the source of truth, because only it knows its own data
|
|
590
|
+
cadence. Config is only an override. (Suggested by the r/mcp community.)
|
|
591
|
+
|
|
592
|
+
**It's opt-in, per tool.** A tool adds one key to its result's `_meta`:
|
|
593
|
+
|
|
594
|
+
```js
|
|
595
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
596
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
597
|
+
import { instrumentMcpServer, FRESHNESS_META_KEY } from 'opentel-mcp';
|
|
598
|
+
import { z } from 'zod';
|
|
599
|
+
|
|
600
|
+
const server = new McpServer({ name: 'market-data', version: '1.0.0' });
|
|
601
|
+
|
|
602
|
+
// Before any registerTool() call.
|
|
603
|
+
instrumentMcpServer(server, {
|
|
604
|
+
serviceName: 'market-data',
|
|
605
|
+
setupNodeSdk: true, // dev mode: prints spans to stderr
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// Stand-in for your real feed. This quote is 4 minutes old.
|
|
609
|
+
async function fetchQuote(symbol) {
|
|
610
|
+
return { symbol, price: 101.5, timestamp: Date.now() - 4 * 60_000 };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
server.registerTool(
|
|
614
|
+
'get_quote',
|
|
615
|
+
{ description: 'Latest quote for a symbol', inputSchema: { symbol: z.string() } },
|
|
616
|
+
async ({ symbol }) => {
|
|
617
|
+
const quote = await fetchQuote(symbol);
|
|
618
|
+
return {
|
|
619
|
+
content: [{ type: 'text', text: JSON.stringify(quote) }],
|
|
620
|
+
_meta: {
|
|
621
|
+
[FRESHNESS_META_KEY]: {
|
|
622
|
+
asOf: quote.timestamp, // when the data is from: epoch ms, or ISO 8601 with a timezone
|
|
623
|
+
maxAgeMs: 5_000, // a quote is fresh for 5 seconds
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
};
|
|
627
|
+
},
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
await server.connect(new StdioServerTransport());
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
Save it as `server.js` and send it one call:
|
|
634
|
+
|
|
635
|
+
```bash
|
|
636
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_quote","arguments":{"symbol":"ACME"}}}' | node server.js
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
The span on stderr (from a real run) is still `OK`, and now says why the
|
|
640
|
+
answer shouldn't be trusted:
|
|
641
|
+
|
|
642
|
+
```
|
|
643
|
+
name: 'tools/call get_quote'
|
|
644
|
+
attributes: {
|
|
645
|
+
...
|
|
646
|
+
'mcp.tool.freshness.verdict': 'stale',
|
|
647
|
+
'mcp.tool.freshness.data_age_ms': 240001,
|
|
648
|
+
'mcp.tool.freshness.max_age_ms': 5000,
|
|
649
|
+
'mcp.tool.freshness.limit_source': 'max_age_ms'
|
|
650
|
+
},
|
|
651
|
+
status: { code: 1 } // OK: stale data isn't an error
|
|
652
|
+
[opentel-mcp metrics] mcp.tool.stale_responses{gen_ai.tool.name=get_quote} = 1
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
### The `_meta` contract
|
|
656
|
+
|
|
657
|
+
`FRESHNESS_META_KEY` is `'io.github.thirumalaiboobathi.opentel-mcp/freshness'`
|
|
658
|
+
(a reverse-DNS key, as the MCP spec recommends for custom `_meta` keys).
|
|
659
|
+
Its value:
|
|
660
|
+
|
|
661
|
+
| Field | Type | Meaning |
|
|
662
|
+
| --- | --- | --- |
|
|
663
|
+
| `asOf` | epoch **milliseconds**, or ISO 8601 string **with** `Z`/`±hh:mm` | When the data is from (the quote's timestamp, the bar's close), not when the tool ran. Required. |
|
|
664
|
+
| `maxAgeMs` | number ≥ 0 | How long the data stays fresh. |
|
|
665
|
+
| `freshUntil` | same as `asOf` | An absolute deadline, for windows that aren't a fixed duration. |
|
|
666
|
+
|
|
667
|
+
Give `asOf` plus `maxAgeMs`, `freshUntil`, or both. With both, the
|
|
668
|
+
**earlier deadline wins**. For data that's good until the trading session
|
|
669
|
+
closes:
|
|
670
|
+
|
|
671
|
+
```js
|
|
672
|
+
_meta: {
|
|
673
|
+
[FRESHNESS_META_KEY]: {
|
|
674
|
+
asOf: bars.closeTime, // '2026-10-12T20:00:00Z'
|
|
675
|
+
freshUntil: nextSessionClose, // '2026-10-13T20:00:00Z'
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
TypeScript users get autocomplete from the exported `FreshnessMeta` type:
|
|
681
|
+
`{ asOf, maxAgeMs } satisfies FreshnessMeta`.
|
|
682
|
+
|
|
683
|
+
### Overriding a tool's window from config
|
|
684
|
+
|
|
685
|
+
```js
|
|
686
|
+
instrumentMcpServer(server, {
|
|
687
|
+
staleness: {
|
|
688
|
+
tools: {
|
|
689
|
+
get_quote: { maxAgeMs: 2_000 }, // replaces get_quote's own maxAgeMs/freshUntil
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
The override replaces the tool's window entirely (`limit_source` becomes
|
|
696
|
+
`config`). It can't supply `asOf`: a response without the freshness key
|
|
697
|
+
gets no verdict, whatever the config says. An invalid value (negative,
|
|
698
|
+
non-finite) is ignored with one `diag.warn`. `staleness: { enabled: false }`
|
|
699
|
+
turns the whole feature off.
|
|
700
|
+
|
|
701
|
+
### What gets emitted
|
|
702
|
+
|
|
703
|
+
| Attribute | Type | When |
|
|
704
|
+
| --- | --- | --- |
|
|
705
|
+
| `mcp.tool.freshness.verdict` | `fresh` \| `stale` \| `unknown` | The tool sent the freshness key |
|
|
706
|
+
| `mcp.tool.freshness.data_age_ms` | integer ms | `asOf` was readable |
|
|
707
|
+
| `mcp.tool.freshness.max_age_ms` | integer ms | A verdict exists: the effective window (deadline minus `asOf`) |
|
|
708
|
+
| `mcp.tool.freshness.limit_source` | `max_age_ms` \| `fresh_until` \| `config` | A verdict exists |
|
|
709
|
+
| `mcp.tool.freshness.unknown_reason` | `invalid_as_of` \| `future_as_of` \| `missing_window` \| `invalid_window` | Verdict is `unknown` |
|
|
710
|
+
|
|
711
|
+
Plus one counter, `mcp.tool.stale_responses` (label: `gen_ai.tool.name`),
|
|
712
|
+
`+1` per stale response.
|
|
713
|
+
|
|
714
|
+
- **Tools that don't report freshness get nothing.** No attribute, no
|
|
715
|
+
verdict; never `fresh`, never `stale`. Their spans are unchanged.
|
|
716
|
+
- **Stale isn't an error.** Span status stays `OK`, there's no
|
|
717
|
+
`error.type`, and it's never counted as a silent failure. Alert on the
|
|
718
|
+
verdict or the counter, not on error rate.
|
|
719
|
+
- **`isError: true` responses are skipped.** They're already failures.
|
|
720
|
+
- **Malformed freshness never breaks a call.** It gets `unknown` with a
|
|
721
|
+
reason, at most one `diag.warn` per tool, and the result goes back to
|
|
722
|
+
the client untouched. Common mistakes it catches: epoch **seconds**
|
|
723
|
+
instead of milliseconds, an ISO string with no timezone, `asOf` more than
|
|
724
|
+
5 s in the future (up to 5 s is treated as clock skew), and `freshUntil`
|
|
725
|
+
before `asOf`.
|
|
726
|
+
- **Privacy:** only `asOf`, `maxAgeMs` and `freshUntil` under that one key
|
|
727
|
+
are read. Result content and other `_meta` fields are never read for
|
|
728
|
+
this, and raw values are never emitted, only the derived numbers
|
|
729
|
+
(checked by a canary test in `test/staleness.test.js`).
|
|
730
|
+
- **Data age is span-only**, never a metric label.
|
|
731
|
+
|
|
732
|
+
### Seeing it in the dashboard
|
|
733
|
+
|
|
734
|
+
`opentel-mcp-ui` 0.3.0+ shows the data age in its feed, a **Stale data**
|
|
735
|
+
filter, and stale rate as a fifth health-grade signal. Start it, then point
|
|
736
|
+
the server above at it with one more option:
|
|
737
|
+
|
|
738
|
+
```bash
|
|
739
|
+
npx opentel-mcp-ui # dashboard at http://127.0.0.1:4319
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
```js
|
|
743
|
+
instrumentMcpServer(server, {
|
|
744
|
+
serviceName: 'market-data',
|
|
745
|
+
setupNodeSdk: true,
|
|
746
|
+
exporterUrl: 'http://127.0.0.1:4319/v1/traces', // send spans to the dashboard too
|
|
747
|
+
});
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
Each run of the `echo … | node server.js` command above now shows up in the
|
|
751
|
+
dashboard's feed and under **Stale data**. The tool gets a grade once it has
|
|
752
|
+
10 calls, and the stale-data signal joins it once 10 of those carried a
|
|
753
|
+
`fresh` or `stale` verdict, so send the command ten times. With every call
|
|
754
|
+
stale, `get_quote` grades **F**, "set by stale data 100% (F)".
|
|
755
|
+
|
|
756
|
+
Design and every edge case: [ADR 027](../../docs/adr/027-staleness-detection.md).
|
|
757
|
+
|
|
505
758
|
## Failure Fingerprinting (v0.4.0+)
|
|
506
759
|
|
|
507
760
|
Groups logically identical failures under one stable identifier, even
|
|
@@ -586,7 +839,11 @@ per-call-site wrapping; tracked in the roadmap below.
|
|
|
586
839
|
**What this controls, and who needs it.** Whenever a `tools/call`/
|
|
587
840
|
`tools/list` handler throws, this library records the thrown error's
|
|
588
841
|
message and stack trace onto the span it creates for that call — by
|
|
589
|
-
default,
|
|
842
|
+
default, as the tool wrote them, except that recognizable credentials
|
|
843
|
+
(Bearer/Basic tokens, AWS key ids, `sk-`/`ghp_`/`xox…` API keys, JWTs,
|
|
844
|
+
passwords in URLs, `token=`/`api_key=`/`password=` values) are replaced
|
|
845
|
+
with `<SECRET>` and the message and stack are capped at 4096 and 16384
|
|
846
|
+
characters (v0.17.0). If your tools call
|
|
590
847
|
third-party services, wrap unaudited dependencies, or you otherwise don't
|
|
591
848
|
control every string a tool might throw, that's a real channel for
|
|
592
849
|
whatever those errors happen to contain (an email address, an internal
|
|
@@ -606,7 +863,7 @@ span:
|
|
|
606
863
|
|
|
607
864
|
| Mode | `exception.message` / status message | `exception.stacktrace` | When to use |
|
|
608
865
|
|---|---|---|---|
|
|
609
|
-
| `'full'` (default) | Raw `err.message`,
|
|
866
|
+
| `'full'` (default) | Raw `err.message`, credentials → `<SECRET>`, capped at 4096 chars (v0.17.0) | Raw `err.stack`, same scrubbing, capped at 16384 chars | Everything else kept: ids, paths, URLs, line numbers. Before v0.17.0 both were unmodified |
|
|
610
867
|
| `'normalized'` | `normalizeMessage(err.message)` — the exact scrubbing pipeline (`src/fingerprint/normalize/message.js`) fingerprinting already runs before hashing: UUIDs, emails, URLs, IPs, timestamps, filesystem paths, hex runs, quoted ids | Reconstructed from `parseAndNormalizeStack()` (`src/fingerprint/normalize/stack.js`) — keeps every function name/file/line, strips only the local `cwd` prefix (and collapses `node_modules` package versions) | Tool results come from third-party or unaudited MCP servers and you want the same scrubbing fingerprinting already trusts, applied to the raw exception content too |
|
|
611
868
|
| `'none'` | Not set — `span.setStatus({ code: ERROR })` with no message, the same pattern already used for tool-level `isError: true` failures | Not set | You rely entirely on `mcp.failure.*` (category/fingerprint/signature — already hashed/normalized) and don't want any free-text exception content on the span at all |
|
|
612
869
|
|
|
@@ -2144,11 +2401,13 @@ found')`, or a tool result puts a credential in a field this library
|
|
|
2144
2401
|
reads, that content is exactly what reaches your telemetry backend. Three
|
|
2145
2402
|
channels carry it:
|
|
2146
2403
|
|
|
2147
|
-
-
|
|
2404
|
+
- **The `exception` event / `span.setStatus({ message })`** on every
|
|
2148
2405
|
thrown `tools/call`/`tools/list` error — `exception.message` and
|
|
2149
|
-
`exception.stacktrace` are set from `err.message`/`err.stack`
|
|
2150
|
-
|
|
2151
|
-
|
|
2406
|
+
`exception.stacktrace` are set from `err.message`/`err.stack` by
|
|
2407
|
+
default, with recognizable credentials replaced by `<SECRET>` and
|
|
2408
|
+
lengths capped (v0.17.0), but otherwise unchanged: a customer's name or
|
|
2409
|
+
any other free text in a message still reaches the span.
|
|
2410
|
+
`errorRecording.mode` controls this; see "Error recording" above.
|
|
2152
2411
|
- **`mcp.tool.model` / `gen_ai.response.model`** — a tool result's own
|
|
2153
2412
|
declared model field, admitted once it passes a length/character-shape
|
|
2154
2413
|
check (v0.13.0). The check bounds shape, not content: a well-formed but
|
|
@@ -2241,9 +2500,11 @@ so they're spelled out instead of forced into one bucket.
|
|
|
2241
2500
|
| `mcp.tool.pricing_status` | span attr, metric label | **blend — library-computed, depends on tool + host** | which of three enum values depends on a tool-controlled model name resolving (or not) in a library-default-or-host-overridden pricing table |
|
|
2242
2501
|
| `mcp.failure.fingerprint` / `.signature` / `.category` / `.origin` / `.channel` | span attr (2 also metric labels) | library-computed | hashes/enums — the governed core this project has kept clean since v0.4.0 |
|
|
2243
2502
|
| `mcp.failure.error_class` | span attr | **blend — tool-controlled, capped only** | `err.name`, length-capped at 128 characters but, unlike everything hashed into the fingerprint, *not* pattern-scrubbed — a documented, deliberate tradeoff (`docs/known-gaps.md` entry 10) |
|
|
2503
|
+
| `mcp.failure.unactionable` / `.content_length_bucket` | span attr | library-computed | a boolean and a 5-value enum derived from the trimmed text LENGTH and non-text item COUNT of an `isError: true` result — no text is read for anything but its length (ADR 025, v0.16.0) |
|
|
2504
|
+
| `mcp.tool.freshness.verdict` / `.data_age_ms` / `.max_age_ms` / `.limit_source` / `.unknown_reason` | span attr | library-computed | enums and integer ms derived from the tool's own `_meta[FRESHNESS_META_KEY]` `asOf`/`maxAgeMs`/`freshUntil`; raw values are never emitted, and no other `_meta` field or content is read (ADR 027, v0.17.0) |
|
|
2244
2505
|
| `mcp.failure.validation_paths` | span attr | mostly library-computed (gated) | one legacy Zod-issue-rendering path can still carry a tool-controlled dynamic object key, now redacted to a `<KEY>` placeholder rather than closed structurally (`docs/known-gaps.md` entry 10, item 3) |
|
|
2245
|
-
| `exception.type` (event) | event attr | tool-controlled | length-capped
|
|
2246
|
-
| `exception.message` / `.stacktrace` (event), status message | event attr + status | **blend — layered** | raw tool content under `'full'
|
|
2506
|
+
| `exception.type` (event) | event attr | tool-controlled | length-capped at 128 characters under both `'full'` and `'normalized'` (v0.17.0) |
|
|
2507
|
+
| `exception.message` / `.stacktrace` (event), status message | event attr + status | **blend — layered** | raw tool content under `'full'`, minus recognizable credentials and length-capped (v0.17.0); this library's `normalizeMessage()`/`parseAndNormalizeStack()` transform of that same content under `'normalized'`; optionally a host-supplied `errorRecording.redactor`'s output feeding *that*, first (v0.14.0) — the most layered-provenance attribute in the system |
|
|
2247
2508
|
| `mcp.tool.schema_drift_detected` / `mcp.tool.thrash_detected` | boolean span attr | library-computed | only ever set to `true`; omitted on a clean call |
|
|
2248
2509
|
| `mcp.tool.schema_drift.type` | event attr, metric label | library-computed | classifier enum |
|
|
2249
2510
|
| `mcp.tool.schema_drift.previous_hash` / `.current_hash` | event attr | library-computed | a hash *of* tool-controlled content (the tool's own `inputSchema`), not the schema itself |
|
|
@@ -2321,6 +2582,9 @@ truth: `src/config.js`.
|
|
|
2321
2582
|
| `thrashDetection` | object | see below | Controls Agent Thrash Detection[^10] — see "Agent Thrash Detection" above. Requires `fingerprinting: true` |
|
|
2322
2583
|
| `schemaDrift` | object | see below | Controls tool schema drift detection[^11] — see "Tool schema drift detection" above. `enabled: true` by default changes `instrumentMcpServer()`'s throw behavior on upgrade — see that section's "Behavior change on upgrade" |
|
|
2323
2584
|
| `errorRecording` | object | see below | Controls what raw exception content reaches `recordException()`/`setStatus()`[^12] — see "Error recording" above |
|
|
2585
|
+
| `unactionableErrors` | object | `{ enabled: true, minTextLength: 10 }` | `{ enabled?, minTextLength? }`: flags `isError: true` results with no usable text via `mcp.failure.unactionable` / `mcp.failure.content_length_bucket` (span-only). Env: `OTEL_MCP_UNACTIONABLE_ERRORS_ENABLED`, `OTEL_MCP_UNACTIONABLE_ERRORS_MIN_TEXT_LENGTH` — see "Errors your agent can't act on" above |
|
|
2586
|
+
| `coverage` | object | `{ resources: false, prompts: false }` | Opt-in tracing of `resources/*` and `prompts/*` — see "Resources and prompts" above |
|
|
2587
|
+
| `staleness` | object | `{ enabled: true }` | `{ enabled?, tools?: { [toolName]: { maxAgeMs } } }`: per-tool override of the freshness window a tool reports in `_meta` — see "Staleness detection" above |
|
|
2324
2588
|
|
|
2325
2589
|
[^10]: `{ enabled?, threshold?, windowMs?, maxTrackedKeys?, entryTtlMs?, reEmitAfter?, assumeSingleSession? }`, all fields optional, individually defaulted, and individually overridable via an `OTEL_MCP_THRASH_*` env var — see "Agent Thrash Detection" → "Configuration" above for the full table.
|
|
2326
2590
|
[^11]: `{ enabled?, maxTrackedTools? }`, all fields optional, individually defaulted, and individually overridable via an `OTEL_MCP_SCHEMA_DRIFT_*` env var — see "Tool schema drift detection" → "Configuration" above for the full table.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opentel-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "One-line OpenTelemetry instrumentation for Model Context Protocol (MCP) servers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -73,7 +73,8 @@
|
|
|
73
73
|
"@opentelemetry/sdk-trace-node": "^2.9.0"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@modelcontextprotocol/
|
|
76
|
+
"@modelcontextprotocol/sdk": "1.32.1",
|
|
77
|
+
"@modelcontextprotocol/server": "2.3.1",
|
|
77
78
|
"@opentelemetry/api": "^1.9.0",
|
|
78
79
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
79
80
|
"@opentelemetry/sdk-trace-base": "^2.9.0",
|