@mhingston5/conduit 1.1.4 → 1.1.5

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/executors/pyodide.worker.ts"],"sourcesContent":["import { parentPort, workerData } from 'node:worker_threads';\nimport { loadPyodide, type PyodideInterface } from 'pyodide';\nimport net from 'node:net';\n\nlet pyodide: PyodideInterface | null = null;\nlet currentStdout = '';\nlet currentStderr = '';\nlet totalOutputBytes = 0;\nlet totalLogEntries = 0;\nlet currentLimits: any = null;\n\nasync function init() {\n if (pyodide) return pyodide;\n\n pyodide = await loadPyodide({\n stdout: (text) => {\n if (currentLimits && (totalOutputBytes > (currentLimits.maxOutputBytes || 1024 * 1024) || totalLogEntries > (currentLimits.maxLogEntries || 10000))) {\n return; // Stop processing logs once limit breached\n }\n currentStdout += text + '\\n';\n totalOutputBytes += text.length + 1;\n totalLogEntries++;\n },\n stderr: (text) => {\n if (currentLimits && (totalOutputBytes > (currentLimits.maxOutputBytes || 1024 * 1024) || totalLogEntries > (currentLimits.maxLogEntries || 10000))) {\n return; // Stop processing logs once limit breached\n }\n currentStderr += text + '\\n';\n totalOutputBytes += text.length + 1;\n totalLogEntries++;\n },\n });\n\n return pyodide;\n}\n\nasync function handleTask(data: any) {\n const { code, limits, ipcInfo, shim } = data;\n currentStdout = '';\n currentStderr = '';\n totalOutputBytes = 0;\n totalLogEntries = 0;\n currentLimits = limits;\n\n try {\n const p = await init();\n\n const sendIPCRequest = async (method: string, params: any) => {\n if (!ipcInfo?.ipcAddress) throw new Error('Conduit IPC address not configured');\n\n return new Promise((resolve, reject) => {\n let client: net.Socket;\n\n if (ipcInfo.ipcAddress.includes(':')) {\n const lastColon = ipcInfo.ipcAddress.lastIndexOf(':');\n const host = ipcInfo.ipcAddress.substring(0, lastColon);\n const port = ipcInfo.ipcAddress.substring(lastColon + 1);\n\n let targetHost = host.replace(/[\\[\\]]/g, '');\n if (targetHost === '0.0.0.0' || targetHost === '::' || targetHost === '::1' || targetHost === '') {\n targetHost = '127.0.0.1';\n }\n\n client = net.createConnection({\n host: targetHost,\n port: parseInt(port)\n });\n } else {\n client = net.createConnection({ path: ipcInfo.ipcAddress });\n }\n\n const id = Math.random().toString(36).substring(7);\n const request = {\n jsonrpc: '2.0',\n id,\n method,\n params: params || {},\n auth: { bearerToken: ipcInfo.ipcToken }\n };\n\n client.on('error', (err) => {\n reject(err);\n client.destroy();\n });\n\n client.write(JSON.stringify(request) + '\\n');\n\n let buffer = '';\n client.on('data', (data) => {\n buffer += data.toString();\n // Robust framing: read until we find a complete JSON object on a line\n const lines = buffer.split('\\n');\n buffer = lines.pop() || ''; // Keep the last partial line\n\n for (const line of lines) {\n if (!line.trim()) continue;\n try {\n const response = JSON.parse(line);\n if (response.id === id) {\n if (response.error) {\n reject(new Error(response.error.message));\n } else {\n resolve(response.result);\n }\n client.end();\n return;\n }\n } catch (e) {\n // If parse fails, it might be a partial line that we haven't seen the end of yet\n // but since we split by \\n, this shouldn't happen unless the \\n was inside the JSON.\n // However, Conduit ensures JSON-RPC is one line.\n }\n }\n });\n\n client.on('end', () => {\n if (buffer.trim()) {\n try {\n const response = JSON.parse(buffer);\n if (response.id === id) {\n if (response.error) {\n reject(new Error(response.error.message));\n } else {\n resolve(response.result);\n }\n }\n } catch (e) { }\n }\n });\n });\n };\n\n (p as any).globals.set('discover_mcp_tools_js', (options: any) => {\n return sendIPCRequest('mcp_discover_tools', options);\n });\n\n (p as any).globals.set('call_mcp_tool_js', (name: string, args: any) => {\n return sendIPCRequest('mcp_call_tool', { name, arguments: args });\n });\n\n if (shim) {\n await p.runPythonAsync(shim);\n }\n\n const result = await p.runPythonAsync(code);\n\n if (totalOutputBytes > (limits.maxOutputBytes || 1024 * 1024)) {\n throw new Error('[LIMIT_OUTPUT]');\n }\n if (totalLogEntries > (limits.maxLogEntries || 10000)) {\n throw new Error('[LIMIT_LOG]');\n }\n\n parentPort?.postMessage({\n stdout: currentStdout,\n stderr: currentStderr,\n result: String(result),\n success: true,\n });\n } catch (err: any) {\n let isOutput = err.message.includes('[LIMIT_OUTPUT]');\n let isLog = err.message.includes('[LIMIT_LOG]');\n\n // Fallback: check counters if message doesn't match (e.g. wrapped in OSError)\n if (!isOutput && !isLog && currentLimits) {\n if (totalOutputBytes > (currentLimits.maxOutputBytes || 1024 * 1024)) {\n isOutput = true;\n }\n // Check specific log limit breach\n if (totalLogEntries > (currentLimits.maxLogEntries || 10000)) {\n isLog = true;\n }\n }\n\n parentPort?.postMessage({\n stdout: currentStdout,\n stderr: currentStderr,\n error: err.message,\n limitBreached: isOutput ? 'output' : (isLog ? 'log' : undefined),\n success: false,\n });\n }\n}\n\nparentPort?.on('message', async (msg) => {\n if (msg.type === 'execute') {\n await handleTask(msg.data);\n } else if (msg.type === 'ping') {\n parentPort?.postMessage({ type: 'pong' });\n }\n});\n\n// Signal ready\nparentPort?.postMessage({ type: 'ready' });\n\n"],"mappings":"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","names":["data"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhingston5/conduit",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "type": "module",
5
5
  "description": "A secure Code Mode execution substrate for MCP agents",
6
6
  "main": "index.js",
package/src/auth.cmd.ts CHANGED
@@ -2,22 +2,143 @@ import Fastify from 'fastify';
2
2
  import axios from 'axios';
3
3
  import open from 'open';
4
4
  import { v4 as uuidv4 } from 'uuid';
5
+ import crypto from 'node:crypto';
5
6
 
6
7
  export interface AuthOptions {
7
- authUrl: string;
8
- tokenUrl: string;
8
+ authUrl?: string;
9
+ tokenUrl?: string;
9
10
  clientId: string;
10
- clientSecret: string;
11
+ clientSecret?: string;
11
12
  scopes?: string;
12
13
  port?: number;
14
+ mcpUrl?: string;
15
+ usePkce?: boolean;
16
+ }
17
+
18
+ type DiscoveredOAuth = {
19
+ authUrl: string;
20
+ tokenUrl: string;
21
+ scopes?: string[];
22
+ resource?: string;
23
+ };
24
+
25
+ const AUTH_REQUEST_PAYLOAD = {
26
+ jsonrpc: '2.0',
27
+ id: 'conduit-auth',
28
+ method: 'initialize',
29
+ params: {
30
+ clientInfo: {
31
+ name: 'conduit-auth',
32
+ version: '1.0.0',
33
+ },
34
+ },
35
+ };
36
+
37
+ function base64UrlEncode(buffer: Buffer): string {
38
+ return buffer
39
+ .toString('base64')
40
+ .replace(/\+/g, '-')
41
+ .replace(/\//g, '_')
42
+ .replace(/=+$/g, '');
43
+ }
44
+
45
+ function createCodeVerifier(): string {
46
+ return base64UrlEncode(crypto.randomBytes(32));
47
+ }
48
+
49
+ function createCodeChallenge(verifier: string): string {
50
+ return base64UrlEncode(crypto.createHash('sha256').update(verifier).digest());
51
+ }
52
+
53
+ function parseResourceMetadataHeader(headerValue: string | string[] | undefined): string | null {
54
+ if (!headerValue) return null;
55
+ const header = Array.isArray(headerValue) ? headerValue.join(',') : headerValue;
56
+ const match = header.match(/resource_metadata="([^"]+)"/i) || header.match(/resource_metadata=([^, ]+)/i);
57
+ return match ? match[1] : null;
58
+ }
59
+
60
+ async function discoverOAuthFromMcp(mcpUrl: string): Promise<DiscoveredOAuth> {
61
+ const attempts = [
62
+ () => axios.get(mcpUrl, { validateStatus: () => true }),
63
+ () => axios.post(mcpUrl, AUTH_REQUEST_PAYLOAD, { validateStatus: () => true }),
64
+ ];
65
+
66
+ let resourceMetadataUrl: string | null = null;
67
+ for (const attempt of attempts) {
68
+ const response = await attempt();
69
+ resourceMetadataUrl = parseResourceMetadataHeader(response.headers['www-authenticate']);
70
+ if (resourceMetadataUrl) break;
71
+ }
72
+
73
+ if (!resourceMetadataUrl) {
74
+ throw new Error('Unable to discover OAuth metadata (missing WWW-Authenticate resource_metadata)');
75
+ }
76
+
77
+ const metadataResponse = await axios.get(resourceMetadataUrl);
78
+ const metadata = metadataResponse.data as Record<string, any>;
79
+
80
+ let authUrl = metadata.authorization_endpoint as string | undefined;
81
+ let tokenUrl = metadata.token_endpoint as string | undefined;
82
+ let scopes = Array.isArray(metadata.scopes_supported) ? metadata.scopes_supported : undefined;
83
+ const resource = typeof metadata.resource === 'string' ? metadata.resource : undefined;
84
+
85
+ if (!authUrl || !tokenUrl) {
86
+ const authServer = (Array.isArray(metadata.authorization_servers) && metadata.authorization_servers[0]) || metadata.issuer;
87
+ if (!authServer) {
88
+ throw new Error('OAuth metadata did not include authorization server info');
89
+ }
90
+
91
+ const asMetadataUrl = new URL('/.well-known/oauth-authorization-server', authServer).toString();
92
+ const asMetadataResponse = await axios.get(asMetadataUrl);
93
+ const asMetadata = asMetadataResponse.data as Record<string, any>;
94
+
95
+ authUrl = authUrl || (asMetadata.authorization_endpoint as string | undefined);
96
+ tokenUrl = tokenUrl || (asMetadata.token_endpoint as string | undefined);
97
+ scopes = scopes || (Array.isArray(asMetadata.scopes_supported) ? asMetadata.scopes_supported : undefined);
98
+ }
99
+
100
+ if (!authUrl || !tokenUrl) {
101
+ throw new Error('OAuth discovery failed: missing authorization or token endpoint');
102
+ }
103
+
104
+ return { authUrl, tokenUrl, scopes, resource };
105
+ }
106
+
107
+ function normalizeScopes(rawScopes?: string): string | undefined {
108
+ if (!rawScopes) return undefined;
109
+ return rawScopes
110
+ .split(',')
111
+ .map(scope => scope.trim())
112
+ .filter(Boolean)
113
+ .join(' ');
13
114
  }
14
115
 
15
116
  export async function handleAuth(options: AuthOptions) {
16
117
  const port = options.port || 3333;
17
118
  const redirectUri = `http://localhost:${port}/callback`;
18
119
  const state = uuidv4();
120
+ const codeVerifier = options.usePkce ? createCodeVerifier() : undefined;
121
+ const codeChallenge = codeVerifier ? createCodeChallenge(codeVerifier) : undefined;
19
122
 
20
123
  const fastify = Fastify();
124
+ let resolvedScopes = normalizeScopes(options.scopes);
125
+ let resolvedAuthUrl = options.authUrl;
126
+ let resolvedTokenUrl = options.tokenUrl;
127
+ let resolvedResource: string | undefined;
128
+
129
+ if (options.mcpUrl) {
130
+ const discovered = await discoverOAuthFromMcp(options.mcpUrl);
131
+ resolvedAuthUrl = discovered.authUrl;
132
+ resolvedTokenUrl = discovered.tokenUrl;
133
+ resolvedResource = discovered.resource;
134
+ if (!resolvedScopes && discovered.scopes && discovered.scopes.length > 0) {
135
+ resolvedScopes = discovered.scopes.join(' ');
136
+ }
137
+ }
138
+
139
+ if (!resolvedAuthUrl || !resolvedTokenUrl) {
140
+ throw new Error('OAuth configuration missing authUrl or tokenUrl (set --mcp-url or provide both)');
141
+ }
21
142
 
22
143
  return new Promise<void>((resolve, reject) => {
23
144
  fastify.get('/callback', async (request, reply) => {
@@ -36,12 +157,26 @@ export async function handleAuth(options: AuthOptions) {
36
157
  }
37
158
 
38
159
  try {
39
- const response = await axios.post(options.tokenUrl, {
40
- grant_type: 'authorization_code',
41
- code,
42
- redirect_uri: redirectUri,
43
- client_id: options.clientId,
44
- client_secret: options.clientSecret,
160
+ const body = new URLSearchParams();
161
+ body.set('grant_type', 'authorization_code');
162
+ body.set('code', code);
163
+ body.set('redirect_uri', redirectUri);
164
+ body.set('client_id', options.clientId);
165
+ if (options.clientSecret) {
166
+ body.set('client_secret', options.clientSecret);
167
+ }
168
+ if (codeVerifier) {
169
+ body.set('code_verifier', codeVerifier);
170
+ }
171
+ if (resolvedResource) {
172
+ body.set('resource', resolvedResource);
173
+ }
174
+
175
+ const response = await axios.post(resolvedTokenUrl, body, {
176
+ headers: {
177
+ 'Content-Type': 'application/x-www-form-urlencoded',
178
+ 'Accept': 'application/json',
179
+ },
45
180
  });
46
181
 
47
182
  const { refresh_token, access_token } = response.data;
@@ -51,9 +186,14 @@ export async function handleAuth(options: AuthOptions) {
51
186
  console.log('credentials:');
52
187
  console.log(' type: oauth2');
53
188
  console.log(` clientId: ${options.clientId}`);
54
- console.log(` clientSecret: ${options.clientSecret}`);
55
- console.log(` tokenUrl: "${options.tokenUrl}"`);
189
+ if (options.clientSecret) {
190
+ console.log(` clientSecret: ${options.clientSecret}`);
191
+ }
192
+ console.log(` tokenUrl: "${resolvedTokenUrl}"`);
56
193
  console.log(` refreshToken: "${refresh_token || 'N/A (No refresh token returned)'}"`);
194
+ if (resolvedScopes) {
195
+ console.log(` scopes: ["${resolvedScopes.split(' ').join('", "')}"]`);
196
+ }
57
197
 
58
198
  if (!refresh_token) {
59
199
  console.log('\nWarning: No refresh token was returned. Ensure your app has "offline_access" scope or similar.');
@@ -78,13 +218,20 @@ export async function handleAuth(options: AuthOptions) {
78
218
  return;
79
219
  }
80
220
 
81
- const authUrl = new URL(options.authUrl);
221
+ const authUrl = new URL(resolvedAuthUrl);
82
222
  authUrl.searchParams.append('client_id', options.clientId);
83
223
  authUrl.searchParams.append('redirect_uri', redirectUri);
84
224
  authUrl.searchParams.append('response_type', 'code');
85
225
  authUrl.searchParams.append('state', state);
86
- if (options.scopes) {
87
- authUrl.searchParams.append('scope', options.scopes);
226
+ if (resolvedScopes) {
227
+ authUrl.searchParams.append('scope', resolvedScopes);
228
+ }
229
+ if (codeChallenge) {
230
+ authUrl.searchParams.append('code_challenge', codeChallenge);
231
+ authUrl.searchParams.append('code_challenge_method', 'S256');
232
+ }
233
+ if (resolvedResource) {
234
+ authUrl.searchParams.append('resource', resolvedResource);
88
235
  }
89
236
 
90
237
  console.log(`Opening browser to: ${authUrl.toString()}`);
@@ -104,6 +104,9 @@ export class RequestController {
104
104
  case 'tools/list': // Standard MCP method name
105
105
  case 'mcp_discover_tools':
106
106
  return this.handleDiscoverTools(params, context, id);
107
+ case 'resources/list':
108
+ case 'prompts/list':
109
+ return { jsonrpc: '2.0', id, result: { items: [] } };
107
110
  case 'mcp_list_tool_packages':
108
111
  return this.handleListToolPackages(params, context, id);
109
112
  case 'mcp_list_tool_stubs':
@@ -215,17 +218,62 @@ export class RequestController {
215
218
  // Route built-in tools to their specific handlers
216
219
  switch (name) {
217
220
  case 'mcp_execute_typescript':
218
- return this.handleExecuteTypeScript(toolArgs, context, id);
221
+ return this.handleExecuteToolCall('typescript', toolArgs, context, id);
219
222
  case 'mcp_execute_python':
220
- return this.handleExecutePython(toolArgs, context, id);
223
+ return this.handleExecuteToolCall('python', toolArgs, context, id);
221
224
  case 'mcp_execute_isolate':
222
- return this.handleExecuteIsolate(toolArgs, context, id);
225
+ return this.handleExecuteToolCall('isolate', toolArgs, context, id);
223
226
  }
224
227
 
225
228
  const response = await this.gatewayService.callTool(name, toolArgs, context);
226
229
  return { ...response, id };
227
230
  }
228
231
 
232
+ private formatExecutionResult(result: { stdout: string; stderr: string; exitCode: number | null }) {
233
+ const structured = {
234
+ stdout: result.stdout,
235
+ stderr: result.stderr,
236
+ exitCode: result.exitCode,
237
+ };
238
+ return {
239
+ content: [{
240
+ type: 'text',
241
+ text: JSON.stringify(structured),
242
+ }],
243
+ structuredContent: structured,
244
+ };
245
+ }
246
+
247
+ private async handleExecuteToolCall(
248
+ mode: 'typescript' | 'python' | 'isolate',
249
+ params: any,
250
+ context: ExecutionContext,
251
+ id: string | number
252
+ ): Promise<JSONRPCResponse> {
253
+ if (!params) return this.errorResponse(id, -32602, 'Missing parameters');
254
+ const { code, limits, allowedTools } = params;
255
+
256
+ if (Array.isArray(allowedTools)) {
257
+ context.allowedTools = allowedTools;
258
+ }
259
+
260
+ const result = mode === 'typescript'
261
+ ? await this.executionService.executeTypeScript(code, limits, context, allowedTools)
262
+ : mode === 'python'
263
+ ? await this.executionService.executePython(code, limits, context, allowedTools)
264
+ : await this.executionService.executeIsolate(code, limits, context, allowedTools);
265
+
266
+ if (result.error) {
267
+ return this.errorResponse(id, result.error.code, result.error.message);
268
+ }
269
+
270
+ return {
271
+ jsonrpc: '2.0',
272
+ id,
273
+ result: this.formatExecutionResult(result),
274
+ };
275
+ }
276
+
229
277
  private async handleExecuteTypeScript(params: any, context: ExecutionContext, id: string | number): Promise<JSONRPCResponse> {
230
278
  if (!params) return this.errorResponse(id, -32602, 'Missing parameters');
231
279
  const { code, limits, allowedTools } = params;
@@ -94,9 +94,15 @@ export class PyodideExecutor implements Executor {
94
94
  }
95
95
 
96
96
  private createWorker(limits?: ConduitResourceLimits): Worker {
97
- let workerPath = path.resolve(__dirname, './pyodide.worker.js');
98
- if (!fs.existsSync(workerPath)) {
99
- workerPath = path.resolve(__dirname, './pyodide.worker.ts');
97
+ const candidates = [
98
+ path.resolve(__dirname, './pyodide.worker.js'),
99
+ path.resolve(__dirname, './pyodide.worker.ts'),
100
+ path.resolve(__dirname, './executors/pyodide.worker.js'),
101
+ path.resolve(__dirname, './executors/pyodide.worker.ts'),
102
+ ];
103
+ const workerPath = candidates.find(p => fs.existsSync(p));
104
+ if (!workerPath) {
105
+ throw new Error(`Pyodide worker not found. Tried: ${candidates.join(', ')}`);
100
106
  }
101
107
 
102
108
  return new Worker(workerPath, {
@@ -324,4 +330,3 @@ export class PyodideExecutor implements Executor {
324
330
  }
325
331
  }
326
332
  }
327
-
@@ -11,6 +11,7 @@ export interface UpstreamCredentials {
11
11
  clientSecret?: string;
12
12
  tokenUrl?: string;
13
13
  refreshToken?: string;
14
+ scopes?: string[];
14
15
  }
15
16
 
16
17
  interface CachedToken {
@@ -73,26 +74,35 @@ export class AuthService {
73
74
  }
74
75
 
75
76
  private async doRefresh(creds: UpstreamCredentials, cacheKey: string): Promise<string> {
76
- if (!creds.tokenUrl || !creds.refreshToken || !creds.clientId || !creds.clientSecret) {
77
+ if (!creds.tokenUrl || !creds.refreshToken || !creds.clientId) {
77
78
  throw new Error('OAuth2 credentials missing required fields for refresh');
78
79
  }
79
80
 
80
81
  this.logger.info({ tokenUrl: creds.tokenUrl, clientId: creds.clientId }, 'Refreshing OAuth2 token');
81
82
 
82
83
  try {
83
- const response = await axios.post(creds.tokenUrl, {
84
- grant_type: 'refresh_token',
85
- refresh_token: creds.refreshToken,
86
- client_id: creds.clientId,
87
- client_secret: creds.clientSecret,
84
+ const body = new URLSearchParams();
85
+ body.set('grant_type', 'refresh_token');
86
+ body.set('refresh_token', creds.refreshToken);
87
+ body.set('client_id', creds.clientId);
88
+ if (creds.clientSecret) {
89
+ body.set('client_secret', creds.clientSecret);
90
+ }
91
+
92
+ const response = await axios.post(creds.tokenUrl, body, {
93
+ headers: {
94
+ 'Content-Type': 'application/x-www-form-urlencoded',
95
+ 'Accept': 'application/json',
96
+ },
88
97
  });
89
98
 
90
99
  const { access_token, expires_in } = response.data;
100
+ const expiresInSeconds = Number(expires_in) || 3600;
91
101
 
92
102
  // Cache the token (don't mutate the input credentials)
93
103
  this.tokenCache.set(cacheKey, {
94
104
  accessToken: access_token,
95
- expiresAt: Date.now() + (expires_in * 1000),
105
+ expiresAt: Date.now() + (expiresInSeconds * 1000),
96
106
  });
97
107
 
98
108
  return `Bearer ${access_token}`;
@@ -13,7 +13,7 @@ import addFormats from 'ajv-formats';
13
13
  const BUILT_IN_TOOLS: ToolSchema[] = [
14
14
  {
15
15
  name: 'mcp_execute_typescript',
16
- description: 'Executes TypeScript code in a secure sandbox with access to `tools.*` SDK.',
16
+ description: 'Executes TypeScript code in a secure sandbox. Access MCP tools via the global `tools` object (e.g. `filesystem__list_directory` -> `await tools.filesystem.list_directory(...)`).',
17
17
  inputSchema: {
18
18
  type: 'object',
19
19
  properties: {
@@ -24,7 +24,7 @@ const BUILT_IN_TOOLS: ToolSchema[] = [
24
24
  allowedTools: {
25
25
  type: 'array',
26
26
  items: { type: 'string' },
27
- description: 'Optional list of tools the script is allowed to call (e.g. ["github.*"]).'
27
+ description: 'List of tool names (e.g. "filesystem.list_directory" or "filesystem.*") that the script is allowed to call.'
28
28
  }
29
29
  },
30
30
  required: ['code']
@@ -32,7 +32,7 @@ const BUILT_IN_TOOLS: ToolSchema[] = [
32
32
  },
33
33
  {
34
34
  name: 'mcp_execute_python',
35
- description: 'Executes Python code in a secure sandbox with access to `tools.*` SDK.',
35
+ description: 'Executes Python code in a secure sandbox. Access MCP tools via the global `tools` object (e.g. `filesystem__list_directory` -> `await tools.filesystem.list_directory(...)`).',
36
36
  inputSchema: {
37
37
  type: 'object',
38
38
  properties: {
@@ -43,7 +43,7 @@ const BUILT_IN_TOOLS: ToolSchema[] = [
43
43
  allowedTools: {
44
44
  type: 'array',
45
45
  items: { type: 'string' },
46
- description: 'Optional list of tools the script is allowed to call (e.g. ["github.*"]).'
46
+ description: 'List of tool names (e.g. "filesystem.list_directory" or "filesystem.*") that the script is allowed to call.'
47
47
  }
48
48
  },
49
49
  required: ['code']
@@ -51,7 +51,7 @@ const BUILT_IN_TOOLS: ToolSchema[] = [
51
51
  },
52
52
  {
53
53
  name: 'mcp_execute_isolate',
54
- description: 'Executes JavaScript code in a high-speed V8 isolate (no Deno/Node APIs).',
54
+ description: 'Executes JavaScript code in a high-speed V8 isolate. Access MCP tools via the global `tools` object (e.g. `await tools.filesystem.list_directory(...)`). No Deno/Node APIs. Use `console.log` for output.',
55
55
  inputSchema: {
56
56
  type: 'object',
57
57
  properties: {
@@ -62,7 +62,7 @@ const BUILT_IN_TOOLS: ToolSchema[] = [
62
62
  allowedTools: {
63
63
  type: 'array',
64
64
  items: { type: 'string' },
65
- description: 'Optional list of tools the script is allowed to call.'
65
+ description: 'List of tool names (e.g. "filesystem.list_directory" or "filesystem.*") that the script is allowed to call.'
66
66
  }
67
67
  },
68
68
  required: ['code']
@@ -139,7 +139,7 @@ export class GatewayService {
139
139
  const response = await client.call({
140
140
  jsonrpc: '2.0',
141
141
  id: 'discovery',
142
- method: 'list_tools',
142
+ method: 'tools/list',
143
143
  }, context);
144
144
 
145
145
  if (response.result?.tools) {
@@ -206,7 +206,7 @@ export class GatewayService {
206
206
  const response = await client.call({
207
207
  jsonrpc: '2.0',
208
208
  id: 'discovery',
209
- method: 'list_tools', // Standard MCP method
209
+ method: 'tools/list', // Standard MCP method
210
210
  }, context);
211
211
 
212
212
  if (response.result?.tools) {
@@ -319,7 +319,7 @@ export class GatewayService {
319
319
  response = await client.call({
320
320
  jsonrpc: '2.0',
321
321
  id: context.correlationId,
322
- method: 'call_tool',
322
+ method: 'tools/call',
323
323
  params: {
324
324
  name: toolName,
325
325
  arguments: params,
@@ -352,7 +352,7 @@ export class GatewayService {
352
352
  const response = await client.call({
353
353
  jsonrpc: '2.0',
354
354
  id: 'health',
355
- method: 'list_tools',
355
+ method: 'tools/list',
356
356
  }, context);
357
357
  upstreamStatus[id] = response.error ? 'degraded' : 'active';
358
358
  } catch (err) {
@@ -90,23 +90,31 @@ export class UpstreamClient {
90
90
  await this.ensureConnected();
91
91
 
92
92
  // Map GatewayService method names to SDK typed methods
93
- if (request.method === 'list_tools') {
93
+ if (request.method === 'list_tools' || request.method === 'tools/list') {
94
94
  const result = await this.mcpClient.listTools();
95
95
  return {
96
96
  jsonrpc: '2.0',
97
97
  id: request.id,
98
98
  result: result
99
99
  };
100
- } else if (request.method === 'call_tool') {
100
+ } else if (request.method === 'call_tool' || request.method === 'tools/call') {
101
101
  const params = request.params as { name: string; arguments?: Record<string, unknown> };
102
102
  const result = await this.mcpClient.callTool({
103
103
  name: params.name,
104
104
  arguments: params.arguments,
105
105
  });
106
+ const normalizedResult = (result && Array.isArray((result as any).content))
107
+ ? result
108
+ : {
109
+ content: [{
110
+ type: 'text',
111
+ text: typeof result === 'string' ? result : JSON.stringify(result ?? null),
112
+ }],
113
+ };
106
114
  return {
107
115
  jsonrpc: '2.0',
108
116
  id: request.id,
109
- result: result
117
+ result: normalizedResult
110
118
  };
111
119
  } else {
112
120
  // Fallback to generic request for other methods
package/src/index.ts CHANGED
@@ -43,10 +43,12 @@ program
43
43
  .description('Help set up OAuth for an upstream MCP server')
44
44
  .requiredOption('--client-id <id>', 'OAuth Client ID')
45
45
  .requiredOption('--client-secret <secret>', 'OAuth Client Secret')
46
- .requiredOption('--auth-url <url>', 'OAuth Authorization URL')
47
- .requiredOption('--token-url <url>', 'OAuth Token URL')
46
+ .option('--auth-url <url>', 'OAuth Authorization URL')
47
+ .option('--token-url <url>', 'OAuth Token URL')
48
+ .option('--mcp-url <url>', 'MCP base URL (auto-discover OAuth metadata)')
48
49
  .option('--scopes <scopes>', 'OAuth Scopes (comma separated)')
49
50
  .option('--port <port>', 'Port for the local callback server', '3333')
51
+ .option('--pkce', 'Use PKCE for the authorization code flow')
50
52
  .action(async (options) => {
51
53
  try {
52
54
  await handleAuth({
@@ -54,8 +56,10 @@ program
54
56
  clientSecret: options.clientSecret,
55
57
  authUrl: options.authUrl,
56
58
  tokenUrl: options.tokenUrl,
59
+ mcpUrl: options.mcpUrl,
57
60
  scopes: options.scopes,
58
61
  port: parseInt(options.port, 10),
62
+ usePkce: options.pkce || Boolean(options.mcpUrl),
59
63
  });
60
64
  console.log('\nSuccess! Configuration generated.');
61
65
  } catch (err: any) {
@@ -121,12 +125,25 @@ async function startServer() {
121
125
  transport = new StdioTransport(logger, requestController, concurrencyService);
122
126
  await transport.start();
123
127
  address = 'stdio';
128
+
129
+ // IMPORTANT: Even in stdio mode, we need a local socket for sandboxes to talk to
130
+ const internalTransport = new SocketTransport(logger, requestController, concurrencyService);
131
+ const internalPort = 0; // Random available port
132
+ const internalAddress = await internalTransport.listen({ port: internalPort });
133
+ executionService.ipcAddress = internalAddress;
134
+
135
+ // Register internal transport for shutdown
136
+ const originalShutdown = transport.close.bind(transport);
137
+ transport.close = async () => {
138
+ await originalShutdown();
139
+ await internalTransport.close();
140
+ };
124
141
  } else {
125
142
  transport = new SocketTransport(logger, requestController, concurrencyService);
126
143
  const port = configService.get('port');
127
144
  address = await transport.listen({ port });
145
+ executionService.ipcAddress = address;
128
146
  }
129
- executionService.ipcAddress = address;
130
147
 
131
148
  // Pre-warm workers
132
149
  await requestController.warmup();
@@ -41,7 +41,7 @@ export class SDKGenerator {
41
41
  lines.push('const __allowedTools = null;');
42
42
  }
43
43
 
44
- lines.push('const tools = {');
44
+ lines.push('const _tools = {');
45
45
 
46
46
  for (const [namespace, tools] of grouped.entries()) {
47
47
  // Validate namespace is a valid identifier
@@ -92,6 +92,18 @@ export class SDKGenerator {
92
92
  }
93
93
 
94
94
  lines.push('};');
95
+ lines.push(`
96
+ const tools = new Proxy(_tools, {
97
+ get: (target, prop) => {
98
+ if (prop in target) return target[prop];
99
+ if (prop === 'then') return undefined;
100
+ if (typeof prop === 'string') {
101
+ throw new Error(\`Namespace '\${prop}' not found. It might be invalid, or all tools in it were disallowed.\`);
102
+ }
103
+ return undefined;
104
+ }
105
+ });
106
+ `);
95
107
  lines.push('(globalThis as any).tools = tools;');
96
108
 
97
109
  return lines.join('\n');
@@ -187,7 +199,7 @@ export class SDKGenerator {
187
199
  lines.push('const __allowedTools = null;');
188
200
  }
189
201
 
190
- lines.push('const tools = {');
202
+ lines.push('const _tools = {');
191
203
 
192
204
  for (const [namespace, tools] of grouped.entries()) {
193
205
  const safeNamespace = this.isValidIdentifier(namespace) ? namespace : `["${this.escapeString(namespace)}"]`;
@@ -232,6 +244,18 @@ export class SDKGenerator {
232
244
  }
233
245
 
234
246
  lines.push('};');
247
+ lines.push(`
248
+ const tools = new Proxy(_tools, {
249
+ get: (target, prop) => {
250
+ if (prop in target) return target[prop];
251
+ if (prop === 'then') return undefined;
252
+ if (typeof prop === 'string') {
253
+ throw new Error(\`Namespace '\${prop}' not found. It might be invalid, or all tools in it were disallowed.\`);
254
+ }
255
+ return undefined;
256
+ }
257
+ });
258
+ `);
235
259
 
236
260
  return lines.join('\n');
237
261
  }