@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 +1 -1
- package/src/backends.test.ts +5 -2
- package/src/backends.ts +67 -20
- package/src/config.ts +7 -0
- package/src/executor.test.ts +3 -3
- package/src/executor.ts +125 -31
- package/src/index.ts +337 -265
- package/src/oauth-client.ts +337 -0
- package/src/skills.test.ts +166 -0
- package/src/skills.ts +153 -0
- package/src/stdio.ts +5 -6
package/package.json
CHANGED
package/src/backends.test.ts
CHANGED
|
@@ -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
|
|
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: {
|
|
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(
|
|
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:
|
|
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
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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 */
|
package/src/executor.test.ts
CHANGED
|
@@ -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
|
|
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
|
|
129
|
+
`const idResult = await mock.getId({});
|
|
130
130
|
const id = idResult.content[0].text;
|
|
131
|
-
const details = await
|
|
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(
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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(
|
|
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
|
-
|
|
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?: {
|
|
204
|
+
opts?: {
|
|
205
|
+
memoryLimit?: number;
|
|
206
|
+
cpuTimeLimitMs?: number;
|
|
207
|
+
skills?: Map<string, Skill>;
|
|
208
|
+
},
|
|
118
209
|
): Promise<Result<ExecuteResult, ExecuteError>> {
|
|
119
|
-
const
|
|
120
|
-
const
|
|
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
|
-
|
|
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,
|
|
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();
|