@mhingston5/conduit 1.1.0 → 1.1.2

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 CHANGED
@@ -75,7 +75,7 @@ upstreams:
75
75
 
76
76
  ### 3. Execute TypeScript
77
77
 
78
- Using any [MCP Client](https://modelcontextprotocol.io/clients) (Claude Desktop, etc.), call `mcp.executeTypeScript`:
78
+ Using any [MCP Client](https://modelcontextprotocol.io/clients) (Claude Desktop, etc.), call `mcp_execute_typescript`:
79
79
 
80
80
  ```ts
81
81
  // The agent writes this code:
@@ -175,6 +175,21 @@ By default, Conduit runs in **Permissive Mode** to allow easy exploration.
175
175
  - permissive mode for exploration
176
176
  - strict mode for production agents
177
177
 
178
+ ## Code Mode Prompting Guide
179
+
180
+ To get the most out of Conduit, you should guide your LLM (agent) to prefer multi-tool scripts over individual tool calls.
181
+
182
+ **Recommended System Prompt Additions:**
183
+ > You are equipped with a "Code Mode" execution environment via Conduit.
184
+ >
185
+ > - **Prefer `mcp_execute_typescript`**: Use this for any task requiring multiple steps, data transformation, or logical branching.
186
+ > - **Use `tools.*` SDK**: Within your scripts, access all upstream tools via the `tools` namespace (e.g., `await tools.github.create_issue(...)`).
187
+ > - **Avoid JSON tool-calling overhead**: Instead of making 5 separate tool calls and waiting for 5 round-trips, write one script that orchestrates the entire operation.
188
+ > - **Data Transformation**: Perform loops, filters, and aggregations directly in your code rather than asking the user (or yourself) to process large datasets in the chat context.
189
+
190
+ Example of a high-efficiency prompt:
191
+ *"Scan the last 50 emails for invoices, total their amounts by currency, and create a single summary ticket in Jira if the total exceeds $1000."*
192
+
178
193
  ---
179
194
 
180
195
  ## Design Principles
@@ -12,7 +12,7 @@ Conduit is built with a modular architecture, designed to be secure, observable,
12
12
 
13
13
  ## Detailed Flow
14
14
 
15
- 1. **Client Request**: A client (like VS Code or Claude Desktop) sends a JSON-RPC request (`mcp.executeTypeScript`).
15
+ 1. **Client Request**: A client (like VS Code or Claude Desktop) sends a JSON-RPC request (`mcp_execute_typescript`).
16
16
  2. **Transportation**: The request is received via `SocketTransport` (TCP/UDS/Pipe).
17
17
  3. **Dispatch**: `RequestController` validates the request and session tokens.
18
18
  4. **Tool Discovery**: `GatewayService` aggregates tools from all upstream MCP servers.
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhingston5/conduit",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "type": "module",
5
5
  "description": "A secure Code Mode execution substrate for MCP agents",
6
6
  "main": "index.js",
@@ -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
 
@@ -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);
@@ -209,6 +210,17 @@ export class RequestController {
209
210
 
210
211
  private async handleCallTool(params: any, context: ExecutionContext, id: string | number): Promise<JSONRPCResponse> {
211
212
  const { name, arguments: toolArgs } = params;
213
+
214
+ // Route built-in tools to their specific handlers
215
+ switch (name) {
216
+ case 'mcp_execute_typescript':
217
+ return this.handleExecuteTypeScript(toolArgs, context, id);
218
+ case 'mcp_execute_python':
219
+ return this.handleExecutePython(toolArgs, context, id);
220
+ case 'mcp_execute_isolate':
221
+ return this.handleExecuteIsolate(toolArgs, context, id);
222
+ }
223
+
212
224
  const response = await this.gatewayService.callTool(name, toolArgs, context);
213
225
  return { ...response, id };
214
226
  }
@@ -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) {
@@ -10,6 +10,66 @@ import { PolicyService, ToolIdentifier } from '../core/policy.service.js';
10
10
  import { Ajv } from 'ajv';
11
11
  import addFormats from 'ajv-formats';
12
12
 
13
+ const BUILT_IN_TOOLS: ToolSchema[] = [
14
+ {
15
+ name: 'mcp_execute_typescript',
16
+ description: 'Executes TypeScript code in a secure sandbox with access to `tools.*` SDK.',
17
+ inputSchema: {
18
+ type: 'object',
19
+ properties: {
20
+ code: {
21
+ type: 'string',
22
+ description: 'The TypeScript code to execute.'
23
+ },
24
+ allowedTools: {
25
+ type: 'array',
26
+ items: { type: 'string' },
27
+ description: 'Optional list of tools the script is allowed to call (e.g. ["github.*"]).'
28
+ }
29
+ },
30
+ required: ['code']
31
+ }
32
+ },
33
+ {
34
+ name: 'mcp_execute_python',
35
+ description: 'Executes Python code in a secure sandbox with access to `tools.*` SDK.',
36
+ inputSchema: {
37
+ type: 'object',
38
+ properties: {
39
+ code: {
40
+ type: 'string',
41
+ description: 'The Python code to execute.'
42
+ },
43
+ allowedTools: {
44
+ type: 'array',
45
+ items: { type: 'string' },
46
+ description: 'Optional list of tools the script is allowed to call (e.g. ["github.*"]).'
47
+ }
48
+ },
49
+ required: ['code']
50
+ }
51
+ },
52
+ {
53
+ name: 'mcp_execute_isolate',
54
+ description: 'Executes JavaScript code in a high-speed V8 isolate (no Deno/Node APIs).',
55
+ inputSchema: {
56
+ type: 'object',
57
+ properties: {
58
+ code: {
59
+ type: 'string',
60
+ description: 'The JavaScript code to execute.'
61
+ },
62
+ allowedTools: {
63
+ type: 'array',
64
+ items: { type: 'string' },
65
+ description: 'Optional list of tools the script is allowed to call.'
66
+ }
67
+ },
68
+ required: ['code']
69
+ }
70
+ }
71
+ ];
72
+
13
73
  export class GatewayService {
14
74
  private logger: Logger;
15
75
  private clients: Map<string, UpstreamClient> = new Map();
@@ -110,8 +170,13 @@ export class GatewayService {
110
170
  }
111
171
 
112
172
  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;
178
+
113
179
  const upstreamId = parsed.namespace;
114
- const toolName = parsed.name;
115
180
 
116
181
  // Ensure we have schemas for this upstream
117
182
  if (!this.schemaCache.get(upstreamId)) {
@@ -132,7 +197,7 @@ export class GatewayService {
132
197
  }
133
198
 
134
199
  async discoverTools(context: ExecutionContext): Promise<ToolSchema[]> {
135
- const allTools: ToolSchema[] = [];
200
+ const allTools: ToolSchema[] = [...BUILT_IN_TOOLS];
136
201
 
137
202
  for (const [id, client] of this.clients.entries()) {
138
203
  let tools = this.schemaCache.get(id);
@@ -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.*'],
@@ -149,7 +149,7 @@ describe('E2E: Stdio Upstream Integration', () => {
149
149
  const response = await sendRequest({
150
150
  jsonrpc: '2.0',
151
151
  id: '1',
152
- method: 'mcp.discoverTools',
152
+ method: 'mcp_discover_tools',
153
153
  params: {},
154
154
  auth: { bearerToken: ipcToken },
155
155
  });
@@ -169,7 +169,7 @@ describe('E2E: Stdio Upstream Integration', () => {
169
169
  const discoverResponse = await sendRequest({
170
170
  jsonrpc: '2.0',
171
171
  id: '1',
172
- method: 'mcp.discoverTools',
172
+ method: 'mcp_discover_tools',
173
173
  params: {},
174
174
  auth: { bearerToken: ipcToken },
175
175
  });
@@ -181,7 +181,7 @@ describe('E2E: Stdio Upstream Integration', () => {
181
181
  const callResponse = await sendRequest({
182
182
  jsonrpc: '2.0',
183
183
  id: '2',
184
- method: 'mcp.callTool',
184
+ method: 'mcp_call_tool',
185
185
  params: {
186
186
  name: echoTool.name,
187
187
  arguments: { message: 'Hello from E2E test!' },
@@ -31,11 +31,21 @@ describe('GatewayService', () => {
31
31
  });
32
32
 
33
33
  const tools = await gateway.discoverTools(context);
34
- expect(tools).toHaveLength(2);
34
+ expect(tools.length).toBeGreaterThanOrEqual(3);
35
+ expect(tools.find(t => t.name === 'mcp_execute_typescript')).toBeDefined();
36
+ expect(tools.find(t => t.name === 'mcp_execute_python')).toBeDefined();
37
+ expect(tools.find(t => t.name === 'mcp_execute_isolate')).toBeDefined();
35
38
  expect(tools.find(t => t.name === 'u1__t1')).toBeDefined();
36
39
  expect(tools.find(t => t.name === 'u2__t2')).toBeDefined();
37
40
  });
38
41
 
42
+ it('should return schema for built-in tools', async () => {
43
+ const schema = await gateway.getToolSchema('mcp_execute_typescript', context);
44
+ expect(schema).toBeDefined();
45
+ expect(schema?.name).toBe('mcp_execute_typescript');
46
+ expect(schema?.inputSchema.required).toContain('code');
47
+ });
48
+
39
49
  it('should route tool calls to correct upstream', async () => {
40
50
  gateway.registerUpstream({ id: 'u1', url: 'http://u1' });
41
51
 
@@ -92,7 +92,7 @@ describe('V1 Hardening Tests', () => {
92
92
  const response = await sendRequest({
93
93
  jsonrpc: '2.0',
94
94
  id: 1,
95
- method: 'mcp.executeTypeScript',
95
+ method: 'mcp_execute_typescript',
96
96
  params: { code: 'console.log("hi")' },
97
97
  auth: { bearerToken: sessionToken }
98
98
  });
@@ -114,7 +114,7 @@ describe('V1 Hardening Tests', () => {
114
114
  const response = await sendRequest({
115
115
  jsonrpc: '2.0',
116
116
  id: 2,
117
- method: 'mcp.discoverTools',
117
+ method: 'mcp_discover_tools',
118
118
  params: {},
119
119
  auth: { bearerToken: sessionToken }
120
120
  });
@@ -127,7 +127,7 @@ describe('V1 Hardening Tests', () => {
127
127
  const response = await sendRequest({
128
128
  jsonrpc: '2.0',
129
129
  id: 3,
130
- method: 'mcp.executeTypeScript',
130
+ method: 'mcp_execute_typescript',
131
131
  params: { code: 'import * as os from "os"; console.log("hi")' },
132
132
  auth: { bearerToken: 'master-token' }
133
133
  });
@@ -84,7 +84,7 @@ describe('RequestController Routing', () => {
84
84
  const result = await controller.handleRequest({
85
85
  jsonrpc: '2.0',
86
86
  id: 1,
87
- method: 'mcp.executeTypeScript',
87
+ method: 'mcp_execute_typescript',
88
88
  params: {
89
89
  code: 'console.log("simple")',
90
90
  limits: {}
@@ -101,7 +101,7 @@ describe('RequestController Routing', () => {
101
101
  const result = await controller.handleRequest({
102
102
  jsonrpc: '2.0',
103
103
  id: 1,
104
- method: 'mcp.executeTypeScript',
104
+ method: 'mcp_execute_typescript',
105
105
  params: {
106
106
  code: 'import { foo } from "bar"; console.log(foo)',
107
107
  limits: {}
@@ -118,7 +118,7 @@ describe('RequestController Routing', () => {
118
118
  const result = await controller.handleRequest({
119
119
  jsonrpc: '2.0',
120
120
  id: 1,
121
- method: 'mcp.executeTypeScript',
121
+ method: 'mcp_execute_typescript',
122
122
  params: {
123
123
  code: 'export const foo = "bar"',
124
124
  limits: {}
@@ -134,7 +134,7 @@ describe('RequestController Routing', () => {
134
134
  const result = await controller.handleRequest({
135
135
  jsonrpc: '2.0',
136
136
  id: 1,
137
- method: 'mcp.executeTypeScript',
137
+ method: 'mcp_execute_typescript',
138
138
  params: {
139
139
  code: 'console.log(Deno.version)',
140
140
  limits: {}
@@ -145,4 +145,24 @@ describe('RequestController Routing', () => {
145
145
  expect(mockDenoExecutor.execute).toHaveBeenCalled();
146
146
  expect(mockIsolateExecutor.execute).not.toHaveBeenCalled();
147
147
  });
148
+
149
+ it('should route tools/call for built-in tools', async () => {
150
+ const result = await controller.handleRequest({
151
+ jsonrpc: '2.0',
152
+ id: 1,
153
+ method: 'tools/call',
154
+ params: {
155
+ name: 'mcp_execute_typescript',
156
+ arguments: {
157
+ code: 'console.log("via tools/call")',
158
+ limits: {}
159
+ }
160
+ },
161
+ auth: { bearerToken: 'master-token' }
162
+ }, mockContext);
163
+
164
+ expect(mockIsolateExecutor.execute).toHaveBeenCalled();
165
+ expect(result!.result.stdout).toBe('isolate');
166
+ });
167
+
148
168
  });
@@ -100,7 +100,7 @@ describe('SocketTransport', () => {
100
100
  });
101
101
  }
102
102
 
103
- it('should handle mcp.executeTypeScript request', async () => {
103
+ it('should handle mcp_execute_typescript request', async () => {
104
104
  transport = new SocketTransport(logger, requestController, concurrencyService);
105
105
  const address = await transport.listen({ port: 0 });
106
106
  const portMatch = address.match(/:(\d+)$/);
@@ -112,7 +112,7 @@ describe('SocketTransport', () => {
112
112
  const request = {
113
113
  jsonrpc: '2.0',
114
114
  id: 1,
115
- method: 'mcp.executeTypeScript',
115
+ method: 'mcp_execute_typescript',
116
116
  params: { code: 'console.log("hello E2E")' },
117
117
  auth: { bearerToken: testToken }
118
118
  };
@@ -153,7 +153,7 @@ describe('SocketTransport', () => {
153
153
  const request = {
154
154
  jsonrpc: '2.0',
155
155
  id: 12345,
156
- method: 'mcp.executeTypeScript',
156
+ method: 'mcp_execute_typescript',
157
157
  params: { code: 'console.log("hello")' },
158
158
  auth: { bearerToken: testToken }
159
159
  };
@@ -24,7 +24,7 @@ describe('E2E: Native Stdio Mode', () => {
24
24
  const request = {
25
25
  jsonrpc: '2.0',
26
26
  id: '1',
27
- method: 'mcp.discoverTools',
27
+ method: 'mcp_discover_tools',
28
28
  params: {},
29
29
  // Use a dummy token, security service might reject if auth is enabled but
30
30
  // the default config generates a random token.