@mhingston5/conduit 1.1.1 → 1.1.3

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/docs/CODE_MODE.md CHANGED
@@ -28,6 +28,6 @@ Because logic executes in a sandbox, you can enforce limits on loops, memory, an
28
28
  ## Implementation in Conduit
29
29
 
30
30
  Conduit provides:
31
- 1. **`executeTypeScript` / `executePython`**: The entry points.
31
+ 1. **`mcp_execute_typescript` / `mcp_execute_python`**: The entry points.
32
32
  2. **`tools.*` SDK**: A dynamically generated client injected into the runtime.
33
33
  3. **Sandboxes**: Deno, Pyodide, and isolated-vm to run the code safely.
package/docs/SECURITY.md CHANGED
@@ -27,7 +27,7 @@ Conduit enforces strict Server-Side Request Forgery (SSRF) protections on upstre
27
27
  ## Authorization
28
28
 
29
29
  - **Master Token**: Full access to all methods (set via `IPC_BEARER_TOKEN`).
30
- - **Session Tokens**: Generated per-execution, restricted to `mcp.discoverTools` and `mcp.callTool` only.
30
+ - **Session Tokens**: Generated per-execution, restricted to `mcp_discover_tools` and `mcp_call_tool` only.
31
31
  - **Tool Allowlisting**: Per-request scope limits which tools code can discover/call (e.g., `["github.*"]`).
32
32
 
33
33
  ## Runtime Security
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@mhingston5/conduit",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "type": "module",
5
5
  "description": "A secure Code Mode execution substrate for MCP agents",
6
6
  "main": "index.js",
7
+ "bin": {
8
+ "conduit": "./dist/index.js"
9
+ },
7
10
  "publishConfig": {
8
11
  "access": "public"
9
12
  },
@@ -47,11 +50,13 @@
47
50
  "ajv": "^8.17.1",
48
51
  "ajv-formats": "^3.0.1",
49
52
  "axios": "^1.13.2",
53
+ "commander": "^14.0.2",
50
54
  "dotenv": "^17.2.3",
51
55
  "fastify": "^5.6.2",
52
56
  "isolated-vm": "^6.0.2",
53
57
  "js-yaml": "^4.1.1",
54
58
  "lru-cache": "^11.2.4",
59
+ "open": "^11.0.0",
55
60
  "p-limit": "^7.2.0",
56
61
  "pino": "^10.1.0",
57
62
  "pyodide": "^0.29.0",
@@ -81,12 +81,12 @@ async function sendIPCRequest(method: string, params: any) {
81
81
 
82
82
  // Internal tool call function - used by generated SDK
83
83
  const __internalCallTool = async (name: string, params: any) => {
84
- return await sendIPCRequest('mcp.callTool', { name, arguments: params });
84
+ return await sendIPCRequest('mcp_call_tool', { name, arguments: params });
85
85
  };
86
86
 
87
87
  // Tool discovery - still available for dynamic scenarios
88
88
  (globalThis as any).discoverMCPTools = async (options: any) => {
89
- const result = await sendIPCRequest('mcp.discoverTools', options);
89
+ const result = await sendIPCRequest('mcp_discover_tools', options);
90
90
  return result.tools || [];
91
91
  };
92
92
 
@@ -0,0 +1,95 @@
1
+ import Fastify from 'fastify';
2
+ import axios from 'axios';
3
+ import open from 'open';
4
+ import { v4 as uuidv4 } from 'uuid';
5
+
6
+ export interface AuthOptions {
7
+ authUrl: string;
8
+ tokenUrl: string;
9
+ clientId: string;
10
+ clientSecret: string;
11
+ scopes?: string;
12
+ port?: number;
13
+ }
14
+
15
+ export async function handleAuth(options: AuthOptions) {
16
+ const port = options.port || 3333;
17
+ const redirectUri = `http://localhost:${port}/callback`;
18
+ const state = uuidv4();
19
+
20
+ const fastify = Fastify();
21
+
22
+ return new Promise<void>((resolve, reject) => {
23
+ fastify.get('/callback', async (request, reply) => {
24
+ const { code, state: returnedState, error, error_description } = request.query as any;
25
+
26
+ if (error) {
27
+ reply.send(`Authentication failed: ${error} - ${error_description}`);
28
+ reject(new Error(`OAuth error: ${error}`));
29
+ return;
30
+ }
31
+
32
+ if (returnedState !== state) {
33
+ reply.send('Invalid state parameter');
34
+ reject(new Error('State mismatch'));
35
+ return;
36
+ }
37
+
38
+ 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,
45
+ });
46
+
47
+ const { refresh_token, access_token } = response.data;
48
+
49
+ console.log('\n--- Authentication Successful ---\n');
50
+ console.log('Use these values in your conduit.yaml:\n');
51
+ console.log('credentials:');
52
+ console.log(' type: oauth2');
53
+ console.log(` clientId: ${options.clientId}`);
54
+ console.log(` clientSecret: ${options.clientSecret}`);
55
+ console.log(` tokenUrl: "${options.tokenUrl}"`);
56
+ console.log(` refreshToken: "${refresh_token || 'N/A (No refresh token returned)'}"`);
57
+
58
+ if (!refresh_token) {
59
+ console.log('\nWarning: No refresh token was returned. Ensure your app has "offline_access" scope or similar.');
60
+ }
61
+
62
+ console.log('\nRaw response data:', JSON.stringify(response.data, null, 2));
63
+
64
+ reply.send('Authentication successful! You can close this window and return to the terminal.');
65
+ resolve();
66
+ } catch (err: any) {
67
+ const msg = err.response?.data?.error_description || err.response?.data?.error || err.message;
68
+ reply.send(`Failed to exchange code for token: ${msg}`);
69
+ reject(new Error(`Token exchange failed: ${msg}`));
70
+ } finally {
71
+ setTimeout(() => fastify.close(), 1000);
72
+ }
73
+ });
74
+
75
+ fastify.listen({ port: port, host: '127.0.0.1' }, async (err) => {
76
+ if (err) {
77
+ reject(err);
78
+ return;
79
+ }
80
+
81
+ const authUrl = new URL(options.authUrl);
82
+ authUrl.searchParams.append('client_id', options.clientId);
83
+ authUrl.searchParams.append('redirect_uri', redirectUri);
84
+ authUrl.searchParams.append('response_type', 'code');
85
+ authUrl.searchParams.append('state', state);
86
+ if (options.scopes) {
87
+ authUrl.searchParams.append('scope', options.scopes);
88
+ }
89
+
90
+ console.log(`Opening browser to: ${authUrl.toString()}`);
91
+ console.log('Waiting for callback...');
92
+ await open(authUrl.toString());
93
+ });
94
+ });
95
+ }
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import dotenv from 'dotenv';
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
+ import crypto from 'node:crypto';
5
6
  import yaml from 'js-yaml';
6
7
 
7
8
  // Silence dotenv logging
@@ -22,12 +23,14 @@ export const ResourceLimitsSchema = z.object({
22
23
  });
23
24
 
24
25
  export const UpstreamCredentialsSchema = z.object({
25
- type: z.enum(['oauth2', 'apikey']), // Add other types as needed
26
+ type: z.enum(['oauth2', 'apiKey', 'bearer']), // Align with AuthType
26
27
  clientId: z.string().optional(),
27
28
  clientSecret: z.string().optional(),
28
29
  tokenUrl: z.string().optional(),
30
+ refreshToken: z.string().optional(),
29
31
  scopes: z.array(z.string()).optional(),
30
32
  apiKey: z.string().optional(),
33
+ bearerToken: z.string().optional(),
31
34
  headerName: z.string().optional(),
32
35
  });
33
36
 
@@ -63,7 +66,7 @@ export const ConfigSchema = z.object({
63
66
  secretRedactionPatterns: z.array(z.string()).default([
64
67
  '[A-Za-z0-9-_]{20,}', // Default pattern from spec
65
68
  ]),
66
- ipcBearerToken: z.string().optional().default(() => Math.random().toString(36).substring(7)),
69
+ ipcBearerToken: z.string().optional().default(() => crypto.randomUUID()),
67
70
  maxConcurrent: z.number().default(10),
68
71
  denoMaxPoolSize: z.number().default(10),
69
72
  pyodideMaxPoolSize: z.number().default(3),
@@ -31,7 +31,7 @@ export class AuthMiddleware implements Middleware {
31
31
 
32
32
  // Strict scoping for session tokens
33
33
  if (isSession) {
34
- const allowedMethods = ['initialize', 'notifications/initialized', 'mcp.discoverTools', 'mcp.callTool', 'ping'];
34
+ const allowedMethods = ['initialize', 'notifications/initialized', 'mcp_discover_tools', 'mcp_call_tool', 'ping', 'tools/list', 'tools/call'];
35
35
  if (!allowedMethods.includes(request.method)) {
36
36
  return {
37
37
  jsonrpc: '2.0',
@@ -102,23 +102,24 @@ export class RequestController {
102
102
 
103
103
  switch (method) {
104
104
  case 'tools/list': // Standard MCP method name
105
- case 'mcp.discoverTools':
105
+ case 'mcp_discover_tools':
106
106
  return this.handleDiscoverTools(params, context, id);
107
- case 'mcp.listToolPackages':
107
+ case 'mcp_list_tool_packages':
108
108
  return this.handleListToolPackages(params, context, id);
109
- case 'mcp.listToolStubs':
109
+ case 'mcp_list_tool_stubs':
110
110
  return this.handleListToolStubs(params, context, id);
111
- case 'mcp.readToolSchema':
111
+ case 'mcp_read_tool_schema':
112
112
  return this.handleReadToolSchema(params, context, id);
113
- case 'mcp.validateTool':
113
+ case 'mcp_validate_tool':
114
114
  return this.handleValidateTool(request, context);
115
- case 'mcp.callTool':
115
+ case 'mcp_call_tool':
116
+ case 'tools/call':
116
117
  return this.handleCallTool(params, context, id);
117
- case 'mcp.executeTypeScript':
118
+ case 'mcp_execute_typescript':
118
119
  return this.handleExecuteTypeScript(params, context, id);
119
- case 'mcp.executePython':
120
+ case 'mcp_execute_python':
120
121
  return this.handleExecutePython(params, context, id);
121
- case 'mcp.executeIsolate':
122
+ case 'mcp_execute_isolate':
122
123
  return this.handleExecuteIsolate(params, context, id);
123
124
  case 'initialize':
124
125
  return this.handleInitialize(params, context, id);
@@ -208,12 +209,25 @@ export class RequestController {
208
209
  }
209
210
 
210
211
  private async handleCallTool(params: any, context: ExecutionContext, id: string | number): Promise<JSONRPCResponse> {
212
+ if (!params) return this.errorResponse(id, -32602, 'Missing parameters');
211
213
  const { name, arguments: toolArgs } = params;
214
+
215
+ // Route built-in tools to their specific handlers
216
+ switch (name) {
217
+ case 'mcp_execute_typescript':
218
+ return this.handleExecuteTypeScript(toolArgs, context, id);
219
+ case 'mcp_execute_python':
220
+ return this.handleExecutePython(toolArgs, context, id);
221
+ case 'mcp_execute_isolate':
222
+ return this.handleExecuteIsolate(toolArgs, context, id);
223
+ }
224
+
212
225
  const response = await this.gatewayService.callTool(name, toolArgs, context);
213
226
  return { ...response, id };
214
227
  }
215
228
 
216
229
  private async handleExecuteTypeScript(params: any, context: ExecutionContext, id: string | number): Promise<JSONRPCResponse> {
230
+ if (!params) return this.errorResponse(id, -32602, 'Missing parameters');
217
231
  const { code, limits, allowedTools } = params;
218
232
 
219
233
  if (Array.isArray(allowedTools)) {
@@ -238,6 +252,7 @@ export class RequestController {
238
252
  }
239
253
 
240
254
  private async handleExecutePython(params: any, context: ExecutionContext, id: string | number): Promise<JSONRPCResponse> {
255
+ if (!params) return this.errorResponse(id, -32602, 'Missing parameters');
241
256
  const { code, limits, allowedTools } = params;
242
257
 
243
258
  if (Array.isArray(allowedTools)) {
@@ -287,6 +302,7 @@ export class RequestController {
287
302
  }
288
303
 
289
304
  private async handleExecuteIsolate(params: any, context: ExecutionContext, id: string | number): Promise<JSONRPCResponse> {
305
+ if (!params) return this.errorResponse(id, -32602, 'Missing parameters');
290
306
  const { code, limits, allowedTools } = params;
291
307
 
292
308
  if (Array.isArray(allowedTools)) {
@@ -9,11 +9,11 @@ export type { Session };
9
9
 
10
10
  export class SecurityService implements IUrlValidator {
11
11
  private logger: Logger;
12
- private ipcToken: string;
12
+ private ipcToken: string | undefined;
13
13
  private networkPolicy: NetworkPolicyService;
14
14
  private sessionManager: SessionManager;
15
15
 
16
- constructor(logger: Logger, ipcToken: string) {
16
+ constructor(logger: Logger, ipcToken: string | undefined) {
17
17
  this.logger = logger;
18
18
  this.ipcToken = ipcToken;
19
19
  this.networkPolicy = new NetworkPolicyService(logger);
@@ -67,7 +67,7 @@ export class SecurityService implements IUrlValidator {
67
67
  }
68
68
 
69
69
 
70
- getIpcToken(): string {
70
+ getIpcToken(): string | undefined {
71
71
  return this.ipcToken;
72
72
  }
73
73
  }
@@ -131,11 +131,11 @@ async function handleTask(data: any) {
131
131
  };
132
132
 
133
133
  (p as any).globals.set('discover_mcp_tools_js', (options: any) => {
134
- return sendIPCRequest('mcp.discoverTools', options);
134
+ return sendIPCRequest('mcp_discover_tools', options);
135
135
  });
136
136
 
137
137
  (p as any).globals.set('call_mcp_tool_js', (name: string, args: any) => {
138
- return sendIPCRequest('mcp.callTool', { name, arguments: args });
138
+ return sendIPCRequest('mcp_call_tool', { name, arguments: args });
139
139
  });
140
140
 
141
141
  if (shim) {
@@ -7,12 +7,10 @@ export interface UpstreamCredentials {
7
7
  type: AuthType;
8
8
  apiKey?: string;
9
9
  bearerToken?: string;
10
- oauth2?: {
11
- clientId: string;
12
- clientSecret: string;
13
- tokenUrl: string;
14
- refreshToken: string;
15
- };
10
+ clientId?: string;
11
+ clientSecret?: string;
12
+ tokenUrl?: string;
13
+ refreshToken?: string;
16
14
  }
17
15
 
18
16
  interface CachedToken {
@@ -45,10 +43,11 @@ export class AuthService {
45
43
  }
46
44
 
47
45
  private async getOAuth2Token(creds: UpstreamCredentials): Promise<string> {
48
- if (!creds.oauth2) throw new Error('OAuth2 credentials missing');
46
+ if (!creds.tokenUrl || !creds.clientId) {
47
+ throw new Error('OAuth2 credentials missing required fields (tokenUrl, clientId)');
48
+ }
49
49
 
50
- const { oauth2 } = creds;
51
- const cacheKey = `${oauth2.clientId}:${oauth2.tokenUrl}`;
50
+ const cacheKey = `${creds.clientId}:${creds.tokenUrl}`;
52
51
 
53
52
  // Check cache first (with 30s buffer)
54
53
  const cached = this.tokenCache.get(cacheKey);
@@ -74,17 +73,18 @@ export class AuthService {
74
73
  }
75
74
 
76
75
  private async doRefresh(creds: UpstreamCredentials, cacheKey: string): Promise<string> {
77
- const { oauth2 } = creds;
78
- if (!oauth2) throw new Error('OAuth2 credentials missing');
76
+ if (!creds.tokenUrl || !creds.refreshToken || !creds.clientId || !creds.clientSecret) {
77
+ throw new Error('OAuth2 credentials missing required fields for refresh');
78
+ }
79
79
 
80
- this.logger.info('Refreshing OAuth2 token');
80
+ this.logger.info({ tokenUrl: creds.tokenUrl, clientId: creds.clientId }, 'Refreshing OAuth2 token');
81
81
 
82
82
  try {
83
- const response = await axios.post(oauth2.tokenUrl, {
83
+ const response = await axios.post(creds.tokenUrl, {
84
84
  grant_type: 'refresh_token',
85
- refresh_token: oauth2.refreshToken,
86
- client_id: oauth2.clientId,
87
- client_secret: oauth2.clientSecret,
85
+ refresh_token: creds.refreshToken,
86
+ client_id: creds.clientId,
87
+ client_secret: creds.clientSecret,
88
88
  });
89
89
 
90
90
  const { access_token, expires_in } = response.data;
@@ -97,8 +97,9 @@ export class AuthService {
97
97
 
98
98
  return `Bearer ${access_token}`;
99
99
  } catch (err: any) {
100
- this.logger.error({ err: err.message }, 'Failed to refresh OAuth2 token');
101
- throw new Error(`OAuth2 refresh failed: ${err.message}`);
100
+ const errorMsg = err.response?.data?.error_description || err.response?.data?.error || err.message;
101
+ this.logger.error({ err: errorMsg }, 'Failed to refresh OAuth2 token');
102
+ throw new Error(`OAuth2 refresh failed: ${errorMsg}`);
102
103
  }
103
104
  }
104
105
  }
@@ -12,7 +12,7 @@ import addFormats from 'ajv-formats';
12
12
 
13
13
  const BUILT_IN_TOOLS: ToolSchema[] = [
14
14
  {
15
- name: 'mcp.executeTypeScript',
15
+ name: 'mcp_execute_typescript',
16
16
  description: 'Executes TypeScript code in a secure sandbox with access to `tools.*` SDK.',
17
17
  inputSchema: {
18
18
  type: 'object',
@@ -31,7 +31,7 @@ const BUILT_IN_TOOLS: ToolSchema[] = [
31
31
  }
32
32
  },
33
33
  {
34
- name: 'mcp.executePython',
34
+ name: 'mcp_execute_python',
35
35
  description: 'Executes Python code in a secure sandbox with access to `tools.*` SDK.',
36
36
  inputSchema: {
37
37
  type: 'object',
@@ -50,7 +50,7 @@ const BUILT_IN_TOOLS: ToolSchema[] = [
50
50
  }
51
51
  },
52
52
  {
53
- name: 'mcp.executeIsolate',
53
+ name: 'mcp_execute_isolate',
54
54
  description: 'Executes JavaScript code in a high-speed V8 isolate (no Deno/Node APIs).',
55
55
  inputSchema: {
56
56
  type: 'object',
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { Command } from 'commander';
1
2
  import { ConfigService } from './core/config.service.js';
2
3
  import { createLogger, loggerStorage } from './core/logger.js';
3
4
  import { SocketTransport } from './transport/socket.transport.js';
@@ -14,10 +15,56 @@ import { IsolateExecutor } from './executors/isolate.executor.js';
14
15
  import { ExecutorRegistry } from './core/registries/executor.registry.js';
15
16
  import { ExecutionService } from './core/execution.service.js';
16
17
  import { buildDefaultMiddleware } from './core/middleware/middleware.builder.js';
17
- async function main() {
18
- // console.error('DEBUG: Starting Conduit main...');
18
+ import { handleAuth } from './auth.cmd.js';
19
+
20
+ const program = new Command();
21
+
22
+ program
23
+ .name('conduit')
24
+ .description('A secure Code Mode execution substrate for MCP agents')
25
+ .version('1.0.0');
26
+
27
+ program
28
+ .command('serve', { isDefault: true })
29
+ .description('Start the Conduit server')
30
+ .option('--stdio', 'Use stdio transport')
31
+ .action(async (options) => {
32
+ try {
33
+ await startServer();
34
+ } catch (err) {
35
+ console.error('Failed to start Conduit:', err);
36
+ process.exit(1);
37
+ }
38
+ });
39
+
40
+ program
41
+ .command('auth')
42
+ .description('Help set up OAuth for an upstream MCP server')
43
+ .requiredOption('--client-id <id>', 'OAuth Client ID')
44
+ .requiredOption('--client-secret <secret>', 'OAuth Client Secret')
45
+ .requiredOption('--auth-url <url>', 'OAuth Authorization URL')
46
+ .requiredOption('--token-url <url>', 'OAuth Token URL')
47
+ .option('--scopes <scopes>', 'OAuth Scopes (comma separated)')
48
+ .option('--port <port>', 'Port for the local callback server', '3333')
49
+ .action(async (options) => {
50
+ try {
51
+ await handleAuth({
52
+ clientId: options.clientId,
53
+ clientSecret: options.clientSecret,
54
+ authUrl: options.authUrl,
55
+ tokenUrl: options.tokenUrl,
56
+ scopes: options.scopes,
57
+ port: parseInt(options.port, 10),
58
+ });
59
+ console.log('\nSuccess! Configuration generated.');
60
+ } catch (err: any) {
61
+ console.error('Authentication helper failed:', err.message);
62
+ process.exit(1);
63
+ }
64
+ });
65
+
66
+ async function startServer() {
19
67
  const configService = new ConfigService();
20
- // console.error('DEBUG: Config loaded');
21
68
  const logger = createLogger(configService);
22
69
 
23
70
  const otelService = new OtelService(logger);
@@ -78,7 +125,7 @@ async function main() {
78
125
  const port = configService.get('port');
79
126
  address = await transport.listen({ port });
80
127
  }
81
- executionService.ipcAddress = address; // Update IPC address on ExecutionService instead of RequestController
128
+ executionService.ipcAddress = address;
82
129
 
83
130
  // Pre-warm workers
84
131
  await requestController.warmup();
@@ -102,7 +149,4 @@ async function main() {
102
149
  });
103
150
  }
104
151
 
105
- main().catch((err) => {
106
- console.error('Failed to start Conduit:', err);
107
- process.exit(1);
108
- });
152
+ program.parse(process.argv);
@@ -1,4 +1,5 @@
1
1
  import net from 'node:net';
2
+ import fs from 'node:fs';
2
3
  import os from 'node:os';
3
4
  import path from 'node:path';
4
5
  import { Logger } from 'pino';
@@ -46,9 +47,13 @@ export class SocketTransport {
46
47
 
47
48
  // Cleanup existing socket if needed (unlikely on Windows, but good for Unix)
48
49
  if (os.platform() !== 'win32' && path.isAbsolute(socketPath)) {
49
- // We rely on caller or deployment to clean up, or error out.
50
- // Trying to unlink here might be dangerous if we don't own it.
51
- // But strictly, we should just listen.
50
+ try {
51
+ fs.unlinkSync(socketPath);
52
+ } catch (error: any) {
53
+ if (error.code !== 'ENOENT') {
54
+ this.logger.warn({ err: error, socketPath }, 'Failed to unlink socket before binding');
55
+ }
56
+ }
52
57
  }
53
58
 
54
59
  this.server.listen(socketPath, () => {
@@ -25,13 +25,10 @@ describe('AuthService', () => {
25
25
  it('should refresh OAuth2 token when expired', async () => {
26
26
  const creds: any = {
27
27
  type: 'oauth2',
28
- oauth2: {
29
- clientId: 'id',
30
- clientSecret: 'secret',
31
- tokenUrl: 'http://token',
32
- refreshToken: 'refresh',
33
- expiresAt: Date.now() - 1000, // Expired
34
- },
28
+ clientId: 'id',
29
+ clientSecret: 'secret',
30
+ tokenUrl: 'http://token',
31
+ refreshToken: 'refresh',
35
32
  };
36
33
 
37
34
  (axios.post as any).mockResolvedValue({
@@ -50,12 +47,10 @@ describe('AuthService', () => {
50
47
  // First call - will trigger refresh
51
48
  const creds: any = {
52
49
  type: 'oauth2',
53
- oauth2: {
54
- clientId: 'id',
55
- clientSecret: 'secret',
56
- tokenUrl: 'http://token',
57
- refreshToken: 'refresh',
58
- },
50
+ clientId: 'id',
51
+ clientSecret: 'secret',
52
+ tokenUrl: 'http://token',
53
+ refreshToken: 'refresh',
59
54
  };
60
55
 
61
56
  (axios.post as any).mockResolvedValue({
@@ -67,4 +67,35 @@ describe('ConfigService', () => {
67
67
  existsSpy.mockRestore();
68
68
  readSpy.mockRestore();
69
69
  });
70
+
71
+ it('should parse OAuth2 credentials correctly', () => {
72
+ const existsSpy = vi.spyOn(fs, 'existsSync').mockImplementation((p: any) => p.endsWith('conduit.test.yaml'));
73
+ const readSpy = vi.spyOn(fs, 'readFileSync').mockReturnValue(`
74
+ upstreams:
75
+ - id: test-oauth
76
+ type: http
77
+ url: http://upstream
78
+ credentials:
79
+ type: oauth2
80
+ clientId: my-id
81
+ clientSecret: my-secret
82
+ tokenUrl: http://token
83
+ refreshToken: my-refresh
84
+ `);
85
+
86
+ vi.stubEnv('CONFIG_FILE', 'conduit.test.yaml');
87
+ const configService = new ConfigService();
88
+ const upstreams = configService.get('upstreams');
89
+ expect(upstreams).toHaveLength(1);
90
+ expect(upstreams![0].credentials).toEqual({
91
+ type: 'oauth2',
92
+ clientId: 'my-id',
93
+ clientSecret: 'my-secret',
94
+ tokenUrl: 'http://token',
95
+ refreshToken: 'my-refresh'
96
+ });
97
+
98
+ existsSpy.mockRestore();
99
+ readSpy.mockRestore();
100
+ });
70
101
  });
@@ -32,8 +32,8 @@ describe('Contract Test: Conduit vs Reference MCP', () => {
32
32
 
33
33
  it('should successfully discover tools from reference MCP', async () => {
34
34
  const tools = await gateway.discoverTools(context);
35
- expect(tools).toHaveLength(1);
36
- expect(tools[0].name).toBe('ref__echo');
35
+ expect(tools.length).toBeGreaterThanOrEqual(1);
36
+ expect(tools.find(t => t.name === 'ref__echo')).toBeDefined();
37
37
  });
38
38
 
39
39
  it('should successfully call tool on reference MCP', async () => {
@@ -96,7 +96,7 @@ describe('Dynamic Tool Calling (E2E)', () => {
96
96
  const response = await requestController.handleRequest({
97
97
  jsonrpc: '2.0',
98
98
  id: 1,
99
- method: 'mcp.executeTypeScript',
99
+ method: 'mcp_execute_typescript',
100
100
  params: { code },
101
101
  auth: { bearerToken: testToken }
102
102
  }, context);
@@ -122,7 +122,7 @@ print(f"RESULT:{result}")
122
122
  const response = await requestController.handleRequest({
123
123
  jsonrpc: '2.0',
124
124
  id: 2,
125
- method: 'mcp.executePython',
125
+ method: 'mcp_execute_python',
126
126
  params: { code },
127
127
  auth: { bearerToken: testToken }
128
128
  }, context);
@@ -151,7 +151,7 @@ print(f"RESULT:{result}")
151
151
  const response = await requestController.handleRequest({
152
152
  jsonrpc: '2.0',
153
153
  id: 3,
154
- method: 'mcp.executeTypeScript',
154
+ method: 'mcp_execute_typescript',
155
155
  params: {
156
156
  code,
157
157
  allowedTools: ['mock.hello'] // Only mock.hello allowed
@@ -178,7 +178,7 @@ print(f"RESULT:{result}")
178
178
  const response = await requestController.handleRequest({
179
179
  jsonrpc: '2.0',
180
180
  id: 4,
181
- method: 'mcp.executeTypeScript',
181
+ method: 'mcp_execute_typescript',
182
182
  params: {
183
183
  code,
184
184
  allowedTools: ['mock.*'] // Wildcard allows all mock tools
@@ -206,7 +206,7 @@ print(f"RESULT:{result}")
206
206
  const response = await requestController.handleRequest({
207
207
  jsonrpc: '2.0',
208
208
  id: 5,
209
- method: 'mcp.executeIsolate',
209
+ method: 'mcp_execute_isolate',
210
210
  params: {
211
211
  code,
212
212
  allowedTools: ['mock.*'],