@telemetry-dev/mcp 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 +21 -0
- package/README.md +65 -0
- package/dist/index.d.mts +16 -0
- package/dist/index.mjs +417 -0
- package/package.json +67 -0
- package/src/index.ts +638 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 telemetry.dev
|
|
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,65 @@
|
|
|
1
|
+
# @telemetry-dev/mcp
|
|
2
|
+
|
|
3
|
+
Model Context Protocol instrumentation for telemetry.dev. It instruments official TypeScript MCP SDK v2 transports and emits request spans using the current OpenTelemetry MCP attribute vocabulary through `@telemetry-dev/sdk`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
For a client:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add @telemetry-dev/sdk @telemetry-dev/mcp @modelcontextprotocol/client
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For a server:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pnpm add @telemetry-dev/sdk @telemetry-dev/mcp @modelcontextprotocol/server
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Client
|
|
20
|
+
|
|
21
|
+
Set `TELEMETRY_DEV_API_KEY`, then instrument the transport before connecting it:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { Client } from "@modelcontextprotocol/client";
|
|
25
|
+
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
26
|
+
import { instrumentMcpTransport } from "@telemetry-dev/mcp";
|
|
27
|
+
import { init } from "@telemetry-dev/sdk";
|
|
28
|
+
|
|
29
|
+
init({ serviceName: "mcp-client" });
|
|
30
|
+
|
|
31
|
+
const client = new Client({ name: "example-client", version: "1.0.0" });
|
|
32
|
+
const transport = instrumentMcpTransport(
|
|
33
|
+
new StdioClientTransport({ command: "example-mcp-server" }),
|
|
34
|
+
);
|
|
35
|
+
await client.connect(transport);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Server
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
42
|
+
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
43
|
+
import { instrumentMcpTransport } from "@telemetry-dev/mcp";
|
|
44
|
+
import { init } from "@telemetry-dev/sdk";
|
|
45
|
+
|
|
46
|
+
init({ serviceName: "mcp-server" });
|
|
47
|
+
|
|
48
|
+
const server = new McpServer({ name: "example-server", version: "1.0.0" });
|
|
49
|
+
const transport = instrumentMcpTransport(new StdioServerTransport());
|
|
50
|
+
await server.connect(transport);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`instrumentMcpTransport(transport)` mutates and returns the same transport and is idempotent.
|
|
54
|
+
|
|
55
|
+
## Payload capture
|
|
56
|
+
|
|
57
|
+
Tool arguments and successful tool results are not captured by default. Enable them with `instrumentMcpTransport(transport, { capturePayloads: true })`. Core `@telemetry-dev/sdk` capture and masking settings still apply.
|
|
58
|
+
|
|
59
|
+
Trace context propagates by default. OpenTelemetry baggage remains local unless you enable it with `instrumentMcpTransport(transport, { propagateBaggage: true })`.
|
|
60
|
+
|
|
61
|
+
## Limitations
|
|
62
|
+
|
|
63
|
+
- Only the stable official TypeScript MCP SDK v2 transport contract is supported. MCP v1 is not supported.
|
|
64
|
+
- Metrics and notification processing spans are not emitted.
|
|
65
|
+
- OpenTelemetry MCP semantic conventions are currently in Development status, so attribute requirements may change in future releases.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
interface McpTransport {
|
|
3
|
+
send: (...args: never[]) => Promise<void>;
|
|
4
|
+
onmessage?: (...args: never[]) => void;
|
|
5
|
+
onclose?: () => void;
|
|
6
|
+
sessionId?: string;
|
|
7
|
+
readonly protocolVersion?: string;
|
|
8
|
+
setProtocolVersion?: (version: string) => void;
|
|
9
|
+
}
|
|
10
|
+
interface InstrumentMcpTransportOptions {
|
|
11
|
+
capturePayloads?: boolean;
|
|
12
|
+
propagateBaggage?: boolean;
|
|
13
|
+
}
|
|
14
|
+
declare function instrumentMcpTransport<T extends McpTransport>(transport: T, options?: InstrumentMcpTransportOptions): T;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { InstrumentMcpTransportOptions, instrumentMcpTransport };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { SpanKind, SpanStatusCode, propagation, trace } from "@opentelemetry/api";
|
|
2
|
+
import { activeContext, extractW3cContext, injectW3cContext, startSpan, withContext } from "@telemetry-dev/sdk";
|
|
3
|
+
//#region src/index.ts
|
|
4
|
+
const INSTRUMENTED = /* @__PURE__ */ new WeakSet();
|
|
5
|
+
const PROTOCOL_VERSIONS = /* @__PURE__ */ new WeakMap();
|
|
6
|
+
const CALLER_ERROR_CODES = new Set([
|
|
7
|
+
-32700,
|
|
8
|
+
-32600,
|
|
9
|
+
-32601,
|
|
10
|
+
-32602,
|
|
11
|
+
-32002,
|
|
12
|
+
-32021,
|
|
13
|
+
-32022
|
|
14
|
+
]);
|
|
15
|
+
const PROTOCOL_VERSION_META_KEY = "io.modelcontextprotocol/protocolVersion";
|
|
16
|
+
function asRecord(value) {
|
|
17
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
18
|
+
}
|
|
19
|
+
function parseMessage(value) {
|
|
20
|
+
const message = asRecord(value);
|
|
21
|
+
if (message === void 0) return void 0;
|
|
22
|
+
const params = asRecord(message.params);
|
|
23
|
+
return {
|
|
24
|
+
message,
|
|
25
|
+
method: requestMethod(message),
|
|
26
|
+
id: requestId(message),
|
|
27
|
+
params,
|
|
28
|
+
meta: asRecord(params?._meta)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function requestId(message) {
|
|
32
|
+
if (!("id" in message)) return void 0;
|
|
33
|
+
const id = message.id;
|
|
34
|
+
return typeof id === "string" || typeof id === "number" ? id : void 0;
|
|
35
|
+
}
|
|
36
|
+
function idKey(id) {
|
|
37
|
+
return `${typeof id}:${String(id)}`;
|
|
38
|
+
}
|
|
39
|
+
function requestMethod(message) {
|
|
40
|
+
return typeof message.method === "string" ? message.method : void 0;
|
|
41
|
+
}
|
|
42
|
+
function isRequest(message) {
|
|
43
|
+
return message?.method !== void 0 && message.id !== void 0 && message.method !== "notifications/cancelled";
|
|
44
|
+
}
|
|
45
|
+
function targetFor(method, params) {
|
|
46
|
+
if (method !== "tools/call" && method !== "prompts/get") return void 0;
|
|
47
|
+
return typeof params?.name === "string" ? params.name : void 0;
|
|
48
|
+
}
|
|
49
|
+
function resourceUri(method, params) {
|
|
50
|
+
if (method !== "resources/read" && method !== "resources/subscribe" && method !== "resources/unsubscribe" && method !== "notifications/resources/updated") return;
|
|
51
|
+
return typeof params?.uri === "string" ? params.uri : void 0;
|
|
52
|
+
}
|
|
53
|
+
function metaProtocolVersion(meta) {
|
|
54
|
+
const version = meta?.[PROTOCOL_VERSION_META_KEY];
|
|
55
|
+
return typeof version === "string" ? version : void 0;
|
|
56
|
+
}
|
|
57
|
+
function protocolVersion(transport, message) {
|
|
58
|
+
const modern = metaProtocolVersion(message.meta);
|
|
59
|
+
if (modern !== void 0) return modern;
|
|
60
|
+
if (message.method === "initialize" && typeof message.params?.protocolVersion === "string") {
|
|
61
|
+
PROTOCOL_VERSIONS.set(transport, message.params.protocolVersion);
|
|
62
|
+
return message.params.protocolVersion;
|
|
63
|
+
}
|
|
64
|
+
return PROTOCOL_VERSIONS.get(transport) ?? transport.protocolVersion;
|
|
65
|
+
}
|
|
66
|
+
function responseProtocolVersion(transport, method, message) {
|
|
67
|
+
const modern = metaProtocolVersion(message.meta);
|
|
68
|
+
if (modern !== void 0) return modern;
|
|
69
|
+
const result = asRecord(message.message.result);
|
|
70
|
+
if (method === "initialize" && typeof result?.protocolVersion === "string") {
|
|
71
|
+
PROTOCOL_VERSIONS.set(transport, result.protocolVersion);
|
|
72
|
+
return result.protocolVersion;
|
|
73
|
+
}
|
|
74
|
+
return PROTOCOL_VERSIONS.get(transport) ?? transport.protocolVersion;
|
|
75
|
+
}
|
|
76
|
+
function requestAttributes(transport, message) {
|
|
77
|
+
const method = message.method;
|
|
78
|
+
const attributes = {
|
|
79
|
+
"gen_ai.operation.name": method === "tools/call" ? "execute_tool" : "mcp",
|
|
80
|
+
"mcp.method.name": method
|
|
81
|
+
};
|
|
82
|
+
if (message.id !== void 0) attributes["jsonrpc.request.id"] = String(message.id);
|
|
83
|
+
if (typeof transport.sessionId === "string") attributes["mcp.session.id"] = transport.sessionId;
|
|
84
|
+
const version = protocolVersion(transport, message);
|
|
85
|
+
if (version !== void 0) attributes["mcp.protocol.version"] = version;
|
|
86
|
+
const target = targetFor(method, message.params);
|
|
87
|
+
if (method === "tools/call" && target !== void 0) attributes["gen_ai.tool.name"] = target;
|
|
88
|
+
if (method === "prompts/get" && target !== void 0) attributes["gen_ai.prompt.name"] = target;
|
|
89
|
+
const uri = resourceUri(method, message.params);
|
|
90
|
+
if (uri !== void 0) attributes["mcp.resource.uri"] = uri;
|
|
91
|
+
return attributes;
|
|
92
|
+
}
|
|
93
|
+
function startRequest(transport, message, receiver, capturePayloads, propagateBaggage) {
|
|
94
|
+
const method = message.method;
|
|
95
|
+
if (method === void 0) return void 0;
|
|
96
|
+
const target = targetFor(method, message.params);
|
|
97
|
+
const ambient = activeContext();
|
|
98
|
+
const extracted = receiver ? extractW3cContext(message.meta ?? {}, { includeBaggage: propagateBaggage }) : void 0;
|
|
99
|
+
const ambientSpan = trace.getSpanContext(ambient);
|
|
100
|
+
const remoteSpan = extracted === void 0 ? void 0 : trace.getSpanContext(extracted);
|
|
101
|
+
const remoteIsValid = remoteSpan !== void 0 && trace.isSpanContextValid(remoteSpan);
|
|
102
|
+
const remoteBaggage = extracted === void 0 ? void 0 : propagation.getBaggage(extracted);
|
|
103
|
+
const parent = remoteIsValid ? extracted : remoteBaggage === void 0 ? void 0 : propagation.setBaggage(ambient, remoteBaggage);
|
|
104
|
+
const links = ambientSpan !== void 0 && trace.isSpanContextValid(ambientSpan) && remoteIsValid && (ambientSpan.traceId !== remoteSpan.traceId || ambientSpan.spanId !== remoteSpan.spanId) ? [{ context: ambientSpan }] : void 0;
|
|
105
|
+
return {
|
|
106
|
+
handle: startSpan(target === void 0 ? method : `${method} ${target}`, {
|
|
107
|
+
type: method === "tools/call" ? "tool" : "span",
|
|
108
|
+
kind: receiver ? SpanKind.SERVER : SpanKind.CLIENT,
|
|
109
|
+
parent,
|
|
110
|
+
links,
|
|
111
|
+
attributes: requestAttributes(transport, message),
|
|
112
|
+
input: capturePayloads && method === "tools/call" ? message.params?.arguments : void 0
|
|
113
|
+
}),
|
|
114
|
+
method,
|
|
115
|
+
receiver,
|
|
116
|
+
capturePayloads,
|
|
117
|
+
completed: false
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function updateObservableAttributes(transport, pending, message) {
|
|
121
|
+
const attributes = {};
|
|
122
|
+
if (typeof transport.sessionId === "string") attributes["mcp.session.id"] = transport.sessionId;
|
|
123
|
+
const version = responseProtocolVersion(transport, pending.method, message);
|
|
124
|
+
if (version !== void 0) attributes["mcp.protocol.version"] = version;
|
|
125
|
+
pending.handle.update({ attributes });
|
|
126
|
+
}
|
|
127
|
+
function setFailure(pending, type, description) {
|
|
128
|
+
pending.handle.span.setAttribute("error.type", type);
|
|
129
|
+
pending.handle.span.setStatus({
|
|
130
|
+
code: SpanStatusCode.ERROR,
|
|
131
|
+
message: description
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function finishResponse(transport, pending, message) {
|
|
135
|
+
updateObservableAttributes(transport, pending, message);
|
|
136
|
+
const error = asRecord(message.message.error);
|
|
137
|
+
if (error !== void 0) {
|
|
138
|
+
const code = typeof error.code === "number" ? error.code : void 0;
|
|
139
|
+
const codeText = code === void 0 ? "_OTHER" : String(code);
|
|
140
|
+
pending.handle.span.setAttribute("rpc.response.status_code", codeText);
|
|
141
|
+
if (!pending.receiver || code === void 0 || !CALLER_ERROR_CODES.has(code)) setFailure(pending, codeText, typeof error.message === "string" ? error.message : void 0);
|
|
142
|
+
} else {
|
|
143
|
+
const result = asRecord(message.message.result);
|
|
144
|
+
if (pending.method === "tools/call" && result?.isError === true) setFailure(pending, "tool_error");
|
|
145
|
+
if (pending.capturePayloads && pending.method === "tools/call" && result?.isError !== true) pending.handle.update({ output: message.message.result });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function completePending(requests, key, pending, update) {
|
|
149
|
+
if (pending.completed) return;
|
|
150
|
+
pending.completed = true;
|
|
151
|
+
if (requests.get(key) === pending) requests.delete(key);
|
|
152
|
+
pending.removeAbortListener?.();
|
|
153
|
+
try {
|
|
154
|
+
update?.();
|
|
155
|
+
} catch {}
|
|
156
|
+
try {
|
|
157
|
+
pending.handle.end();
|
|
158
|
+
} catch {}
|
|
159
|
+
}
|
|
160
|
+
function completeAs(requests, id, type) {
|
|
161
|
+
const key = idKey(id);
|
|
162
|
+
const pending = requests.get(key);
|
|
163
|
+
if (pending !== void 0) completePending(requests, key, pending, () => setFailure(pending, type));
|
|
164
|
+
}
|
|
165
|
+
function cancellationId(message) {
|
|
166
|
+
if (message.method !== "notifications/cancelled") return void 0;
|
|
167
|
+
if (message.params === void 0 || !("requestId" in message.params)) return void 0;
|
|
168
|
+
const id = message.params.requestId;
|
|
169
|
+
return typeof id === "string" || typeof id === "number" ? id : void 0;
|
|
170
|
+
}
|
|
171
|
+
function prepareInjectedRequest(message) {
|
|
172
|
+
const params = { ...message.params };
|
|
173
|
+
const meta = { ...message.meta };
|
|
174
|
+
params._meta = meta;
|
|
175
|
+
return {
|
|
176
|
+
message: {
|
|
177
|
+
...message.message,
|
|
178
|
+
params
|
|
179
|
+
},
|
|
180
|
+
meta
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function instrumentMcpTransport(transport, options = {}) {
|
|
184
|
+
if (INSTRUMENTED.has(transport)) return transport;
|
|
185
|
+
INSTRUMENTED.add(transport);
|
|
186
|
+
const target = transport;
|
|
187
|
+
const originalSend = target.send.bind(target);
|
|
188
|
+
const originalSetProtocolVersion = target.setProtocolVersion?.bind(target);
|
|
189
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
190
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
191
|
+
const dispatching = /* @__PURE__ */ new WeakSet();
|
|
192
|
+
const capturePayloads = options.capturePayloads === true;
|
|
193
|
+
const propagateBaggage = options.propagateBaggage === true;
|
|
194
|
+
let onmessage = target.onmessage;
|
|
195
|
+
const wrapClose = (handler) => function() {
|
|
196
|
+
for (const [key, pending] of outgoing) completePending(outgoing, key, pending, () => setFailure(pending, "connection_error"));
|
|
197
|
+
for (const [key, pending] of incoming) completePending(incoming, key, pending, () => setFailure(pending, "connection_error"));
|
|
198
|
+
handler?.call(target);
|
|
199
|
+
};
|
|
200
|
+
let onclose = wrapClose(target.onclose);
|
|
201
|
+
target.send = async (value, sendOptions) => {
|
|
202
|
+
const values = Array.isArray(value) ? value : [value];
|
|
203
|
+
let messages = [];
|
|
204
|
+
let cancellations = [];
|
|
205
|
+
let started = [];
|
|
206
|
+
let requestSignal;
|
|
207
|
+
let sent = value;
|
|
208
|
+
let forwardedOptions = sendOptions;
|
|
209
|
+
try {
|
|
210
|
+
messages = values.map(parseMessage);
|
|
211
|
+
cancellations = messages.map((message) => message === void 0 ? void 0 : cancellationId(message));
|
|
212
|
+
const rawOptions = asRecord(sendOptions);
|
|
213
|
+
const copiedOptions = rawOptions === void 0 ? void 0 : { ...rawOptions };
|
|
214
|
+
const resumeOnly = typeof copiedOptions?.resumptionToken === "string" && copiedOptions.resumptionToken.length > 0;
|
|
215
|
+
requestSignal = copiedOptions?.requestSignal;
|
|
216
|
+
const originalStreamEnd = copiedOptions?.onRequestStreamEnd;
|
|
217
|
+
const requestCounts = /* @__PURE__ */ new Map();
|
|
218
|
+
if (!resumeOnly) for (const message of messages) {
|
|
219
|
+
if (!isRequest(message)) continue;
|
|
220
|
+
const key = idKey(message.id);
|
|
221
|
+
requestCounts.set(key, (requestCounts.get(key) ?? 0) + 1);
|
|
222
|
+
}
|
|
223
|
+
const transformed = [...values];
|
|
224
|
+
const prepared = [];
|
|
225
|
+
const cancelledKeys = /* @__PURE__ */ new Set();
|
|
226
|
+
for (const [index, message] of messages.entries()) {
|
|
227
|
+
const cancelled = cancellations[index];
|
|
228
|
+
if (cancelled !== void 0) {
|
|
229
|
+
const key = idKey(cancelled);
|
|
230
|
+
cancelledKeys.add(key);
|
|
231
|
+
}
|
|
232
|
+
if (!isRequest(message) || resumeOnly) continue;
|
|
233
|
+
const key = idKey(message.id);
|
|
234
|
+
if (requestCounts.get(key) !== 1 || outgoing.has(key) && !cancelledKeys.has(key)) continue;
|
|
235
|
+
const injected = prepareInjectedRequest(message);
|
|
236
|
+
prepared.push({
|
|
237
|
+
index,
|
|
238
|
+
key,
|
|
239
|
+
parsed: message,
|
|
240
|
+
...injected
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
for (const request of prepared) {
|
|
244
|
+
const pending = startRequest(target, request.parsed, false, capturePayloads, propagateBaggage);
|
|
245
|
+
if (pending !== void 0) {
|
|
246
|
+
started.push({
|
|
247
|
+
index: request.index,
|
|
248
|
+
key: request.key,
|
|
249
|
+
pending
|
|
250
|
+
});
|
|
251
|
+
injectW3cContext(pending.handle.context, request.meta, { includeBaggage: propagateBaggage });
|
|
252
|
+
transformed[request.index] = request.message;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (started.length > 0) sent = Array.isArray(value) ? transformed : transformed[0];
|
|
256
|
+
if (started.length > 0 && copiedOptions !== void 0) forwardedOptions = {
|
|
257
|
+
...copiedOptions,
|
|
258
|
+
onRequestStreamEnd: function() {
|
|
259
|
+
try {
|
|
260
|
+
originalStreamEnd?.call(this);
|
|
261
|
+
} finally {
|
|
262
|
+
for (const { key, pending } of started) completePending(outgoing, key, pending, () => setFailure(pending, "connection_error"));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
} catch {
|
|
267
|
+
for (const { pending } of started) try {
|
|
268
|
+
pending.handle.end();
|
|
269
|
+
} catch {}
|
|
270
|
+
messages = [];
|
|
271
|
+
cancellations = [];
|
|
272
|
+
started = [];
|
|
273
|
+
requestSignal = void 0;
|
|
274
|
+
sent = value;
|
|
275
|
+
forwardedOptions = sendOptions;
|
|
276
|
+
}
|
|
277
|
+
const startedByIndex = new Map(started.map((request) => [request.index, request]));
|
|
278
|
+
const signal = requestSignal;
|
|
279
|
+
for (const [index, cancelled] of cancellations.entries()) {
|
|
280
|
+
if (cancelled !== void 0) completeAs(outgoing, cancelled, "cancelled");
|
|
281
|
+
const request = startedByIndex.get(index);
|
|
282
|
+
if (request !== void 0) {
|
|
283
|
+
const { key, pending } = request;
|
|
284
|
+
if (outgoing.has(key)) {
|
|
285
|
+
completePending(outgoing, key, pending, () => setFailure(pending, "duplicate_request_id"));
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
outgoing.set(key, pending);
|
|
289
|
+
if (signal !== void 0) {
|
|
290
|
+
const onAbort = () => completePending(outgoing, key, pending, () => setFailure(pending, "cancelled"));
|
|
291
|
+
try {
|
|
292
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
293
|
+
pending.removeAbortListener = () => signal.removeEventListener("abort", onAbort);
|
|
294
|
+
if (signal.aborted) onAbort();
|
|
295
|
+
} catch {}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
await originalSend(sent, forwardedOptions);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
for (const { key, pending } of started) completePending(outgoing, key, pending, () => pending.handle.update({ error }));
|
|
303
|
+
for (const message of messages) {
|
|
304
|
+
if (message === void 0 || message.method !== void 0 || message.id === void 0) continue;
|
|
305
|
+
const key = idKey(message.id);
|
|
306
|
+
const inbound = incoming.get(key);
|
|
307
|
+
if (inbound !== void 0) completePending(incoming, key, inbound, () => inbound.handle.update({ error }));
|
|
308
|
+
}
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
for (const message of messages) {
|
|
313
|
+
if (message === void 0 || message.method !== void 0 || message.id === void 0) continue;
|
|
314
|
+
const key = idKey(message.id);
|
|
315
|
+
const inbound = incoming.get(key);
|
|
316
|
+
if (inbound !== void 0) completePending(incoming, key, inbound, () => finishResponse(target, inbound, message));
|
|
317
|
+
}
|
|
318
|
+
} catch {}
|
|
319
|
+
};
|
|
320
|
+
if (originalSetProtocolVersion !== void 0) target.setProtocolVersion = (version) => {
|
|
321
|
+
PROTOCOL_VERSIONS.set(target, version);
|
|
322
|
+
originalSetProtocolVersion(version);
|
|
323
|
+
};
|
|
324
|
+
Object.defineProperty(target, "onmessage", {
|
|
325
|
+
configurable: true,
|
|
326
|
+
enumerable: true,
|
|
327
|
+
get: () => onmessage,
|
|
328
|
+
set: (handler) => {
|
|
329
|
+
onmessage = handler === void 0 ? void 0 : function(value, extra) {
|
|
330
|
+
const object = value !== null && typeof value === "object" ? value : void 0;
|
|
331
|
+
if (object !== void 0 && dispatching.has(object)) {
|
|
332
|
+
handler.call(target, value, extra);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (object !== void 0) dispatching.add(object);
|
|
336
|
+
let invoked = false;
|
|
337
|
+
const invoke = () => {
|
|
338
|
+
invoked = true;
|
|
339
|
+
return handler.call(target, value, extra);
|
|
340
|
+
};
|
|
341
|
+
const received = [];
|
|
342
|
+
const completeFailure = (error) => {
|
|
343
|
+
for (const { key, pending } of received) completePending(incoming, key, pending, () => pending.handle.update({ error }));
|
|
344
|
+
};
|
|
345
|
+
const completeAsyncFailure = (result) => {
|
|
346
|
+
if (result === null || typeof result !== "object" && typeof result !== "function" || !("then" in result) || typeof result.then !== "function") return result;
|
|
347
|
+
return Promise.resolve(result).catch((error) => {
|
|
348
|
+
completeFailure(error);
|
|
349
|
+
throw error;
|
|
350
|
+
});
|
|
351
|
+
};
|
|
352
|
+
try {
|
|
353
|
+
try {
|
|
354
|
+
const messages = (Array.isArray(value) ? value : [value]).map(parseMessage);
|
|
355
|
+
const requestCounts = /* @__PURE__ */ new Map();
|
|
356
|
+
for (const message of messages) {
|
|
357
|
+
if (!isRequest(message)) continue;
|
|
358
|
+
const key = idKey(message.id);
|
|
359
|
+
requestCounts.set(key, (requestCounts.get(key) ?? 0) + 1);
|
|
360
|
+
}
|
|
361
|
+
for (const message of messages) {
|
|
362
|
+
if (message === void 0) continue;
|
|
363
|
+
const cancelled = cancellationId(message);
|
|
364
|
+
if (cancelled !== void 0) completeAs(incoming, cancelled, "cancelled");
|
|
365
|
+
if (isRequest(message)) {
|
|
366
|
+
const key = idKey(message.id);
|
|
367
|
+
if (requestCounts.get(key) !== 1 || incoming.has(key)) continue;
|
|
368
|
+
const pending = startRequest(target, message, true, capturePayloads, propagateBaggage);
|
|
369
|
+
if (pending !== void 0) {
|
|
370
|
+
incoming.set(key, pending);
|
|
371
|
+
received.push({
|
|
372
|
+
key,
|
|
373
|
+
pending
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
} else if (message.method === void 0 && message.id !== void 0) {
|
|
377
|
+
const key = idKey(message.id);
|
|
378
|
+
const pending = outgoing.get(key);
|
|
379
|
+
if (pending !== void 0) completePending(outgoing, key, pending, () => finishResponse(target, pending, message));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
} catch {
|
|
383
|
+
for (const { key, pending } of received) completePending(incoming, key, pending);
|
|
384
|
+
received.length = 0;
|
|
385
|
+
}
|
|
386
|
+
const requestContext = received.length === 1 ? received[0]?.pending.handle.context : void 0;
|
|
387
|
+
try {
|
|
388
|
+
return completeAsyncFailure(requestContext === void 0 ? invoke() : withContext(requestContext, invoke));
|
|
389
|
+
} catch (error) {
|
|
390
|
+
if (!invoked) try {
|
|
391
|
+
return completeAsyncFailure(invoke());
|
|
392
|
+
} catch (handlerError) {
|
|
393
|
+
completeFailure(handlerError);
|
|
394
|
+
throw handlerError;
|
|
395
|
+
}
|
|
396
|
+
completeFailure(error);
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
} finally {
|
|
400
|
+
if (object !== void 0) dispatching.delete(object);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
Object.defineProperty(target, "onclose", {
|
|
406
|
+
configurable: true,
|
|
407
|
+
enumerable: true,
|
|
408
|
+
get: () => onclose,
|
|
409
|
+
set: (handler) => {
|
|
410
|
+
onclose = wrapClose(handler);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
if (onmessage !== void 0) target.onmessage = onmessage;
|
|
414
|
+
return transport;
|
|
415
|
+
}
|
|
416
|
+
//#endregion
|
|
417
|
+
export { instrumentMcpTransport };
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@telemetry-dev/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Model Context Protocol SDK instrumentation for telemetry.dev: instruments MCP v2 transports and emits OpenTelemetry spans.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"observability",
|
|
9
|
+
"opentelemetry",
|
|
10
|
+
"telemetry",
|
|
11
|
+
"tracing"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://telemetry.dev",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/telemetry-dev/telemetry.dev.git",
|
|
18
|
+
"directory": "packages/mcp"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.mts",
|
|
28
|
+
"import": "./dist/index.mjs",
|
|
29
|
+
"default": "./dist/index.mjs"
|
|
30
|
+
},
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@modelcontextprotocol/client": "2.0.0",
|
|
38
|
+
"@modelcontextprotocol/server": "2.0.0",
|
|
39
|
+
"@opentelemetry/api": "^1.9.1",
|
|
40
|
+
"@opentelemetry/sdk-trace-base": "^2.7.1",
|
|
41
|
+
"@types/node": "^24",
|
|
42
|
+
"typescript": "^5",
|
|
43
|
+
"vite-plus": "0.1.20",
|
|
44
|
+
"vitest": "npm:@voidzero-dev/vite-plus-test@0.1.20",
|
|
45
|
+
"@telemetry-dev/sdk": "0.1.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@modelcontextprotocol/client": ">=2.0.0 <3",
|
|
49
|
+
"@modelcontextprotocol/server": ">=2.0.0 <3",
|
|
50
|
+
"@opentelemetry/api": ">=1.9.0 <2",
|
|
51
|
+
"@telemetry-dev/sdk": "^0.1.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"@modelcontextprotocol/client": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
57
|
+
"@modelcontextprotocol/server": {
|
|
58
|
+
"optional": true
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "pnpm exec vp pack",
|
|
63
|
+
"dev": "pnpm exec vp pack --watch",
|
|
64
|
+
"test": "vp test",
|
|
65
|
+
"check": "vp check"
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
import { propagation, SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
2
|
+
import {
|
|
3
|
+
activeContext,
|
|
4
|
+
extractW3cContext,
|
|
5
|
+
injectW3cContext,
|
|
6
|
+
type SpanHandle,
|
|
7
|
+
startSpan,
|
|
8
|
+
withContext,
|
|
9
|
+
} from "@telemetry-dev/sdk";
|
|
10
|
+
|
|
11
|
+
interface McpTransport {
|
|
12
|
+
send: (...args: never[]) => Promise<void>;
|
|
13
|
+
onmessage?: (...args: never[]) => void;
|
|
14
|
+
onclose?: () => void;
|
|
15
|
+
sessionId?: string;
|
|
16
|
+
readonly protocolVersion?: string;
|
|
17
|
+
setProtocolVersion?: (version: string) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface InstrumentMcpTransportOptions {
|
|
21
|
+
capturePayloads?: boolean;
|
|
22
|
+
propagateBaggage?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type JsonRpcId = string | number;
|
|
26
|
+
type JsonRecord = Record<string, unknown>;
|
|
27
|
+
type MessageHandler = (message: unknown, extra?: unknown) => void;
|
|
28
|
+
type Send = (message: unknown, options?: unknown) => Promise<void>;
|
|
29
|
+
|
|
30
|
+
interface SendOptions extends JsonRecord {
|
|
31
|
+
requestSignal?: AbortSignal;
|
|
32
|
+
onRequestStreamEnd?: () => void;
|
|
33
|
+
resumptionToken?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface InstrumentableTransport {
|
|
37
|
+
send: Send;
|
|
38
|
+
onmessage?: MessageHandler;
|
|
39
|
+
onclose?: () => void;
|
|
40
|
+
sessionId?: string;
|
|
41
|
+
protocolVersion?: string;
|
|
42
|
+
setProtocolVersion?: (version: string) => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface PendingRequest {
|
|
46
|
+
handle: SpanHandle;
|
|
47
|
+
method: string;
|
|
48
|
+
receiver: boolean;
|
|
49
|
+
capturePayloads: boolean;
|
|
50
|
+
completed: boolean;
|
|
51
|
+
removeAbortListener?: () => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface ParsedMessage {
|
|
55
|
+
message: JsonRecord;
|
|
56
|
+
method?: string;
|
|
57
|
+
id?: JsonRpcId;
|
|
58
|
+
params?: JsonRecord;
|
|
59
|
+
meta?: JsonRecord;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface RequestSpanAttributes extends Record<string, string> {
|
|
63
|
+
"gen_ai.operation.name": string;
|
|
64
|
+
"mcp.method.name": string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface InjectedRequest extends JsonRecord {
|
|
68
|
+
params: JsonRecord;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const INSTRUMENTED = new WeakSet<object>();
|
|
72
|
+
const PROTOCOL_VERSIONS = new WeakMap<object, string>();
|
|
73
|
+
const CALLER_ERROR_CODES = new Set([-32700, -32600, -32601, -32602, -32002, -32021, -32022]);
|
|
74
|
+
const PROTOCOL_VERSION_META_KEY = "io.modelcontextprotocol/protocolVersion";
|
|
75
|
+
|
|
76
|
+
function asRecord(value: unknown): JsonRecord | undefined {
|
|
77
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
78
|
+
? (value as JsonRecord)
|
|
79
|
+
: undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseMessage(value: unknown): ParsedMessage | undefined {
|
|
83
|
+
const message = asRecord(value);
|
|
84
|
+
if (message === undefined) return undefined;
|
|
85
|
+
const params = asRecord(message.params);
|
|
86
|
+
return {
|
|
87
|
+
message,
|
|
88
|
+
method: requestMethod(message),
|
|
89
|
+
id: requestId(message),
|
|
90
|
+
params,
|
|
91
|
+
meta: asRecord(params?._meta),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function requestId(message: JsonRecord): JsonRpcId | undefined {
|
|
96
|
+
if (!("id" in message)) return undefined;
|
|
97
|
+
const id = message.id;
|
|
98
|
+
return typeof id === "string" || typeof id === "number" ? id : undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function idKey(id: JsonRpcId): string {
|
|
102
|
+
return `${typeof id}:${String(id)}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function requestMethod(message: JsonRecord): string | undefined {
|
|
106
|
+
return typeof message.method === "string" ? message.method : undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isRequest(
|
|
110
|
+
message: ParsedMessage | undefined,
|
|
111
|
+
): message is ParsedMessage & { method: string; id: JsonRpcId } {
|
|
112
|
+
return (
|
|
113
|
+
message?.method !== undefined &&
|
|
114
|
+
message.id !== undefined &&
|
|
115
|
+
message.method !== "notifications/cancelled"
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function targetFor(method: string, params: JsonRecord | undefined): string | undefined {
|
|
120
|
+
if (method !== "tools/call" && method !== "prompts/get") return undefined;
|
|
121
|
+
return typeof params?.name === "string" ? params.name : undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function resourceUri(method: string, params: JsonRecord | undefined): string | undefined {
|
|
125
|
+
if (
|
|
126
|
+
method !== "resources/read" &&
|
|
127
|
+
method !== "resources/subscribe" &&
|
|
128
|
+
method !== "resources/unsubscribe" &&
|
|
129
|
+
method !== "notifications/resources/updated"
|
|
130
|
+
) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
return typeof params?.uri === "string" ? params.uri : undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function metaProtocolVersion(meta: JsonRecord | undefined): string | undefined {
|
|
137
|
+
const version = meta?.[PROTOCOL_VERSION_META_KEY];
|
|
138
|
+
return typeof version === "string" ? version : undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function protocolVersion(
|
|
142
|
+
transport: InstrumentableTransport,
|
|
143
|
+
message: ParsedMessage,
|
|
144
|
+
): string | undefined {
|
|
145
|
+
const modern = metaProtocolVersion(message.meta);
|
|
146
|
+
if (modern !== undefined) return modern;
|
|
147
|
+
if (message.method === "initialize" && typeof message.params?.protocolVersion === "string") {
|
|
148
|
+
PROTOCOL_VERSIONS.set(transport, message.params.protocolVersion);
|
|
149
|
+
return message.params.protocolVersion;
|
|
150
|
+
}
|
|
151
|
+
return PROTOCOL_VERSIONS.get(transport) ?? transport.protocolVersion;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function responseProtocolVersion(
|
|
155
|
+
transport: InstrumentableTransport,
|
|
156
|
+
method: string,
|
|
157
|
+
message: ParsedMessage,
|
|
158
|
+
): string | undefined {
|
|
159
|
+
const modern = metaProtocolVersion(message.meta);
|
|
160
|
+
if (modern !== undefined) return modern;
|
|
161
|
+
const result = asRecord(message.message.result);
|
|
162
|
+
if (method === "initialize" && typeof result?.protocolVersion === "string") {
|
|
163
|
+
PROTOCOL_VERSIONS.set(transport, result.protocolVersion);
|
|
164
|
+
return result.protocolVersion;
|
|
165
|
+
}
|
|
166
|
+
return PROTOCOL_VERSIONS.get(transport) ?? transport.protocolVersion;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function requestAttributes(
|
|
170
|
+
transport: InstrumentableTransport,
|
|
171
|
+
message: ParsedMessage,
|
|
172
|
+
): RequestSpanAttributes {
|
|
173
|
+
const method = message.method!;
|
|
174
|
+
const attributes: RequestSpanAttributes = {
|
|
175
|
+
"gen_ai.operation.name": method === "tools/call" ? "execute_tool" : "mcp",
|
|
176
|
+
"mcp.method.name": method,
|
|
177
|
+
};
|
|
178
|
+
if (message.id !== undefined) {
|
|
179
|
+
attributes["jsonrpc.request.id"] = String(message.id);
|
|
180
|
+
}
|
|
181
|
+
if (typeof transport.sessionId === "string") attributes["mcp.session.id"] = transport.sessionId;
|
|
182
|
+
const version = protocolVersion(transport, message);
|
|
183
|
+
if (version !== undefined) attributes["mcp.protocol.version"] = version;
|
|
184
|
+
const target = targetFor(method, message.params);
|
|
185
|
+
if (method === "tools/call" && target !== undefined) attributes["gen_ai.tool.name"] = target;
|
|
186
|
+
if (method === "prompts/get" && target !== undefined) attributes["gen_ai.prompt.name"] = target;
|
|
187
|
+
const uri = resourceUri(method, message.params);
|
|
188
|
+
if (uri !== undefined) attributes["mcp.resource.uri"] = uri;
|
|
189
|
+
return attributes;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function startRequest(
|
|
193
|
+
transport: InstrumentableTransport,
|
|
194
|
+
message: ParsedMessage,
|
|
195
|
+
receiver: boolean,
|
|
196
|
+
capturePayloads: boolean,
|
|
197
|
+
propagateBaggage: boolean,
|
|
198
|
+
): PendingRequest | undefined {
|
|
199
|
+
const method = message.method;
|
|
200
|
+
if (method === undefined) return undefined;
|
|
201
|
+
const target = targetFor(method, message.params);
|
|
202
|
+
const ambient = activeContext();
|
|
203
|
+
const extracted = receiver
|
|
204
|
+
? extractW3cContext(message.meta ?? {}, { includeBaggage: propagateBaggage })
|
|
205
|
+
: undefined;
|
|
206
|
+
const ambientSpan = trace.getSpanContext(ambient);
|
|
207
|
+
const remoteSpan = extracted === undefined ? undefined : trace.getSpanContext(extracted);
|
|
208
|
+
const remoteIsValid = remoteSpan !== undefined && trace.isSpanContextValid(remoteSpan);
|
|
209
|
+
const remoteBaggage = extracted === undefined ? undefined : propagation.getBaggage(extracted);
|
|
210
|
+
const parent = remoteIsValid
|
|
211
|
+
? extracted
|
|
212
|
+
: remoteBaggage === undefined
|
|
213
|
+
? undefined
|
|
214
|
+
: propagation.setBaggage(ambient, remoteBaggage);
|
|
215
|
+
const links =
|
|
216
|
+
ambientSpan !== undefined &&
|
|
217
|
+
trace.isSpanContextValid(ambientSpan) &&
|
|
218
|
+
remoteIsValid &&
|
|
219
|
+
(ambientSpan.traceId !== remoteSpan.traceId || ambientSpan.spanId !== remoteSpan.spanId)
|
|
220
|
+
? [{ context: ambientSpan }]
|
|
221
|
+
: undefined;
|
|
222
|
+
const handle = startSpan(target === undefined ? method : `${method} ${target}`, {
|
|
223
|
+
type: method === "tools/call" ? "tool" : "span",
|
|
224
|
+
kind: receiver ? SpanKind.SERVER : SpanKind.CLIENT,
|
|
225
|
+
parent,
|
|
226
|
+
links,
|
|
227
|
+
attributes: requestAttributes(transport, message),
|
|
228
|
+
input: capturePayloads && method === "tools/call" ? message.params?.arguments : undefined,
|
|
229
|
+
});
|
|
230
|
+
return { handle, method, receiver, capturePayloads, completed: false };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function updateObservableAttributes(
|
|
234
|
+
transport: InstrumentableTransport,
|
|
235
|
+
pending: PendingRequest,
|
|
236
|
+
message: ParsedMessage,
|
|
237
|
+
): void {
|
|
238
|
+
const attributes: Record<string, string> = {};
|
|
239
|
+
if (typeof transport.sessionId === "string") attributes["mcp.session.id"] = transport.sessionId;
|
|
240
|
+
const version = responseProtocolVersion(transport, pending.method, message);
|
|
241
|
+
if (version !== undefined) attributes["mcp.protocol.version"] = version;
|
|
242
|
+
pending.handle.update({ attributes });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function setFailure(pending: PendingRequest, type: string, description?: string): void {
|
|
246
|
+
pending.handle.span.setAttribute("error.type", type);
|
|
247
|
+
pending.handle.span.setStatus({ code: SpanStatusCode.ERROR, message: description });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function finishResponse(
|
|
251
|
+
transport: InstrumentableTransport,
|
|
252
|
+
pending: PendingRequest,
|
|
253
|
+
message: ParsedMessage,
|
|
254
|
+
): void {
|
|
255
|
+
updateObservableAttributes(transport, pending, message);
|
|
256
|
+
const error = asRecord(message.message.error);
|
|
257
|
+
if (error !== undefined) {
|
|
258
|
+
const code = typeof error.code === "number" ? error.code : undefined;
|
|
259
|
+
const codeText = code === undefined ? "_OTHER" : String(code);
|
|
260
|
+
pending.handle.span.setAttribute("rpc.response.status_code", codeText);
|
|
261
|
+
if (!pending.receiver || code === undefined || !CALLER_ERROR_CODES.has(code)) {
|
|
262
|
+
setFailure(pending, codeText, typeof error.message === "string" ? error.message : undefined);
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
const result = asRecord(message.message.result);
|
|
266
|
+
if (pending.method === "tools/call" && result?.isError === true) {
|
|
267
|
+
setFailure(pending, "tool_error");
|
|
268
|
+
}
|
|
269
|
+
if (pending.capturePayloads && pending.method === "tools/call" && result?.isError !== true) {
|
|
270
|
+
pending.handle.update({ output: message.message.result });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function completePending(
|
|
276
|
+
requests: Map<string, PendingRequest>,
|
|
277
|
+
key: string,
|
|
278
|
+
pending: PendingRequest,
|
|
279
|
+
update?: () => void,
|
|
280
|
+
): void {
|
|
281
|
+
if (pending.completed) return;
|
|
282
|
+
pending.completed = true;
|
|
283
|
+
if (requests.get(key) === pending) requests.delete(key);
|
|
284
|
+
pending.removeAbortListener?.();
|
|
285
|
+
try {
|
|
286
|
+
update?.();
|
|
287
|
+
} catch {}
|
|
288
|
+
try {
|
|
289
|
+
pending.handle.end();
|
|
290
|
+
} catch {}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function completeAs(
|
|
294
|
+
requests: Map<string, PendingRequest>,
|
|
295
|
+
id: JsonRpcId,
|
|
296
|
+
type: "cancelled" | "connection_error",
|
|
297
|
+
): void {
|
|
298
|
+
const key = idKey(id);
|
|
299
|
+
const pending = requests.get(key);
|
|
300
|
+
if (pending !== undefined) {
|
|
301
|
+
completePending(requests, key, pending, () => setFailure(pending, type));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function cancellationId(message: ParsedMessage): JsonRpcId | undefined {
|
|
306
|
+
if (message.method !== "notifications/cancelled") return undefined;
|
|
307
|
+
if (message.params === undefined || !("requestId" in message.params)) return undefined;
|
|
308
|
+
const id = message.params.requestId;
|
|
309
|
+
return typeof id === "string" || typeof id === "number" ? id : undefined;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function prepareInjectedRequest(message: ParsedMessage) {
|
|
313
|
+
const params = { ...message.params };
|
|
314
|
+
const meta = { ...message.meta };
|
|
315
|
+
params._meta = meta;
|
|
316
|
+
return { message: { ...message.message, params }, meta };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function instrumentMcpTransport<T extends McpTransport>(
|
|
320
|
+
transport: T,
|
|
321
|
+
options: InstrumentMcpTransportOptions = {},
|
|
322
|
+
): T {
|
|
323
|
+
if (INSTRUMENTED.has(transport)) return transport;
|
|
324
|
+
INSTRUMENTED.add(transport);
|
|
325
|
+
|
|
326
|
+
const target = transport as T & InstrumentableTransport;
|
|
327
|
+
const originalSend = target.send.bind(target);
|
|
328
|
+
const originalSetProtocolVersion = target.setProtocolVersion?.bind(target);
|
|
329
|
+
const outgoing = new Map<string, PendingRequest>();
|
|
330
|
+
const incoming = new Map<string, PendingRequest>();
|
|
331
|
+
const dispatching = new WeakSet<object>();
|
|
332
|
+
const capturePayloads = options.capturePayloads === true;
|
|
333
|
+
const propagateBaggage = options.propagateBaggage === true;
|
|
334
|
+
let onmessage = target.onmessage;
|
|
335
|
+
const wrapClose = (handler: (() => void) | undefined) =>
|
|
336
|
+
function () {
|
|
337
|
+
for (const [key, pending] of outgoing) {
|
|
338
|
+
completePending(outgoing, key, pending, () => setFailure(pending, "connection_error"));
|
|
339
|
+
}
|
|
340
|
+
for (const [key, pending] of incoming) {
|
|
341
|
+
completePending(incoming, key, pending, () => setFailure(pending, "connection_error"));
|
|
342
|
+
}
|
|
343
|
+
handler?.call(target);
|
|
344
|
+
};
|
|
345
|
+
let onclose = wrapClose(target.onclose);
|
|
346
|
+
|
|
347
|
+
target.send = async (value, sendOptions) => {
|
|
348
|
+
const values = Array.isArray(value) ? value : [value];
|
|
349
|
+
let messages: Array<ParsedMessage | undefined> = [];
|
|
350
|
+
let cancellations: Array<JsonRpcId | undefined> = [];
|
|
351
|
+
let started: Array<{ index: number; key: string; pending: PendingRequest }> = [];
|
|
352
|
+
let requestSignal: AbortSignal | undefined;
|
|
353
|
+
let sent = value;
|
|
354
|
+
let forwardedOptions = sendOptions;
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
messages = values.map(parseMessage);
|
|
358
|
+
cancellations = messages.map((message) =>
|
|
359
|
+
message === undefined ? undefined : cancellationId(message),
|
|
360
|
+
);
|
|
361
|
+
const rawOptions = asRecord(sendOptions) as SendOptions | undefined;
|
|
362
|
+
const copiedOptions = rawOptions === undefined ? undefined : { ...rawOptions };
|
|
363
|
+
const resumeOnly =
|
|
364
|
+
typeof copiedOptions?.resumptionToken === "string" &&
|
|
365
|
+
copiedOptions.resumptionToken.length > 0;
|
|
366
|
+
requestSignal = copiedOptions?.requestSignal;
|
|
367
|
+
const originalStreamEnd = copiedOptions?.onRequestStreamEnd;
|
|
368
|
+
const requestCounts = new Map<string, number>();
|
|
369
|
+
if (!resumeOnly) {
|
|
370
|
+
for (const message of messages) {
|
|
371
|
+
if (!isRequest(message)) continue;
|
|
372
|
+
const key = idKey(message.id);
|
|
373
|
+
requestCounts.set(key, (requestCounts.get(key) ?? 0) + 1);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const transformed = [...values];
|
|
378
|
+
const prepared: Array<{
|
|
379
|
+
index: number;
|
|
380
|
+
key: string;
|
|
381
|
+
parsed: ParsedMessage;
|
|
382
|
+
message: InjectedRequest;
|
|
383
|
+
meta: JsonRecord;
|
|
384
|
+
}> = [];
|
|
385
|
+
const cancelledKeys = new Set<string>();
|
|
386
|
+
for (const [index, message] of messages.entries()) {
|
|
387
|
+
const cancelled = cancellations[index];
|
|
388
|
+
if (cancelled !== undefined) {
|
|
389
|
+
const key = idKey(cancelled);
|
|
390
|
+
cancelledKeys.add(key);
|
|
391
|
+
}
|
|
392
|
+
if (!isRequest(message) || resumeOnly) continue;
|
|
393
|
+
const key = idKey(message.id);
|
|
394
|
+
if (requestCounts.get(key) !== 1 || (outgoing.has(key) && !cancelledKeys.has(key))) {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const injected = prepareInjectedRequest(message);
|
|
398
|
+
prepared.push({ index, key, parsed: message, ...injected });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
for (const request of prepared) {
|
|
402
|
+
const pending = startRequest(
|
|
403
|
+
target,
|
|
404
|
+
request.parsed,
|
|
405
|
+
false,
|
|
406
|
+
capturePayloads,
|
|
407
|
+
propagateBaggage,
|
|
408
|
+
);
|
|
409
|
+
if (pending !== undefined) {
|
|
410
|
+
started.push({ index: request.index, key: request.key, pending });
|
|
411
|
+
injectW3cContext(pending.handle.context, request.meta, {
|
|
412
|
+
includeBaggage: propagateBaggage,
|
|
413
|
+
});
|
|
414
|
+
transformed[request.index] = request.message;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (started.length > 0) sent = Array.isArray(value) ? transformed : transformed[0];
|
|
419
|
+
if (started.length > 0 && copiedOptions !== undefined) {
|
|
420
|
+
forwardedOptions = {
|
|
421
|
+
...copiedOptions,
|
|
422
|
+
onRequestStreamEnd: function (this: unknown) {
|
|
423
|
+
try {
|
|
424
|
+
originalStreamEnd?.call(this);
|
|
425
|
+
} finally {
|
|
426
|
+
for (const { key, pending } of started) {
|
|
427
|
+
completePending(outgoing, key, pending, () =>
|
|
428
|
+
setFailure(pending, "connection_error"),
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
} satisfies SendOptions;
|
|
434
|
+
}
|
|
435
|
+
} catch {
|
|
436
|
+
for (const { pending } of started) {
|
|
437
|
+
try {
|
|
438
|
+
pending.handle.end();
|
|
439
|
+
} catch {}
|
|
440
|
+
}
|
|
441
|
+
messages = [];
|
|
442
|
+
cancellations = [];
|
|
443
|
+
started = [];
|
|
444
|
+
requestSignal = undefined;
|
|
445
|
+
sent = value;
|
|
446
|
+
forwardedOptions = sendOptions;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const startedByIndex = new Map(started.map((request) => [request.index, request]));
|
|
450
|
+
const signal = requestSignal;
|
|
451
|
+
for (const [index, cancelled] of cancellations.entries()) {
|
|
452
|
+
if (cancelled !== undefined) completeAs(outgoing, cancelled, "cancelled");
|
|
453
|
+
const request = startedByIndex.get(index);
|
|
454
|
+
if (request !== undefined) {
|
|
455
|
+
const { key, pending } = request;
|
|
456
|
+
if (outgoing.has(key)) {
|
|
457
|
+
completePending(outgoing, key, pending, () =>
|
|
458
|
+
setFailure(pending, "duplicate_request_id"),
|
|
459
|
+
);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
outgoing.set(key, pending);
|
|
463
|
+
if (signal !== undefined) {
|
|
464
|
+
const onAbort = () =>
|
|
465
|
+
completePending(outgoing, key, pending, () => setFailure(pending, "cancelled"));
|
|
466
|
+
try {
|
|
467
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
468
|
+
pending.removeAbortListener = () => signal.removeEventListener("abort", onAbort);
|
|
469
|
+
if (signal.aborted) onAbort();
|
|
470
|
+
} catch {}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
try {
|
|
476
|
+
await originalSend(sent, forwardedOptions);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
for (const { key, pending } of started) {
|
|
479
|
+
completePending(outgoing, key, pending, () => pending.handle.update({ error }));
|
|
480
|
+
}
|
|
481
|
+
for (const message of messages) {
|
|
482
|
+
if (message === undefined || message.method !== undefined || message.id === undefined)
|
|
483
|
+
continue;
|
|
484
|
+
const key = idKey(message.id);
|
|
485
|
+
const inbound = incoming.get(key);
|
|
486
|
+
if (inbound !== undefined) {
|
|
487
|
+
completePending(incoming, key, inbound, () => inbound.handle.update({ error }));
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
throw error;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
for (const message of messages) {
|
|
495
|
+
if (message === undefined || message.method !== undefined || message.id === undefined)
|
|
496
|
+
continue;
|
|
497
|
+
const key = idKey(message.id);
|
|
498
|
+
const inbound = incoming.get(key);
|
|
499
|
+
if (inbound !== undefined) {
|
|
500
|
+
completePending(incoming, key, inbound, () => finishResponse(target, inbound, message));
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
} catch {}
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
if (originalSetProtocolVersion !== undefined) {
|
|
507
|
+
target.setProtocolVersion = (version) => {
|
|
508
|
+
PROTOCOL_VERSIONS.set(target, version);
|
|
509
|
+
originalSetProtocolVersion(version);
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
Object.defineProperty(target, "onmessage", {
|
|
514
|
+
configurable: true,
|
|
515
|
+
enumerable: true,
|
|
516
|
+
get: () => onmessage,
|
|
517
|
+
set: (handler: MessageHandler | undefined) => {
|
|
518
|
+
onmessage =
|
|
519
|
+
handler === undefined
|
|
520
|
+
? undefined
|
|
521
|
+
: function (value, extra) {
|
|
522
|
+
const object = value !== null && typeof value === "object" ? value : undefined;
|
|
523
|
+
if (object !== undefined && dispatching.has(object)) {
|
|
524
|
+
handler.call(target, value, extra);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (object !== undefined) dispatching.add(object);
|
|
528
|
+
|
|
529
|
+
let invoked = false;
|
|
530
|
+
const invoke = () => {
|
|
531
|
+
invoked = true;
|
|
532
|
+
return handler.call(target, value, extra);
|
|
533
|
+
};
|
|
534
|
+
const received: Array<{ key: string; pending: PendingRequest }> = [];
|
|
535
|
+
const completeFailure = (error: unknown) => {
|
|
536
|
+
for (const { key, pending } of received) {
|
|
537
|
+
completePending(incoming, key, pending, () => pending.handle.update({ error }));
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
const completeAsyncFailure = (result: unknown): unknown => {
|
|
541
|
+
if (
|
|
542
|
+
result === null ||
|
|
543
|
+
(typeof result !== "object" && typeof result !== "function") ||
|
|
544
|
+
!("then" in result) ||
|
|
545
|
+
typeof result.then !== "function"
|
|
546
|
+
) {
|
|
547
|
+
return result;
|
|
548
|
+
}
|
|
549
|
+
return Promise.resolve(result).catch((error: unknown) => {
|
|
550
|
+
completeFailure(error);
|
|
551
|
+
throw error;
|
|
552
|
+
});
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
try {
|
|
557
|
+
const values = Array.isArray(value) ? value : [value];
|
|
558
|
+
const messages = values.map(parseMessage);
|
|
559
|
+
const requestCounts = new Map<string, number>();
|
|
560
|
+
for (const message of messages) {
|
|
561
|
+
if (!isRequest(message)) continue;
|
|
562
|
+
const key = idKey(message.id);
|
|
563
|
+
requestCounts.set(key, (requestCounts.get(key) ?? 0) + 1);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
for (const message of messages) {
|
|
567
|
+
if (message === undefined) continue;
|
|
568
|
+
const cancelled = cancellationId(message);
|
|
569
|
+
if (cancelled !== undefined) completeAs(incoming, cancelled, "cancelled");
|
|
570
|
+
|
|
571
|
+
if (isRequest(message)) {
|
|
572
|
+
const key = idKey(message.id);
|
|
573
|
+
if (requestCounts.get(key) !== 1 || incoming.has(key)) continue;
|
|
574
|
+
const pending = startRequest(
|
|
575
|
+
target,
|
|
576
|
+
message,
|
|
577
|
+
true,
|
|
578
|
+
capturePayloads,
|
|
579
|
+
propagateBaggage,
|
|
580
|
+
);
|
|
581
|
+
if (pending !== undefined) {
|
|
582
|
+
incoming.set(key, pending);
|
|
583
|
+
received.push({ key, pending });
|
|
584
|
+
}
|
|
585
|
+
} else if (message.method === undefined && message.id !== undefined) {
|
|
586
|
+
const key = idKey(message.id);
|
|
587
|
+
const pending = outgoing.get(key);
|
|
588
|
+
if (pending !== undefined) {
|
|
589
|
+
completePending(outgoing, key, pending, () =>
|
|
590
|
+
finishResponse(target, pending, message),
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
} catch {
|
|
596
|
+
for (const { key, pending } of received) {
|
|
597
|
+
completePending(incoming, key, pending);
|
|
598
|
+
}
|
|
599
|
+
received.length = 0;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const requestContext =
|
|
603
|
+
received.length === 1 ? received[0]?.pending.handle.context : undefined;
|
|
604
|
+
try {
|
|
605
|
+
return completeAsyncFailure(
|
|
606
|
+
requestContext === undefined ? invoke() : withContext(requestContext, invoke),
|
|
607
|
+
);
|
|
608
|
+
} catch (error) {
|
|
609
|
+
if (!invoked) {
|
|
610
|
+
try {
|
|
611
|
+
return completeAsyncFailure(invoke());
|
|
612
|
+
} catch (handlerError) {
|
|
613
|
+
completeFailure(handlerError);
|
|
614
|
+
throw handlerError;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
completeFailure(error);
|
|
618
|
+
throw error;
|
|
619
|
+
}
|
|
620
|
+
} finally {
|
|
621
|
+
if (object !== undefined) dispatching.delete(object);
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
},
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
Object.defineProperty(target, "onclose", {
|
|
628
|
+
configurable: true,
|
|
629
|
+
enumerable: true,
|
|
630
|
+
get: () => onclose,
|
|
631
|
+
set: (handler: (() => void) | undefined) => {
|
|
632
|
+
onclose = wrapClose(handler);
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
if (onmessage !== undefined) target.onmessage = onmessage;
|
|
637
|
+
return transport;
|
|
638
|
+
}
|