@tigerdata/mcp-boilerplate 0.2.1 → 0.4.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.
@@ -17,7 +17,8 @@ export async function cliEntrypoint(stdioEntrypoint, httpEntrypoint, instrumenta
17
17
  let cleanup;
18
18
  if (args.includes('--instrument') ||
19
19
  process.env.INSTRUMENT === 'true') {
20
- ({ cleanup } = await import(instrumentation));
20
+ const { instrument } = await import(instrumentation);
21
+ ({ cleanup } = instrument());
21
22
  }
22
23
  // Import and run the HTTP server
23
24
  const { registerCleanupFn } = await import(httpEntrypoint);
package/dist/http/mcp.js CHANGED
@@ -1,9 +1,13 @@
1
1
  import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
2
2
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3
+ import { trace, context as otelContext, propagation, SpanKind, SpanStatusCode, } from '@opentelemetry/api';
4
+ import { ATTR_HTTP_RESPONSE_STATUS_CODE } from '@opentelemetry/semantic-conventions';
3
5
  import { Router } from 'express';
4
6
  import { randomUUID } from 'node:crypto';
5
7
  import getRawBody from 'raw-body';
6
8
  import { log } from '../logger.js';
9
+ const name = process.env.OTEL_SERVICE_NAME;
10
+ const tracer = trace.getTracer(name ? `${name}.router.mcp` : 'router.mcp');
7
11
  export const mcpRouterFactory = (context, createServer, { name, stateful = true, } = {}) => {
8
12
  const router = Router();
9
13
  const transports = new Map();
@@ -75,24 +79,43 @@ export const mcpRouterFactory = (context, createServer, { name, stateful = true,
75
79
  await transport.handleRequest(req, res, body);
76
80
  };
77
81
  router.post('/', async (req, res) => {
78
- try {
79
- await (stateful
80
- ? handleStatefulRequest(req, res)
81
- : handleStatelessRequest(req, res));
82
+ let traceContext = otelContext.active();
83
+ if (req.headers.traceparent) {
84
+ // Some MCP clients (e.g. pydantic) pass the parent trace context
85
+ traceContext = propagation.extract(traceContext, {
86
+ traceparent: req.headers.traceparent,
87
+ });
82
88
  }
83
- catch (error) {
84
- log.error('Error handling MCP request:', error);
85
- if (!res.headersSent) {
86
- res.status(500).json({
87
- jsonrpc: '2.0',
88
- error: {
89
- code: -32603,
90
- message: 'Internal server error',
91
- },
92
- id: null,
89
+ await tracer.startActiveSpan('mcp.http.post', { kind: SpanKind.SERVER }, traceContext, async (span) => {
90
+ try {
91
+ await (stateful
92
+ ? handleStatefulRequest(req, res)
93
+ : handleStatelessRequest(req, res));
94
+ span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, res.statusCode);
95
+ span.setStatus({ code: SpanStatusCode.OK });
96
+ }
97
+ catch (error) {
98
+ log.error('Error handling MCP request:', error);
99
+ span.recordException(error);
100
+ span.setStatus({
101
+ code: SpanStatusCode.ERROR,
102
+ message: error.message,
93
103
  });
104
+ if (!res.headersSent) {
105
+ res.status(500).json({
106
+ jsonrpc: '2.0',
107
+ error: {
108
+ code: -32603,
109
+ message: 'Internal server error',
110
+ },
111
+ id: null,
112
+ });
113
+ }
94
114
  }
95
- }
115
+ finally {
116
+ span.end();
117
+ }
118
+ });
96
119
  });
97
120
  // Reusable handler for GET and DELETE requests
98
121
  const handleSessionRequest = async (req, res) => {
@@ -1,3 +1,13 @@
1
1
  import { NodeSDK } from '@opentelemetry/sdk-node';
2
- export declare const sdk: NodeSDK;
3
- export declare const cleanup: () => Promise<void>;
2
+ import { InstrumentationConfigMap } from '@opentelemetry/auto-instrumentations-node';
3
+ import type { Instrumentation } from '@opentelemetry/instrumentation';
4
+ interface Options {
5
+ environment?: string;
6
+ instrumentations?: Instrumentation[];
7
+ nodeAutoInstrumentationsOptions?: InstrumentationConfigMap;
8
+ }
9
+ export declare const instrument: (options?: Options) => {
10
+ cleanup: () => Promise<void>;
11
+ sdk: NodeSDK;
12
+ };
13
+ export {};
@@ -2,57 +2,67 @@ import { NodeSDK } from '@opentelemetry/sdk-node';
2
2
  import { resourceFromAttributes } from '@opentelemetry/resources';
3
3
  import { OTLPTraceExporter as GrpcTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
4
4
  import { OTLPTraceExporter as HttpTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
5
- import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
5
+ import { getNodeAutoInstrumentations, } from '@opentelemetry/auto-instrumentations-node';
6
6
  import { BatchSpanProcessor, } from '@opentelemetry/sdk-trace-base';
7
7
  import { BatchLogRecordProcessor, } from '@opentelemetry/sdk-logs';
8
8
  import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
9
9
  import { log } from './logger.js';
10
- const spanProcessors = [];
11
- const logRecordProcessors = [];
12
- if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {
13
- spanProcessors.push(new BatchSpanProcessor(new GrpcTraceExporter()));
14
- }
15
- if (process.env.JAEGER_TRACES_ENDPOINT) {
16
- spanProcessors.push(new BatchSpanProcessor(new GrpcTraceExporter({
17
- url: process.env.JAEGER_TRACES_ENDPOINT,
18
- })));
19
- }
20
- if (process.env.LOGFIRE_TRACES_ENDPOINT) {
21
- spanProcessors.push(new BatchSpanProcessor(new HttpTraceExporter({
22
- url: process.env.LOGFIRE_TRACES_ENDPOINT,
23
- headers: process.env.LOGFIRE_TOKEN
24
- ? { Authorization: `Bearer ${process.env.LOGFIRE_TOKEN}` }
25
- : {},
26
- })));
27
- }
28
- if (process.env.LOGFIRE_LOGS_ENDPOINT) {
29
- logRecordProcessors.push(new BatchLogRecordProcessor(new OTLPLogExporter({
30
- url: process.env.LOGFIRE_LOGS_ENDPOINT,
31
- headers: process.env.LOGFIRE_TOKEN
32
- ? { Authorization: `Bearer ${process.env.LOGFIRE_TOKEN}` }
33
- : {},
34
- })));
35
- }
36
- export const sdk = new NodeSDK({
37
- instrumentations: [getNodeAutoInstrumentations()],
38
- spanProcessors,
39
- logRecordProcessors,
40
- resource: resourceFromAttributes({
41
- 'deployment.environment.name': process.env.LOGFIRE_ENVIRONMENT || process.env.NODE_ENV || 'development',
42
- 'service.instance.id': process.env.HOSTNAME,
43
- }),
44
- });
45
- // Initialize the SDK and register with the OpenTelemetry API
46
- sdk.start();
47
- log.info('OpenTelemetry initialized');
48
- export const cleanup = async () => {
49
- try {
50
- await Promise.all(spanProcessors.map((sp) => sp.shutdown()));
51
- await Promise.all(logRecordProcessors.map((lp) => lp.shutdown()));
52
- await sdk.shutdown();
53
- log.info('OpenTelemetry terminated');
10
+ export const instrument = (options = {}) => {
11
+ const spanProcessors = [];
12
+ const logRecordProcessors = [];
13
+ if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {
14
+ spanProcessors.push(new BatchSpanProcessor(new GrpcTraceExporter()));
54
15
  }
55
- catch (error) {
56
- log.error('Error terminating OpenTelemetry', error);
16
+ if (process.env.JAEGER_TRACES_ENDPOINT) {
17
+ spanProcessors.push(new BatchSpanProcessor(new GrpcTraceExporter({
18
+ url: process.env.JAEGER_TRACES_ENDPOINT,
19
+ })));
57
20
  }
21
+ if (process.env.LOGFIRE_TRACES_ENDPOINT) {
22
+ spanProcessors.push(new BatchSpanProcessor(new HttpTraceExporter({
23
+ url: process.env.LOGFIRE_TRACES_ENDPOINT,
24
+ headers: process.env.LOGFIRE_TOKEN
25
+ ? { Authorization: `Bearer ${process.env.LOGFIRE_TOKEN}` }
26
+ : {},
27
+ })));
28
+ }
29
+ if (process.env.LOGFIRE_LOGS_ENDPOINT) {
30
+ logRecordProcessors.push(new BatchLogRecordProcessor(new OTLPLogExporter({
31
+ url: process.env.LOGFIRE_LOGS_ENDPOINT,
32
+ headers: process.env.LOGFIRE_TOKEN
33
+ ? { Authorization: `Bearer ${process.env.LOGFIRE_TOKEN}` }
34
+ : {},
35
+ })));
36
+ }
37
+ const sdk = new NodeSDK({
38
+ instrumentations: options.instrumentations || [
39
+ getNodeAutoInstrumentations(options.nodeAutoInstrumentationsOptions),
40
+ ],
41
+ spanProcessors,
42
+ logRecordProcessors,
43
+ resource: resourceFromAttributes({
44
+ 'deployment.environment.name': options.environment ||
45
+ process.env.LOGFIRE_ENVIRONMENT ||
46
+ process.env.NODE_ENV ||
47
+ 'development',
48
+ 'service.instance.id': process.env.HOSTNAME,
49
+ }),
50
+ });
51
+ // Initialize the SDK and register with the OpenTelemetry API
52
+ sdk.start();
53
+ log.info('OpenTelemetry initialized');
54
+ return {
55
+ cleanup: async () => {
56
+ try {
57
+ await Promise.all(spanProcessors.map((sp) => sp.shutdown()));
58
+ await Promise.all(logRecordProcessors.map((lp) => lp.shutdown()));
59
+ await sdk.shutdown();
60
+ log.info('OpenTelemetry terminated');
61
+ }
62
+ catch (error) {
63
+ log.error('Error terminating OpenTelemetry', error);
64
+ }
65
+ },
66
+ sdk,
67
+ };
58
68
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tigerdata/mcp-boilerplate",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "MCP boilerplate code for Node.js",
5
5
  "license": "Apache-2.0",
6
6
  "author": "TigerData",
@@ -41,6 +41,7 @@
41
41
  "@opentelemetry/sdk-metrics": "^2.2.0",
42
42
  "@opentelemetry/sdk-node": "^0.207.0",
43
43
  "@opentelemetry/sdk-trace-node": "^2.2.0",
44
+ "@opentelemetry/semantic-conventions": "^1.37.0",
44
45
  "express": "^5.1.0",
45
46
  "raw-body": "^3.0.1",
46
47
  "zod": "^3.23.8"