@mhingston5/conduit 1.1.6 → 1.1.7
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 +29 -1
- package/dist/index.js +157 -41
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/auth.cmd.ts +26 -14
- package/src/core/config.service.ts +22 -1
- package/src/gateway/auth.service.ts +55 -13
- package/src/gateway/gateway.service.ts +22 -14
- package/src/gateway/upstream.client.ts +84 -15
- package/tests/__snapshots__/assets.test.ts.snap +17 -15
- package/tests/auth.service.test.ts +57 -0
- package/tests/config.service.test.ts +29 -1
- package/tests/upstream.transports.test.ts +117 -0
- package/tests/debug.fallback.test.ts +0 -40
- package/tests/debug_upstream.ts +0 -69
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const mcpClientMocks = {
|
|
4
|
+
connect: vi.fn(async () => undefined),
|
|
5
|
+
listTools: vi.fn(async () => ({ tools: [{ name: 'hello', description: 'hi', inputSchema: {} }] })),
|
|
6
|
+
callTool: vi.fn(async () => ({ content: [{ type: 'text', text: 'ok' }] })),
|
|
7
|
+
request: vi.fn(async () => ({ ok: true })),
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const transportMocks = {
|
|
11
|
+
streamableHttpCtor: vi.fn(),
|
|
12
|
+
sseCtor: vi.fn(),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
vi.mock('@modelcontextprotocol/sdk/client/index.js', () => {
|
|
16
|
+
return {
|
|
17
|
+
Client: class {
|
|
18
|
+
connect = mcpClientMocks.connect;
|
|
19
|
+
listTools = mcpClientMocks.listTools;
|
|
20
|
+
callTool = mcpClientMocks.callTool;
|
|
21
|
+
request = mcpClientMocks.request;
|
|
22
|
+
constructor() {}
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
vi.mock('@modelcontextprotocol/sdk/client/streamableHttp.js', () => {
|
|
28
|
+
return {
|
|
29
|
+
StreamableHTTPClientTransport: class {
|
|
30
|
+
url: URL;
|
|
31
|
+
opts: any;
|
|
32
|
+
constructor(url: URL, opts: any) {
|
|
33
|
+
this.url = url;
|
|
34
|
+
this.opts = opts;
|
|
35
|
+
transportMocks.streamableHttpCtor(url, opts);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
vi.mock('@modelcontextprotocol/sdk/client/sse.js', () => {
|
|
42
|
+
return {
|
|
43
|
+
SSEClientTransport: class {
|
|
44
|
+
url: URL;
|
|
45
|
+
opts: any;
|
|
46
|
+
constructor(url: URL, opts: any) {
|
|
47
|
+
this.url = url;
|
|
48
|
+
this.opts = opts;
|
|
49
|
+
transportMocks.sseCtor(url, opts);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Not used directly but imported by UpstreamClient
|
|
56
|
+
vi.mock('@modelcontextprotocol/sdk/client/stdio.js', () => {
|
|
57
|
+
return {
|
|
58
|
+
StdioClientTransport: class {},
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('UpstreamClient (remote transports)', () => {
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
vi.clearAllMocks();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('uses Streamable HTTP client transport for type=streamableHttp', async () => {
|
|
68
|
+
const { UpstreamClient } = await import('../src/gateway/upstream.client.js');
|
|
69
|
+
|
|
70
|
+
const logger: any = { child: () => logger, debug: vi.fn(), info: vi.fn(), error: vi.fn() };
|
|
71
|
+
const authService: any = { getAuthHeaders: vi.fn(async () => ({ Authorization: 'Bearer t' })) };
|
|
72
|
+
const urlValidator: any = { validateUrl: vi.fn(async () => ({ valid: true })) };
|
|
73
|
+
|
|
74
|
+
const client = new UpstreamClient(
|
|
75
|
+
logger,
|
|
76
|
+
{ id: 'atl', type: 'streamableHttp', url: 'https://mcp.atlassian.com/v1/sse' } as any,
|
|
77
|
+
authService,
|
|
78
|
+
urlValidator
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const res = await client.call({ jsonrpc: '2.0', id: '1', method: 'tools/list' } as any, { correlationId: 'c1' } as any);
|
|
82
|
+
|
|
83
|
+
expect(transportMocks.streamableHttpCtor).toHaveBeenCalled();
|
|
84
|
+
expect(urlValidator.validateUrl).toHaveBeenCalled();
|
|
85
|
+
expect(mcpClientMocks.connect).toHaveBeenCalled();
|
|
86
|
+
expect(mcpClientMocks.listTools).toHaveBeenCalled();
|
|
87
|
+
expect(res.result).toBeDefined();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('lazily creates SSE transport for type=sse and attaches auth headers', async () => {
|
|
91
|
+
const { UpstreamClient } = await import('../src/gateway/upstream.client.js');
|
|
92
|
+
|
|
93
|
+
const logger: any = { child: () => logger, debug: vi.fn(), info: vi.fn(), error: vi.fn() };
|
|
94
|
+
const authService: any = { getAuthHeaders: vi.fn(async () => ({ Authorization: 'Bearer t' })) };
|
|
95
|
+
const urlValidator: any = { validateUrl: vi.fn(async () => ({ valid: true })) };
|
|
96
|
+
|
|
97
|
+
const client = new UpstreamClient(
|
|
98
|
+
logger,
|
|
99
|
+
{
|
|
100
|
+
id: 'atl',
|
|
101
|
+
type: 'sse',
|
|
102
|
+
url: 'https://mcp.atlassian.com/v1/sse',
|
|
103
|
+
credentials: { type: 'bearer', bearerToken: 't' },
|
|
104
|
+
} as any,
|
|
105
|
+
authService,
|
|
106
|
+
urlValidator
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
await client.call({ jsonrpc: '2.0', id: '1', method: 'tools/list' } as any, { correlationId: 'c1' } as any);
|
|
110
|
+
|
|
111
|
+
expect(authService.getAuthHeaders).toHaveBeenCalled();
|
|
112
|
+
expect(transportMocks.sseCtor).toHaveBeenCalled();
|
|
113
|
+
const [, opts] = transportMocks.sseCtor.mock.calls[0];
|
|
114
|
+
expect(opts.requestInit.headers).toMatchObject({ Authorization: 'Bearer t' });
|
|
115
|
+
expect(mcpClientMocks.connect).toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { GatewayService } from '../src/gateway/gateway.service.js';
|
|
3
|
-
import { ExecutionContext } from '../src/core/execution.context.js';
|
|
4
|
-
import pino from 'pino';
|
|
5
|
-
|
|
6
|
-
const logger = pino({ level: 'silent' });
|
|
7
|
-
|
|
8
|
-
describe('GatewayService Namespace Fallback Debug', () => {
|
|
9
|
-
let gateway: GatewayService;
|
|
10
|
-
let context: ExecutionContext;
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
const securityService = {
|
|
14
|
-
validateUrl: vi.fn().mockReturnValue({ valid: true }),
|
|
15
|
-
} as any;
|
|
16
|
-
gateway = new GatewayService(logger, securityService);
|
|
17
|
-
context = new ExecutionContext({ logger });
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('should show detailed error when upstream not found', async () => {
|
|
21
|
-
const response = await gateway.callTool('nonexistent__tool', {}, context);
|
|
22
|
-
expect(response.error?.message).toContain("Upstream not found: 'nonexistent'");
|
|
23
|
-
expect(response.error?.message).toContain("Available: none");
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('should hit fallback for namespaceless tool', async () => {
|
|
27
|
-
// Register an upstream with a tool
|
|
28
|
-
gateway.registerUpstream({ id: 'fs', url: 'http://fs' });
|
|
29
|
-
|
|
30
|
-
// Mock discovery
|
|
31
|
-
vi.spyOn(gateway, 'discoverTools').mockResolvedValue([
|
|
32
|
-
{ name: 'fs__list_directory', description: '', inputSchema: {} }
|
|
33
|
-
] as any);
|
|
34
|
-
|
|
35
|
-
const response = await gateway.callTool('list_directory', {}, context);
|
|
36
|
-
// It should NOT return "Upstream not found: ''"
|
|
37
|
-
expect(response.error?.message).not.toContain("Upstream not found");
|
|
38
|
-
// It should try to call fs__list_directory (which will fail due to lack of axios mock here, but that's fine)
|
|
39
|
-
});
|
|
40
|
-
});
|
package/tests/debug_upstream.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Debug script to verify the filesystem upstream is working correctly.
|
|
3
|
-
* Run with: npx tsx tests/debug_upstream.ts
|
|
4
|
-
*/
|
|
5
|
-
import { ConfigService } from '../src/core/config.service.js';
|
|
6
|
-
import { createLogger } from '../src/core/logger.js';
|
|
7
|
-
import { GatewayService } from '../src/gateway/gateway.service.js';
|
|
8
|
-
import { SecurityService } from '../src/core/security.service.js';
|
|
9
|
-
import { ExecutionContext } from '../src/core/execution.context.js';
|
|
10
|
-
|
|
11
|
-
async function main() {
|
|
12
|
-
console.log('=== Conduit Upstream Debug ===\n');
|
|
13
|
-
|
|
14
|
-
const configService = new ConfigService();
|
|
15
|
-
const logger = createLogger(configService);
|
|
16
|
-
|
|
17
|
-
console.log('Config loaded from:', process.cwd());
|
|
18
|
-
console.log('Upstreams configured:', JSON.stringify(configService.get('upstreams'), null, 2));
|
|
19
|
-
console.log();
|
|
20
|
-
|
|
21
|
-
const securityService = new SecurityService(logger, undefined);
|
|
22
|
-
const gatewayService = new GatewayService(logger, securityService);
|
|
23
|
-
|
|
24
|
-
// Register upstreams from config
|
|
25
|
-
const upstreams = configService.get('upstreams') || [];
|
|
26
|
-
for (const upstream of upstreams) {
|
|
27
|
-
console.log(`Registering upstream: ${upstream.id} (${upstream.type})`);
|
|
28
|
-
gatewayService.registerUpstream(upstream);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
console.log('\n=== Testing Tool Discovery ===\n');
|
|
32
|
-
|
|
33
|
-
const context = new ExecutionContext({ logger });
|
|
34
|
-
|
|
35
|
-
// Give the filesystem server a moment to start
|
|
36
|
-
console.log('Waiting 3 seconds for upstream servers to initialize...');
|
|
37
|
-
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
const tools = await gatewayService.discoverTools(context);
|
|
41
|
-
console.log(`\nDiscovered ${tools.length} tools:`);
|
|
42
|
-
for (const tool of tools) {
|
|
43
|
-
console.log(` - ${tool.name}: ${tool.description || '(no description)'}`);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Try to find list_directory
|
|
47
|
-
const listDirTool = tools.find(t => t.name.includes('list_directory') || t.name.includes('list_dir'));
|
|
48
|
-
if (listDirTool) {
|
|
49
|
-
console.log(`\n✅ Found list_directory tool: ${listDirTool.name}`);
|
|
50
|
-
|
|
51
|
-
// Try calling it
|
|
52
|
-
console.log('\n=== Testing Tool Call ===\n');
|
|
53
|
-
const result = await gatewayService.callTool(listDirTool.name, { path: '/private/tmp' }, context);
|
|
54
|
-
console.log('Result:', JSON.stringify(result, null, 2));
|
|
55
|
-
} else {
|
|
56
|
-
console.log('\n❌ list_directory tool NOT found in discovered tools');
|
|
57
|
-
}
|
|
58
|
-
} catch (error: any) {
|
|
59
|
-
console.error('Error during discovery:', error.message);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
console.log('\n=== Debug Complete ===');
|
|
63
|
-
process.exit(0);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
main().catch(err => {
|
|
67
|
-
console.error('Fatal error:', err);
|
|
68
|
-
process.exit(1);
|
|
69
|
-
});
|