@anyshift/mcp-proxy 0.6.9 → 0.6.11

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.
Files changed (2) hide show
  1. package/dist/index.js +345 -306
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@
14
14
  * - Wrapper Mode: When MCP_PROXY_CHILD_COMMAND is set, wraps another MCP server
15
15
  * - Standalone Mode: When MCP_PROXY_CHILD_COMMAND is not set, provides only JQ tool
16
16
  */
17
+ import * as Sentry from '@sentry/node';
17
18
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
18
19
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
19
20
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
@@ -29,6 +30,21 @@ import { createFileWriter } from './fileWriter/index.js';
29
30
  import { generateToolId } from './utils/filename.js';
30
31
  import { PROXY_PARAMS } from './types/index.js';
31
32
  // ============================================================================
33
+ // SENTRY INITIALIZATION
34
+ // ============================================================================
35
+ const MCP_NAME = process.env.MCP_PROXY_MCP_NAME || 'unknown';
36
+ const SENTRY_DSN = process.env.SENTRY_DSN;
37
+ const SENTRY_ENV = process.env.SENTRY_ENV;
38
+ const SENTRY_TRACE_HEADER = process.env.SENTRY_TRACE_HEADER;
39
+ const SENTRY_BAGGAGE_HEADER = process.env.SENTRY_BAGGAGE_HEADER;
40
+ if (SENTRY_DSN) {
41
+ Sentry.init({
42
+ dsn: SENTRY_DSN,
43
+ environment: SENTRY_ENV,
44
+ tracesSampleRate: 1.0,
45
+ });
46
+ }
47
+ // ============================================================================
32
48
  // HELPER FUNCTIONS
33
49
  // ============================================================================
34
50
  /**
@@ -293,10 +309,12 @@ if (ENABLE_DOCS && DOCS_INDEXED_PATHS.length === 0) {
293
309
  */
294
310
  const childEnv = {};
295
311
  for (const [key, value] of Object.entries(process.env)) {
296
- // Skip proxy configuration variables
297
312
  if (key.startsWith('MCP_PROXY_')) {
298
313
  continue;
299
314
  }
315
+ if (key === 'SENTRY_DSN' || key === 'SENTRY_ENV' || key === 'MCP_PROXY_MCP_NAME') {
316
+ continue;
317
+ }
300
318
  // Only include defined values
301
319
  if (value !== undefined) {
302
320
  childEnv[key] = value;
@@ -345,94 +363,96 @@ async function main() {
345
363
  // ------------------------------------------------------------------------
346
364
  let childClient = null;
347
365
  let childToolsResponse = { tools: [] };
348
- if (REMOTE_URL) {
349
- // Remote mode: connect to a remote MCP server over SSE
350
- console.debug(`[mcp-proxy] Connecting to remote MCP: ${REMOTE_URL}`);
351
- let headers = {};
352
- if (REMOTE_HEADERS) {
353
- try {
354
- headers = JSON.parse(REMOTE_HEADERS);
366
+ const connectChild = async () => {
367
+ if (REMOTE_URL) {
368
+ console.debug(`[mcp-proxy] Connecting to remote MCP: ${REMOTE_URL}`);
369
+ let headers = {};
370
+ if (REMOTE_HEADERS) {
371
+ try {
372
+ headers = JSON.parse(REMOTE_HEADERS);
373
+ }
374
+ catch (e) {
375
+ console.error('ERROR: MCP_PROXY_REMOTE_HEADERS contains invalid JSON');
376
+ console.error(` Value: ${REMOTE_HEADERS.substring(0, 100)}${REMOTE_HEADERS.length > 100 ? '...' : ''}`);
377
+ console.error(` Parse error: ${e instanceof Error ? e.message : String(e)}`);
378
+ process.exit(1);
379
+ }
355
380
  }
356
- catch (e) {
357
- console.error('ERROR: MCP_PROXY_REMOTE_HEADERS contains invalid JSON');
358
- console.error(` Value: ${REMOTE_HEADERS.substring(0, 100)}${REMOTE_HEADERS.length > 100 ? '...' : ''}`);
359
- console.error(` Parse error: ${e instanceof Error ? e.message : String(e)}`);
360
- process.exit(1);
381
+ if (REMOTE_CREDENTIALS) {
382
+ try {
383
+ JSON.parse(REMOTE_CREDENTIALS); // Validate JSON before sending
384
+ }
385
+ catch (e) {
386
+ console.error('ERROR: MCP_PROXY_REMOTE_CREDENTIALS contains invalid JSON');
387
+ console.error(` Value: ${REMOTE_CREDENTIALS.substring(0, 100)}${REMOTE_CREDENTIALS.length > 100 ? '...' : ''}`);
388
+ console.error(` Parse error: ${e instanceof Error ? e.message : String(e)}`);
389
+ process.exit(1);
390
+ }
391
+ headers['X-Client-Credentials'] = REMOTE_CREDENTIALS;
361
392
  }
362
- }
363
- if (REMOTE_CREDENTIALS) {
364
- try {
365
- JSON.parse(REMOTE_CREDENTIALS); // Validate JSON before sending
393
+ let childTransport;
394
+ if (REMOTE_TRANSPORT === 'streamable-http') {
395
+ childTransport = new StreamableHTTPClientTransport(new URL(REMOTE_URL), {
396
+ requestInit: { headers },
397
+ });
366
398
  }
367
- catch (e) {
368
- console.error('ERROR: MCP_PROXY_REMOTE_CREDENTIALS contains invalid JSON');
369
- console.error(` Value: ${REMOTE_CREDENTIALS.substring(0, 100)}${REMOTE_CREDENTIALS.length > 100 ? '...' : ''}`);
370
- console.error(` Parse error: ${e instanceof Error ? e.message : String(e)}`);
371
- process.exit(1);
399
+ else {
400
+ childTransport = new SSEClientTransport(new URL(REMOTE_URL), {
401
+ eventSourceInit: {
402
+ fetch: (url, init) => {
403
+ const merged = new Headers(init?.headers);
404
+ for (const [key, value] of Object.entries(headers)) {
405
+ merged.set(key, value);
406
+ }
407
+ return fetch(url, { ...init, headers: merged });
408
+ },
409
+ },
410
+ requestInit: { headers },
411
+ });
372
412
  }
373
- headers['X-Client-Credentials'] = REMOTE_CREDENTIALS;
413
+ childClient = new Client({
414
+ name: 'mcp-proxy-client',
415
+ version: '1.0.0'
416
+ }, {
417
+ capabilities: {}
418
+ });
419
+ await childClient.connect(childTransport);
420
+ console.debug('[mcp-proxy] Connected to remote MCP');
421
+ childToolsResponse = await childClient.listTools();
422
+ console.debug(`[mcp-proxy] Discovered ${childToolsResponse.tools.length} tools from remote MCP`);
374
423
  }
375
- let childTransport;
376
- if (REMOTE_TRANSPORT === 'streamable-http') {
377
- childTransport = new StreamableHTTPClientTransport(new URL(REMOTE_URL), {
378
- requestInit: { headers },
424
+ else if (CHILD_COMMAND) {
425
+ console.debug(`[mcp-proxy] Spawning child MCP: ${CHILD_COMMAND}`);
426
+ const childTransport = new StdioClientTransport({
427
+ command: CHILD_COMMAND,
428
+ args: CHILD_ARGS,
429
+ env: childEnv // All values are defined strings (filtered in extraction loop)
379
430
  });
431
+ childTransport.onerror = (error) => {
432
+ console.error('[mcp-proxy] Child error:', error.message);
433
+ };
434
+ childClient = new Client({
435
+ name: 'mcp-proxy-client',
436
+ version: '1.0.0'
437
+ }, {
438
+ capabilities: {}
439
+ });
440
+ await childClient.connect(childTransport);
441
+ console.debug('[mcp-proxy] Connected to child MCP');
442
+ await new Promise(resolve => setTimeout(resolve, 50));
443
+ childToolsResponse = await childClient.listTools();
444
+ console.debug(`[mcp-proxy] Discovered ${childToolsResponse.tools.length} tools from child MCP`);
380
445
  }
381
446
  else {
382
- childTransport = new SSEClientTransport(new URL(REMOTE_URL), {
383
- eventSourceInit: {
384
- fetch: (url, init) => {
385
- const merged = new Headers(init?.headers);
386
- for (const [key, value] of Object.entries(headers)) {
387
- merged.set(key, value);
388
- }
389
- return fetch(url, { ...init, headers: merged });
390
- },
391
- },
392
- requestInit: { headers },
393
- });
447
+ console.debug('[mcp-proxy] Standalone mode - no child MCP');
394
448
  }
395
- childClient = new Client({
396
- name: 'mcp-proxy-client',
397
- version: '1.0.0'
398
- }, {
399
- capabilities: {}
400
- });
401
- await childClient.connect(childTransport);
402
- console.debug('[mcp-proxy] Connected to remote MCP');
403
- // Discover tools from remote MCP
404
- childToolsResponse = await childClient.listTools();
405
- console.debug(`[mcp-proxy] Discovered ${childToolsResponse.tools.length} tools from remote MCP`);
406
- }
407
- else if (CHILD_COMMAND) {
408
- console.debug(`[mcp-proxy] Spawning child MCP: ${CHILD_COMMAND}`);
409
- const childTransport = new StdioClientTransport({
410
- command: CHILD_COMMAND,
411
- args: CHILD_ARGS,
412
- env: childEnv // All values are defined strings (filtered in extraction loop)
413
- });
414
- // Log errors instead of crashing silently
415
- childTransport.onerror = (error) => {
416
- console.error('[mcp-proxy] Child error:', error.message);
417
- };
418
- childClient = new Client({
419
- name: 'mcp-proxy-client',
420
- version: '1.0.0'
421
- }, {
422
- capabilities: {}
423
- });
424
- await childClient.connect(childTransport);
425
- console.debug('[mcp-proxy] Connected to child MCP');
426
- // Give child 50ms to stabilize before making requests
427
- await new Promise(resolve => setTimeout(resolve, 50));
428
- // ------------------------------------------------------------------------
429
- // 2. DISCOVER TOOLS FROM CHILD MCP
430
- // ------------------------------------------------------------------------
431
- childToolsResponse = await childClient.listTools();
432
- console.debug(`[mcp-proxy] Discovered ${childToolsResponse.tools.length} tools from child MCP`);
449
+ };
450
+ if (SENTRY_DSN && (SENTRY_TRACE_HEADER || SENTRY_BAGGAGE_HEADER)) {
451
+ await Sentry.continueTrace({ sentryTrace: SENTRY_TRACE_HEADER || '', baggage: SENTRY_BAGGAGE_HEADER }, () => Sentry.startSpan({ name: MCP_NAME, op: 'mcp.connection' }, connectChild));
452
+ await Sentry.flush(2000);
433
453
  }
434
454
  else {
435
- console.debug('[mcp-proxy] Standalone mode - no child MCP');
455
+ await connectChild();
436
456
  }
437
457
  // ------------------------------------------------------------------------
438
458
  // 2.5. STORE ORIGINAL TOOL SCHEMAS FOR SMART PARAMETER SANITIZATION
@@ -586,76 +606,225 @@ async function main() {
586
606
  console.debug(`[mcp-proxy] Retry of: ${toolArgs.originalToolId}`);
587
607
  }
588
608
  }
589
- try {
590
- let result;
591
- // Handle JQ tool locally (if enabled)
592
- if (toolName === 'execute_jq_query' && jqTool) {
593
- if (ENABLE_LOGGING) {
594
- console.debug('[mcp-proxy] Executing JQ tool locally');
595
- }
596
- result = await jqTool.handler({
597
- params: { arguments: toolArgs }
598
- });
599
- // JQ tool returns directly, wrap in unified format
600
- const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
601
- const unifiedResponse = createContentResponse(tool_id, result.content?.[0]?.text, toolArgs);
602
- return {
603
- content: [{
604
- type: 'text',
605
- text: JSON.stringify(unifiedResponse, null, 2)
606
- }],
607
- isError: result.isError
608
- };
609
- }
610
- // Handle Timeseries anomaly detection tool locally (if enabled)
611
- if (toolName === 'detect_timeseries_anomalies' && timeseriesTool) {
612
- if (ENABLE_LOGGING) {
613
- console.debug('[mcp-proxy] Executing Timeseries anomaly detection tool locally');
614
- }
615
- result = await timeseriesTool.handler({
616
- params: { arguments: toolArgs }
617
- });
618
- // Timeseries tool returns directly, wrap in unified format
619
- const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
620
- let outputContent = null;
621
- if (result.content?.[0]?.text) {
622
- try {
623
- outputContent = JSON.parse(result.content[0].text);
624
- }
625
- catch {
626
- // If JSON parsing fails, use the raw text
627
- outputContent = result.content[0].text;
609
+ const executeToolCall = async () => {
610
+ try {
611
+ let result;
612
+ // Handle JQ tool locally (if enabled)
613
+ if (toolName === 'execute_jq_query' && jqTool) {
614
+ if (ENABLE_LOGGING) {
615
+ console.debug('[mcp-proxy] Executing JQ tool locally');
628
616
  }
617
+ result = await Sentry.startSpan({ name: toolName, op: 'mcp.tool_call.local' }, async () => {
618
+ return await jqTool.handler({
619
+ params: { arguments: toolArgs }
620
+ });
621
+ });
622
+ // JQ tool returns directly, wrap in unified format
623
+ const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
624
+ const unifiedResponse = createContentResponse(tool_id, result.content?.[0]?.text, toolArgs);
625
+ return {
626
+ content: [{
627
+ type: 'text',
628
+ text: JSON.stringify(unifiedResponse, null, 2)
629
+ }],
630
+ isError: result.isError
631
+ };
629
632
  }
630
- const unifiedResponse = {
631
- tool_id,
632
- wroteToFile: false,
633
- outputContent
634
- };
635
- return {
636
- content: [{
637
- type: 'text',
638
- text: JSON.stringify(unifiedResponse, null, 2)
639
- }],
640
- isError: result.isError
641
- };
642
- }
643
- // Handle Docs tools locally (if enabled)
644
- // Route through file writer for large doc handling (same as child MCP tools)
645
- const docsToolNames = ['docs_glob', 'docs_grep', 'docs_read'];
646
- if (docsToolNames.includes(toolName) && docsTools.length > 0) {
647
- const docTool = docsTools.find(t => t.toolDefinition.name === toolName);
648
- if (docTool) {
633
+ // Handle Timeseries anomaly detection tool locally (if enabled)
634
+ if (toolName === 'detect_timeseries_anomalies' && timeseriesTool) {
649
635
  if (ENABLE_LOGGING) {
650
- console.debug(`[mcp-proxy] Executing ${toolName} locally`);
636
+ console.debug('[mcp-proxy] Executing Timeseries anomaly detection tool locally');
651
637
  }
652
- try {
653
- result = await docTool.handler({
638
+ result = await Sentry.startSpan({ name: toolName, op: 'mcp.tool_call.local' }, async () => {
639
+ return await timeseriesTool.handler({
654
640
  params: { arguments: toolArgs }
655
641
  });
656
- const contentStr = result.content?.[0]?.text || '';
642
+ });
643
+ // Timeseries tool returns directly, wrap in unified format
644
+ const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
645
+ let outputContent = null;
646
+ if (result.content?.[0]?.text) {
647
+ try {
648
+ outputContent = JSON.parse(result.content[0].text);
649
+ }
650
+ catch {
651
+ // If JSON parsing fails, use the raw text
652
+ outputContent = result.content[0].text;
653
+ }
654
+ }
655
+ const unifiedResponse = {
656
+ tool_id,
657
+ wroteToFile: false,
658
+ outputContent
659
+ };
660
+ return {
661
+ content: [{
662
+ type: 'text',
663
+ text: JSON.stringify(unifiedResponse, null, 2)
664
+ }],
665
+ isError: result.isError
666
+ };
667
+ }
668
+ // Handle Docs tools locally (if enabled)
669
+ // Route through file writer for large doc handling (same as child MCP tools)
670
+ const docsToolNames = ['docs_glob', 'docs_grep', 'docs_read'];
671
+ if (docsToolNames.includes(toolName) && docsTools.length > 0) {
672
+ const docTool = docsTools.find(t => t.toolDefinition.name === toolName);
673
+ if (docTool) {
674
+ if (ENABLE_LOGGING) {
675
+ console.debug(`[mcp-proxy] Executing ${toolName} locally`);
676
+ }
677
+ try {
678
+ result = await Sentry.startSpan({ name: toolName, op: 'mcp.tool_call.local' }, async () => {
679
+ return await docTool.handler({
680
+ params: { arguments: toolArgs }
681
+ });
682
+ });
683
+ const contentStr = result.content?.[0]?.text || '';
684
+ const originalLength = contentStr.length;
685
+ // Route through file writer (same pipeline as child MCP tools)
686
+ const unifiedResponse = await fileWriter.handleResponse(toolName, toolArgs, {
687
+ content: [{ type: 'text', text: contentStr }]
688
+ });
689
+ if (ENABLE_LOGGING) {
690
+ if (unifiedResponse.wroteToFile) {
691
+ console.debug(`[mcp-proxy] File written for ${toolName} (${originalLength} chars) → ${unifiedResponse.filePath}`);
692
+ }
693
+ else {
694
+ console.debug(`[mcp-proxy] Response for ${toolName} (${originalLength} chars) returned directly`);
695
+ }
696
+ }
697
+ // If not written to file, apply truncation to outputContent
698
+ if (!unifiedResponse.wroteToFile && unifiedResponse.outputContent) {
699
+ const outputStr = typeof unifiedResponse.outputContent === 'string'
700
+ ? unifiedResponse.outputContent
701
+ : JSON.stringify(unifiedResponse.outputContent);
702
+ const truncated = truncateResponseIfNeeded(truncationConfig, outputStr);
703
+ if (truncated.length < outputStr.length) {
704
+ if (ENABLE_LOGGING) {
705
+ console.debug(`[mcp-proxy] Truncated response: ${outputStr.length} → ${truncated.length} chars`);
706
+ }
707
+ // Keep as string for docs (markdown content)
708
+ unifiedResponse.outputContent = truncated;
709
+ }
710
+ }
711
+ // Add retry metadata and return unified response
712
+ addRetryMetadata(unifiedResponse, toolArgs);
713
+ return {
714
+ content: [{
715
+ type: 'text',
716
+ text: JSON.stringify(unifiedResponse, null, 2)
717
+ }],
718
+ isError: false
719
+ };
720
+ }
721
+ catch (error) {
722
+ const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
723
+ return {
724
+ content: [{
725
+ type: 'text',
726
+ text: JSON.stringify(createErrorResponse(tool_id, error.message || String(error), toolArgs), null, 2)
727
+ }],
728
+ isError: true
729
+ };
730
+ }
731
+ }
732
+ }
733
+ // Forward all other tools to child MCP (if child exists)
734
+ if (!childClient) {
735
+ const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
736
+ return {
737
+ content: [{
738
+ type: 'text',
739
+ text: JSON.stringify(createErrorResponse(tool_id, `Tool ${toolName} not available in standalone mode (no child MCP)`, toolArgs), null, 2)
740
+ }],
741
+ isError: true
742
+ };
743
+ }
744
+ if (ENABLE_LOGGING) {
745
+ console.debug(`[mcp-proxy] Forwarding to child MCP: ${toolName}`);
746
+ }
747
+ // Sanitize arguments: remove proxy parameters that weren't in original tool schema
748
+ const sanitizedArgs = sanitizeToolArgs(toolName, toolArgs);
749
+ result = await Sentry.startSpan({ name: toolName, op: 'mcp.tool_call.remote' }, () => childClient.callTool({
750
+ name: toolName,
751
+ arguments: sanitizedArgs
752
+ }, undefined, {
753
+ timeout: REQUEST_TIMEOUT_MS
754
+ }));
755
+ // Check if child MCP returned an error
756
+ const childReturnedError = !!result.isError;
757
+ // Preserve _meta from child response (e.g. parsed_commands) for passthrough
758
+ const childMeta = result._meta && typeof result._meta === 'object' && Object.keys(result._meta).length > 0
759
+ ? result._meta
760
+ : undefined;
761
+ // Process result through file writer to get unified response
762
+ if (result.content && Array.isArray(result.content) && result.content.length > 0) {
763
+ // Extract text content and embedded resources from all content items
764
+ const textContents = [];
765
+ const resources = [];
766
+ for (const item of result.content) {
767
+ if (item.type === 'text' && typeof item.text === 'string') {
768
+ textContents.push(item.text);
769
+ }
770
+ else if (item.type === 'resource' && item.resource) {
771
+ // Handle EmbeddedResource - extract content from resource field
772
+ const resource = item.resource;
773
+ const resourceObj = {};
774
+ if (resource.uri)
775
+ resourceObj.uri = resource.uri;
776
+ if (resource.mimeType)
777
+ resourceObj.mimeType = resource.mimeType;
778
+ // Content can be in 'text' (string) or 'blob' (base64 binary)
779
+ if (resource.text) {
780
+ resourceObj.content = resource.text;
781
+ }
782
+ else if (resource.blob) {
783
+ // For binary content, keep as base64 or decode based on mimeType
784
+ resourceObj.content = typeof resource.blob === 'string'
785
+ ? resource.blob
786
+ : Buffer.from(resource.blob).toString('base64');
787
+ }
788
+ resources.push(resourceObj);
789
+ }
790
+ }
791
+ // Build combined content object
792
+ let combinedContent;
793
+ if (resources.length > 0) {
794
+ // Has resources - combine text and resources into single object
795
+ combinedContent = {
796
+ text: textContents.length === 1 ? textContents[0] : textContents,
797
+ resources: resources.length === 1 ? resources[0] : resources
798
+ };
799
+ if (ENABLE_LOGGING) {
800
+ console.debug(`[mcp-proxy] Combined ${textContents.length} text items and ${resources.length} resources`);
801
+ }
802
+ }
803
+ else if (textContents.length > 0) {
804
+ // Text only - use first text content (original behavior)
805
+ combinedContent = textContents[0];
806
+ }
807
+ if (combinedContent !== undefined) {
808
+ const contentStr = typeof combinedContent === 'string'
809
+ ? combinedContent
810
+ : JSON.stringify(combinedContent);
657
811
  const originalLength = contentStr.length;
658
- // Route through file writer (same pipeline as child MCP tools)
812
+ // If child returned error, pass through directly without file writing
813
+ if (childReturnedError) {
814
+ const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
815
+ if (ENABLE_LOGGING) {
816
+ console.debug(`[mcp-proxy] Child MCP returned error for ${toolName}: ${contentStr.substring(0, 100)}...`);
817
+ }
818
+ return {
819
+ content: [{
820
+ type: 'text',
821
+ text: JSON.stringify(createErrorResponse(tool_id, contentStr, toolArgs), null, 2)
822
+ }],
823
+ isError: true,
824
+ ...(childMeta ? { _meta: childMeta } : {})
825
+ };
826
+ }
827
+ // Get unified response from file writer
659
828
  const unifiedResponse = await fileWriter.handleResponse(toolName, toolArgs, {
660
829
  content: [{ type: 'text', text: contentStr }]
661
830
  });
@@ -677,191 +846,56 @@ async function main() {
677
846
  if (ENABLE_LOGGING) {
678
847
  console.debug(`[mcp-proxy] Truncated response: ${outputStr.length} → ${truncated.length} chars`);
679
848
  }
680
- // Keep as string for docs (markdown content)
681
- unifiedResponse.outputContent = truncated;
849
+ // Re-parse if it was JSON, otherwise keep as string
850
+ try {
851
+ unifiedResponse.outputContent = JSON.parse(truncated);
852
+ }
853
+ catch {
854
+ unifiedResponse.outputContent = truncated;
855
+ }
682
856
  }
683
857
  }
684
- // Add retry metadata and return unified response
858
+ // Add retry metadata and return unified response as JSON
685
859
  addRetryMetadata(unifiedResponse, toolArgs);
686
860
  return {
687
861
  content: [{
688
862
  type: 'text',
689
863
  text: JSON.stringify(unifiedResponse, null, 2)
690
864
  }],
691
- isError: false
692
- };
693
- }
694
- catch (error) {
695
- const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
696
- return {
697
- content: [{
698
- type: 'text',
699
- text: JSON.stringify(createErrorResponse(tool_id, error.message || String(error), toolArgs), null, 2)
700
- }],
701
- isError: true
865
+ isError: !!unifiedResponse.error,
866
+ ...(childMeta ? { _meta: childMeta } : {})
702
867
  };
703
868
  }
704
869
  }
870
+ // Fallback: return result with generated tool_id
871
+ const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
872
+ return {
873
+ content: [{
874
+ type: 'text',
875
+ text: JSON.stringify(createContentResponse(tool_id, result, toolArgs), null, 2)
876
+ }],
877
+ isError: childReturnedError,
878
+ ...(childMeta ? { _meta: childMeta } : {})
879
+ };
705
880
  }
706
- // Forward all other tools to child MCP (if child exists)
707
- if (!childClient) {
881
+ catch (error) {
882
+ console.error(`[mcp-proxy] Error executing tool ${toolName}:`, error);
708
883
  const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
709
884
  return {
710
885
  content: [{
711
886
  type: 'text',
712
- text: JSON.stringify(createErrorResponse(tool_id, `Tool ${toolName} not available in standalone mode (no child MCP)`, toolArgs), null, 2)
887
+ text: JSON.stringify(createErrorResponse(tool_id, `Error executing ${toolName}: ${error.message || String(error)}`, toolArgs), null, 2)
713
888
  }],
714
889
  isError: true
715
890
  };
716
891
  }
717
- if (ENABLE_LOGGING) {
718
- console.debug(`[mcp-proxy] Forwarding to child MCP: ${toolName}`);
719
- }
720
- // Sanitize arguments: remove proxy parameters that weren't in original tool schema
721
- const sanitizedArgs = sanitizeToolArgs(toolName, toolArgs);
722
- result = await childClient.callTool({
723
- name: toolName,
724
- arguments: sanitizedArgs
725
- }, undefined, {
726
- timeout: REQUEST_TIMEOUT_MS
727
- });
728
- // Check if child MCP returned an error
729
- const childReturnedError = !!result.isError;
730
- // Preserve _meta from child response (e.g. parsed_commands) for passthrough
731
- const childMeta = result._meta && typeof result._meta === 'object' && Object.keys(result._meta).length > 0
732
- ? result._meta
733
- : undefined;
734
- // Process result through file writer to get unified response
735
- if (result.content && Array.isArray(result.content) && result.content.length > 0) {
736
- // Extract text content and embedded resources from all content items
737
- const textContents = [];
738
- const resources = [];
739
- for (const item of result.content) {
740
- if (item.type === 'text' && typeof item.text === 'string') {
741
- textContents.push(item.text);
742
- }
743
- else if (item.type === 'resource' && item.resource) {
744
- // Handle EmbeddedResource - extract content from resource field
745
- const resource = item.resource;
746
- const resourceObj = {};
747
- if (resource.uri)
748
- resourceObj.uri = resource.uri;
749
- if (resource.mimeType)
750
- resourceObj.mimeType = resource.mimeType;
751
- // Content can be in 'text' (string) or 'blob' (base64 binary)
752
- if (resource.text) {
753
- resourceObj.content = resource.text;
754
- }
755
- else if (resource.blob) {
756
- // For binary content, keep as base64 or decode based on mimeType
757
- resourceObj.content = typeof resource.blob === 'string'
758
- ? resource.blob
759
- : Buffer.from(resource.blob).toString('base64');
760
- }
761
- resources.push(resourceObj);
762
- }
763
- }
764
- // Build combined content object
765
- let combinedContent;
766
- if (resources.length > 0) {
767
- // Has resources - combine text and resources into single object
768
- combinedContent = {
769
- text: textContents.length === 1 ? textContents[0] : textContents,
770
- resources: resources.length === 1 ? resources[0] : resources
771
- };
772
- if (ENABLE_LOGGING) {
773
- console.debug(`[mcp-proxy] Combined ${textContents.length} text items and ${resources.length} resources`);
774
- }
775
- }
776
- else if (textContents.length > 0) {
777
- // Text only - use first text content (original behavior)
778
- combinedContent = textContents[0];
779
- }
780
- if (combinedContent !== undefined) {
781
- const contentStr = typeof combinedContent === 'string'
782
- ? combinedContent
783
- : JSON.stringify(combinedContent);
784
- const originalLength = contentStr.length;
785
- // If child returned error, pass through directly without file writing
786
- if (childReturnedError) {
787
- const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
788
- if (ENABLE_LOGGING) {
789
- console.debug(`[mcp-proxy] Child MCP returned error for ${toolName}: ${contentStr.substring(0, 100)}...`);
790
- }
791
- return {
792
- content: [{
793
- type: 'text',
794
- text: JSON.stringify(createErrorResponse(tool_id, contentStr, toolArgs), null, 2)
795
- }],
796
- isError: true,
797
- ...(childMeta ? { _meta: childMeta } : {})
798
- };
799
- }
800
- // Get unified response from file writer
801
- const unifiedResponse = await fileWriter.handleResponse(toolName, toolArgs, {
802
- content: [{ type: 'text', text: contentStr }]
803
- });
804
- if (ENABLE_LOGGING) {
805
- if (unifiedResponse.wroteToFile) {
806
- console.debug(`[mcp-proxy] File written for ${toolName} (${originalLength} chars) → ${unifiedResponse.filePath}`);
807
- }
808
- else {
809
- console.debug(`[mcp-proxy] Response for ${toolName} (${originalLength} chars) returned directly`);
810
- }
811
- }
812
- // If not written to file, apply truncation to outputContent
813
- if (!unifiedResponse.wroteToFile && unifiedResponse.outputContent) {
814
- const outputStr = typeof unifiedResponse.outputContent === 'string'
815
- ? unifiedResponse.outputContent
816
- : JSON.stringify(unifiedResponse.outputContent);
817
- const truncated = truncateResponseIfNeeded(truncationConfig, outputStr);
818
- if (truncated.length < outputStr.length) {
819
- if (ENABLE_LOGGING) {
820
- console.debug(`[mcp-proxy] Truncated response: ${outputStr.length} → ${truncated.length} chars`);
821
- }
822
- // Re-parse if it was JSON, otherwise keep as string
823
- try {
824
- unifiedResponse.outputContent = JSON.parse(truncated);
825
- }
826
- catch {
827
- unifiedResponse.outputContent = truncated;
828
- }
829
- }
830
- }
831
- // Add retry metadata and return unified response as JSON
832
- addRetryMetadata(unifiedResponse, toolArgs);
833
- return {
834
- content: [{
835
- type: 'text',
836
- text: JSON.stringify(unifiedResponse, null, 2)
837
- }],
838
- isError: !!unifiedResponse.error,
839
- ...(childMeta ? { _meta: childMeta } : {})
840
- };
841
- }
842
- }
843
- // Fallback: return result with generated tool_id
844
- const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
845
- return {
846
- content: [{
847
- type: 'text',
848
- text: JSON.stringify(createContentResponse(tool_id, result, toolArgs), null, 2)
849
- }],
850
- isError: childReturnedError,
851
- ...(childMeta ? { _meta: childMeta } : {})
852
- };
853
- }
854
- catch (error) {
855
- console.error(`[mcp-proxy] Error executing tool ${toolName}:`, error);
856
- const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
857
- return {
858
- content: [{
859
- type: 'text',
860
- text: JSON.stringify(createErrorResponse(tool_id, `Error executing ${toolName}: ${error.message || String(error)}`, toolArgs), null, 2)
861
- }],
862
- isError: true
863
- };
892
+ };
893
+ if (SENTRY_DSN && (SENTRY_TRACE_HEADER || SENTRY_BAGGAGE_HEADER)) {
894
+ const result = await Sentry.continueTrace({ sentryTrace: SENTRY_TRACE_HEADER || '', baggage: SENTRY_BAGGAGE_HEADER }, () => Sentry.startSpan({ name: `${MCP_NAME}/${toolName}`, op: 'mcp.tool_call' }, executeToolCall));
895
+ await Sentry.flush(2000);
896
+ return result;
864
897
  }
898
+ return executeToolCall();
865
899
  });
866
900
  // ------------------------------------------------------------------------
867
901
  // 8. CONNECT PROXY TO STDIO
@@ -870,8 +904,13 @@ async function main() {
870
904
  await server.connect(transport);
871
905
  console.debug('[mcp-proxy] Proxy server ready on stdio');
872
906
  console.debug('[mcp-proxy] Waiting for MCP protocol messages...');
873
- // Prevent EPIPE from crashing the process
874
907
  process.on('SIGPIPE', () => { });
908
+ const shutdown = async () => {
909
+ await Sentry.close(2000);
910
+ process.exit(0);
911
+ };
912
+ process.on('SIGTERM', shutdown);
913
+ process.on('SIGINT', shutdown);
875
914
  }
876
915
  // ============================================================================
877
916
  // START THE PROXY
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anyshift/mcp-proxy",
3
- "version": "0.6.9",
3
+ "version": "0.6.11",
4
4
  "description": "Generic MCP proxy that adds truncation, file writing, and JQ capabilities to any MCP server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -16,6 +16,7 @@
16
16
  "@modelcontextprotocol/sdk": "^1.24.0",
17
17
  "glob": "^13.0.0",
18
18
  "papaparse": "^5.5.3",
19
+ "@sentry/node": "^9.0.0",
19
20
  "zod": "^3.24.2"
20
21
  },
21
22
  "devDependencies": {