@mhingston5/conduit 1.1.5 → 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.
- package/dist/index.js +266 -96
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/config.service.ts +5 -1
- package/src/core/execution.service.ts +5 -0
- package/src/core/policy.service.ts +5 -0
- package/src/core/request.controller.ts +32 -7
- package/src/gateway/gateway.service.ts +142 -65
- package/src/gateway/host.client.ts +65 -0
- package/src/gateway/upstream.client.ts +10 -11
- package/src/index.ts +13 -4
- package/src/sdk/sdk-generator.ts +66 -30
- package/src/transport/stdio.transport.ts +44 -3
- package/tests/__snapshots__/assets.test.ts.snap +31 -2
- package/tests/code-mode-lite-gateway.test.ts +4 -4
- package/tests/debug.fallback.test.ts +40 -0
- package/tests/debug_upstream.ts +69 -0
- package/tests/gateway.service.test.ts +5 -5
- package/tests/routing.test.ts +7 -0
- package/tests/sdk/sdk-generator.test.ts +7 -7
package/src/sdk/sdk-generator.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
/**
|
|
@@ -98,7 +100,18 @@ const tools = new Proxy(_tools, {
|
|
|
98
100
|
if (prop in target) return target[prop];
|
|
99
101
|
if (prop === 'then') return undefined;
|
|
100
102
|
if (typeof prop === 'string') {
|
|
101
|
-
|
|
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.\`);
|
|
102
115
|
}
|
|
103
116
|
return undefined;
|
|
104
117
|
}
|
|
@@ -131,35 +144,47 @@ const tools = new Proxy(_tools, {
|
|
|
131
144
|
}
|
|
132
145
|
|
|
133
146
|
lines.push('');
|
|
134
|
-
lines.push('class _ToolNamespace:');
|
|
135
|
-
lines.push(' def __init__(self, methods):');
|
|
136
|
-
lines.push(' for name, fn in methods.items():');
|
|
137
|
-
lines.push(' setattr(self, name, fn)');
|
|
138
|
-
lines.push('');
|
|
139
|
-
lines.push('class _Tools:');
|
|
140
|
-
lines.push(' def __init__(self):');
|
|
141
147
|
|
|
148
|
+
// Generate namespace classes
|
|
142
149
|
for (const [namespace, tools] of grouped.entries()) {
|
|
143
150
|
const safeNamespace = this.toSnakeCase(namespace);
|
|
144
|
-
|
|
151
|
+
lines.push(`class _${safeNamespace}_Namespace:`);
|
|
145
152
|
|
|
146
153
|
for (const tool of tools) {
|
|
147
154
|
const methodName = this.toSnakeCase(tool.methodName);
|
|
148
155
|
const fullName = tool.name;
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
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)`);
|
|
152
160
|
}
|
|
161
|
+
lines.push('');
|
|
162
|
+
}
|
|
153
163
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
+
}
|
|
157
174
|
}
|
|
158
175
|
|
|
159
|
-
|
|
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
|
+
|
|
160
185
|
if (enableRawFallback) {
|
|
161
186
|
lines.push('');
|
|
162
|
-
lines.push(' async def raw(self, name, args):');
|
|
187
|
+
lines.push(' async def raw(self, name, args=None):');
|
|
163
188
|
lines.push(' """Call a tool by its full name (escape hatch for dynamic/unknown tools)"""');
|
|
164
189
|
lines.push(' normalized = name.replace(".", "__")');
|
|
165
190
|
lines.push(' if _allowed_tools is not None:');
|
|
@@ -169,7 +194,7 @@ const tools = new Proxy(_tools, {
|
|
|
169
194
|
lines.push(' )');
|
|
170
195
|
lines.push(' if not allowed:');
|
|
171
196
|
lines.push(' raise PermissionError(f"Tool {name} is not in the allowlist")');
|
|
172
|
-
lines.push(' return await _internal_call_tool(normalized, args)');
|
|
197
|
+
lines.push(' return await _internal_call_tool(normalized, args or {})');
|
|
173
198
|
}
|
|
174
199
|
|
|
175
200
|
lines.push('');
|
|
@@ -224,23 +249,23 @@ const tools = new Proxy(_tools, {
|
|
|
224
249
|
lines.push(` },`);
|
|
225
250
|
}
|
|
226
251
|
|
|
227
|
-
lines.push(
|
|
252
|
+
lines.push(' },');
|
|
228
253
|
}
|
|
229
254
|
|
|
230
255
|
// Add $raw escape hatch
|
|
231
256
|
if (enableRawFallback) {
|
|
232
|
-
lines.push(
|
|
257
|
+
lines.push(' async $raw(name, args) {');
|
|
233
258
|
lines.push(` const normalized = name.replace(/\\./g, '__');`);
|
|
234
|
-
lines.push(
|
|
259
|
+
lines.push(' if (__allowedTools) {');
|
|
235
260
|
lines.push(` const allowed = __allowedTools.some(p => {`);
|
|
236
261
|
lines.push(` if (p.endsWith('__*')) return normalized.startsWith(p.slice(0, -1));`);
|
|
237
|
-
lines.push(
|
|
238
|
-
lines.push(
|
|
239
|
-
lines.push(
|
|
240
|
-
lines.push(
|
|
241
|
-
lines.push(
|
|
242
|
-
lines.push(
|
|
243
|
-
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(' },');
|
|
244
269
|
}
|
|
245
270
|
|
|
246
271
|
lines.push('};');
|
|
@@ -250,7 +275,18 @@ const tools = new Proxy(_tools, {
|
|
|
250
275
|
if (prop in target) return target[prop];
|
|
251
276
|
if (prop === 'then') return undefined;
|
|
252
277
|
if (typeof prop === 'string') {
|
|
253
|
-
|
|
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.\`);
|
|
254
290
|
}
|
|
255
291
|
return undefined;
|
|
256
292
|
}
|
|
@@ -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
|
|
80
|
+
let message: any;
|
|
52
81
|
try {
|
|
53
|
-
|
|
82
|
+
message = JSON.parse(line);
|
|
54
83
|
} catch (err) {
|
|
55
|
-
this.logger.error({ err, line }, 'Failed to parse JSON-RPC
|
|
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',
|
|
@@ -35,7 +35,18 @@ const tools = new Proxy(_tools, {
|
|
|
35
35
|
if (prop in target) return target[prop];
|
|
36
36
|
if (prop === 'then') return undefined;
|
|
37
37
|
if (typeof prop === 'string') {
|
|
38
|
-
|
|
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.\`);
|
|
39
50
|
}
|
|
40
51
|
return undefined;
|
|
41
52
|
}
|
|
@@ -61,6 +72,13 @@ class _Tools:
|
|
|
61
72
|
"create_issue": lambda args, n="github__create_issue": _internal_call_tool(n, args)
|
|
62
73
|
})
|
|
63
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
|
+
|
|
64
82
|
async def raw(self, name, args):
|
|
65
83
|
"""Call a tool by its full name (escape hatch for dynamic/unknown tools)"""
|
|
66
84
|
normalized = name.replace(".", "__")
|
|
@@ -111,7 +129,18 @@ const tools = new Proxy(_tools, {
|
|
|
111
129
|
if (prop in target) return target[prop];
|
|
112
130
|
if (prop === 'then') return undefined;
|
|
113
131
|
if (typeof prop === 'string') {
|
|
114
|
-
|
|
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.\`);
|
|
115
144
|
}
|
|
116
145
|
return undefined;
|
|
117
146
|
}
|
|
@@ -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(
|
|
42
|
-
expect(packages
|
|
43
|
-
expect(packages
|
|
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
|
+
});
|
|
@@ -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 === '
|
|
36
|
-
expect(tools.find(t => t.name === '
|
|
37
|
-
expect(tools.find(t => t.name === '
|
|
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('
|
|
43
|
+
const schema = await gateway.getToolSchema('conduit__mcp_execute_typescript', context);
|
|
44
44
|
expect(schema).toBeDefined();
|
|
45
|
-
expect(schema?.name).toBe('
|
|
45
|
+
expect(schema?.name).toBe('conduit__mcp_execute_typescript');
|
|
46
46
|
expect(schema?.inputSchema.required).toContain('code');
|
|
47
47
|
});
|
|
48
48
|
|
package/tests/routing.test.ts
CHANGED
|
@@ -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 =
|
|
78
|
-
expect(code).toContain('
|
|
79
|
-
expect(code).toContain('self.slack =
|
|
80
|
-
expect(code).toContain('
|
|
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', () => {
|