@teampitch/mcpx 0.2.1 → 0.3.1

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": "@teampitch/mcpx",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Self-hosted MCP Code Mode gateway — aggregate multiple MCP servers behind 2 tools with V8 isolate execution",
5
5
  "keywords": [
6
6
  "claude",
@@ -1,4 +1,5 @@
1
1
  import { describe, test, expect } from "bun:test";
2
+
2
3
  import {
3
4
  generateTypeDefinitions,
4
5
  generateToolListing,
@@ -49,7 +50,7 @@ describe("generateTypeDefinitions", () => {
49
50
  test("returns header comment for empty backends", () => {
50
51
  const backends = new Map<string, Backend>();
51
52
  const result = generateTypeDefinitions(backends);
52
- expect(result).toContain("Available MCP tool functions");
53
+ expect(result).toContain("Available tool functions");
53
54
  });
54
55
 
55
56
  test("generates declare function for each tool", () => {
@@ -59,7 +60,9 @@ describe("generateTypeDefinitions", () => {
59
60
  name: "search_dashboards",
60
61
  description: "Search dashboards",
61
62
  inputSchema: {
62
- properties: { query: { type: "string", description: "Search query" } },
63
+ properties: {
64
+ query: { type: "string", description: "Search query" },
65
+ },
63
66
  required: ["query"],
64
67
  },
65
68
  },
package/src/backends.ts CHANGED
@@ -3,6 +3,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
3
3
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
4
 
5
5
  import type { BackendConfig } from "./config.js";
6
+ import { getAccessToken } from "./oauth-client.js";
6
7
  import { createOpenApiBackend } from "./openapi.js";
7
8
 
8
9
  export interface ToolInfo {
@@ -42,11 +43,23 @@ async function connectStdio(name: string, config: BackendConfig): Promise<Backen
42
43
  }
43
44
 
44
45
  /** Connect to a backend MCP server via HTTP (Streamable HTTP) */
45
- async function connectHttp(name: string, config: BackendConfig): Promise<Backend> {
46
+ async function connectHttp(
47
+ name: string,
48
+ config: BackendConfig,
49
+ tokensDir?: string,
50
+ ): Promise<Backend> {
46
51
  if (!config.url) throw new Error(`Backend "${name}" missing url`);
47
52
 
53
+ // Build headers — static headers + OAuth token if configured
54
+ const headers: Record<string, string> = { ...config.headers };
55
+
56
+ if (config.oauth && tokensDir) {
57
+ const token = await getAccessToken(name, config.url, tokensDir, config.oauth);
58
+ headers.Authorization = `Bearer ${token}`;
59
+ }
60
+
48
61
  const transport = new StreamableHTTPClientTransport(new URL(config.url), {
49
- requestInit: config.headers ? { headers: config.headers } : undefined,
62
+ requestInit: Object.keys(headers).length > 0 ? { headers } : undefined,
50
63
  });
51
64
 
52
65
  const client = new Client({ name: `mcpx-${name}`, version: "0.1.0" });
@@ -66,6 +79,7 @@ async function connectHttp(name: string, config: BackendConfig): Promise<Backend
66
79
  /** Connect to all configured backends */
67
80
  export async function connectBackends(
68
81
  configs: Record<string, BackendConfig>,
82
+ opts?: { tokensDir?: string },
69
83
  ): Promise<Map<string, Backend>> {
70
84
  const backends = new Map<string, Backend>();
71
85
 
@@ -75,7 +89,7 @@ export async function connectBackends(
75
89
  const backend = await connectStdio(name, config);
76
90
  backends.set(name, backend);
77
91
  } else if (config.transport === "http") {
78
- const backend = await connectHttp(name, config);
92
+ const backend = await connectHttp(name, config, opts?.tokensDir);
79
93
  backends.set(name, backend);
80
94
  } else if (config.transport === "openapi") {
81
95
  const backend = await createOpenApiBackend(name, config);
@@ -110,33 +124,66 @@ export async function refreshAllTools(backends: Map<string, Backend>): Promise<v
110
124
  }
111
125
  }
112
126
 
127
+ /** Map a JSON Schema type to a TypeScript type string */
128
+ function schemaTypeToTs(schema: Record<string, unknown>): string {
129
+ const type = schema.type as string | undefined;
130
+ if (schema.enum) return (schema.enum as unknown[]).map((v) => JSON.stringify(v)).join(" | ");
131
+ if (type === "array") {
132
+ const items = schema.items as Record<string, unknown> | undefined;
133
+ return items ? `${schemaTypeToTs(items)}[]` : "any[]";
134
+ }
135
+ if (type === "object") {
136
+ const props = schema.properties as Record<string, Record<string, unknown>> | undefined;
137
+ if (!props) return "Record<string, unknown>";
138
+ const fields = Object.entries(props)
139
+ .map(([k, v]) => `${k}: ${schemaTypeToTs(v)}`)
140
+ .join("; ");
141
+ return `{ ${fields} }`;
142
+ }
143
+ if (type === "string") return "string";
144
+ if (type === "number" || type === "integer") return "number";
145
+ if (type === "boolean") return "boolean";
146
+ return "any";
147
+ }
148
+
113
149
  /** Generate TypeScript type definitions from all backend tools for the LLM */
114
150
  export function generateTypeDefinitions(backends: Map<string, Backend>): string {
115
151
  const lines: string[] = [
116
- "// Available MCP tool functions — call these in your execute code",
117
- "// Each function returns a Promise<{ content: Array<{ type: string, text: string }> }>",
152
+ "// Available tool functions — call via namespace: await backend.toolName(args)",
118
153
  "",
119
154
  ];
120
155
 
121
156
  for (const [name, backend] of backends) {
122
157
  lines.push(`// === ${name} ===`);
123
158
  for (const tool of backend.tools) {
124
- const params = tool.inputSchema?.properties
125
- ? Object.entries(
126
- tool.inputSchema.properties as Record<string, { type?: string; description?: string }>,
127
- )
128
- .map(([k, v]) => {
129
- const required = (tool.inputSchema.required as string[] | undefined)?.includes(k);
130
- return `${k}${required ? "" : "?"}: ${v.type === "array" ? "any[]" : (v.type ?? "any")}`;
131
- })
132
- .join(", ")
133
- : "";
134
- const desc = tool.description ? ` — ${tool.description.slice(0, 80)}` : "";
135
- lines.push(
136
- `declare function ${name}_${sanitizeName(tool.name)}(args: { ${params} }): Promise<any>;${desc}`,
137
- );
159
+ const props = tool.inputSchema?.properties as
160
+ | Record<string, Record<string, unknown>>
161
+ | undefined;
162
+ const required = (tool.inputSchema?.required as string[]) ?? [];
163
+
164
+ if (props && Object.keys(props).length > 0) {
165
+ // Generate interface
166
+ const ifaceName = `${name}_${sanitizeName(tool.name)}_Input`;
167
+ lines.push(`interface ${ifaceName} {`);
168
+ for (const [k, v] of Object.entries(props)) {
169
+ const opt = required.includes(k) ? "" : "?";
170
+ const desc = v.description ? ` // ${(v.description as string).slice(0, 60)}` : "";
171
+ lines.push(` ${k}${opt}: ${schemaTypeToTs(v)};${desc}`);
172
+ }
173
+ lines.push("}");
174
+
175
+ const desc = tool.description ? ` — ${tool.description.slice(0, 80)}` : "";
176
+ lines.push(
177
+ `declare function ${name}_${sanitizeName(tool.name)}(args: ${ifaceName}): Promise<any>;${desc}`,
178
+ );
179
+ } else {
180
+ const desc = tool.description ? ` — ${tool.description.slice(0, 80)}` : "";
181
+ lines.push(
182
+ `declare function ${name}_${sanitizeName(tool.name)}(args?: {}): Promise<any>;${desc}`,
183
+ );
184
+ }
185
+ lines.push("");
138
186
  }
139
- lines.push("");
140
187
  }
141
188
 
142
189
  return lines.join("\n");
package/src/config.ts CHANGED
@@ -17,6 +17,13 @@ export interface BackendConfig {
17
17
  env?: Record<string, string>;
18
18
  /** HTTP headers for http transport — supports ${VAR} interpolation */
19
19
  headers?: Record<string, string>;
20
+ /** OAuth client config for HTTP backends (MCP OAuth flow) */
21
+ oauth?: {
22
+ /** Callback port for local OAuth redirect (default: 9876) */
23
+ callbackPort?: number;
24
+ /** Custom redirect URI (default: http://localhost:{callbackPort}/oauth/callback) */
25
+ redirectUri?: string;
26
+ };
20
27
  /** JWT roles allowed to access this backend */
21
28
  allowedRoles?: string[];
22
29
  /** JWT teams allowed to access this backend */
@@ -89,7 +89,7 @@ describe("executeCode", () => {
89
89
  const backends = new Map([["test", mockBackend]]);
90
90
 
91
91
  const result = await executeCode(
92
- 'const r = await test_echo({ msg: "hello" }); return r.content[0].text;',
92
+ 'const r = await test.echo({ msg: "hello" }); return r.content[0].text;',
93
93
  backends,
94
94
  );
95
95
  expect(result.isOk()).toBe(true);
@@ -126,9 +126,9 @@ describe("executeCode", () => {
126
126
  const backends = new Map([["mock", mockBackend]]);
127
127
 
128
128
  const result = await executeCode(
129
- `const idResult = await mock_get_id({});
129
+ `const idResult = await mock.getId({});
130
130
  const id = idResult.content[0].text;
131
- const details = await mock_get_details({ id });
131
+ const details = await mock.getDetails({ id });
132
132
  return details.content[0].text;`,
133
133
  backends,
134
134
  );
package/src/executor.ts CHANGED
@@ -3,6 +3,7 @@ import { createNodeDriver, NodeExecutionDriver, type BindingFunction } from "sec
3
3
 
4
4
  import { validateSyntax } from "./ast.js";
5
5
  import type { Backend } from "./backends.js";
6
+ import type { Skill } from "./skills.js";
6
7
 
7
8
  type ToolFunction = (args: Record<string, unknown>) => Promise<unknown>;
8
9
 
@@ -11,9 +12,28 @@ export interface LogEntry {
11
12
  args: unknown[];
12
13
  }
13
14
 
15
+ export interface ExecutionEvent {
16
+ type:
17
+ | "tool_call"
18
+ | "tool_result"
19
+ | "tool_error"
20
+ | "console"
21
+ | "execution_start"
22
+ | "execution_end";
23
+ timestamp: number;
24
+ tool?: string;
25
+ args?: unknown;
26
+ result?: unknown;
27
+ error?: string;
28
+ durationMs?: number;
29
+ level?: string;
30
+ message?: string;
31
+ }
32
+
14
33
  export interface ExecuteResult {
15
34
  value: unknown;
16
35
  logs: LogEntry[];
36
+ events: ExecutionEvent[];
17
37
  }
18
38
 
19
39
  export type ExecuteError =
@@ -33,26 +53,98 @@ export function snakeToCamel(name: string): string {
33
53
  }
34
54
 
35
55
  /** Build a map of prefixed tool names → backend tool call functions */
36
- function buildToolRegistry(backends: Map<string, Backend>): Map<string, ToolFunction> {
56
+ function buildToolRegistry(
57
+ backends: Map<string, Backend>,
58
+ skills: Map<string, Skill>,
59
+ events: ExecutionEvent[],
60
+ ): Map<string, ToolFunction> {
37
61
  const registry = new Map<string, ToolFunction>();
38
62
 
63
+ // Backend tools
39
64
  for (const [name, backend] of backends) {
40
65
  for (const tool of backend.tools) {
41
66
  const prefixed = `${name}_${sanitizeName(tool.name)}`;
42
67
  registry.set(prefixed, async (args) => {
43
- return backend.client.callTool({
44
- name: tool.name,
45
- arguments: args,
68
+ const start = Date.now();
69
+ events.push({
70
+ type: "tool_call",
71
+ timestamp: start,
72
+ tool: prefixed,
73
+ args,
46
74
  });
75
+ try {
76
+ const result = await backend.client.callTool({
77
+ name: tool.name,
78
+ arguments: args,
79
+ });
80
+ events.push({
81
+ type: "tool_result",
82
+ timestamp: Date.now(),
83
+ tool: prefixed,
84
+ result,
85
+ durationMs: Date.now() - start,
86
+ });
87
+ return result;
88
+ } catch (e) {
89
+ const msg = (e as Error).message;
90
+ events.push({
91
+ type: "tool_error",
92
+ timestamp: Date.now(),
93
+ tool: prefixed,
94
+ error: msg,
95
+ durationMs: Date.now() - start,
96
+ });
97
+ return { error: msg };
98
+ }
47
99
  });
48
100
  }
49
101
  }
50
102
 
103
+ // Skill tools — execute saved code in the same sandbox context
104
+ for (const [, skill] of skills) {
105
+ const prefixed = `skill_${sanitizeName(skill.name)}`;
106
+ registry.set(prefixed, async (args) => {
107
+ const start = Date.now();
108
+ events.push({
109
+ type: "tool_call",
110
+ timestamp: start,
111
+ tool: prefixed,
112
+ args,
113
+ });
114
+ try {
115
+ // Skills run as inline functions — they have access to the same tool bindings
116
+ const fn = new Function("args", `return (async () => { ${skill.code} })()`);
117
+ const result = await fn(args);
118
+ events.push({
119
+ type: "tool_result",
120
+ timestamp: Date.now(),
121
+ tool: prefixed,
122
+ result,
123
+ durationMs: Date.now() - start,
124
+ });
125
+ return result;
126
+ } catch (e) {
127
+ const msg = (e as Error).message;
128
+ events.push({
129
+ type: "tool_error",
130
+ timestamp: Date.now(),
131
+ tool: prefixed,
132
+ error: msg,
133
+ durationMs: Date.now() - start,
134
+ });
135
+ return { error: msg };
136
+ }
137
+ });
138
+ }
139
+
51
140
  return registry;
52
141
  }
53
142
 
54
143
  /** Build a map of backend name → tool names (for namespace generation) */
55
- function buildBackendToolMap(backends: Map<string, Backend>): Map<string, string[]> {
144
+ function buildBackendToolMap(
145
+ backends: Map<string, Backend>,
146
+ skills: Map<string, Skill>,
147
+ ): Map<string, string[]> {
56
148
  const map = new Map<string, string[]>();
57
149
  for (const [name, backend] of backends) {
58
150
  map.set(
@@ -60,18 +152,20 @@ function buildBackendToolMap(backends: Map<string, Backend>): Map<string, string
60
152
  backend.tools.map((t) => t.name),
61
153
  );
62
154
  }
155
+
156
+ // Skills get their own namespace
157
+ if (skills.size > 0) {
158
+ map.set(
159
+ "skill",
160
+ Array.from(skills.values()).map((s) => s.name),
161
+ );
162
+ }
163
+
63
164
  return map;
64
165
  }
65
166
 
66
- /**
67
- * Generate code bindings: flat aliases + namespace proxies + console capture.
68
- */
69
- function wrapCodeWithBindings(
70
- code: string,
71
- toolNames: string[],
72
- backendTools: Map<string, string[]>,
73
- ): string {
74
- // Console capture
167
+ /** Generate namespace proxy code for the sandbox */
168
+ function wrapCodeWithBindings(code: string, backendTools: Map<string, string[]>): string {
75
169
  const consoleOverride = `
76
170
  const __consoleLogs = [];
77
171
  const console = {
@@ -82,12 +176,6 @@ const console = {
82
176
  debug: (...args) => __consoleLogs.push({ level: "debug", args }),
83
177
  };`;
84
178
 
85
- // Flat aliases (backward compat)
86
- const aliases = toolNames
87
- .map((n) => `const ${n} = (args) => SecureExec.bindings.callTool("${n}", args || {});`)
88
- .join("\n");
89
-
90
- // Namespace proxies
91
179
  const namespaces: string[] = [];
92
180
  for (const [backendName, tools] of backendTools) {
93
181
  const methods = tools
@@ -102,7 +190,6 @@ const console = {
102
190
 
103
191
  return `
104
192
  ${consoleOverride}
105
- ${aliases}
106
193
  ${namespaces.join("\n")}
107
194
 
108
195
  const __userResult = await (async () => { ${code} })();
@@ -114,10 +201,18 @@ export default { value: __userResult, logs: __consoleLogs };
114
201
  export async function executeCode(
115
202
  code: string,
116
203
  backends: Map<string, Backend>,
117
- opts?: { memoryLimit?: number; cpuTimeLimitMs?: number },
204
+ opts?: {
205
+ memoryLimit?: number;
206
+ cpuTimeLimitMs?: number;
207
+ skills?: Map<string, Skill>;
208
+ },
118
209
  ): Promise<Result<ExecuteResult, ExecuteError>> {
119
- const registry = buildToolRegistry(backends);
120
- const backendTools = buildBackendToolMap(backends);
210
+ const events: ExecutionEvent[] = [];
211
+ const skills = opts?.skills ?? new Map();
212
+ const registry = buildToolRegistry(backends, skills, events);
213
+ const backendTools = buildBackendToolMap(backends, skills);
214
+
215
+ events.push({ type: "execution_start", timestamp: Date.now() });
121
216
 
122
217
  const callTool: BindingFunction = async (name: unknown, args: unknown) => {
123
218
  const toolName = name as string;
@@ -126,16 +221,11 @@ export async function executeCode(
126
221
  if (!fn) {
127
222
  return { error: `Unknown tool: ${toolName}` };
128
223
  }
129
- try {
130
- return await fn(toolArgs);
131
- } catch (e) {
132
- return { error: (e as Error).message };
133
- }
224
+ return fn(toolArgs);
134
225
  };
135
226
 
136
- const wrappedCode = wrapCodeWithBindings(code, [...registry.keys()], backendTools);
227
+ const wrappedCode = wrapCodeWithBindings(code, backendTools);
137
228
 
138
- // AST validation — catch syntax errors with better messages before V8 execution
139
229
  const syntaxError = validateSyntax(wrappedCode);
140
230
  if (syntaxError) {
141
231
  return err({
@@ -170,6 +260,8 @@ export async function executeCode(
170
260
  try {
171
261
  const runResult = await driver.run(wrappedCode, "/entry.mjs");
172
262
 
263
+ events.push({ type: "execution_end", timestamp: Date.now() });
264
+
173
265
  if (runResult.code !== 0) {
174
266
  return err({ kind: "runtime", code: runResult.code });
175
267
  }
@@ -182,8 +274,10 @@ export async function executeCode(
182
274
  return ok({
183
275
  value: result?.value ?? null,
184
276
  logs: result?.logs ?? [],
277
+ events,
185
278
  });
186
279
  } catch (e) {
280
+ events.push({ type: "execution_end", timestamp: Date.now() });
187
281
  return err({ kind: "exception", message: (e as Error).message });
188
282
  } finally {
189
283
  driver.dispose();