@mhingston5/conduit 1.1.3 → 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.
- package/README.md +13 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +265 -51
- package/dist/index.js.map +1 -1
- package/dist/pyodide.worker.js.map +1 -0
- package/package.json +3 -3
- package/src/auth.cmd.ts +161 -14
- package/src/core/request.controller.ts +51 -3
- package/src/executors/pyodide.executor.ts +9 -4
- package/src/gateway/auth.service.ts +17 -7
- package/src/gateway/gateway.service.ts +10 -10
- package/src/gateway/upstream.client.ts +11 -3
- package/src/index.ts +21 -3
- package/src/sdk/sdk-generator.ts +26 -2
- package/tests/__snapshots__/assets.test.ts.snap +27 -3
- package/tests/dynamic.tool.test.ts +3 -3
- package/tests/gateway.manifest.test.ts +1 -1
- package/tests/gateway.service.test.ts +1 -1
- package/tests/reference_mcp.ts +5 -3
- package/tests/routing.test.ts +1 -1
- package/tests/sdk/sdk-generator.test.ts +3 -2
- package/tsup.config.ts +1 -1
- package/dist/executors/pyodide.worker.js.map +0 -1
- /package/dist/{executors/pyodide.worker.d.ts → pyodide.worker.d.ts} +0 -0
- /package/dist/{executors/pyodide.worker.js → pyodide.worker.js} +0 -0
|
@@ -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.
|
|
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",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"homepage": "https://github.com/mhingston/conduit#readme",
|
|
37
37
|
"packageManager": "pnpm@10.8.0",
|
|
38
38
|
"engines": {
|
|
39
|
-
"node": "24.0.0"
|
|
39
|
+
"node": ">=24.0.0"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
@@ -76,4 +76,4 @@
|
|
|
76
76
|
"vitest": "^4.0.16",
|
|
77
77
|
"vitest-mock-extended": "^3.1.0"
|
|
78
78
|
}
|
|
79
|
-
}
|
|
79
|
+
}
|
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
|
|
8
|
-
tokenUrl
|
|
8
|
+
authUrl?: string;
|
|
9
|
+
tokenUrl?: string;
|
|
9
10
|
clientId: string;
|
|
10
|
-
clientSecret
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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(
|
|
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 (
|
|
87
|
-
authUrl.searchParams.append('scope',
|
|
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.
|
|
221
|
+
return this.handleExecuteToolCall('typescript', toolArgs, context, id);
|
|
219
222
|
case 'mcp_execute_python':
|
|
220
|
-
return this.
|
|
223
|
+
return this.handleExecuteToolCall('python', toolArgs, context, id);
|
|
221
224
|
case 'mcp_execute_isolate':
|
|
222
|
-
return this.
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
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
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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() + (
|
|
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
|
|
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: '
|
|
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
|
|
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: '
|
|
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 (
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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:
|
|
117
|
+
result: normalizedResult
|
|
110
118
|
};
|
|
111
119
|
} else {
|
|
112
120
|
// Fallback to generic request for other methods
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
import { Command } from 'commander';
|
|
2
3
|
import { ConfigService } from './core/config.service.js';
|
|
3
4
|
import { createLogger, loggerStorage } from './core/logger.js';
|
|
@@ -42,10 +43,12 @@ program
|
|
|
42
43
|
.description('Help set up OAuth for an upstream MCP server')
|
|
43
44
|
.requiredOption('--client-id <id>', 'OAuth Client ID')
|
|
44
45
|
.requiredOption('--client-secret <secret>', 'OAuth Client Secret')
|
|
45
|
-
.
|
|
46
|
-
.
|
|
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)')
|
|
47
49
|
.option('--scopes <scopes>', 'OAuth Scopes (comma separated)')
|
|
48
50
|
.option('--port <port>', 'Port for the local callback server', '3333')
|
|
51
|
+
.option('--pkce', 'Use PKCE for the authorization code flow')
|
|
49
52
|
.action(async (options) => {
|
|
50
53
|
try {
|
|
51
54
|
await handleAuth({
|
|
@@ -53,8 +56,10 @@ program
|
|
|
53
56
|
clientSecret: options.clientSecret,
|
|
54
57
|
authUrl: options.authUrl,
|
|
55
58
|
tokenUrl: options.tokenUrl,
|
|
59
|
+
mcpUrl: options.mcpUrl,
|
|
56
60
|
scopes: options.scopes,
|
|
57
61
|
port: parseInt(options.port, 10),
|
|
62
|
+
usePkce: options.pkce || Boolean(options.mcpUrl),
|
|
58
63
|
});
|
|
59
64
|
console.log('\nSuccess! Configuration generated.');
|
|
60
65
|
} catch (err: any) {
|
|
@@ -120,12 +125,25 @@ async function startServer() {
|
|
|
120
125
|
transport = new StdioTransport(logger, requestController, concurrencyService);
|
|
121
126
|
await transport.start();
|
|
122
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
|
+
};
|
|
123
141
|
} else {
|
|
124
142
|
transport = new SocketTransport(logger, requestController, concurrencyService);
|
|
125
143
|
const port = configService.get('port');
|
|
126
144
|
address = await transport.listen({ port });
|
|
145
|
+
executionService.ipcAddress = address;
|
|
127
146
|
}
|
|
128
|
-
executionService.ipcAddress = address;
|
|
129
147
|
|
|
130
148
|
// Pre-warm workers
|
|
131
149
|
await requestController.warmup();
|
package/src/sdk/sdk-generator.ts
CHANGED
|
@@ -41,7 +41,7 @@ export class SDKGenerator {
|
|
|
41
41
|
lines.push('const __allowedTools = null;');
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
lines.push('const
|
|
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
|
|
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
|
}
|