diff --git a/.env.vault-mapping b/.env.vault-mapping index 9deff9a..821a815 100644 --- a/.env.vault-mapping +++ b/.env.vault-mapping @@ -3,6 +3,10 @@ # # Generated for task-mcp OpenBao integration # Date: 2026-01-24 +# Updated: 2026-01-29 for Sentry integration (CF-536) + +# Sentry error tracking DSN +kv:kv:secret/sentry/backend-node:dsn=SENTRY_DSN # LLM API Key (for embeddings) kv:kv:litellm/master_key:value=LLM_API_KEY diff --git a/src/index.ts b/src/index.ts index 847a681..84e504d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,22 +13,15 @@ import dotenv from 'dotenv'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; -import * as Sentry from '@sentry/node'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const envPath = join(__dirname, '..', '.env'); const result = dotenv.config({ path: envPath, override: true }); -// Initialize Sentry for error tracking -if (process.env.SENTRY_DSN) { - Sentry.init({ - dsn: process.env.SENTRY_DSN, - environment: process.env.SENTRY_ENVIRONMENT || 'production', - tracesSampleRate: parseFloat(process.env.SENTRY_API_TRACE_RATE || '0.1'), - integrations: [Sentry.postgresIntegration()], - }); -} +// Initialize Sentry for error tracking (with MCP-aware filtering and PII scrubbing) +import { initSentry } from './sentry.js'; +initSentry(process.env.SENTRY_ENVIRONMENT || 'production'); // Log environment loading status (goes to MCP server logs) if (result.error) { diff --git a/src/sentry.ts b/src/sentry.ts new file mode 100644 index 0000000..02802ad --- /dev/null +++ b/src/sentry.ts @@ -0,0 +1,115 @@ +/** + * Sentry SDK integration for MCP servers. + * + * Provides error tracking and performance monitoring for MCP protocol tools + * while filtering out normal error responses (isError: true). + * + * Usage: + * import { initSentry } from "./sentry.js"; + * + * initSentry(process.env.ENVIRONMENT || "development"); + */ + +import * as Sentry from "@sentry/node"; +import { nodeProfilingIntegration } from "@sentry/profiling-node"; + +export function initSentry(environment: string = "development"): void { + const dsn = process.env.SENTRY_DSN || ""; + + if (!dsn) { + console.error("SENTRY_DSN not set, Sentry disabled"); + return; + } + + Sentry.init({ + dsn, + environment, + tracesSampleRate: parseFloat(process.env.SENTRY_TRACE_SAMPLE_RATE || "0.1"), + profilesSampleRate: parseFloat( + process.env.SENTRY_PROFILE_SAMPLE_RATE || "0.01" + ), + integrations: [ + nodeProfilingIntegration(), + new Sentry.Integrations.Http({ tracing: true }), + new Sentry.Integrations.Postgres({ recordStatementAsSpans: true }), + ], + beforeSend(event, hint) { + // MCP protocol: Don't send normal error responses (isError: true) + const originalException = hint.originalException as any; + if (originalException?.isError === true) { + return null; // This is a normal MCP error response, not an exception + } + + // Strip PII from headers + if (event.request?.headers) { + const headers = { ...event.request.headers }; + delete headers.Authorization; + delete headers.Cookie; + delete headers["X-API-Key"]; + event.request.headers = headers; + } + + // Redact sensitive data in request body + if (event.request?.data) { + let dataStr = JSON.stringify(event.request.data); + // Redact API keys + dataStr = dataStr.replace( + /(api[_-]?key|token|secret)["']?\s*[:=]\s*["']?[\w-]+/gi, + "$1=REDACTED" + ); + // Redact emails + dataStr = dataStr.replace( + /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, + "EMAIL_REDACTED" + ); + try { + event.request.data = JSON.parse(dataStr); + } catch { + event.request.data = dataStr; + } + } + + return event; + }, + maxBreadcrumbs: 30, + attachStacktrace: true, + release: process.env.APP_VERSION || "unknown", + sendDefaultPii: false, // Never send PII automatically + }); + + console.log( + `✅ Sentry initialized: ${environment} - ${process.env.APP_VERSION || "unknown"}` + ); +} + +/** + * Wrap MCP tool handler with Sentry transaction tracking. + * + * Usage: + * const result = await withSentryTransaction("tool_name", async () => { + * return await performToolOperation(); + * }); + */ +export async function withSentryTransaction( + toolName: string, + handler: () => Promise +): Promise { + return Sentry.startActiveSpan( + { name: `tool_${toolName}`, op: "mcp.tool" }, + async (span) => { + try { + const result = await handler(); + span.setStatus("ok"); + return result; + } catch (error: any) { + // Capture exception to Sentry (unless it's a normal MCP error) + if (!error.isError) { + Sentry.captureException(error); + } + span.recordException(error); + span.setStatus("error"); + throw error; + } + } + ); +}