opentel-mcp 0.1.0 → 0.2.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/README.md +23 -5
- package/package.json +14 -11
- package/src/config.js +35 -4
- package/src/index.d.ts +97 -0
package/README.md
CHANGED
|
@@ -5,9 +5,23 @@
|
|
|
5
5
|
> long they take, and which ones fail — via standard OTel traces.
|
|
6
6
|
|
|
7
7
|
[](https://github.com/Thirumalaiboobathi/opentel-mcp/actions/workflows/ci.yml)
|
|
8
|
+
[](https://www.npmjs.com/package/opentel-mcp)
|
|
8
9
|
[](https://github.com/Thirumalaiboobathi/opentel-mcp/blob/main/LICENSE)
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
## Status
|
|
12
|
+
|
|
13
|
+
`0.x` — the [MCP semantic conventions](https://github.com/open-telemetry/semantic-conventions-genai)
|
|
14
|
+
this library implements are Development-stage and may change. Breaking
|
|
15
|
+
attribute changes will land in minor versions until `1.0`.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install opentel-mcp @opentelemetry/api
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`opentel-mcp` is an ES module. Your project needs `"type": "module"` in
|
|
24
|
+
its package.json, or you must use `.mjs` file extensions.
|
|
11
25
|
|
|
12
26
|
## Why
|
|
13
27
|
|
|
@@ -31,7 +45,7 @@ const server = new Server({ name: 'my-server', version: '1.0.0' }, {
|
|
|
31
45
|
});
|
|
32
46
|
|
|
33
47
|
instrumentMcpServer(server, {
|
|
34
|
-
serviceName: 'my-mcp-server',
|
|
48
|
+
serviceName: 'my-mcp-server', // required because setupNodeSdk is true below
|
|
35
49
|
setupNodeSdk: true, // dev-friendly stderr output; omit in prod
|
|
36
50
|
// if you already have OTel configured
|
|
37
51
|
});
|
|
@@ -49,7 +63,7 @@ import { instrumentMcpServer } from 'opentel-mcp';
|
|
|
49
63
|
const server = new McpServer({ name: 'my-server', version: '1.0.0' });
|
|
50
64
|
|
|
51
65
|
instrumentMcpServer(server, {
|
|
52
|
-
serviceName: 'my-mcp-server',
|
|
66
|
+
serviceName: 'my-mcp-server', // required because setupNodeSdk is true below
|
|
53
67
|
setupNodeSdk: true,
|
|
54
68
|
});
|
|
55
69
|
|
|
@@ -101,12 +115,16 @@ expresses success/failure through span status, not a status attribute.
|
|
|
101
115
|
**One-line (dev):** `setupNodeSdk: true` sets up a NodeTracerProvider
|
|
102
116
|
that prints spans to stderr (safe alongside stdio-transport MCP servers —
|
|
103
117
|
see ADR 003), optionally + an OTLP exporter if `exporterUrl` is provided.
|
|
104
|
-
No separate OTel SDK setup needed — `serviceName` is
|
|
118
|
+
No separate OTel SDK setup needed — `serviceName` is required in this
|
|
119
|
+
mode, since it names the resource of the provider opentel-mcp creates.
|
|
105
120
|
|
|
106
121
|
**Bring-your-own-SDK (prod):** Omit `setupNodeSdk` (default false).
|
|
107
122
|
opentel-mcp uses whatever tracer provider you've already registered
|
|
108
123
|
via `trace.setGlobalTracerProvider()`. This means it plugs into any
|
|
109
|
-
existing OTel setup without conflict.
|
|
124
|
+
existing OTel setup without conflict. In this mode the host's
|
|
125
|
+
TracerProvider owns the resource, so `serviceName` is not needed and has
|
|
126
|
+
no effect — set `service.name` on the host's Resource instead. Passing
|
|
127
|
+
`serviceName` anyway is harmless but logs a one-time `diag.warn`.
|
|
110
128
|
|
|
111
129
|
## Ordering constraint
|
|
112
130
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opentel-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "One-line OpenTelemetry instrumentation for Model Context Protocol (MCP) servers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
7
8
|
"exports": {
|
|
8
|
-
".":
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.d.ts",
|
|
11
|
+
"default": "./src/index.js"
|
|
12
|
+
}
|
|
9
13
|
},
|
|
10
14
|
"files": [
|
|
11
15
|
"src",
|
|
@@ -15,13 +19,11 @@
|
|
|
15
19
|
"engines": {
|
|
16
20
|
"node": ">=20"
|
|
17
21
|
},
|
|
18
|
-
"workspaces": [
|
|
19
|
-
"examples/hello-server",
|
|
20
|
-
"examples/hello-mcpserver"
|
|
21
|
-
],
|
|
22
22
|
"scripts": {
|
|
23
23
|
"test": "vitest run",
|
|
24
|
-
"test:watch": "vitest"
|
|
24
|
+
"test:watch": "vitest",
|
|
25
|
+
"prepack": "node scripts/strip-workspaces.js",
|
|
26
|
+
"postpack": "node scripts/restore-workspaces.js"
|
|
25
27
|
},
|
|
26
28
|
"keywords": [
|
|
27
29
|
"mcp",
|
|
@@ -48,13 +50,14 @@
|
|
|
48
50
|
"@opentelemetry/api": "^1.9.0"
|
|
49
51
|
},
|
|
50
52
|
"dependencies": {
|
|
51
|
-
"@opentelemetry/sdk-trace-node": "^2.9.0",
|
|
52
53
|
"@opentelemetry/exporter-trace-otlp-http": "^0.220.0",
|
|
53
|
-
"@opentelemetry/resources": "^2.9.0"
|
|
54
|
+
"@opentelemetry/resources": "^2.9.0",
|
|
55
|
+
"@opentelemetry/sdk-trace-node": "^2.9.0"
|
|
54
56
|
},
|
|
55
57
|
"devDependencies": {
|
|
56
|
-
"vitest": "^2.1.8",
|
|
57
58
|
"@opentelemetry/api": "^1.9.0",
|
|
58
|
-
"@opentelemetry/sdk-trace-base": "^2.9.0"
|
|
59
|
+
"@opentelemetry/sdk-trace-base": "^2.9.0",
|
|
60
|
+
"typescript": "^7.0.2",
|
|
61
|
+
"vitest": "^2.1.8"
|
|
59
62
|
}
|
|
60
63
|
}
|
package/src/config.js
CHANGED
|
@@ -3,9 +3,15 @@
|
|
|
3
3
|
* Options parsing and defaults for instrumentMcpServer().
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { diag } from '@opentelemetry/api';
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* @typedef {object} InstrumentOptions
|
|
8
|
-
* @property {string} serviceName -
|
|
10
|
+
* @property {string} [serviceName] - Names the resource of the NodeTracerProvider this library creates.
|
|
11
|
+
* Required (and must be a non-empty string) when `setupNodeSdk` is true. Has no effect when `setupNodeSdk`
|
|
12
|
+
* is false or omitted — in that mode, resource attributes (including `service.name`) come from whatever
|
|
13
|
+
* TracerProvider the host application has already registered; passing `serviceName` anyway is harmless but
|
|
14
|
+
* logs a one-time `diag.warn`.
|
|
9
15
|
* @property {string} [exporterUrl] - OTLP/HTTP traces endpoint (e.g. 'http://localhost:4318/v1/traces').
|
|
10
16
|
* Only takes effect when `setupNodeSdk` is true.
|
|
11
17
|
* @property {boolean} [enabled=true] - Set to false to disable instrumentation entirely; instrumentMcpServer()
|
|
@@ -18,6 +24,17 @@
|
|
|
18
24
|
* instrumentMcpServer() from ever overriding a host application's own OpenTelemetry setup.
|
|
19
25
|
*/
|
|
20
26
|
|
|
27
|
+
// Guards the "serviceName has no effect" diagnostic below so it fires once
|
|
28
|
+
// per process rather than once per instrumented server.
|
|
29
|
+
let warnedServiceNameIgnored = false;
|
|
30
|
+
|
|
31
|
+
// Test-only: lets test/instrument.test.js get a clean slate for the
|
|
32
|
+
// once-per-process warning above regardless of what earlier tests in the
|
|
33
|
+
// same file already triggered. Not part of the public API.
|
|
34
|
+
export function __resetServiceNameWarnedForTests() {
|
|
35
|
+
warnedServiceNameIgnored = false;
|
|
36
|
+
}
|
|
37
|
+
|
|
21
38
|
/**
|
|
22
39
|
* Validates and applies defaults to raw instrumentMcpServer() options.
|
|
23
40
|
*
|
|
@@ -26,15 +43,29 @@
|
|
|
26
43
|
*/
|
|
27
44
|
export function resolveOptions(options) {
|
|
28
45
|
const opts = options ?? {};
|
|
46
|
+
const setupNodeSdk = opts.setupNodeSdk ?? false;
|
|
47
|
+
const hasServiceName = typeof opts.serviceName === 'string' && opts.serviceName.trim() !== '';
|
|
48
|
+
|
|
49
|
+
if (setupNodeSdk && !hasServiceName) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
'opentel-mcp: options.serviceName is required when setupNodeSdk is true ' +
|
|
52
|
+
'(it names the resource of the tracer provider this library creates). ' +
|
|
53
|
+
"It is not needed otherwise — the host application's registered provider owns the resource.",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
29
56
|
|
|
30
|
-
if (
|
|
31
|
-
|
|
57
|
+
if (!setupNodeSdk && hasServiceName && !warnedServiceNameIgnored) {
|
|
58
|
+
warnedServiceNameIgnored = true;
|
|
59
|
+
diag.warn(
|
|
60
|
+
'opentel-mcp: serviceName was provided but setupNodeSdk is false, so it has no effect. ' +
|
|
61
|
+
'Resource attributes (including service.name) come from the TracerProvider the host application registered.',
|
|
62
|
+
);
|
|
32
63
|
}
|
|
33
64
|
|
|
34
65
|
return {
|
|
35
66
|
serviceName: opts.serviceName,
|
|
36
67
|
exporterUrl: opts.exporterUrl,
|
|
37
68
|
enabled: opts.enabled ?? true,
|
|
38
|
-
setupNodeSdk
|
|
69
|
+
setupNodeSdk,
|
|
39
70
|
};
|
|
40
71
|
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Options for {@link instrumentMcpServer}.
|
|
6
|
+
*/
|
|
7
|
+
export interface InstrumentOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Names the resource of the `NodeTracerProvider` this library creates.
|
|
10
|
+
*
|
|
11
|
+
* Required (and must be a non-empty string) when `setupNodeSdk` is `true` —
|
|
12
|
+
* omitting it in that mode throws at runtime. Has no effect when
|
|
13
|
+
* `setupNodeSdk` is `false` or omitted; in that mode, resource attributes
|
|
14
|
+
* (including `service.name`) come from whatever `TracerProvider` the host
|
|
15
|
+
* application has already registered, and passing `serviceName` anyway is
|
|
16
|
+
* harmless but logs a one-time `diag.warn`.
|
|
17
|
+
*/
|
|
18
|
+
serviceName?: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* OTLP/HTTP traces endpoint (e.g. `'http://localhost:4318/v1/traces'`).
|
|
22
|
+
*
|
|
23
|
+
* Only takes effect when `setupNodeSdk` is `true`.
|
|
24
|
+
*/
|
|
25
|
+
exporterUrl?: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Set to `false` to disable instrumentation entirely; {@link instrumentMcpServer}
|
|
29
|
+
* becomes a no-op.
|
|
30
|
+
*
|
|
31
|
+
* @default true
|
|
32
|
+
*/
|
|
33
|
+
enabled?: boolean;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* When `true`, {@link instrumentMcpServer} creates and registers its own
|
|
37
|
+
* `NodeTracerProvider` (always exporting to stderr; additionally to
|
|
38
|
+
* `exporterUrl` via OTLP/HTTP if set).
|
|
39
|
+
*
|
|
40
|
+
* When `false` (the default), spans are emitted via whatever OpenTelemetry
|
|
41
|
+
* `TracerProvider` the host application has already registered globally —
|
|
42
|
+
* or dropped silently if none has been registered. This default keeps
|
|
43
|
+
* {@link instrumentMcpServer} from ever overriding a host application's own
|
|
44
|
+
* OpenTelemetry setup.
|
|
45
|
+
*
|
|
46
|
+
* @default false
|
|
47
|
+
*/
|
|
48
|
+
setupNodeSdk?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A high-level McpServer-like object, matched structurally the same way
|
|
53
|
+
* {@link instrumentMcpServer} itself matches it at runtime (see ADR 001):
|
|
54
|
+
* an object exposing a `.server` that looks like a low-level `Server` (has
|
|
55
|
+
* `setRequestHandler`), plus a `.tool` or `.registerTool` method.
|
|
56
|
+
*
|
|
57
|
+
* This structural fallback exists because the imported `McpServer` class
|
|
58
|
+
* has private fields, which makes TypeScript treat assignability to it as
|
|
59
|
+
* effectively nominal — an `McpServer` instance created by a *different*
|
|
60
|
+
* resolved copy of `@modelcontextprotocol/sdk` (e.g. a hoisting mismatch in
|
|
61
|
+
* a monorepo) would otherwise fail the type check even though it works
|
|
62
|
+
* fine at runtime, since the runtime never uses `instanceof McpServer` in
|
|
63
|
+
* the first place. This type mirrors the duck-typing the runtime already
|
|
64
|
+
* performs instead of relying on class identity.
|
|
65
|
+
*/
|
|
66
|
+
export type DuckTypedMcpServer = {
|
|
67
|
+
server: { setRequestHandler: (...args: any[]) => any };
|
|
68
|
+
tool?: (...args: any[]) => any;
|
|
69
|
+
registerTool?: (...args: any[]) => any;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Instruments an MCP server so every tool call emits an OpenTelemetry span.
|
|
74
|
+
*
|
|
75
|
+
* Accepts either a low-level `Server` (from
|
|
76
|
+
* `@modelcontextprotocol/sdk/server/index.js`) or a high-level `McpServer`
|
|
77
|
+
* (from `@modelcontextprotocol/sdk/server/mcp.js`). Must be called before any
|
|
78
|
+
* `tools/call` handler is registered — i.e. before any
|
|
79
|
+
* `server.setRequestHandler(CallToolRequestSchema, ...)` (low-level) or
|
|
80
|
+
* `.tool()`/`.registerTool()` (McpServer) calls. Idempotent: calling this
|
|
81
|
+
* more than once — on the same object, or on the outer `McpServer` and its
|
|
82
|
+
* inner `Server` interchangeably — is a no-op after the first call.
|
|
83
|
+
*
|
|
84
|
+
* @param server - The server instance to instrument.
|
|
85
|
+
* @param options - Instrumentation options.
|
|
86
|
+
* @returns The same object that was passed in, for chaining. When
|
|
87
|
+
* `options.setupNodeSdk` is `true`, the returned object also gets a
|
|
88
|
+
* `shutdown()` method that flushes and shuts down the `NodeTracerProvider`
|
|
89
|
+
* created for it — call it during your process's own shutdown sequence to
|
|
90
|
+
* avoid losing buffered spans. `shutdown` is typed as optional because it
|
|
91
|
+
* is only attached at runtime when `setupNodeSdk` is `true`; check for its
|
|
92
|
+
* presence before calling.
|
|
93
|
+
*/
|
|
94
|
+
export function instrumentMcpServer<T extends Server | McpServer | DuckTypedMcpServer>(
|
|
95
|
+
server: T,
|
|
96
|
+
options?: InstrumentOptions,
|
|
97
|
+
): T & { shutdown?: () => Promise<void> };
|