@mhingston5/conduit 1.1.4 → 1.1.6

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.
@@ -10,7 +10,9 @@ export class SDKGenerator {
10
10
  * Convert camelCase to snake_case for Python
11
11
  */
12
12
  private toSnakeCase(str: string): string {
13
- return str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
13
+ const snake = str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '').replace(/[^a-z0-9_]/g, '_');
14
+ // Ensure it doesn't start with a number
15
+ return /^[0-9]/.test(snake) ? `_${snake}` : snake;
14
16
  }
15
17
 
16
18
  /**
@@ -41,7 +43,7 @@ export class SDKGenerator {
41
43
  lines.push('const __allowedTools = null;');
42
44
  }
43
45
 
44
- lines.push('const tools = {');
46
+ lines.push('const _tools = {');
45
47
 
46
48
  for (const [namespace, tools] of grouped.entries()) {
47
49
  // Validate namespace is a valid identifier
@@ -92,6 +94,29 @@ export class SDKGenerator {
92
94
  }
93
95
 
94
96
  lines.push('};');
97
+ lines.push(`
98
+ const tools = new Proxy(_tools, {
99
+ get: (target, prop) => {
100
+ if (prop in target) return target[prop];
101
+ if (prop === 'then') return undefined;
102
+ if (typeof prop === 'string') {
103
+ // Flat tool access fallback: search all namespaces for a matching tool
104
+ for (const nsName of Object.keys(target)) {
105
+ if (nsName === '$raw') continue;
106
+ const ns = target[nsName];
107
+ if (ns && typeof ns === 'object' && ns[prop]) {
108
+ return ns[prop];
109
+ }
110
+ }
111
+
112
+ const forbidden = ['\x24raw'];
113
+ const namespaces = Object.keys(target).filter(k => !forbidden.includes(k));
114
+ throw new Error(\`Namespace or Tool '\${prop}' not found. Available namespaces: \${namespaces.join(', ') || 'none'}. Use tools.$raw(name, args) for dynamic calls.\`);
115
+ }
116
+ return undefined;
117
+ }
118
+ });
119
+ `);
95
120
  lines.push('(globalThis as any).tools = tools;');
96
121
 
97
122
  return lines.join('\n');
@@ -119,35 +144,47 @@ export class SDKGenerator {
119
144
  }
120
145
 
121
146
  lines.push('');
122
- lines.push('class _ToolNamespace:');
123
- lines.push(' def __init__(self, methods):');
124
- lines.push(' for name, fn in methods.items():');
125
- lines.push(' setattr(self, name, fn)');
126
- lines.push('');
127
- lines.push('class _Tools:');
128
- lines.push(' def __init__(self):');
129
147
 
148
+ // Generate namespace classes
130
149
  for (const [namespace, tools] of grouped.entries()) {
131
150
  const safeNamespace = this.toSnakeCase(namespace);
132
- const methodsDict: string[] = [];
151
+ lines.push(`class _${safeNamespace}_Namespace:`);
133
152
 
134
153
  for (const tool of tools) {
135
154
  const methodName = this.toSnakeCase(tool.methodName);
136
155
  const fullName = tool.name;
137
- // Use async lambda - Python doesn't have async lambdas natively,
138
- // so we define methods that return awaitable coroutines
139
- methodsDict.push(` "${methodName}": lambda args, n="${this.escapeString(fullName)}": _internal_call_tool(n, args)`);
156
+ // Accept both dict as first arg OR kwargs for flexibility
157
+ lines.push(` async def ${methodName}(self, args=None, **kwargs):`);
158
+ lines.push(` params = args if args is not None else kwargs`);
159
+ lines.push(` return await _internal_call_tool("${this.escapeString(fullName)}", params)`);
140
160
  }
161
+ lines.push('');
162
+ }
141
163
 
142
- lines.push(` self.${safeNamespace} = _ToolNamespace({`);
143
- lines.push(methodsDict.join(',\n'));
144
- lines.push(` })`);
164
+ // Generate the main Tools class
165
+ lines.push('class _Tools:');
166
+ lines.push(' def __init__(self):');
167
+ if (grouped.size === 0) {
168
+ lines.push(' pass');
169
+ } else {
170
+ for (const [namespace] of grouped.entries()) {
171
+ const safeNamespace = this.toSnakeCase(namespace);
172
+ lines.push(` self.${safeNamespace} = _${safeNamespace}_Namespace()`);
173
+ }
145
174
  }
146
175
 
147
- // Add raw escape hatch with allowlist validation
176
+ lines.push('');
177
+ lines.push(' def __getattr__(self, name):');
178
+ lines.push(' # Flat access fallback: search all namespaces');
179
+ lines.push(' for attr_name in dir(self):');
180
+ lines.push(' attr = getattr(self, attr_name, None)');
181
+ lines.push(' if attr and hasattr(attr, name):');
182
+ lines.push(' return getattr(attr, name)');
183
+ lines.push(' raise AttributeError(f"Namespace or Tool \'{name}\' not found")');
184
+
148
185
  if (enableRawFallback) {
149
186
  lines.push('');
150
- lines.push(' async def raw(self, name, args):');
187
+ lines.push(' async def raw(self, name, args=None):');
151
188
  lines.push(' """Call a tool by its full name (escape hatch for dynamic/unknown tools)"""');
152
189
  lines.push(' normalized = name.replace(".", "__")');
153
190
  lines.push(' if _allowed_tools is not None:');
@@ -157,7 +194,7 @@ export class SDKGenerator {
157
194
  lines.push(' )');
158
195
  lines.push(' if not allowed:');
159
196
  lines.push(' raise PermissionError(f"Tool {name} is not in the allowlist")');
160
- lines.push(' return await _internal_call_tool(normalized, args)');
197
+ lines.push(' return await _internal_call_tool(normalized, args or {})');
161
198
  }
162
199
 
163
200
  lines.push('');
@@ -187,7 +224,7 @@ export class SDKGenerator {
187
224
  lines.push('const __allowedTools = null;');
188
225
  }
189
226
 
190
- lines.push('const tools = {');
227
+ lines.push('const _tools = {');
191
228
 
192
229
  for (const [namespace, tools] of grouped.entries()) {
193
230
  const safeNamespace = this.isValidIdentifier(namespace) ? namespace : `["${this.escapeString(namespace)}"]`;
@@ -212,26 +249,49 @@ export class SDKGenerator {
212
249
  lines.push(` },`);
213
250
  }
214
251
 
215
- lines.push(` },`);
252
+ lines.push(' },');
216
253
  }
217
254
 
218
255
  // Add $raw escape hatch
219
256
  if (enableRawFallback) {
220
- lines.push(` async $raw(name, args) {`);
257
+ lines.push(' async $raw(name, args) {');
221
258
  lines.push(` const normalized = name.replace(/\\./g, '__');`);
222
- lines.push(` if (__allowedTools) {`);
259
+ lines.push(' if (__allowedTools) {');
223
260
  lines.push(` const allowed = __allowedTools.some(p => {`);
224
261
  lines.push(` if (p.endsWith('__*')) return normalized.startsWith(p.slice(0, -1));`);
225
- lines.push(` return normalized === p;`);
226
- lines.push(` });`);
227
- lines.push(` if (!allowed) throw new Error(\`Tool \${name} is not in the allowlist\`);`);
228
- lines.push(` }`);
229
- lines.push(` const resStr = await __callTool(normalized, JSON.stringify(args || {}));`);
230
- lines.push(` return JSON.parse(resStr);`);
231
- lines.push(` },`);
262
+ lines.push(' return normalized === p;');
263
+ lines.push(' });');
264
+ lines.push(' if (!allowed) throw new Error(`Tool ${name} is not in the allowlist`);');
265
+ lines.push(' }');
266
+ lines.push(' const resStr = await __callTool(normalized, JSON.stringify(args || {}));');
267
+ lines.push(' return JSON.parse(resStr);');
268
+ lines.push(' },');
232
269
  }
233
270
 
234
271
  lines.push('};');
272
+ lines.push(`
273
+ const tools = new Proxy(_tools, {
274
+ get: (target, prop) => {
275
+ if (prop in target) return target[prop];
276
+ if (prop === 'then') return undefined;
277
+ if (typeof prop === 'string') {
278
+ // Flat tool access fallback: search all namespaces for a matching tool
279
+ for (const nsName of Object.keys(target)) {
280
+ if (nsName === '$raw') continue;
281
+ const ns = target[nsName];
282
+ if (ns && typeof ns === 'object' && ns[prop]) {
283
+ return ns[prop];
284
+ }
285
+ }
286
+
287
+ const forbidden = ['\x24raw'];
288
+ const namespaces = Object.keys(target).filter(k => !forbidden.includes(k));
289
+ throw new Error(\`Namespace or Tool '\${prop}' not found. Available namespaces: \${namespaces.join(', ') || 'none'}. Use tools.$raw(name, args) for dynamic calls.\`);
290
+ }
291
+ return undefined;
292
+ }
293
+ });
294
+ `);
235
295
 
236
296
  return lines.join('\n');
237
297
  }
@@ -10,6 +10,7 @@ export class StdioTransport {
10
10
  private requestController: RequestController;
11
11
  private concurrencyService: ConcurrencyService;
12
12
  private buffer: string = '';
13
+ private pendingRequests = new Map<string | number, (response: any) => void>();
13
14
 
14
15
  constructor(
15
16
  logger: Logger,
@@ -33,6 +34,34 @@ export class StdioTransport {
33
34
  });
34
35
  }
35
36
 
37
+ async callHost(method: string, params: any): Promise<any> {
38
+ const id = Math.random().toString(36).substring(7);
39
+ const request = {
40
+ jsonrpc: '2.0',
41
+ id,
42
+ method,
43
+ params
44
+ };
45
+
46
+ return new Promise((resolve, reject) => {
47
+ const timeout = setTimeout(() => {
48
+ this.pendingRequests.delete(id);
49
+ reject(new Error(`Timeout waiting for host response to ${method}`));
50
+ }, 30000);
51
+
52
+ this.pendingRequests.set(id, (response) => {
53
+ clearTimeout(timeout);
54
+ if (response.error) {
55
+ reject(new Error(response.error.message));
56
+ } else {
57
+ resolve(response.result);
58
+ }
59
+ });
60
+
61
+ this.sendResponse(request);
62
+ });
63
+ }
64
+
36
65
  private handleData(chunk: string) {
37
66
  this.buffer += chunk;
38
67
 
@@ -48,11 +77,11 @@ export class StdioTransport {
48
77
  }
49
78
 
50
79
  private async processLine(line: string) {
51
- let request: JSONRPCRequest;
80
+ let message: any;
52
81
  try {
53
- request = JSON.parse(line) as JSONRPCRequest;
82
+ message = JSON.parse(line);
54
83
  } catch (err) {
55
- this.logger.error({ err, line }, 'Failed to parse JSON-RPC request');
84
+ this.logger.error({ err, line }, 'Failed to parse JSON-RPC message');
56
85
  const errorResponse = {
57
86
  jsonrpc: '2.0',
58
87
  id: null,
@@ -65,6 +94,18 @@ export class StdioTransport {
65
94
  return;
66
95
  }
67
96
 
97
+ // Handle Response
98
+ if (message.id !== undefined && (message.result !== undefined || message.error !== undefined)) {
99
+ const pending = this.pendingRequests.get(message.id);
100
+ if (pending) {
101
+ this.pendingRequests.delete(message.id);
102
+ pending(message);
103
+ return;
104
+ }
105
+ }
106
+
107
+ // Handle Request
108
+ const request = message as JSONRPCRequest;
68
109
  const context = new ExecutionContext({
69
110
  logger: this.logger,
70
111
  remoteAddress: 'stdio',
@@ -3,7 +3,7 @@
3
3
  exports[`Asset Integrity (Golden Tests) > should match Isolate SDK snapshot 1`] = `
4
4
  "// Generated SDK for isolated-vm
5
5
  const __allowedTools = ["test__*","github__*"];
6
- const tools = {
6
+ const _tools = {
7
7
  test: {
8
8
  async hello(args) {
9
9
  const resStr = await __callTool("test__hello", JSON.stringify(args || {}));
@@ -28,7 +28,30 @@ const tools = {
28
28
  const resStr = await __callTool(normalized, JSON.stringify(args || {}));
29
29
  return JSON.parse(resStr);
30
30
  },
31
- };"
31
+ };
32
+
33
+ const tools = new Proxy(_tools, {
34
+ get: (target, prop) => {
35
+ if (prop in target) return target[prop];
36
+ if (prop === 'then') return undefined;
37
+ if (typeof prop === 'string') {
38
+ // Flat tool access fallback: search all namespaces for a matching tool
39
+ for (const nsName of Object.keys(target)) {
40
+ if (nsName === '$raw') continue;
41
+ const ns = target[nsName];
42
+ if (ns && typeof ns === 'object' && ns[prop]) {
43
+ return ns[prop];
44
+ }
45
+ }
46
+
47
+ const forbidden = ['$raw'];
48
+ const namespaces = Object.keys(target).filter(k => !forbidden.includes(k));
49
+ throw new Error(\`Namespace or Tool '\${prop}' not found. Available namespaces: \${namespaces.join(', ') || 'none'}. Use tools.$raw(name, args) for dynamic calls.\`);
50
+ }
51
+ return undefined;
52
+ }
53
+ });
54
+ "
32
55
  `;
33
56
 
34
57
  exports[`Asset Integrity (Golden Tests) > should match Python SDK snapshot 1`] = `
@@ -49,6 +72,13 @@ class _Tools:
49
72
  "create_issue": lambda args, n="github__create_issue": _internal_call_tool(n, args)
50
73
  })
51
74
 
75
+ def __getattr__(self, name):
76
+ # Flat access fallback: search all namespaces
77
+ for ns in self.__dict__.values():
78
+ if isinstance(ns, _ToolNamespace) and hasattr(ns, name):
79
+ return getattr(ns, name)
80
+ raise AttributeError(f"Namespace or Tool '{name}' not found")
81
+
52
82
  async def raw(self, name, args):
53
83
  """Call a tool by its full name (escape hatch for dynamic/unknown tools)"""
54
84
  normalized = name.replace(".", "__")
@@ -67,7 +97,7 @@ tools = _Tools()"
67
97
  exports[`Asset Integrity (Golden Tests) > should match TypeScript SDK snapshot 1`] = `
68
98
  "// Generated SDK - Do not edit
69
99
  const __allowedTools = ["test__*","github__*"];
70
- const tools = {
100
+ const _tools = {
71
101
  test: {
72
102
  /** Returns a greeting */
73
103
  async hello(args) {
@@ -93,5 +123,28 @@ const tools = {
93
123
  return await __internalCallTool(normalized, args);
94
124
  },
95
125
  };
126
+
127
+ const tools = new Proxy(_tools, {
128
+ get: (target, prop) => {
129
+ if (prop in target) return target[prop];
130
+ if (prop === 'then') return undefined;
131
+ if (typeof prop === 'string') {
132
+ // Flat tool access fallback: search all namespaces for a matching tool
133
+ for (const nsName of Object.keys(target)) {
134
+ if (nsName === '$raw') continue;
135
+ const ns = target[nsName];
136
+ if (ns && typeof ns === 'object' && ns[prop]) {
137
+ return ns[prop];
138
+ }
139
+ }
140
+
141
+ const forbidden = ['$raw'];
142
+ const namespaces = Object.keys(target).filter(k => !forbidden.includes(k));
143
+ throw new Error(\`Namespace or Tool '\${prop}' not found. Available namespaces: \${namespaces.join(', ') || 'none'}. Use tools.$raw(name, args) for dynamic calls.\`);
144
+ }
145
+ return undefined;
146
+ }
147
+ });
148
+
96
149
  (globalThis as any).tools = tools;"
97
150
  `;
@@ -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
 
@@ -0,0 +1,40 @@
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
+ });
@@ -0,0 +1,69 @@
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
+ });
@@ -44,10 +44,10 @@ describe('Dynamic Tool Calling (E2E)', () => {
44
44
  // Register a mock upstream tool
45
45
  (gatewayService as any).clients.set('mock', {
46
46
  call: vi.fn().mockImplementation((req) => {
47
- if (req.method === 'call_tool' && req.params.name === 'hello') {
47
+ if (req.method === 'tools/call' && req.params.name === 'hello') {
48
48
  return { jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text: `Hello ${req.params.arguments.name}` }] } };
49
49
  }
50
- if (req.method === 'list_tools') {
50
+ if (req.method === 'tools/list') {
51
51
  return { jsonrpc: '2.0', id: req.id, result: { tools: [{ name: 'hello', inputSchema: {} }] } };
52
52
  }
53
53
  return { jsonrpc: '2.0', id: req.id, result: {} };
@@ -227,7 +227,7 @@ print(f"RESULT:{result}")
227
227
  const request = callArgs[0];
228
228
 
229
229
  expect(request).toMatchObject({
230
- method: 'call_tool',
230
+ method: 'tools/call',
231
231
  params: {
232
232
  name: 'hello',
233
233
  arguments: { name: 'Isolate' }
@@ -56,7 +56,7 @@ describe('GatewayService (Manifests)', () => {
56
56
  const stubs = await gateway.listToolStubs('test-upstream', context);
57
57
 
58
58
  expect(mockClient.getManifest).toHaveBeenCalled();
59
- expect(mockClient.call).toHaveBeenCalledWith(expect.objectContaining({ method: 'list_tools' }), context);
59
+ expect(mockClient.call).toHaveBeenCalledWith(expect.objectContaining({ method: 'tools/list' }), context);
60
60
  expect(stubs).toHaveLength(1);
61
61
  expect(stubs[0].id).toBe('test-upstream__tool_rpc');
62
62
  });
@@ -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
 
@@ -58,7 +58,7 @@ describe('GatewayService', () => {
58
58
  expect(axios.post).toHaveBeenCalledWith(
59
59
  'http://u1',
60
60
  expect.objectContaining({
61
- method: 'call_tool',
61
+ method: 'tools/call',
62
62
  params: expect.objectContaining({ name: 't1' })
63
63
  }),
64
64
  expect.anything()
@@ -5,7 +5,7 @@ const server = Fastify();
5
5
  server.post('/', async (request, reply) => {
6
6
  const { method, params, id } = request.body as any;
7
7
 
8
- if (method === 'list_tools') {
8
+ if (method === 'list_tools' || method === 'tools/list') {
9
9
  return {
10
10
  jsonrpc: '2.0',
11
11
  id,
@@ -21,12 +21,14 @@ server.post('/', async (request, reply) => {
21
21
  };
22
22
  }
23
23
 
24
- if (method === 'call_tool') {
24
+ if (method === 'call_tool' || method === 'tools/call') {
25
+ const toolName = params.name || '';
26
+ const args = params.arguments || {};
25
27
  return {
26
28
  jsonrpc: '2.0',
27
29
  id,
28
30
  result: {
29
- content: [{ type: 'text', text: `Echo: ${JSON.stringify(params.arguments)}` }]
31
+ content: [{ type: 'text', text: `Echo: ${JSON.stringify(args)}` }]
30
32
  }
31
33
  };
32
34
  }
@@ -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 }),
@@ -162,7 +169,7 @@ describe('RequestController Routing', () => {
162
169
  }, mockContext);
163
170
 
164
171
  expect(mockIsolateExecutor.execute).toHaveBeenCalled();
165
- expect(result!.result.stdout).toBe('isolate');
172
+ expect(result!.result.structuredContent.stdout).toBe('isolate');
166
173
  });
167
174
 
168
175
  });
@@ -15,12 +15,13 @@ describe('SDKGenerator', () => {
15
15
 
16
16
  const code = generator.generateTypeScript(bindings);
17
17
 
18
- expect(code).toContain('const tools = {');
18
+ expect(code).toContain('const _tools = {');
19
19
  expect(code).toContain('github: {');
20
20
  expect(code).toContain('async createIssue(args)');
21
21
  expect(code).toContain('await __internalCallTool("github__createIssue", args)');
22
22
  expect(code).toContain('slack: {');
23
23
  expect(code).toContain('async sendMessage(args)');
24
+ expect(code).toContain('const tools = new Proxy(_tools');
24
25
  expect(code).toContain('(globalThis as any).tools = tools');
25
26
  });
26
27
 
@@ -42,7 +43,7 @@ describe('SDKGenerator', () => {
42
43
 
43
44
  const code = generator.generateTypeScript(bindings, undefined, false);
44
45
 
45
- expect(code).not.toContain('$raw');
46
+ expect(code).not.toContain('async $raw(name, args)');
46
47
  });
47
48
 
48
49
  it('should include JSDoc comments from descriptions', () => {
@@ -58,7 +59,7 @@ describe('SDKGenerator', () => {
58
59
  it('should handle empty bindings', () => {
59
60
  const code = generator.generateTypeScript([]);
60
61
 
61
- expect(code).toContain('const tools = {');
62
+ expect(code).toContain('const _tools = {');
62
63
  expect(code).toContain('async $raw(name, args)');
63
64
  });
64
65
  });
@@ -73,10 +74,10 @@ describe('SDKGenerator', () => {
73
74
  const code = generator.generatePython(bindings);
74
75
 
75
76
  expect(code).toContain('class _Tools:');
76
- expect(code).toContain('self.github = _ToolNamespace');
77
- expect(code).toContain('"create_issue"'); // snake_case conversion
78
- expect(code).toContain('self.slack = _ToolNamespace');
79
- 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
80
81
  expect(code).toContain('tools = _Tools()');
81
82
  });
82
83
 
@@ -87,8 +88,8 @@ describe('SDKGenerator', () => {
87
88
 
88
89
  const code = generator.generatePython(bindings);
89
90
 
90
- expect(code).toContain('async def raw(self, name, args)');
91
- 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 {})');
92
93
  });
93
94
 
94
95
  it('should inject allowlist when provided', () => {