@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.
- package/dist/index.js +345 -306
- 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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
393
|
+
let childTransport;
|
|
394
|
+
if (REMOTE_TRANSPORT === 'streamable-http') {
|
|
395
|
+
childTransport = new StreamableHTTPClientTransport(new URL(REMOTE_URL), {
|
|
396
|
+
requestInit: { headers },
|
|
397
|
+
});
|
|
366
398
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
childTransport = new
|
|
378
|
-
|
|
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
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
if (
|
|
594
|
-
|
|
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
|
-
|
|
631
|
-
|
|
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(
|
|
636
|
+
console.debug('[mcp-proxy] Executing Timeseries anomaly detection tool locally');
|
|
651
637
|
}
|
|
652
|
-
|
|
653
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
681
|
-
|
|
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:
|
|
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
|
-
|
|
707
|
-
|
|
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, `
|
|
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
|
-
|
|
718
|
-
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
|
|
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.
|
|
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": {
|