@mhingston5/conduit 1.1.4 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,6 @@
1
1
  import { Logger } from 'pino';
2
+ import { HostClient } from './host.client.js';
3
+ import { StdioTransport } from '../transport/stdio.transport.js';
2
4
  import { UpstreamClient, UpstreamInfo } from './upstream.client.js';
3
5
  import { AuthService } from './auth.service.js';
4
6
  import { SchemaCache, ToolSchema } from './schema.cache.js';
@@ -13,7 +15,7 @@ import addFormats from 'ajv-formats';
13
15
  const BUILT_IN_TOOLS: ToolSchema[] = [
14
16
  {
15
17
  name: 'mcp_execute_typescript',
16
- description: 'Executes TypeScript code in a secure sandbox with access to `tools.*` SDK.',
18
+ 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
19
  inputSchema: {
18
20
  type: 'object',
19
21
  properties: {
@@ -24,7 +26,7 @@ const BUILT_IN_TOOLS: ToolSchema[] = [
24
26
  allowedTools: {
25
27
  type: 'array',
26
28
  items: { type: 'string' },
27
- description: 'Optional list of tools the script is allowed to call (e.g. ["github.*"]).'
29
+ description: 'List of tool names (e.g. "filesystem.list_directory" or "filesystem.*") that the script is allowed to call.'
28
30
  }
29
31
  },
30
32
  required: ['code']
@@ -32,7 +34,7 @@ const BUILT_IN_TOOLS: ToolSchema[] = [
32
34
  },
33
35
  {
34
36
  name: 'mcp_execute_python',
35
- description: 'Executes Python code in a secure sandbox with access to `tools.*` SDK.',
37
+ 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
38
  inputSchema: {
37
39
  type: 'object',
38
40
  properties: {
@@ -43,7 +45,7 @@ const BUILT_IN_TOOLS: ToolSchema[] = [
43
45
  allowedTools: {
44
46
  type: 'array',
45
47
  items: { type: 'string' },
46
- description: 'Optional list of tools the script is allowed to call (e.g. ["github.*"]).'
48
+ description: 'List of tool names (e.g. "filesystem.list_directory" or "filesystem.*") that the script is allowed to call.'
47
49
  }
48
50
  },
49
51
  required: ['code']
@@ -51,7 +53,7 @@ const BUILT_IN_TOOLS: ToolSchema[] = [
51
53
  },
52
54
  {
53
55
  name: 'mcp_execute_isolate',
54
- description: 'Executes JavaScript code in a high-speed V8 isolate (no Deno/Node APIs).',
56
+ 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
57
  inputSchema: {
56
58
  type: 'object',
57
59
  properties: {
@@ -62,7 +64,7 @@ const BUILT_IN_TOOLS: ToolSchema[] = [
62
64
  allowedTools: {
63
65
  type: 'array',
64
66
  items: { type: 'string' },
65
- description: 'Optional list of tools the script is allowed to call.'
67
+ description: 'List of tool names (e.g. "filesystem.list_directory" or "filesystem.*") that the script is allowed to call.'
66
68
  }
67
69
  },
68
70
  required: ['code']
@@ -72,41 +74,69 @@ const BUILT_IN_TOOLS: ToolSchema[] = [
72
74
 
73
75
  export class GatewayService {
74
76
  private logger: Logger;
75
- private clients: Map<string, UpstreamClient> = new Map();
77
+ private clients: Map<string, any> = new Map();
76
78
  private authService: AuthService;
77
79
  private schemaCache: SchemaCache;
78
80
  private urlValidator: IUrlValidator;
79
- private policyService: PolicyService;
81
+ public policyService: PolicyService;
80
82
  private ajv: Ajv;
81
83
  // Cache compiled validators to avoid recompilation on every call
82
84
  private validatorCache = new Map<string, any>();
83
85
 
84
86
  constructor(logger: Logger, urlValidator: IUrlValidator, policyService?: PolicyService) {
85
- this.logger = logger;
87
+ this.logger = logger.child({ component: 'GatewayService' });
88
+ this.logger.debug('GatewayService instance created');
86
89
  this.urlValidator = urlValidator;
87
90
  this.authService = new AuthService(logger);
88
91
  this.schemaCache = new SchemaCache(logger);
89
92
  this.policyService = policyService ?? new PolicyService();
90
- this.ajv = new Ajv({ strict: false }); // Strict mode off for now to be permissive with upstream schemas
91
- // eslint-disable-next-line @typescript-eslint/no-var-requires
93
+ this.ajv = new Ajv({ strict: false });
92
94
  (addFormats as any).default(this.ajv);
93
95
  }
94
96
 
95
97
  registerUpstream(info: UpstreamInfo) {
96
98
  const client = new UpstreamClient(this.logger, info, this.authService, this.urlValidator);
97
99
  this.clients.set(info.id, client);
98
- this.logger.info({ upstreamId: info.id }, 'Registered upstream MCP');
100
+ this.logger.info({ upstreamId: info.id, totalRegistered: this.clients.size }, 'Registered upstream MCP');
101
+ }
102
+
103
+ registerHost(transport: StdioTransport) {
104
+ // NOTE: The host (VS Code) cannot receive tools/call requests - it's the CLIENT.
105
+ // We only register it for potential future use (e.g., sampling requests).
106
+ // DO NOT use the host as a tool provider fallback.
107
+ this.logger.debug('Host transport available but not registered as tool upstream (protocol limitation)');
99
108
  }
100
109
 
101
110
  async listToolPackages(): Promise<ToolPackage[]> {
102
- return Array.from(this.clients.entries()).map(([id, client]) => ({
111
+ const upstreams = Array.from(this.clients.entries()).map(([id, client]) => ({
103
112
  id,
104
- description: `Upstream ${id}`, // NOTE: Upstream description fetching deferred to V2
113
+ description: `Upstream ${id}`,
105
114
  version: '1.0.0'
106
115
  }));
116
+
117
+ return [
118
+ { id: 'conduit', description: 'Conduit built-in execution tools', version: '1.0.0' },
119
+ ...upstreams
120
+ ];
121
+ }
122
+
123
+ getBuiltInTools(): ToolSchema[] {
124
+ return BUILT_IN_TOOLS;
107
125
  }
108
126
 
109
127
  async listToolStubs(packageId: string, context: ExecutionContext): Promise<ToolStub[]> {
128
+ if (packageId === 'conduit') {
129
+ const stubs = BUILT_IN_TOOLS.map(t => ({
130
+ id: `conduit__${t.name}`,
131
+ name: t.name,
132
+ description: t.description
133
+ }));
134
+ if (context.allowedTools) {
135
+ return stubs.filter(t => this.policyService.isToolAllowed(t.id, context.allowedTools!));
136
+ }
137
+ return stubs;
138
+ }
139
+
110
140
  const client = this.clients.get(packageId);
111
141
  if (!client) {
112
142
  throw new Error(`Upstream package not found: ${packageId}`);
@@ -117,40 +147,40 @@ export class GatewayService {
117
147
  // Try manifest first if tools not cached
118
148
  if (!tools) {
119
149
  try {
120
- // Try to fetch manifest first
150
+ // Try to get manifest FIRST
121
151
  const manifest = await client.getManifest(context);
122
- if (manifest) {
123
- const stubs: ToolStub[] = manifest.tools.map((t: any) => ({
124
- id: `${packageId}__${t.name}`,
125
- name: t.name,
126
- description: t.description
127
- }));
128
-
129
- if (context.allowedTools) {
130
- return stubs.filter(t => this.policyService.isToolAllowed(t.id, context.allowedTools!));
152
+ if (manifest && manifest.tools) {
153
+ tools = manifest.tools as ToolSchema[];
154
+ } else {
155
+ // Fall back to RPC discovery
156
+ if (typeof (client as any).listTools === 'function') {
157
+ tools = await (client as any).listTools();
158
+ } else {
159
+ const response = await client.call({
160
+ jsonrpc: '2.0',
161
+ id: 'discovery',
162
+ method: 'tools/list',
163
+ }, context);
164
+
165
+ if (response.result?.tools) {
166
+ tools = response.result.tools as ToolSchema[];
167
+ } else {
168
+ this.logger.warn({ upstreamId: packageId, error: response.error }, 'Failed to discover tools via RPC');
169
+ }
131
170
  }
132
- return stubs;
133
171
  }
134
- } catch (e) {
135
- // Manifest fetch failed, fall back
136
- this.logger.debug({ packageId, err: e }, 'Manifest fetch failed, falling back to RPC');
137
- }
138
172
 
139
- const response = await client.call({
140
- jsonrpc: '2.0',
141
- id: 'discovery',
142
- method: 'list_tools',
143
- }, context);
144
-
145
- if (response.result?.tools) {
146
- tools = response.result.tools as ToolSchema[];
147
- this.schemaCache.set(packageId, tools);
148
- } else {
149
- this.logger.warn({ upstreamId: packageId, error: response.error }, 'Failed to discover tools from upstream');
150
- tools = [];
173
+ if (tools && tools.length > 0) {
174
+ this.schemaCache.set(packageId, tools);
175
+ this.logger.info({ upstreamId: packageId, toolCount: tools.length }, 'Discovered tools from upstream');
176
+ }
177
+ } catch (e: any) {
178
+ this.logger.error({ upstreamId: packageId, err: e.message }, 'Error during tool discovery');
151
179
  }
152
180
  }
153
181
 
182
+ if (!tools) tools = [];
183
+
154
184
  const stubs: ToolStub[] = tools.map(t => ({
155
185
  id: `${packageId}__${t.name}`,
156
186
  name: t.name,
@@ -170,17 +200,29 @@ export class GatewayService {
170
200
  }
171
201
 
172
202
  const parsed = this.policyService.parseToolName(toolId);
173
- const toolName = parsed.name; // Use a new variable for the un-namespaced name
174
-
175
- // Check for built-in tools
176
- const builtIn = BUILT_IN_TOOLS.find(t => t.name === toolId); // Compare with the full toolId
177
- if (builtIn) return builtIn;
203
+ const namespace = parsed.namespace;
204
+ const toolName = parsed.name;
205
+
206
+ // Check for built-in tools (now namespaced as conduit__*)
207
+ if (namespace === 'conduit' || namespace === '') {
208
+ const builtIn = BUILT_IN_TOOLS.find(t => t.name === toolName);
209
+ if (builtIn) {
210
+ return { ...builtIn, name: `conduit__${builtIn.name}` };
211
+ }
212
+ }
178
213
 
179
- const upstreamId = parsed.namespace;
214
+ const upstreamId = namespace;
215
+ if (!upstreamId) {
216
+ // Un-namespaced tool lookup: try all upstreams
217
+ for (const id of this.clients.keys()) {
218
+ const schema = await this.getToolSchema(`${id}__${toolName}`, context);
219
+ if (schema) return schema;
220
+ }
221
+ return null;
222
+ }
180
223
 
181
224
  // Ensure we have schemas for this upstream
182
225
  if (!this.schemaCache.get(upstreamId)) {
183
- // Force refresh if missing
184
226
  await this.listToolStubs(upstreamId, context);
185
227
  }
186
228
 
@@ -189,7 +231,6 @@ export class GatewayService {
189
231
 
190
232
  if (!tool) return null;
191
233
 
192
- // Return schema with namespaced name
193
234
  return {
194
235
  ...tool,
195
236
  name: toolId
@@ -197,41 +238,48 @@ export class GatewayService {
197
238
  }
198
239
 
199
240
  async discoverTools(context: ExecutionContext): Promise<ToolSchema[]> {
200
- const allTools: ToolSchema[] = [...BUILT_IN_TOOLS];
241
+ const allTools: ToolSchema[] = BUILT_IN_TOOLS.map(t => ({
242
+ ...t,
243
+ name: `conduit__${t.name}`
244
+ }));
245
+
246
+ this.logger.debug({ clientCount: this.clients.size, clientIds: Array.from(this.clients.keys()) }, 'Starting tool discovery');
201
247
 
202
248
  for (const [id, client] of this.clients.entries()) {
203
- let tools = this.schemaCache.get(id);
249
+ // Skip host - it's not a tool provider
250
+ if (id === 'host') {
251
+ continue;
252
+ }
204
253
 
205
- if (!tools) {
206
- const response = await client.call({
207
- jsonrpc: '2.0',
208
- id: 'discovery',
209
- method: 'list_tools', // Standard MCP method
210
- }, context);
254
+ this.logger.debug({ upstreamId: id }, 'Discovering tools from upstream');
211
255
 
212
- if (response.result?.tools) {
213
- tools = response.result.tools as ToolSchema[];
214
- this.schemaCache.set(id, tools);
215
- } else {
216
- this.logger.warn({ upstreamId: id, error: response.error }, 'Failed to discover tools from upstream');
217
- tools = [];
218
- }
256
+ // reuse unified discovery logic
257
+ try {
258
+ await this.listToolStubs(id, context);
259
+ } catch (e: any) {
260
+ this.logger.error({ upstreamId: id, err: e.message }, 'Failed to list tool stubs');
219
261
  }
262
+ const tools = this.schemaCache.get(id) || [];
220
263
 
221
- const prefixedTools = tools.map(t => ({ ...t, name: `${id}__${t.name}` }));
264
+ this.logger.debug({ upstreamId: id, toolCount: tools.length }, 'Discovery result');
222
265
 
223
- if (context.allowedTools) {
224
- // Support wildcard patterns: "mock.*" matches "mock__hello"
225
- allTools.push(...prefixedTools.filter(t => this.policyService.isToolAllowed(t.name, context.allowedTools!)));
226
- } else {
227
- allTools.push(...prefixedTools);
266
+ if (tools && tools.length > 0) {
267
+ const prefixedTools = tools.map(t => ({ ...t, name: `${id}__${t.name}` }));
268
+ if (context.allowedTools) {
269
+ allTools.push(...prefixedTools.filter(t => this.policyService.isToolAllowed(t.name, context.allowedTools!)));
270
+ } else {
271
+ allTools.push(...prefixedTools);
272
+ }
228
273
  }
229
274
  }
230
275
 
276
+ this.logger.info({ totalTools: allTools.length }, 'Tool discovery complete');
231
277
  return allTools;
232
278
  }
233
279
 
234
280
  async callTool(name: string, params: any, context: ExecutionContext): Promise<JSONRPCResponse> {
281
+ this.logger.debug({ name, upstreamCount: this.clients.size }, 'GatewayService.callTool called');
282
+
235
283
  if (context.allowedTools && !this.policyService.isToolAllowed(name, context.allowedTools)) {
236
284
  this.logger.warn({ name, allowedTools: context.allowedTools }, 'Tool call blocked by allowlist');
237
285
  return {
@@ -248,14 +296,43 @@ export class GatewayService {
248
296
  const upstreamId = toolId.namespace;
249
297
  const toolName = toolId.name;
250
298
 
299
+ this.logger.debug({ name, upstreamId, toolName }, 'Parsed tool name');
300
+
301
+ // Fallback for namespaceless calls: try to find the tool in any registered upstream
302
+ if (!upstreamId) {
303
+ this.logger.debug({ toolName }, 'Namespaceless call, attempting discovery across upstreams');
304
+ const allStubs = await this.discoverTools(context);
305
+ const found = allStubs.find(t => {
306
+ const parts = t.name.split('__');
307
+ return parts[parts.length - 1] === toolName;
308
+ });
309
+
310
+ if (found) {
311
+ this.logger.debug({ original: name, resolved: found.name }, 'Resolved namespaceless tool');
312
+ return this.callTool(found.name, params, context);
313
+ }
314
+
315
+ // No fallback to host - it doesn't support server-to-client tool calls
316
+ const upstreamList = Array.from(this.clients.keys()).filter(k => k !== 'host');
317
+ return {
318
+ jsonrpc: '2.0',
319
+ id: 0,
320
+ error: {
321
+ code: -32601,
322
+ message: `Tool '${toolName}' not found. Discovered ${allStubs.length} tools from upstreams: [${upstreamList.join(', ') || 'none'}]. Available tools: ${allStubs.map(t => t.name).slice(0, 10).join(', ')}${allStubs.length > 10 ? '...' : ''}`,
323
+ },
324
+ };
325
+ }
326
+
251
327
  const client = this.clients.get(upstreamId);
252
328
  if (!client) {
329
+ this.logger.error({ upstreamId, availableUpstreams: Array.from(this.clients.keys()) }, 'Upstream not found');
253
330
  return {
254
331
  jsonrpc: '2.0',
255
332
  id: 0,
256
333
  error: {
257
334
  code: -32003,
258
- message: `Upstream not found: ${upstreamId}`,
335
+ message: `Upstream not found: '${upstreamId}'. Available: ${Array.from(this.clients.keys()).join(', ') || 'none'}`,
259
336
  },
260
337
  };
261
338
  }
@@ -319,7 +396,7 @@ export class GatewayService {
319
396
  response = await client.call({
320
397
  jsonrpc: '2.0',
321
398
  id: context.correlationId,
322
- method: 'call_tool',
399
+ method: 'tools/call',
323
400
  params: {
324
401
  name: toolName,
325
402
  arguments: params,
@@ -352,7 +429,7 @@ export class GatewayService {
352
429
  const response = await client.call({
353
430
  jsonrpc: '2.0',
354
431
  id: 'health',
355
- method: 'list_tools',
432
+ method: 'tools/list',
356
433
  }, context);
357
434
  upstreamStatus[id] = response.error ? 'degraded' : 'active';
358
435
  } catch (err) {
@@ -0,0 +1,65 @@
1
+ import { Logger } from 'pino';
2
+ import { JSONRPCRequest, JSONRPCResponse } from '../core/types.js';
3
+ import { ExecutionContext } from '../core/execution.context.js';
4
+ import { StdioTransport } from '../transport/stdio.transport.js';
5
+
6
+ /**
7
+ * HostClient - Proxies tool calls back to the MCP client (e.g. VS Code)
8
+ * that is hosting this Conduit process.
9
+ */
10
+ export class HostClient {
11
+ constructor(
12
+ private logger: Logger,
13
+ private transport: StdioTransport
14
+ ) { }
15
+
16
+ async call(request: JSONRPCRequest, context: ExecutionContext): Promise<JSONRPCResponse> {
17
+ try {
18
+ this.logger.debug({ method: request.method }, 'Forwarding request to host');
19
+
20
+ let method = request.method;
21
+ let params = request.params;
22
+
23
+ // Bridge mcp_* calls to standard MCP calls for the host
24
+ if (method === 'mcp_call_tool' || method === 'call_tool') {
25
+ method = 'tools/call';
26
+ } else if (method === 'mcp_discover_tools' || method === 'discover_tools') {
27
+ method = 'tools/list';
28
+ params = {};
29
+ }
30
+
31
+ const result = await this.transport.callHost(method, params);
32
+
33
+ return {
34
+ jsonrpc: '2.0',
35
+ id: request.id,
36
+ result
37
+ };
38
+ } catch (error: any) {
39
+ this.logger.error({ err: error.message }, 'Host call failed');
40
+ return {
41
+ jsonrpc: '2.0',
42
+ id: request.id,
43
+ error: {
44
+ code: -32008,
45
+ message: `Host error: ${error.message}`,
46
+ },
47
+ };
48
+ }
49
+ }
50
+
51
+ async listTools(): Promise<any[]> {
52
+ try {
53
+ this.logger.debug('Fetching tool list from host');
54
+ const result = await this.transport.callHost('tools/list', {});
55
+ return result.tools || [];
56
+ } catch (error: any) {
57
+ this.logger.warn({ err: error.message }, 'Failed to fetch tools from host');
58
+ return [];
59
+ }
60
+ }
61
+
62
+ async getManifest() {
63
+ return null;
64
+ }
65
+ }
@@ -24,6 +24,7 @@ export class UpstreamClient {
24
24
  private urlValidator: IUrlValidator;
25
25
  private mcpClient?: Client;
26
26
  private transport?: StdioClientTransport;
27
+ private connected: boolean = false;
27
28
 
28
29
  constructor(logger: Logger, info: UpstreamInfo, authService: AuthService, urlValidator: IUrlValidator) {
29
30
  this.logger = logger.child({ upstreamId: info.id });
@@ -55,18 +56,16 @@ export class UpstreamClient {
55
56
 
56
57
  private async ensureConnected() {
57
58
  if (!this.mcpClient || !this.transport) return;
58
- // There isn't a public isConnected property easily accessible,
59
- // usually we just connect once.
60
- // We can track connected state or just try/catch connect.
61
- // For simplicity, we connect once and existing sdk handles reconnection or errors usually kill it.
62
- // Actually SDK Client.connect() is for the transport.
59
+ if (this.connected) return;
60
+
63
61
  try {
64
- // @ts-ignore - Check internal state or just attempt connect if we haven't
65
- if (!this.transport.connection) {
66
- await this.mcpClient.connect(this.transport);
67
- }
68
- } catch (e) {
69
- // connection might already be active
62
+ this.logger.debug('Connecting to upstream transport...');
63
+ await this.mcpClient.connect(this.transport);
64
+ this.connected = true;
65
+ this.logger.info('Connected to upstream MCP');
66
+ } catch (e: any) {
67
+ this.logger.error({ err: e.message }, 'Failed to connect to upstream');
68
+ throw e;
70
69
  }
71
70
  }
72
71
 
@@ -90,23 +89,31 @@ export class UpstreamClient {
90
89
  await this.ensureConnected();
91
90
 
92
91
  // Map GatewayService method names to SDK typed methods
93
- if (request.method === 'list_tools') {
92
+ if (request.method === 'list_tools' || request.method === 'tools/list') {
94
93
  const result = await this.mcpClient.listTools();
95
94
  return {
96
95
  jsonrpc: '2.0',
97
96
  id: request.id,
98
97
  result: result
99
98
  };
100
- } else if (request.method === 'call_tool') {
99
+ } else if (request.method === 'call_tool' || request.method === 'tools/call') {
101
100
  const params = request.params as { name: string; arguments?: Record<string, unknown> };
102
101
  const result = await this.mcpClient.callTool({
103
102
  name: params.name,
104
103
  arguments: params.arguments,
105
104
  });
105
+ const normalizedResult = (result && Array.isArray((result as any).content))
106
+ ? result
107
+ : {
108
+ content: [{
109
+ type: 'text',
110
+ text: typeof result === 'string' ? result : JSON.stringify(result ?? null),
111
+ }],
112
+ };
106
113
  return {
107
114
  jsonrpc: '2.0',
108
115
  id: request.id,
109
- result: result
116
+ result: normalizedResult
110
117
  };
111
118
  } else {
112
119
  // Fallback to generic request for other methods
package/src/index.ts CHANGED
@@ -29,9 +29,10 @@ program
29
29
  .command('serve', { isDefault: true })
30
30
  .description('Start the Conduit server')
31
31
  .option('--stdio', 'Use stdio transport')
32
+ .option('--config <path>', 'Path to config file')
32
33
  .action(async (options) => {
33
34
  try {
34
- await startServer();
35
+ await startServer(options);
35
36
  } catch (err) {
36
37
  console.error('Failed to start Conduit:', err);
37
38
  process.exit(1);
@@ -43,10 +44,12 @@ program
43
44
  .description('Help set up OAuth for an upstream MCP server')
44
45
  .requiredOption('--client-id <id>', 'OAuth Client ID')
45
46
  .requiredOption('--client-secret <secret>', 'OAuth Client Secret')
46
- .requiredOption('--auth-url <url>', 'OAuth Authorization URL')
47
- .requiredOption('--token-url <url>', 'OAuth Token URL')
47
+ .option('--auth-url <url>', 'OAuth Authorization URL')
48
+ .option('--token-url <url>', 'OAuth Token URL')
49
+ .option('--mcp-url <url>', 'MCP base URL (auto-discover OAuth metadata)')
48
50
  .option('--scopes <scopes>', 'OAuth Scopes (comma separated)')
49
51
  .option('--port <port>', 'Port for the local callback server', '3333')
52
+ .option('--pkce', 'Use PKCE for the authorization code flow')
50
53
  .action(async (options) => {
51
54
  try {
52
55
  await handleAuth({
@@ -54,8 +57,10 @@ program
54
57
  clientSecret: options.clientSecret,
55
58
  authUrl: options.authUrl,
56
59
  tokenUrl: options.tokenUrl,
60
+ mcpUrl: options.mcpUrl,
57
61
  scopes: options.scopes,
58
62
  port: parseInt(options.port, 10),
63
+ usePkce: options.pkce || Boolean(options.mcpUrl),
59
64
  });
60
65
  console.log('\nSuccess! Configuration generated.');
61
66
  } catch (err: any) {
@@ -64,8 +69,13 @@ program
64
69
  }
65
70
  });
66
71
 
67
- async function startServer() {
68
- const configService = new ConfigService();
72
+ async function startServer(options: any = {}) {
73
+ // Merge command line options into config overrides
74
+ const overrides: any = {};
75
+ if (options.stdio) overrides.transport = 'stdio';
76
+ if (options.config) process.env.CONFIG_FILE = options.config;
77
+
78
+ const configService = new ConfigService(overrides);
69
79
  const logger = createLogger(configService);
70
80
 
71
81
  const otelService = new OtelService(logger);
@@ -80,6 +90,7 @@ async function startServer() {
80
90
 
81
91
  const gatewayService = new GatewayService(logger, securityService);
82
92
  const upstreams = configService.get('upstreams') || [];
93
+ logger.info({ upstreamCount: upstreams.length, upstreamIds: upstreams.map((u: any) => u.id) }, 'Registering upstreams from config');
83
94
  for (const upstream of upstreams) {
84
95
  gatewayService.registerUpstream(upstream);
85
96
  }
@@ -118,15 +129,30 @@ async function startServer() {
118
129
  let address: string;
119
130
 
120
131
  if (configService.get('transport') === 'stdio') {
121
- transport = new StdioTransport(logger, requestController, concurrencyService);
132
+ const stdioTransport = new StdioTransport(logger, requestController, concurrencyService);
133
+ transport = stdioTransport;
122
134
  await transport.start();
135
+ gatewayService.registerHost(stdioTransport);
123
136
  address = 'stdio';
137
+
138
+ // IMPORTANT: Even in stdio mode, we need a local socket for sandboxes to talk to
139
+ const internalTransport = new SocketTransport(logger, requestController, concurrencyService);
140
+ const internalPort = 0; // Random available port
141
+ const internalAddress = await internalTransport.listen({ port: internalPort });
142
+ executionService.ipcAddress = internalAddress;
143
+
144
+ // Register internal transport for shutdown
145
+ const originalShutdown = transport.close.bind(transport);
146
+ transport.close = async () => {
147
+ await originalShutdown();
148
+ await internalTransport.close();
149
+ };
124
150
  } else {
125
151
  transport = new SocketTransport(logger, requestController, concurrencyService);
126
152
  const port = configService.get('port');
127
153
  address = await transport.listen({ port });
154
+ executionService.ipcAddress = address;
128
155
  }
129
- executionService.ipcAddress = address;
130
156
 
131
157
  // Pre-warm workers
132
158
  await requestController.warmup();