@mhingston5/conduit 1.1.5 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhingston5/conduit",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "type": "module",
5
5
  "description": "A secure Code Mode execution substrate for MCP agents",
6
6
  "main": "index.js",
@@ -134,10 +134,14 @@ export class ConfigService {
134
134
  (fs.existsSync(path.resolve(process.cwd(), 'conduit.yaml')) ? 'conduit.yaml' :
135
135
  (fs.existsSync(path.resolve(process.cwd(), 'conduit.json')) ? 'conduit.json' : null));
136
136
 
137
- if (!configPath) return {};
137
+ if (!configPath) {
138
+ console.warn(`[Conduit] No config file found in ${process.cwd()}. Running with default settings.`);
139
+ return {};
140
+ }
138
141
 
139
142
  try {
140
143
  const fullPath = path.resolve(process.cwd(), configPath);
144
+ console.error(`[Conduit] Loading config from ${fullPath}`);
141
145
  let fileContent = fs.readFileSync(fullPath, 'utf-8');
142
146
 
143
147
  // Env var substitution: ${VAR} or ${VAR:-default}
@@ -137,17 +137,22 @@ export class ExecutionService {
137
137
  const packages = await this.gatewayService.listToolPackages();
138
138
  const allBindings = [];
139
139
 
140
+ this.logger.debug({ packageCount: packages.length, packages: packages.map(p => p.id) }, 'Fetching tool bindings');
141
+
140
142
  for (const pkg of packages) {
141
143
  try {
142
144
  // Determine if we need to fetch tools for this package
143
145
  // Optimization: if allowedTools is strict, we could filter packages here
144
146
 
145
147
  const stubs = await this.gatewayService.listToolStubs(pkg.id, context);
148
+ this.logger.debug({ packageId: pkg.id, stubCount: stubs.length }, 'Got stubs from package');
146
149
  allBindings.push(...stubs.map(s => toToolBinding(s.id, undefined, s.description)));
147
150
  } catch (err: any) {
148
151
  this.logger.warn({ packageId: pkg.id, err: err.message }, 'Failed to list stubs for package');
149
152
  }
150
153
  }
154
+
155
+ this.logger.info({ totalBindings: allBindings.length }, 'Tool bindings ready for SDK generation');
151
156
  return allBindings;
152
157
  }
153
158
 
@@ -66,6 +66,11 @@ export class PolicyService {
66
66
  return true;
67
67
  }
68
68
 
69
+ // Improved matching: if pattern has only one part, match it against the tool's name part
70
+ if (patternParts.length === 1 && toolParts.length > 1) {
71
+ return patternParts[0] === toolParts[toolParts.length - 1];
72
+ }
73
+
69
74
  // Exact match: pattern parts must equal tool parts
70
75
  if (patternParts.length !== toolParts.length) return false;
71
76
  for (let i = 0; i < patternParts.length; i++) {
@@ -128,6 +128,8 @@ export class RequestController {
128
128
  return this.handleInitialize(params, context, id);
129
129
  case 'notifications/initialized':
130
130
  return null; // Notifications don't get responses per MCP spec
131
+ case 'mcp_register_upstream':
132
+ return this.handleRegisterUpstream(params, context, id);
131
133
  case 'ping':
132
134
  return { jsonrpc: '2.0', id, result: {} };
133
135
  default:
@@ -138,6 +140,23 @@ export class RequestController {
138
140
  }
139
141
  }
140
142
 
143
+ private async handleRegisterUpstream(params: any, context: ExecutionContext, id: string | number): Promise<JSONRPCResponse> {
144
+ if (!params || !params.id || !params.type || (!params.url && !params.command)) {
145
+ return this.errorResponse(id, -32602, 'Missing registration parameters (id, type, url/command)');
146
+ }
147
+
148
+ try {
149
+ this.gatewayService.registerUpstream(params);
150
+ return {
151
+ jsonrpc: '2.0',
152
+ id,
153
+ result: { success: true }
154
+ };
155
+ } catch (err: any) {
156
+ return this.errorResponse(id, -32001, err.message);
157
+ }
158
+ }
159
+
141
160
  private async handleDiscoverTools(params: any, context: ExecutionContext, id: string | number): Promise<JSONRPCResponse> {
142
161
  const tools = await this.gatewayService.discoverTools(context);
143
162
 
@@ -215,14 +234,20 @@ export class RequestController {
215
234
  if (!params) return this.errorResponse(id, -32602, 'Missing parameters');
216
235
  const { name, arguments: toolArgs } = params;
217
236
 
237
+ const toolId = this.gatewayService.policyService.parseToolName(name);
238
+ const baseName = toolId.name;
239
+ const isConduit = toolId.namespace === 'conduit' || toolId.namespace === '';
240
+
218
241
  // Route built-in tools to their specific handlers
219
- switch (name) {
220
- case 'mcp_execute_typescript':
221
- return this.handleExecuteToolCall('typescript', toolArgs, context, id);
222
- case 'mcp_execute_python':
223
- return this.handleExecuteToolCall('python', toolArgs, context, id);
224
- case 'mcp_execute_isolate':
225
- return this.handleExecuteToolCall('isolate', toolArgs, context, id);
242
+ if (isConduit) {
243
+ switch (baseName) {
244
+ case 'mcp_execute_typescript':
245
+ return this.handleExecuteToolCall('typescript', toolArgs, context, id);
246
+ case 'mcp_execute_python':
247
+ return this.handleExecuteToolCall('python', toolArgs, context, id);
248
+ case 'mcp_execute_isolate':
249
+ return this.handleExecuteToolCall('isolate', toolArgs, context, id);
250
+ }
226
251
  }
227
252
 
228
253
  const response = await this.gatewayService.callTool(name, toolArgs, context);
@@ -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';
@@ -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: 'tools/list',
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: 'tools/list', // 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
  }
@@ -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
 
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);
@@ -68,8 +69,13 @@ program
68
69
  }
69
70
  });
70
71
 
71
- async function startServer() {
72
- 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);
73
79
  const logger = createLogger(configService);
74
80
 
75
81
  const otelService = new OtelService(logger);
@@ -84,6 +90,7 @@ async function startServer() {
84
90
 
85
91
  const gatewayService = new GatewayService(logger, securityService);
86
92
  const upstreams = configService.get('upstreams') || [];
93
+ logger.info({ upstreamCount: upstreams.length, upstreamIds: upstreams.map((u: any) => u.id) }, 'Registering upstreams from config');
87
94
  for (const upstream of upstreams) {
88
95
  gatewayService.registerUpstream(upstream);
89
96
  }
@@ -122,8 +129,10 @@ async function startServer() {
122
129
  let address: string;
123
130
 
124
131
  if (configService.get('transport') === 'stdio') {
125
- transport = new StdioTransport(logger, requestController, concurrencyService);
132
+ const stdioTransport = new StdioTransport(logger, requestController, concurrencyService);
133
+ transport = stdioTransport;
126
134
  await transport.start();
135
+ gatewayService.registerHost(stdioTransport);
127
136
  address = 'stdio';
128
137
 
129
138
  // IMPORTANT: Even in stdio mode, we need a local socket for sandboxes to talk to