@mhingston5/conduit 1.1.5 → 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.
@@ -70,4 +70,61 @@ describe('AuthService', () => {
70
70
  expect(headers2['Authorization']).toBe('Bearer cached-access');
71
71
  expect(axios.post).toHaveBeenCalledTimes(1); // Still 1, not 2
72
72
  });
73
+
74
+ it('should send JSON token refresh for Atlassian token endpoint', async () => {
75
+ const creds: any = {
76
+ type: 'oauth2',
77
+ clientId: 'id',
78
+ clientSecret: 'secret',
79
+ tokenUrl: 'https://auth.atlassian.com/oauth/token',
80
+ refreshToken: 'refresh',
81
+ };
82
+
83
+ (axios.post as any).mockResolvedValue({
84
+ data: {
85
+ access_token: 'new-access',
86
+ expires_in: 0,
87
+ },
88
+ });
89
+
90
+ await authService.getAuthHeaders(creds);
91
+
92
+ const [, body, config] = (axios.post as any).mock.calls[0];
93
+ expect(body).toMatchObject({
94
+ grant_type: 'refresh_token',
95
+ refresh_token: 'refresh',
96
+ client_id: 'id',
97
+ client_secret: 'secret',
98
+ });
99
+ expect(config.headers['Content-Type']).toBe('application/json');
100
+ });
101
+
102
+ it('should include tokenParams and cache rotating refresh tokens', async () => {
103
+ const creds: any = {
104
+ type: 'oauth2',
105
+ clientId: 'id',
106
+ clientSecret: 'secret',
107
+ tokenUrl: 'https://auth.atlassian.com/oauth/token',
108
+ refreshToken: 'r1',
109
+ tokenRequestFormat: 'json',
110
+ tokenParams: { audience: 'api.atlassian.com' },
111
+ };
112
+
113
+ (axios.post as any)
114
+ .mockResolvedValueOnce({
115
+ data: { access_token: 'a1', expires_in: 0, refresh_token: 'r2' },
116
+ })
117
+ .mockResolvedValueOnce({
118
+ data: { access_token: 'a2', expires_in: 0 },
119
+ });
120
+
121
+ await authService.getAuthHeaders(creds);
122
+ await authService.getAuthHeaders(creds);
123
+
124
+ const firstBody = (axios.post as any).mock.calls[0][1];
125
+ expect(firstBody).toMatchObject({ refresh_token: 'r1', audience: 'api.atlassian.com' });
126
+
127
+ const secondBody = (axios.post as any).mock.calls[1][1];
128
+ expect(secondBody).toMatchObject({ refresh_token: 'r2', audience: 'api.atlassian.com' });
129
+ });
73
130
  });
@@ -36,11 +36,11 @@ describe('GatewayService (Code Mode Lite)', () => {
36
36
  });
37
37
 
38
38
  describe('listToolPackages', () => {
39
- it('should return registered tool packages', async () => {
39
+ it('should return registered tool packages including built-ins', async () => {
40
40
  const packages = await gateway.listToolPackages();
41
- expect(packages).toHaveLength(1);
42
- expect(packages[0].id).toBe('mock-upstream');
43
- expect(packages[0].description).toContain('Upstream mock-upstream');
41
+ expect(packages).toHaveLength(2); // conduit + mock-upstream
42
+ expect(packages.find(p => p.id === 'conduit')).toBeDefined();
43
+ expect(packages.find(p => p.id === 'mock-upstream')).toBeDefined();
44
44
  });
45
45
  });
46
46
 
@@ -81,6 +81,9 @@ upstreams:
81
81
  clientSecret: my-secret
82
82
  tokenUrl: http://token
83
83
  refreshToken: my-refresh
84
+ tokenRequestFormat: json
85
+ tokenParams:
86
+ audience: api.atlassian.com
84
87
  `);
85
88
 
86
89
  vi.stubEnv('CONFIG_FILE', 'conduit.test.yaml');
@@ -92,10 +95,35 @@ upstreams:
92
95
  clientId: 'my-id',
93
96
  clientSecret: 'my-secret',
94
97
  tokenUrl: 'http://token',
95
- refreshToken: 'my-refresh'
98
+ refreshToken: 'my-refresh',
99
+ tokenRequestFormat: 'json',
100
+ tokenParams: { audience: 'api.atlassian.com' },
96
101
  });
97
102
 
98
103
  existsSpy.mockRestore();
99
104
  readSpy.mockRestore();
100
105
  });
106
+
107
+ it('should parse streamableHttp upstream correctly', () => {
108
+ const existsSpy = vi.spyOn(fs, 'existsSync').mockImplementation((p: any) => p.endsWith('conduit.test.yaml'));
109
+ const readSpy = vi.spyOn(fs, 'readFileSync').mockReturnValue(`
110
+ upstreams:
111
+ - id: atlassian
112
+ type: streamableHttp
113
+ url: https://mcp.atlassian.com/v1/sse
114
+ credentials:
115
+ type: bearer
116
+ bearerToken: test-token
117
+ `);
118
+
119
+ vi.stubEnv('CONFIG_FILE', 'conduit.test.yaml');
120
+ const configService = new ConfigService();
121
+ const upstreams = configService.get('upstreams');
122
+ expect(upstreams).toHaveLength(1);
123
+ expect(upstreams![0].type).toBe('streamableHttp');
124
+ expect((upstreams![0] as any).url).toBe('https://mcp.atlassian.com/v1/sse');
125
+
126
+ existsSpy.mockRestore();
127
+ readSpy.mockRestore();
128
+ });
101
129
  });
@@ -32,17 +32,17 @@ describe('GatewayService', () => {
32
32
 
33
33
  const tools = await gateway.discoverTools(context);
34
34
  expect(tools.length).toBeGreaterThanOrEqual(3);
35
- expect(tools.find(t => t.name === 'mcp_execute_typescript')).toBeDefined();
36
- expect(tools.find(t => t.name === 'mcp_execute_python')).toBeDefined();
37
- expect(tools.find(t => t.name === 'mcp_execute_isolate')).toBeDefined();
35
+ expect(tools.find(t => t.name === 'conduit__mcp_execute_typescript')).toBeDefined();
36
+ expect(tools.find(t => t.name === 'conduit__mcp_execute_python')).toBeDefined();
37
+ expect(tools.find(t => t.name === 'conduit__mcp_execute_isolate')).toBeDefined();
38
38
  expect(tools.find(t => t.name === 'u1__t1')).toBeDefined();
39
39
  expect(tools.find(t => t.name === 'u2__t2')).toBeDefined();
40
40
  });
41
41
 
42
42
  it('should return schema for built-in tools', async () => {
43
- const schema = await gateway.getToolSchema('mcp_execute_typescript', context);
43
+ const schema = await gateway.getToolSchema('conduit__mcp_execute_typescript', context);
44
44
  expect(schema).toBeDefined();
45
- expect(schema?.name).toBe('mcp_execute_typescript');
45
+ expect(schema?.name).toBe('conduit__mcp_execute_typescript');
46
46
  expect(schema?.inputSchema.required).toContain('code');
47
47
  });
48
48
 
@@ -21,6 +21,13 @@ describe('RequestController Routing', () => {
21
21
  mockGatewayService = {
22
22
  callTool: vi.fn(),
23
23
  discoverTools: vi.fn().mockResolvedValue([]),
24
+ policyService: {
25
+ parseToolName: (name: string) => {
26
+ const idx = name.indexOf('__');
27
+ if (idx === -1) return { namespace: '', name };
28
+ return { namespace: name.substring(0, idx), name: name.substring(idx + 2) };
29
+ }
30
+ }
24
31
  };
25
32
  mockDenoExecutor = {
26
33
  execute: vi.fn().mockResolvedValue({ stdout: 'deno', stderr: '', exitCode: 0 }),
@@ -43,7 +43,7 @@ describe('SDKGenerator', () => {
43
43
 
44
44
  const code = generator.generateTypeScript(bindings, undefined, false);
45
45
 
46
- expect(code).not.toContain('$raw');
46
+ expect(code).not.toContain('async $raw(name, args)');
47
47
  });
48
48
 
49
49
  it('should include JSDoc comments from descriptions', () => {
@@ -74,10 +74,10 @@ describe('SDKGenerator', () => {
74
74
  const code = generator.generatePython(bindings);
75
75
 
76
76
  expect(code).toContain('class _Tools:');
77
- expect(code).toContain('self.github = _ToolNamespace');
78
- expect(code).toContain('"create_issue"'); // snake_case conversion
79
- expect(code).toContain('self.slack = _ToolNamespace');
80
- expect(code).toContain('"send_message"'); // snake_case conversion
77
+ expect(code).toContain('self.github = _github_Namespace');
78
+ expect(code).toContain('async def create_issue(self, args=None, **kwargs)'); // accepts dict or kwargs
79
+ expect(code).toContain('self.slack = _slack_Namespace');
80
+ expect(code).toContain('async def send_message(self, args=None, **kwargs)'); // accepts dict or kwargs
81
81
  expect(code).toContain('tools = _Tools()');
82
82
  });
83
83
 
@@ -88,8 +88,8 @@ describe('SDKGenerator', () => {
88
88
 
89
89
  const code = generator.generatePython(bindings);
90
90
 
91
- expect(code).toContain('async def raw(self, name, args)');
92
- expect(code).toContain('await _internal_call_tool(normalized, args)');
91
+ expect(code).toContain('async def raw(self, name, args=None)');
92
+ expect(code).toContain('await _internal_call_tool(normalized, args or {})');
93
93
  });
94
94
 
95
95
  it('should inject allowlist when provided', () => {
@@ -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
+ });