@slashfi/agents-sdk 0.62.0 → 0.64.0
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/dist/adk-tools.d.ts +15 -0
- package/dist/adk-tools.d.ts.map +1 -1
- package/dist/adk-tools.js +9 -2
- package/dist/adk-tools.js.map +1 -1
- package/dist/cjs/adk-tools.js +9 -2
- package/dist/cjs/adk-tools.js.map +1 -1
- package/dist/cjs/index.js +1 -29
- package/dist/cjs/index.js.map +1 -1
- package/dist/index.d.ts +2 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -18
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adk-tools.ts +24 -2
- package/src/index.ts +2 -46
- package/src/types.ts +8 -0
- package/dist/cjs/client.js +0 -193
- package/dist/cjs/client.js.map +0 -1
- package/dist/cjs/codegen.js +0 -1071
- package/dist/cjs/codegen.js.map +0 -1
- package/dist/cjs/introspect.js +0 -136
- package/dist/cjs/introspect.js.map +0 -1
- package/dist/cjs/jsonc.js +0 -74
- package/dist/cjs/jsonc.js.map +0 -1
- package/dist/cjs/pack.js +0 -256
- package/dist/cjs/pack.js.map +0 -1
- package/dist/client.d.ts +0 -49
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -190
- package/dist/client.js.map +0 -1
- package/dist/codegen.d.ts +0 -163
- package/dist/codegen.d.ts.map +0 -1
- package/dist/codegen.js +0 -1059
- package/dist/codegen.js.map +0 -1
- package/dist/introspect.d.ts +0 -16
- package/dist/introspect.d.ts.map +0 -1
- package/dist/introspect.js +0 -133
- package/dist/introspect.js.map +0 -1
- package/dist/jsonc.d.ts +0 -15
- package/dist/jsonc.d.ts.map +0 -1
- package/dist/jsonc.js +0 -70
- package/dist/jsonc.js.map +0 -1
- package/dist/pack.d.ts +0 -59
- package/dist/pack.d.ts.map +0 -1
- package/dist/pack.js +0 -252
- package/dist/pack.js.map +0 -1
- package/src/client.ts +0 -273
- package/src/codegen.test.ts +0 -537
- package/src/codegen.ts +0 -1423
- package/src/introspect.ts +0 -171
- package/src/jsonc.ts +0 -83
- package/src/pack.ts +0 -394
package/src/codegen.ts
DELETED
|
@@ -1,1423 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP Codegen
|
|
3
|
-
*
|
|
4
|
-
* Connects to any MCP server, introspects its tools via `tools/list`,
|
|
5
|
-
* and generates readable agent-definition source files.
|
|
6
|
-
*
|
|
7
|
-
* Supports three transport modes:
|
|
8
|
-
* - stdio: spawn a process (e.g., `npx @modelcontextprotocol/server-notion`)
|
|
9
|
-
* - sse: connect to an SSE endpoint
|
|
10
|
-
* - http: connect to an HTTP JSON-RPC endpoint
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```typescript
|
|
14
|
-
* import { codegen } from '@slashfi/agents-sdk';
|
|
15
|
-
*
|
|
16
|
-
* await codegen({
|
|
17
|
-
* server: 'npx @modelcontextprotocol/server-notion',
|
|
18
|
-
* outDir: './generated/notion',
|
|
19
|
-
* agentPath: '@notion',
|
|
20
|
-
* });
|
|
21
|
-
* ```
|
|
22
|
-
*
|
|
23
|
-
* @example CLI
|
|
24
|
-
* ```bash
|
|
25
|
-
* agents-sdk codegen --server 'npx @mcp/notion' --name notion
|
|
26
|
-
* agents-sdk use notion search_pages '{"query": "hello"}'
|
|
27
|
-
* agents-sdk use notion --list
|
|
28
|
-
* ```
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
import { mkdirSync, writeFileSync, existsSync, readFileSync } from "node:fs";
|
|
32
|
-
import { join, resolve } from "node:path";
|
|
33
|
-
import type { JsonSchema } from "./types.js";
|
|
34
|
-
import { discoverOAuthMetadata, type OAuthServerMetadata } from "./mcp-client.js";
|
|
35
|
-
|
|
36
|
-
// ============================================
|
|
37
|
-
// Types
|
|
38
|
-
// ============================================
|
|
39
|
-
|
|
40
|
-
/** MCP tool definition as returned by tools/list */
|
|
41
|
-
export interface McpToolDefinition {
|
|
42
|
-
name: string;
|
|
43
|
-
description?: string;
|
|
44
|
-
inputSchema?: JsonSchema;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** MCP server info from initialize response */
|
|
48
|
-
export interface McpServerInfo {
|
|
49
|
-
name?: string;
|
|
50
|
-
version?: string;
|
|
51
|
-
protocolVersion?: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Transport for communicating with an MCP server */
|
|
55
|
-
export interface McpTransport {
|
|
56
|
-
send(method: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
57
|
-
/** Send a JSON-RPC notification (no id, no response expected) */
|
|
58
|
-
notify(method: string, params?: Record<string, unknown>): Promise<void>;
|
|
59
|
-
close(): Promise<void>;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Server source — string command, URL, or explicit config */
|
|
63
|
-
export type ServerSource =
|
|
64
|
-
| string
|
|
65
|
-
| { command: string; args?: string[]; env?: Record<string, string> }
|
|
66
|
-
| { url: string; headers?: Record<string, string> }
|
|
67
|
-
| { spawn: string; args?: string[]; env?: Record<string, string>; port?: number; endpoint?: string };
|
|
68
|
-
|
|
69
|
-
/** Options for the codegen function */
|
|
70
|
-
export interface CodegenOptions {
|
|
71
|
-
/** MCP server source — command string, URL, or config object */
|
|
72
|
-
server: ServerSource;
|
|
73
|
-
|
|
74
|
-
/** Output directory for generated files */
|
|
75
|
-
outDir: string;
|
|
76
|
-
|
|
77
|
-
/** Agent path override (default: derived from server name) */
|
|
78
|
-
agentPath?: string;
|
|
79
|
-
|
|
80
|
-
/** Agent display name */
|
|
81
|
-
name?: string;
|
|
82
|
-
|
|
83
|
-
/** SDK import path (default: '@slashfi/agents-sdk') */
|
|
84
|
-
sdkImport?: string;
|
|
85
|
-
|
|
86
|
-
/** Whether to generate a CLI entrypoint (default: true) */
|
|
87
|
-
cli?: boolean;
|
|
88
|
-
|
|
89
|
-
/** Whether to generate TypeScript interfaces from schemas (default: true) */
|
|
90
|
-
types?: boolean;
|
|
91
|
-
|
|
92
|
-
/** Visibility for the generated agent (default: 'public') */
|
|
93
|
-
visibility?: "public" | "internal" | "private";
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** Result of codegen */
|
|
97
|
-
export interface CodegenResult {
|
|
98
|
-
/** Path to the output directory */
|
|
99
|
-
outDir: string;
|
|
100
|
-
|
|
101
|
-
/** Server info from the MCP initialize handshake */
|
|
102
|
-
serverInfo: McpServerInfo;
|
|
103
|
-
|
|
104
|
-
/** Number of tools generated */
|
|
105
|
-
toolCount: number;
|
|
106
|
-
|
|
107
|
-
/** Names of generated tool files */
|
|
108
|
-
toolFiles: string[];
|
|
109
|
-
|
|
110
|
-
/** All generated file paths */
|
|
111
|
-
files: string[];
|
|
112
|
-
|
|
113
|
-
/** OAuth server metadata (if discovered via .well-known/oauth-authorization-server) */
|
|
114
|
-
oauth?: OAuthServerMetadata;
|
|
115
|
-
|
|
116
|
-
/** Connection spec for consumers */
|
|
117
|
-
connection?: ConnectionSpec;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// ============================================
|
|
121
|
-
// Protocol Constants
|
|
122
|
-
// ============================================
|
|
123
|
-
|
|
124
|
-
const LATEST_PROTOCOL_VERSION = "2025-03-26";
|
|
125
|
-
const SUPPORTED_PROTOCOL_VERSIONS = [
|
|
126
|
-
"2025-03-26",
|
|
127
|
-
"2024-11-05",
|
|
128
|
-
"2024-10-07",
|
|
129
|
-
];
|
|
130
|
-
|
|
131
|
-
// ============================================
|
|
132
|
-
// Safe Environment (security)
|
|
133
|
-
// ============================================
|
|
134
|
-
|
|
135
|
-
/** Only inherit safe env vars when spawning MCP servers (prevents secret leakage). */
|
|
136
|
-
const DEFAULT_INHERITED_ENV_VARS =
|
|
137
|
-
process.platform === "win32"
|
|
138
|
-
? [
|
|
139
|
-
"APPDATA",
|
|
140
|
-
"HOMEDRIVE",
|
|
141
|
-
"HOMEPATH",
|
|
142
|
-
"LOCALAPPDATA",
|
|
143
|
-
"PATH",
|
|
144
|
-
"PROCESSOR_ARCHITECTURE",
|
|
145
|
-
"SYSTEMDRIVE",
|
|
146
|
-
"SYSTEMROOT",
|
|
147
|
-
"TEMP",
|
|
148
|
-
"USERNAME",
|
|
149
|
-
"USERPROFILE",
|
|
150
|
-
"PROGRAMFILES",
|
|
151
|
-
]
|
|
152
|
-
: ["HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER"];
|
|
153
|
-
|
|
154
|
-
function getDefaultEnvironment(): Record<string, string> {
|
|
155
|
-
const env: Record<string, string> = {};
|
|
156
|
-
for (const key of DEFAULT_INHERITED_ENV_VARS) {
|
|
157
|
-
const value = process.env[key];
|
|
158
|
-
if (value === undefined || value.startsWith("()")) continue;
|
|
159
|
-
env[key] = value;
|
|
160
|
-
}
|
|
161
|
-
return env;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// ============================================
|
|
165
|
-
// Transport: stdio
|
|
166
|
-
// ============================================
|
|
167
|
-
|
|
168
|
-
import spawn from "cross-spawn";
|
|
169
|
-
import { spawn as _nodeSpawn } from "node:child_process";
|
|
170
|
-
|
|
171
|
-
function createStdioTransport(source: {
|
|
172
|
-
command: string;
|
|
173
|
-
args?: string[];
|
|
174
|
-
env?: Record<string, string>;
|
|
175
|
-
}): McpTransport {
|
|
176
|
-
// Use cross-spawn for Windows compatibility (.cmd/.bat wrappers, spaces in paths)
|
|
177
|
-
// Env: if explicit env provided, use safe defaults + explicit. Otherwise inherit process.env.
|
|
178
|
-
const env = source.env
|
|
179
|
-
? { ...getDefaultEnvironment(), ...source.env }
|
|
180
|
-
: { ...process.env } as Record<string, string>;
|
|
181
|
-
const proc = spawn(source.command, source.args ?? [], {
|
|
182
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
183
|
-
env,
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
let requestId = 0;
|
|
187
|
-
const pending = new Map<
|
|
188
|
-
number,
|
|
189
|
-
{ resolve: (v: unknown) => void; reject: (e: Error) => void }
|
|
190
|
-
>();
|
|
191
|
-
|
|
192
|
-
// Error handlers for pipes (prevent unhandled errors)
|
|
193
|
-
proc.on("error", (err: Error) => {
|
|
194
|
-
for (const [id, p] of pending) {
|
|
195
|
-
p.reject(new Error(`Process error: ${err.message}`));
|
|
196
|
-
pending.delete(id);
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
proc.stdin?.on("error", () => { /* ignore broken pipe */ });
|
|
200
|
-
proc.stdout?.on("error", () => { /* ignore */ });
|
|
201
|
-
proc.stderr?.on("error", () => { /* ignore */ });
|
|
202
|
-
|
|
203
|
-
// Read stdout with Content-Length framing (MCP spec) or newline-delimited fallback
|
|
204
|
-
let buffer = "";
|
|
205
|
-
|
|
206
|
-
proc.stdout!.on("data", (chunk: Buffer) => {
|
|
207
|
-
buffer += chunk.toString();
|
|
208
|
-
|
|
209
|
-
// Try to parse messages from buffer
|
|
210
|
-
while (buffer.length > 0) {
|
|
211
|
-
// Check for Content-Length framing (MCP standard)
|
|
212
|
-
const clMatch = buffer.match(
|
|
213
|
-
/^Content-Length:\s*(\d+)\r?\n(?:[^\r\n]+\r?\n)*\r?\n/,
|
|
214
|
-
);
|
|
215
|
-
if (clMatch) {
|
|
216
|
-
const contentLength = parseInt(clMatch[1], 10);
|
|
217
|
-
const headerEnd = clMatch[0].length;
|
|
218
|
-
if (buffer.length >= headerEnd + contentLength) {
|
|
219
|
-
const body = buffer.slice(headerEnd, headerEnd + contentLength);
|
|
220
|
-
buffer = buffer.slice(headerEnd + contentLength);
|
|
221
|
-
try {
|
|
222
|
-
const msg = JSON.parse(body);
|
|
223
|
-
if (msg.id !== undefined && pending.has(msg.id)) {
|
|
224
|
-
const p = pending.get(msg.id)!;
|
|
225
|
-
pending.delete(msg.id);
|
|
226
|
-
if (msg.error) {
|
|
227
|
-
p.reject(
|
|
228
|
-
new Error(
|
|
229
|
-
`MCP error ${msg.error.code}: ${msg.error.message}`,
|
|
230
|
-
),
|
|
231
|
-
);
|
|
232
|
-
} else {
|
|
233
|
-
p.resolve(msg.result);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
} catch {
|
|
237
|
-
// malformed JSON, skip
|
|
238
|
-
}
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
241
|
-
// Not enough data yet, wait for more
|
|
242
|
-
break;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Fallback: newline-delimited JSON
|
|
246
|
-
const newlineIdx = buffer.indexOf("\n");
|
|
247
|
-
if (newlineIdx === -1) break;
|
|
248
|
-
|
|
249
|
-
const line = buffer.slice(0, newlineIdx).trim();
|
|
250
|
-
buffer = buffer.slice(newlineIdx + 1);
|
|
251
|
-
if (!line) continue;
|
|
252
|
-
|
|
253
|
-
try {
|
|
254
|
-
const msg = JSON.parse(line);
|
|
255
|
-
if (msg.id !== undefined && pending.has(msg.id)) {
|
|
256
|
-
const p = pending.get(msg.id)!;
|
|
257
|
-
pending.delete(msg.id);
|
|
258
|
-
if (msg.error) {
|
|
259
|
-
p.reject(
|
|
260
|
-
new Error(
|
|
261
|
-
`MCP error ${msg.error.code}: ${msg.error.message}`,
|
|
262
|
-
),
|
|
263
|
-
);
|
|
264
|
-
} else {
|
|
265
|
-
p.resolve(msg.result);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
} catch {
|
|
269
|
-
// ignore non-JSON lines (e.g., server logs)
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
return {
|
|
275
|
-
async send(method: string, params?: Record<string, unknown>) {
|
|
276
|
-
const id = ++requestId;
|
|
277
|
-
const message = JSON.stringify({
|
|
278
|
-
jsonrpc: "2.0",
|
|
279
|
-
id,
|
|
280
|
-
method,
|
|
281
|
-
params: params ?? {},
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
// Send as newline-delimited JSON (compatible with all MCP SDK versions)
|
|
285
|
-
proc.stdin!.write(message + "\n");
|
|
286
|
-
|
|
287
|
-
return new Promise((resolve, reject) => {
|
|
288
|
-
const timeout = setTimeout(() => {
|
|
289
|
-
pending.delete(id);
|
|
290
|
-
reject(new Error(`MCP request timed out: ${method}`));
|
|
291
|
-
}, 30_000);
|
|
292
|
-
|
|
293
|
-
pending.set(id, {
|
|
294
|
-
resolve: (v) => {
|
|
295
|
-
clearTimeout(timeout);
|
|
296
|
-
resolve(v);
|
|
297
|
-
},
|
|
298
|
-
reject: (e) => {
|
|
299
|
-
clearTimeout(timeout);
|
|
300
|
-
reject(e);
|
|
301
|
-
},
|
|
302
|
-
});
|
|
303
|
-
});
|
|
304
|
-
},
|
|
305
|
-
async notify(method: string, params?: Record<string, unknown>) {
|
|
306
|
-
// Notification = no id field, no response expected
|
|
307
|
-
const message = JSON.stringify({
|
|
308
|
-
jsonrpc: "2.0",
|
|
309
|
-
method,
|
|
310
|
-
...(params ? { params } : {}),
|
|
311
|
-
});
|
|
312
|
-
proc.stdin!.write(message + "\n");
|
|
313
|
-
},
|
|
314
|
-
async close() {
|
|
315
|
-
// Graceful shutdown: stdin.end → wait → SIGTERM → wait → SIGKILL
|
|
316
|
-
const closePromise = new Promise<void>((resolve) => {
|
|
317
|
-
proc.once("close", () => resolve());
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
try { proc.stdin?.end(); } catch { /* ignore */ }
|
|
321
|
-
await Promise.race([closePromise, new Promise((r) => setTimeout(r, 2000))]);
|
|
322
|
-
|
|
323
|
-
if (proc.exitCode === null) {
|
|
324
|
-
try { proc.kill("SIGTERM"); } catch { /* ignore */ }
|
|
325
|
-
await Promise.race([closePromise, new Promise((r) => setTimeout(r, 2000))]);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
if (proc.exitCode === null) {
|
|
329
|
-
try { proc.kill("SIGKILL"); } catch { /* ignore */ }
|
|
330
|
-
}
|
|
331
|
-
},
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// ============================================
|
|
336
|
-
// Transport: HTTP JSON-RPC (Streamable HTTP)
|
|
337
|
-
// ============================================
|
|
338
|
-
|
|
339
|
-
function createHttpTransport(source: {
|
|
340
|
-
url: string;
|
|
341
|
-
headers?: Record<string, string>;
|
|
342
|
-
}): McpTransport {
|
|
343
|
-
let requestId = 0;
|
|
344
|
-
let sessionId: string | null = null;
|
|
345
|
-
|
|
346
|
-
return {
|
|
347
|
-
async send(method: string, params?: Record<string, unknown>) {
|
|
348
|
-
const id = ++requestId;
|
|
349
|
-
const res = await fetch(source.url, {
|
|
350
|
-
method: "POST",
|
|
351
|
-
headers: {
|
|
352
|
-
"Content-Type": "application/json",
|
|
353
|
-
"Accept": "application/json, text/event-stream",
|
|
354
|
-
...source.headers,
|
|
355
|
-
...(sessionId ? { "mcp-session-id": sessionId } : {}),
|
|
356
|
-
},
|
|
357
|
-
body: JSON.stringify({
|
|
358
|
-
jsonrpc: "2.0",
|
|
359
|
-
id,
|
|
360
|
-
method,
|
|
361
|
-
params: params ?? {},
|
|
362
|
-
}),
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
if (!res.ok) {
|
|
366
|
-
throw new Error(`HTTP ${res.status}: ${await res.text()}`);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Capture session ID if returned
|
|
370
|
-
const newSessionId = res.headers.get("mcp-session-id");
|
|
371
|
-
if (newSessionId) sessionId = newSessionId;
|
|
372
|
-
|
|
373
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
374
|
-
|
|
375
|
-
// Handle SSE-wrapped responses (Streamable HTTP)
|
|
376
|
-
if (contentType.includes("text/event-stream")) {
|
|
377
|
-
const text = await res.text();
|
|
378
|
-
// Parse SSE: look for "data: {...}" lines
|
|
379
|
-
for (const line of text.split("\n")) {
|
|
380
|
-
if (line.startsWith("data: ")) {
|
|
381
|
-
try {
|
|
382
|
-
const msg = JSON.parse(line.slice(6));
|
|
383
|
-
if (msg.error) {
|
|
384
|
-
throw new Error(
|
|
385
|
-
`MCP error ${msg.error.code}: ${msg.error.message}`,
|
|
386
|
-
);
|
|
387
|
-
}
|
|
388
|
-
return msg.result;
|
|
389
|
-
} catch (e) {
|
|
390
|
-
if (e instanceof SyntaxError) continue;
|
|
391
|
-
throw e;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
throw new Error("No JSON-RPC response found in SSE stream");
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Standard JSON response
|
|
399
|
-
const msg = (await res.json()) as {
|
|
400
|
-
result?: unknown;
|
|
401
|
-
error?: { code: number; message: string };
|
|
402
|
-
};
|
|
403
|
-
if (msg.error) {
|
|
404
|
-
throw new Error(
|
|
405
|
-
`MCP error ${msg.error.code}: ${msg.error.message}`,
|
|
406
|
-
);
|
|
407
|
-
}
|
|
408
|
-
return msg.result;
|
|
409
|
-
},
|
|
410
|
-
async notify(method: string, params?: Record<string, unknown>) {
|
|
411
|
-
// Send notification (no id, accept 202 Accepted)
|
|
412
|
-
const res = await fetch(source.url, {
|
|
413
|
-
method: "POST",
|
|
414
|
-
headers: {
|
|
415
|
-
"Content-Type": "application/json",
|
|
416
|
-
"Accept": "application/json, text/event-stream",
|
|
417
|
-
...source.headers,
|
|
418
|
-
...(sessionId ? { "mcp-session-id": sessionId } : {}),
|
|
419
|
-
},
|
|
420
|
-
body: JSON.stringify({
|
|
421
|
-
jsonrpc: "2.0",
|
|
422
|
-
method,
|
|
423
|
-
...(params ? { params } : {}),
|
|
424
|
-
}),
|
|
425
|
-
});
|
|
426
|
-
// 202 Accepted is expected for notifications
|
|
427
|
-
if (!res.ok && res.status !== 202) {
|
|
428
|
-
await res.text().catch(() => {});
|
|
429
|
-
}
|
|
430
|
-
},
|
|
431
|
-
async close() {
|
|
432
|
-
// nothing to close for HTTP
|
|
433
|
-
},
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// ============================================
|
|
438
|
-
// Transport: SSE (legacy MCP SSE protocol)
|
|
439
|
-
// ============================================
|
|
440
|
-
|
|
441
|
-
function createSseTransport(source: {
|
|
442
|
-
url: string;
|
|
443
|
-
headers?: Record<string, string>;
|
|
444
|
-
}): McpTransport {
|
|
445
|
-
let postEndpoint: string | null = null;
|
|
446
|
-
let requestId = 0;
|
|
447
|
-
const pending = new Map<
|
|
448
|
-
number,
|
|
449
|
-
{ resolve: (v: unknown) => void; reject: (e: Error) => void }
|
|
450
|
-
>();
|
|
451
|
-
let sseController: ReadableStreamDefaultReader | null = null;
|
|
452
|
-
|
|
453
|
-
// Connect to SSE and discover the POST endpoint
|
|
454
|
-
const connectPromise = (async () => {
|
|
455
|
-
const res = await fetch(source.url, {
|
|
456
|
-
headers: {
|
|
457
|
-
Accept: "text/event-stream",
|
|
458
|
-
...source.headers,
|
|
459
|
-
},
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
if (!res.ok) {
|
|
463
|
-
throw new Error(`SSE connect failed: ${res.status} ${await res.text()}`);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
const reader = res.body!.getReader() as ReadableStreamDefaultReader<Uint8Array>;
|
|
467
|
-
sseController = reader;
|
|
468
|
-
const decoder = new TextDecoder();
|
|
469
|
-
let buffer = "";
|
|
470
|
-
|
|
471
|
-
// Read SSE events in background
|
|
472
|
-
(async () => {
|
|
473
|
-
try {
|
|
474
|
-
while (true) {
|
|
475
|
-
const { done, value } = await reader.read();
|
|
476
|
-
if (done) break;
|
|
477
|
-
buffer += decoder.decode(value, { stream: true });
|
|
478
|
-
|
|
479
|
-
// Process complete SSE events
|
|
480
|
-
while (buffer.includes("\n\n")) {
|
|
481
|
-
const eventEnd = buffer.indexOf("\n\n");
|
|
482
|
-
const eventText = buffer.slice(0, eventEnd);
|
|
483
|
-
buffer = buffer.slice(eventEnd + 2);
|
|
484
|
-
|
|
485
|
-
let eventType = "message";
|
|
486
|
-
let eventData = "";
|
|
487
|
-
|
|
488
|
-
for (const line of eventText.split("\n")) {
|
|
489
|
-
if (line.startsWith("event: ")) {
|
|
490
|
-
eventType = line.slice(7).trim();
|
|
491
|
-
} else if (line.startsWith("data: ")) {
|
|
492
|
-
eventData += line.slice(6);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
if (eventType === "endpoint" && eventData) {
|
|
497
|
-
// Resolve relative URLs
|
|
498
|
-
const base = new URL(source.url);
|
|
499
|
-
postEndpoint = eventData.startsWith("http")
|
|
500
|
-
? eventData
|
|
501
|
-
: `${base.origin}${eventData}`;
|
|
502
|
-
} else if (eventType === "message" && eventData) {
|
|
503
|
-
try {
|
|
504
|
-
const msg = JSON.parse(eventData);
|
|
505
|
-
if (msg.id !== undefined && pending.has(msg.id)) {
|
|
506
|
-
const p = pending.get(msg.id)!;
|
|
507
|
-
pending.delete(msg.id);
|
|
508
|
-
if (msg.error) {
|
|
509
|
-
p.reject(
|
|
510
|
-
new Error(
|
|
511
|
-
`MCP error ${msg.error.code}: ${msg.error.message}`,
|
|
512
|
-
),
|
|
513
|
-
);
|
|
514
|
-
} else {
|
|
515
|
-
p.resolve(msg.result);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
} catch {
|
|
519
|
-
// ignore malformed JSON
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
} catch {
|
|
525
|
-
// stream closed
|
|
526
|
-
}
|
|
527
|
-
})();
|
|
528
|
-
|
|
529
|
-
// Wait for endpoint to be discovered
|
|
530
|
-
const deadline = Date.now() + 10_000;
|
|
531
|
-
while (!postEndpoint && Date.now() < deadline) {
|
|
532
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
533
|
-
}
|
|
534
|
-
if (!postEndpoint) {
|
|
535
|
-
throw new Error("SSE endpoint not discovered within timeout");
|
|
536
|
-
}
|
|
537
|
-
})();
|
|
538
|
-
|
|
539
|
-
return {
|
|
540
|
-
async send(method: string, params?: Record<string, unknown>) {
|
|
541
|
-
await connectPromise;
|
|
542
|
-
|
|
543
|
-
const id = ++requestId;
|
|
544
|
-
const res = await fetch(postEndpoint!, {
|
|
545
|
-
method: "POST",
|
|
546
|
-
headers: {
|
|
547
|
-
"Content-Type": "application/json",
|
|
548
|
-
...source.headers,
|
|
549
|
-
},
|
|
550
|
-
body: JSON.stringify({
|
|
551
|
-
jsonrpc: "2.0",
|
|
552
|
-
id,
|
|
553
|
-
method,
|
|
554
|
-
params: params ?? {},
|
|
555
|
-
}),
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
if (!res.ok) {
|
|
559
|
-
const body = await res.text();
|
|
560
|
-
throw new Error(`HTTP ${res.status}: ${body}`);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// Response may come via SSE stream or directly
|
|
564
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
565
|
-
if (contentType.includes("application/json")) {
|
|
566
|
-
const msg = (await res.json()) as {
|
|
567
|
-
result?: unknown;
|
|
568
|
-
error?: { code: number; message: string };
|
|
569
|
-
};
|
|
570
|
-
if (msg.error) {
|
|
571
|
-
throw new Error(
|
|
572
|
-
`MCP error ${msg.error.code}: ${msg.error.message}`,
|
|
573
|
-
);
|
|
574
|
-
}
|
|
575
|
-
return msg.result;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// Otherwise wait for response via SSE stream
|
|
579
|
-
return new Promise((resolve, reject) => {
|
|
580
|
-
const timeout = setTimeout(() => {
|
|
581
|
-
pending.delete(id);
|
|
582
|
-
reject(new Error(`MCP SSE request timed out: ${method}`));
|
|
583
|
-
}, 30_000);
|
|
584
|
-
|
|
585
|
-
pending.set(id, {
|
|
586
|
-
resolve: (v) => {
|
|
587
|
-
clearTimeout(timeout);
|
|
588
|
-
resolve(v);
|
|
589
|
-
},
|
|
590
|
-
reject: (e) => {
|
|
591
|
-
clearTimeout(timeout);
|
|
592
|
-
reject(e);
|
|
593
|
-
},
|
|
594
|
-
});
|
|
595
|
-
});
|
|
596
|
-
},
|
|
597
|
-
async close() {
|
|
598
|
-
if (sseController) {
|
|
599
|
-
await sseController.cancel().catch(() => {});
|
|
600
|
-
}
|
|
601
|
-
},
|
|
602
|
-
async notify(method: string, params?: Record<string, unknown>) {
|
|
603
|
-
await connectPromise;
|
|
604
|
-
await fetch(postEndpoint!, {
|
|
605
|
-
method: "POST",
|
|
606
|
-
headers: {
|
|
607
|
-
"Content-Type": "application/json",
|
|
608
|
-
...source.headers,
|
|
609
|
-
},
|
|
610
|
-
body: JSON.stringify({
|
|
611
|
-
jsonrpc: "2.0",
|
|
612
|
-
method,
|
|
613
|
-
...(params ? { params } : {}),
|
|
614
|
-
}),
|
|
615
|
-
}).catch(() => {});
|
|
616
|
-
},
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// ============================================
|
|
621
|
-
// Source Parsing
|
|
622
|
-
// ============================================
|
|
623
|
-
|
|
624
|
-
/**
|
|
625
|
-
* Extract the base URL from a server source (for OAuth discovery).
|
|
626
|
-
* Returns null for stdio/command-based sources.
|
|
627
|
-
*/
|
|
628
|
-
function resolveServerUrl(source: ServerSource): string | null {
|
|
629
|
-
if (typeof source === "string") {
|
|
630
|
-
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
631
|
-
return source.replace(/\/sse$/, "");
|
|
632
|
-
}
|
|
633
|
-
return null;
|
|
634
|
-
}
|
|
635
|
-
if ("url" in source) {
|
|
636
|
-
return source.url.replace(/\/sse$/, "");
|
|
637
|
-
}
|
|
638
|
-
return null;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
function parseServerSource(source: ServerSource): McpTransport {
|
|
642
|
-
if (typeof source === "string") {
|
|
643
|
-
// URL -> HTTP or SSE transport
|
|
644
|
-
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
645
|
-
// URLs ending in /sse use SSE transport
|
|
646
|
-
if (source.endsWith("/sse")) {
|
|
647
|
-
return createSseTransport({ url: source });
|
|
648
|
-
}
|
|
649
|
-
return createHttpTransport({ url: source });
|
|
650
|
-
}
|
|
651
|
-
// Command string -> stdio transport
|
|
652
|
-
const parts = source.split(/\s+/);
|
|
653
|
-
return createStdioTransport({
|
|
654
|
-
command: parts[0],
|
|
655
|
-
args: parts.slice(1),
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
if ("url" in source) {
|
|
660
|
-
// URLs ending in /sse use SSE transport
|
|
661
|
-
if (source.url.endsWith("/sse")) {
|
|
662
|
-
return createSseTransport(source);
|
|
663
|
-
}
|
|
664
|
-
return createHttpTransport(source);
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
if ("spawn" in source) {
|
|
668
|
-
return createSpawnHttpTransport(source);
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
return createStdioTransport(source);
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
// ============================================
|
|
675
|
-
// Transport: Spawn + HTTP (for servers needing a port)
|
|
676
|
-
// ============================================
|
|
677
|
-
|
|
678
|
-
function createSpawnHttpTransport(source: {
|
|
679
|
-
spawn: string;
|
|
680
|
-
args?: string[];
|
|
681
|
-
env?: Record<string, string>;
|
|
682
|
-
port?: number;
|
|
683
|
-
endpoint?: string;
|
|
684
|
-
}): McpTransport {
|
|
685
|
-
const port = source.port ?? Math.floor(40000 + Math.random() * 10000);
|
|
686
|
-
const endpoint = source.endpoint ?? "/mcp";
|
|
687
|
-
const args = [...(source.args ?? []), "--port", String(port)];
|
|
688
|
-
|
|
689
|
-
const spawnEnv = source.env
|
|
690
|
-
? { ...getDefaultEnvironment(), ...source.env }
|
|
691
|
-
: { ...process.env } as Record<string, string>;
|
|
692
|
-
const proc = Bun.spawn([source.spawn, ...args], {
|
|
693
|
-
stdin: "pipe",
|
|
694
|
-
stdout: "pipe",
|
|
695
|
-
stderr: "pipe",
|
|
696
|
-
env: spawnEnv,
|
|
697
|
-
});
|
|
698
|
-
|
|
699
|
-
// Create inner HTTP transport
|
|
700
|
-
const inner = createHttpTransport({ url: `http://127.0.0.1:${port}${endpoint}` });
|
|
701
|
-
|
|
702
|
-
// Wait for server to be ready
|
|
703
|
-
const readyPromise = (async () => {
|
|
704
|
-
const deadline = Date.now() + 15_000;
|
|
705
|
-
while (Date.now() < deadline) {
|
|
706
|
-
try {
|
|
707
|
-
await fetch(`http://127.0.0.1:${port}${endpoint}`, {
|
|
708
|
-
method: "POST",
|
|
709
|
-
headers: { "Content-Type": "application/json" },
|
|
710
|
-
body: "{}",
|
|
711
|
-
signal: AbortSignal.timeout(1000),
|
|
712
|
-
});
|
|
713
|
-
// Any response (even 400) means server is up
|
|
714
|
-
return;
|
|
715
|
-
} catch {
|
|
716
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
throw new Error(`Server failed to start on port ${port} within 15s`);
|
|
720
|
-
})();
|
|
721
|
-
|
|
722
|
-
return {
|
|
723
|
-
async send(method: string, params?: Record<string, unknown>) {
|
|
724
|
-
await readyPromise;
|
|
725
|
-
return inner.send(method, params);
|
|
726
|
-
},
|
|
727
|
-
async notify(method: string, params?: Record<string, unknown>) {
|
|
728
|
-
await readyPromise;
|
|
729
|
-
return inner.notify(method, params);
|
|
730
|
-
},
|
|
731
|
-
async close() {
|
|
732
|
-
await inner.close();
|
|
733
|
-
proc.kill();
|
|
734
|
-
},
|
|
735
|
-
};
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
// ============================================
|
|
739
|
-
// Code Generation Helpers
|
|
740
|
-
// ============================================
|
|
741
|
-
|
|
742
|
-
/** Convert tool name to a valid TypeScript identifier (camelCase) */
|
|
743
|
-
export function toIdentifier(name: string): string {
|
|
744
|
-
return name
|
|
745
|
-
.replace(/[^a-zA-Z0-9_]/g, "_")
|
|
746
|
-
.split("_")
|
|
747
|
-
.filter(Boolean)
|
|
748
|
-
.map((part, i) =>
|
|
749
|
-
i === 0
|
|
750
|
-
? part.toLowerCase()
|
|
751
|
-
: part.charAt(0).toUpperCase() + part.slice(1).toLowerCase(),
|
|
752
|
-
)
|
|
753
|
-
.join("");
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
/** Convert tool name to PascalCase for type names */
|
|
757
|
-
export function toPascalCase(name: string): string {
|
|
758
|
-
return name
|
|
759
|
-
.replace(/[^a-zA-Z0-9_]/g, "_")
|
|
760
|
-
.split("_")
|
|
761
|
-
.filter(Boolean)
|
|
762
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
|
763
|
-
.join("");
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
/** Convert tool name to kebab-case for file names */
|
|
767
|
-
function toKebabCase(name: string): string {
|
|
768
|
-
return name
|
|
769
|
-
.replace(/[^a-zA-Z0-9]/g, "-")
|
|
770
|
-
.replace(/-+/g, "-")
|
|
771
|
-
.replace(/^-|-$/g, "")
|
|
772
|
-
.toLowerCase();
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
/** Serialize a JSON schema to a readable TypeScript literal string */
|
|
776
|
-
export function schemaToString(schema: JsonSchema, indent = 4): string {
|
|
777
|
-
const pad = " ".repeat(indent);
|
|
778
|
-
const lines: string[] = [];
|
|
779
|
-
|
|
780
|
-
lines.push("{");
|
|
781
|
-
lines.push(`${pad}type: '${schema.type}' as const,`);
|
|
782
|
-
|
|
783
|
-
if (schema.description) {
|
|
784
|
-
lines.push(
|
|
785
|
-
`${pad}description: ${JSON.stringify(schema.description)},`,
|
|
786
|
-
);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
if (schema.enum) {
|
|
790
|
-
lines.push(`${pad}enum: ${JSON.stringify(schema.enum)},`);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
if (schema.properties) {
|
|
794
|
-
lines.push(`${pad}properties: {`);
|
|
795
|
-
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
796
|
-
const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
797
|
-
lines.push(
|
|
798
|
-
`${pad} ${safeKey}: ${schemaToString(prop as JsonSchema, indent + 4)},`,
|
|
799
|
-
);
|
|
800
|
-
}
|
|
801
|
-
lines.push(`${pad}},`);
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
if (schema.items) {
|
|
805
|
-
lines.push(
|
|
806
|
-
`${pad}items: ${schemaToString(schema.items as JsonSchema, indent + 2)},`,
|
|
807
|
-
);
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
if (schema.required && schema.required.length > 0) {
|
|
811
|
-
lines.push(
|
|
812
|
-
`${pad}required: [${schema.required.map((r) => `'${r}'`).join(", ")}] as const,`,
|
|
813
|
-
);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
if (schema.additionalProperties !== undefined) {
|
|
817
|
-
if (typeof schema.additionalProperties === "boolean") {
|
|
818
|
-
lines.push(
|
|
819
|
-
`${pad}additionalProperties: ${schema.additionalProperties},`,
|
|
820
|
-
);
|
|
821
|
-
} else {
|
|
822
|
-
lines.push(
|
|
823
|
-
`${pad}additionalProperties: ${schemaToString(schema.additionalProperties as JsonSchema, indent + 2)},`,
|
|
824
|
-
);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
if (schema.default !== undefined) {
|
|
829
|
-
lines.push(`${pad}default: ${JSON.stringify(schema.default)},`);
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
lines.push(`${" ".repeat(indent - 2)}}`);
|
|
833
|
-
return lines.join("\n");
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
/** Generate a TypeScript interface from a JSON Schema */
|
|
837
|
-
export function schemaToInterface(
|
|
838
|
-
name: string,
|
|
839
|
-
schema: JsonSchema,
|
|
840
|
-
): string {
|
|
841
|
-
if (schema.type !== "object" || !schema.properties) {
|
|
842
|
-
return `export type ${name} = unknown;`;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
const required = new Set(schema.required ?? []);
|
|
846
|
-
const lines: string[] = [`export interface ${name} {`];
|
|
847
|
-
|
|
848
|
-
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
849
|
-
const p = prop as JsonSchema;
|
|
850
|
-
const optional = required.has(key) ? "" : "?";
|
|
851
|
-
const tsType = jsonSchemaToTsType(p);
|
|
852
|
-
if (p.description) {
|
|
853
|
-
lines.push(` /** ${p.description} */`);
|
|
854
|
-
}
|
|
855
|
-
lines.push(
|
|
856
|
-
` ${/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key)}${optional}: ${tsType};`,
|
|
857
|
-
);
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
lines.push("}");
|
|
861
|
-
return lines.join("\n");
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
/** Convert a JSON Schema type to a TypeScript type string */
|
|
865
|
-
function jsonSchemaToTsType(schema: JsonSchema): string {
|
|
866
|
-
// const literal
|
|
867
|
-
if (schema.const !== undefined) {
|
|
868
|
-
return JSON.stringify(schema.const);
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
// enum
|
|
872
|
-
if (schema.enum) {
|
|
873
|
-
return schema.enum.map((v) => JSON.stringify(v)).join(" | ");
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
// oneOf / anyOf → union
|
|
877
|
-
if (schema.oneOf) {
|
|
878
|
-
return (schema.oneOf as JsonSchema[]).map(jsonSchemaToTsType).join(" | ");
|
|
879
|
-
}
|
|
880
|
-
if (schema.anyOf) {
|
|
881
|
-
return (schema.anyOf as JsonSchema[]).map(jsonSchemaToTsType).join(" | ");
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
// allOf → intersection
|
|
885
|
-
if (schema.allOf) {
|
|
886
|
-
return (schema.allOf as JsonSchema[]).map(jsonSchemaToTsType).join(" & ");
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// not → exclude
|
|
890
|
-
if (schema.not) {
|
|
891
|
-
return `Exclude<unknown, ${jsonSchemaToTsType(schema.not as JsonSchema)}>`;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// $ref
|
|
895
|
-
if (schema.$ref) {
|
|
896
|
-
const ref = schema.$ref as string;
|
|
897
|
-
// Extract name from #/$defs/Foo or #/definitions/Foo
|
|
898
|
-
const match = ref.match(/\/([^/]+)$/);
|
|
899
|
-
return match ? match[1] : "unknown";
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
switch (schema.type) {
|
|
903
|
-
case "string":
|
|
904
|
-
// Include format hint if present
|
|
905
|
-
if (schema.format) return `string /* ${schema.format} */`;
|
|
906
|
-
return "string";
|
|
907
|
-
case "integer":
|
|
908
|
-
return "number /* integer */";
|
|
909
|
-
case "number":
|
|
910
|
-
return "number";
|
|
911
|
-
case "boolean":
|
|
912
|
-
return "boolean";
|
|
913
|
-
case "null":
|
|
914
|
-
return "null";
|
|
915
|
-
case "array":
|
|
916
|
-
if (schema.items) {
|
|
917
|
-
return `${jsonSchemaToTsType(schema.items as JsonSchema)}[]`;
|
|
918
|
-
}
|
|
919
|
-
// Tuple arrays (prefixItems)
|
|
920
|
-
if (schema.prefixItems) {
|
|
921
|
-
const items = (schema.prefixItems as JsonSchema[]).map(jsonSchemaToTsType);
|
|
922
|
-
return `[${items.join(", ")}]`;
|
|
923
|
-
}
|
|
924
|
-
return "unknown[]";
|
|
925
|
-
case "object":
|
|
926
|
-
if (schema.properties) {
|
|
927
|
-
const required = new Set((schema.required as string[]) ?? []);
|
|
928
|
-
const props = Object.entries(schema.properties)
|
|
929
|
-
.map(([k, v]) => {
|
|
930
|
-
const opt = required.has(k) ? "" : "?";
|
|
931
|
-
return `${k}${opt}: ${jsonSchemaToTsType(v as JsonSchema)}`;
|
|
932
|
-
})
|
|
933
|
-
.join("; ");
|
|
934
|
-
// additionalProperties
|
|
935
|
-
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
|
936
|
-
const addlType = jsonSchemaToTsType(schema.additionalProperties as JsonSchema);
|
|
937
|
-
return props
|
|
938
|
-
? `{ ${props}; [key: string]: ${addlType} }`
|
|
939
|
-
: `Record<string, ${addlType}>`;
|
|
940
|
-
}
|
|
941
|
-
return `{ ${props} }`;
|
|
942
|
-
}
|
|
943
|
-
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
|
944
|
-
return `Record<string, ${jsonSchemaToTsType(schema.additionalProperties as JsonSchema)}>`;
|
|
945
|
-
}
|
|
946
|
-
return "Record<string, unknown>";
|
|
947
|
-
default:
|
|
948
|
-
// type as array (e.g., ["string", "null"])
|
|
949
|
-
if (Array.isArray(schema.type)) {
|
|
950
|
-
return schema.type.map((t: string) => (t === "null" ? "null" : t === "integer" ? "number" : t)).join(" | ");
|
|
951
|
-
}
|
|
952
|
-
return "unknown";
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
// ============================================
|
|
957
|
-
// File Generators
|
|
958
|
-
// ============================================
|
|
959
|
-
|
|
960
|
-
function generateToolFile(
|
|
961
|
-
tool: McpToolDefinition,
|
|
962
|
-
_sdkImport: string,
|
|
963
|
-
_generateTypes: boolean,
|
|
964
|
-
): string {
|
|
965
|
-
const schema = tool.inputSchema ?? {
|
|
966
|
-
type: "object" as const,
|
|
967
|
-
properties: {},
|
|
968
|
-
};
|
|
969
|
-
|
|
970
|
-
const lines: string[] = [];
|
|
971
|
-
|
|
972
|
-
// Title
|
|
973
|
-
lines.push(`# ${tool.name}`);
|
|
974
|
-
lines.push("");
|
|
975
|
-
if (tool.description) {
|
|
976
|
-
lines.push(tool.description);
|
|
977
|
-
lines.push("");
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
// Parameters table
|
|
981
|
-
const props = (schema.properties ?? {}) as Record<string, JsonSchema>;
|
|
982
|
-
const required = new Set((schema.required as string[]) ?? []);
|
|
983
|
-
const paramNames = Object.keys(props);
|
|
984
|
-
|
|
985
|
-
if (paramNames.length > 0) {
|
|
986
|
-
lines.push("## Parameters");
|
|
987
|
-
lines.push("");
|
|
988
|
-
lines.push("| Name | Type | Required | Description |");
|
|
989
|
-
lines.push("|------|------|----------|-------------|");
|
|
990
|
-
for (const name of paramNames) {
|
|
991
|
-
const prop = props[name];
|
|
992
|
-
const type = jsonSchemaToTsType(prop);
|
|
993
|
-
const req = required.has(name) ? "\u2713" : "";
|
|
994
|
-
const desc = (prop.description ?? "").replace(/\|/g, "\\|");
|
|
995
|
-
lines.push(`| ${name} | \`${type}\` | ${req} | ${desc} |`);
|
|
996
|
-
}
|
|
997
|
-
lines.push("");
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
return lines.join("\n");
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
function generateAgentConfig(
|
|
1004
|
-
serverInfo: McpServerInfo,
|
|
1005
|
-
_tools: McpToolDefinition[],
|
|
1006
|
-
agentPath: string,
|
|
1007
|
-
sdkImport: string,
|
|
1008
|
-
visibility: string,
|
|
1009
|
-
): string {
|
|
1010
|
-
const lines: string[] = [];
|
|
1011
|
-
|
|
1012
|
-
lines.push(`/**`);
|
|
1013
|
-
lines.push(` * Agent: ${agentPath}`);
|
|
1014
|
-
if (serverInfo.name) {
|
|
1015
|
-
lines.push(` * MCP Server: ${serverInfo.name} v${serverInfo.version ?? "unknown"}`);
|
|
1016
|
-
}
|
|
1017
|
-
lines.push(` *`);
|
|
1018
|
-
lines.push(` * Auto-generated by agents-sdk codegen.`);
|
|
1019
|
-
lines.push(` */`);
|
|
1020
|
-
lines.push("");
|
|
1021
|
-
lines.push(`import { defineAgent } from '${sdkImport}';`);
|
|
1022
|
-
lines.push("");
|
|
1023
|
-
lines.push(`export default defineAgent({`);
|
|
1024
|
-
lines.push(` path: '${agentPath}',`);
|
|
1025
|
-
lines.push(` entrypoint: './entrypoint.md',`);
|
|
1026
|
-
lines.push(` visibility: '${visibility}' as const,`);
|
|
1027
|
-
lines.push(`});`);
|
|
1028
|
-
lines.push("");
|
|
1029
|
-
|
|
1030
|
-
return lines.join("\n");
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
function generateEntrypoint(
|
|
1034
|
-
serverInfo: McpServerInfo,
|
|
1035
|
-
tools: McpToolDefinition[],
|
|
1036
|
-
agentPath: string,
|
|
1037
|
-
): string {
|
|
1038
|
-
const lines: string[] = [];
|
|
1039
|
-
|
|
1040
|
-
const name = serverInfo.name ?? agentPath;
|
|
1041
|
-
lines.push(`# ${name}`);
|
|
1042
|
-
lines.push("");
|
|
1043
|
-
if (serverInfo.version) {
|
|
1044
|
-
lines.push(`> MCP Server v${serverInfo.version}`);
|
|
1045
|
-
lines.push("");
|
|
1046
|
-
}
|
|
1047
|
-
lines.push(`You are an agent wrapping the ${name} MCP server.`);
|
|
1048
|
-
lines.push("");
|
|
1049
|
-
lines.push(`## Available Tools`);
|
|
1050
|
-
lines.push("");
|
|
1051
|
-
for (const tool of tools) {
|
|
1052
|
-
lines.push(`- **${tool.name}**: ${tool.description ?? "No description"}`);
|
|
1053
|
-
}
|
|
1054
|
-
lines.push("");
|
|
1055
|
-
|
|
1056
|
-
return lines.join("\n");
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
function generateIndex(
|
|
1060
|
-
_tools: McpToolDefinition[],
|
|
1061
|
-
): string {
|
|
1062
|
-
const lines: string[] = [];
|
|
1063
|
-
|
|
1064
|
-
lines.push(`/**`);
|
|
1065
|
-
lines.push(` * Auto-generated index.`);
|
|
1066
|
-
lines.push(` */`);
|
|
1067
|
-
lines.push("");
|
|
1068
|
-
lines.push(`export { default as agent } from './agent.config.js';`);
|
|
1069
|
-
lines.push("");
|
|
1070
|
-
|
|
1071
|
-
return lines.join("\n");
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
function generateCli(
|
|
1075
|
-
serverInfo: McpServerInfo,
|
|
1076
|
-
tools: McpToolDefinition[],
|
|
1077
|
-
agentPath: string,
|
|
1078
|
-
): string {
|
|
1079
|
-
const name = serverInfo.name ?? agentPath;
|
|
1080
|
-
const lines: string[] = [];
|
|
1081
|
-
|
|
1082
|
-
lines.push(`#!/usr/bin/env bun`);
|
|
1083
|
-
lines.push(`/**`);
|
|
1084
|
-
lines.push(` * CLI for ${name}`);
|
|
1085
|
-
lines.push(` *`);
|
|
1086
|
-
lines.push(` * Usage:`);
|
|
1087
|
-
lines.push(` * bun cli.ts <tool_name> [json_params]`);
|
|
1088
|
-
lines.push(` * bun cli.ts --list`);
|
|
1089
|
-
lines.push(` *`);
|
|
1090
|
-
lines.push(` * Auto-generated by agents-sdk codegen.`);
|
|
1091
|
-
lines.push(` */`);
|
|
1092
|
-
lines.push("");
|
|
1093
|
-
lines.push(`const tools = ${JSON.stringify(tools.map(t => ({ name: t.name, description: t.description ?? '' })), null, 2)};`);
|
|
1094
|
-
lines.push("");
|
|
1095
|
-
lines.push(`const args = process.argv.slice(2);`);
|
|
1096
|
-
lines.push("");
|
|
1097
|
-
lines.push(`if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {`);
|
|
1098
|
-
lines.push(` console.log('${name} CLI\\n');`);
|
|
1099
|
-
lines.push(` console.log('Usage: bun cli.ts <tool> [params_json]\\n');`);
|
|
1100
|
-
lines.push(` console.log('Available tools:');`);
|
|
1101
|
-
lines.push(` for (const t of tools) {`);
|
|
1102
|
-
lines.push(` console.log(\` \${t.name.padEnd(30)} \${t.description}\`);`);
|
|
1103
|
-
lines.push(` }`);
|
|
1104
|
-
lines.push(` process.exit(0);`);
|
|
1105
|
-
lines.push(`}`);
|
|
1106
|
-
lines.push("");
|
|
1107
|
-
lines.push(`if (args[0] === '--list') {`);
|
|
1108
|
-
lines.push(` for (const t of tools) {`);
|
|
1109
|
-
lines.push(` console.log(\`\${t.name}\\t\${t.description}\`);`);
|
|
1110
|
-
lines.push(` }`);
|
|
1111
|
-
lines.push(` process.exit(0);`);
|
|
1112
|
-
lines.push(`}`);
|
|
1113
|
-
lines.push("");
|
|
1114
|
-
lines.push(`const toolName = args[0];`);
|
|
1115
|
-
lines.push(`const params = args[1] ? JSON.parse(args[1]) : {};`);
|
|
1116
|
-
lines.push("");
|
|
1117
|
-
lines.push(`if (!tools.find(t => t.name === toolName)) {`);
|
|
1118
|
-
lines.push(` console.error(\`Unknown tool: \${toolName}\`);`);
|
|
1119
|
-
lines.push(` console.error(\`Available: \${tools.map(t => t.name).join(', ')}\`);`);
|
|
1120
|
-
lines.push(` process.exit(1);`);
|
|
1121
|
-
lines.push(`}`);
|
|
1122
|
-
lines.push("");
|
|
1123
|
-
lines.push(`// TODO: Connect to MCP server and execute the tool`);
|
|
1124
|
-
lines.push(`console.log(JSON.stringify({ tool: toolName, params, status: 'not_connected' }, null, 2));`);
|
|
1125
|
-
lines.push("");
|
|
1126
|
-
|
|
1127
|
-
return lines.join("\n");
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
// ============================================
|
|
1131
|
-
// Manifest
|
|
1132
|
-
// ============================================
|
|
1133
|
-
|
|
1134
|
-
/** Manifest stored in outDir for `agents-sdk use` */
|
|
1135
|
-
/** How a consumer connects to this MCP server */
|
|
1136
|
-
export interface ConnectionSpec {
|
|
1137
|
-
/** MCP server URL (for HTTP/SSE transports) */
|
|
1138
|
-
url?: string;
|
|
1139
|
-
/** Transport type */
|
|
1140
|
-
transport: 'http' | 'sse' | 'stdio';
|
|
1141
|
-
/** Auth requirements */
|
|
1142
|
-
auth: {
|
|
1143
|
-
/** Auth type: oauth, api_key, or none */
|
|
1144
|
-
type: 'oauth' | 'api_key' | 'none';
|
|
1145
|
-
/** OAuth discovery URL (.well-known/oauth-authorization-server) */
|
|
1146
|
-
discovery?: string;
|
|
1147
|
-
/** Whether dynamic client registration (RFC 7591) is supported */
|
|
1148
|
-
dynamic_registration?: boolean;
|
|
1149
|
-
};
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
export interface CodegenManifest {
|
|
1153
|
-
agentPath: string;
|
|
1154
|
-
serverSource: ServerSource;
|
|
1155
|
-
serverInfo: McpServerInfo;
|
|
1156
|
-
tools: { name: string; description?: string; inputSchema?: Record<string, unknown> }[];
|
|
1157
|
-
/** How to connect to and authenticate with this MCP server */
|
|
1158
|
-
connection?: ConnectionSpec;
|
|
1159
|
-
/** Raw OAuth server metadata (from .well-known discovery) */
|
|
1160
|
-
oauth?: OAuthServerMetadata;
|
|
1161
|
-
generatedAt: string;
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
function generateManifest(
|
|
1165
|
-
serverSource: ServerSource,
|
|
1166
|
-
serverInfo: McpServerInfo,
|
|
1167
|
-
tools: McpToolDefinition[],
|
|
1168
|
-
agentPath: string,
|
|
1169
|
-
oauth?: OAuthServerMetadata | null,
|
|
1170
|
-
): string {
|
|
1171
|
-
// Build connection spec from server source + OAuth discovery
|
|
1172
|
-
const serverUrl = resolveServerUrl(serverSource);
|
|
1173
|
-
const connection: ConnectionSpec | undefined = serverUrl
|
|
1174
|
-
? {
|
|
1175
|
-
url: serverUrl,
|
|
1176
|
-
transport: (typeof serverSource === 'string' && serverSource.endsWith('/sse')) ||
|
|
1177
|
-
(typeof serverSource === 'object' && 'url' in serverSource && serverSource.url.endsWith('/sse'))
|
|
1178
|
-
? 'sse' as const
|
|
1179
|
-
: 'http' as const,
|
|
1180
|
-
auth: oauth
|
|
1181
|
-
? {
|
|
1182
|
-
type: 'oauth' as const,
|
|
1183
|
-
discovery: `${serverUrl.replace(/\/mcp$/, '')}/.well-known/oauth-authorization-server`,
|
|
1184
|
-
dynamic_registration: !!oauth.registration_endpoint,
|
|
1185
|
-
}
|
|
1186
|
-
: { type: 'none' as const },
|
|
1187
|
-
}
|
|
1188
|
-
: undefined;
|
|
1189
|
-
|
|
1190
|
-
const manifest: CodegenManifest = {
|
|
1191
|
-
agentPath,
|
|
1192
|
-
serverSource,
|
|
1193
|
-
serverInfo,
|
|
1194
|
-
tools: tools.map((t) => ({ name: t.name, description: t.description, ...(t.inputSchema ? { inputSchema: t.inputSchema } : {}) })),
|
|
1195
|
-
...(connection ? { connection } : {}),
|
|
1196
|
-
...(oauth ? { oauth } : {}),
|
|
1197
|
-
generatedAt: new Date().toISOString(),
|
|
1198
|
-
};
|
|
1199
|
-
return JSON.stringify(manifest, null, 2) + "\n";
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
// ============================================
|
|
1203
|
-
// Main: codegen()
|
|
1204
|
-
// ============================================
|
|
1205
|
-
|
|
1206
|
-
/**
|
|
1207
|
-
* Connect to an MCP server, introspect its tools, and generate
|
|
1208
|
-
* agent-definition source files.
|
|
1209
|
-
*/
|
|
1210
|
-
export async function codegen(options: CodegenOptions): Promise<CodegenResult> {
|
|
1211
|
-
const sdkImport = options.sdkImport ?? "@slashfi/agents-sdk";
|
|
1212
|
-
const generateTypes = options.types !== false;
|
|
1213
|
-
const generateCliFile = options.cli !== false;
|
|
1214
|
-
const visibility = options.visibility ?? "public";
|
|
1215
|
-
const outDir = resolve(options.outDir);
|
|
1216
|
-
|
|
1217
|
-
// 1. Connect to MCP server
|
|
1218
|
-
const transport = parseServerSource(options.server);
|
|
1219
|
-
|
|
1220
|
-
let serverInfo: McpServerInfo = {};
|
|
1221
|
-
let tools: McpToolDefinition[] = [];
|
|
1222
|
-
|
|
1223
|
-
try {
|
|
1224
|
-
// 2. Initialize handshake
|
|
1225
|
-
const initResult = (await transport.send("initialize", {
|
|
1226
|
-
protocolVersion: LATEST_PROTOCOL_VERSION,
|
|
1227
|
-
capabilities: {},
|
|
1228
|
-
clientInfo: { name: "agents-sdk-codegen", version: "1.0.0" },
|
|
1229
|
-
})) as {
|
|
1230
|
-
serverInfo?: McpServerInfo;
|
|
1231
|
-
protocolVersion?: string;
|
|
1232
|
-
capabilities?: { tools?: unknown };
|
|
1233
|
-
};
|
|
1234
|
-
|
|
1235
|
-
// Validate protocol version
|
|
1236
|
-
if (
|
|
1237
|
-
initResult?.protocolVersion &&
|
|
1238
|
-
!SUPPORTED_PROTOCOL_VERSIONS.includes(initResult.protocolVersion)
|
|
1239
|
-
) {
|
|
1240
|
-
throw new Error(
|
|
1241
|
-
`Server protocol version ${initResult.protocolVersion} is not supported. Supported: ${SUPPORTED_PROTOCOL_VERSIONS.join(", ")}`,
|
|
1242
|
-
);
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
serverInfo = initResult?.serverInfo ?? {};
|
|
1246
|
-
|
|
1247
|
-
// Send initialized notification (no id — this is a notification, not a request)
|
|
1248
|
-
await transport.notify("notifications/initialized");
|
|
1249
|
-
|
|
1250
|
-
// 3. List tools (with pagination)
|
|
1251
|
-
const allTools: McpToolDefinition[] = [];
|
|
1252
|
-
let cursor: string | undefined;
|
|
1253
|
-
do {
|
|
1254
|
-
const toolsResult = (await transport.send("tools/list", {
|
|
1255
|
-
...(cursor ? { cursor } : {}),
|
|
1256
|
-
})) as {
|
|
1257
|
-
tools?: McpToolDefinition[];
|
|
1258
|
-
nextCursor?: string;
|
|
1259
|
-
};
|
|
1260
|
-
allTools.push(...(toolsResult?.tools ?? []));
|
|
1261
|
-
cursor = toolsResult?.nextCursor;
|
|
1262
|
-
} while (cursor);
|
|
1263
|
-
tools = allTools;
|
|
1264
|
-
} finally {
|
|
1265
|
-
await transport.close();
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
if (tools.length === 0) {
|
|
1269
|
-
throw new Error(
|
|
1270
|
-
"MCP server returned no tools. Is the server running and configured correctly?",
|
|
1271
|
-
);
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
// 3.5. Discover OAuth metadata (for URL-based servers)
|
|
1275
|
-
let oauth: OAuthServerMetadata | null = null;
|
|
1276
|
-
const serverUrl = resolveServerUrl(options.server);
|
|
1277
|
-
if (serverUrl) {
|
|
1278
|
-
oauth = await discoverOAuthMetadata(serverUrl);
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
// 4. Derive agent path
|
|
1282
|
-
const agentPath =
|
|
1283
|
-
options.agentPath ??
|
|
1284
|
-
`@${(options.name ?? serverInfo.name ?? "mcp-agent").toLowerCase().replace(/[^a-z0-9-]/g, "-")}`;
|
|
1285
|
-
|
|
1286
|
-
// 5. Create output directory
|
|
1287
|
-
mkdirSync(outDir, { recursive: true });
|
|
1288
|
-
|
|
1289
|
-
const files: string[] = [];
|
|
1290
|
-
|
|
1291
|
-
// 6. Generate tool files
|
|
1292
|
-
const toolFiles: string[] = [];
|
|
1293
|
-
for (const tool of tools) {
|
|
1294
|
-
const fileName = `${toKebabCase(tool.name)}.tool.md`;
|
|
1295
|
-
const content = generateToolFile(tool, sdkImport, generateTypes);
|
|
1296
|
-
writeFileSync(join(outDir, fileName), content);
|
|
1297
|
-
toolFiles.push(fileName);
|
|
1298
|
-
files.push(fileName);
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
// 7. Generate entrypoint.md
|
|
1302
|
-
const entrypoint = generateEntrypoint(serverInfo, tools, agentPath);
|
|
1303
|
-
writeFileSync(join(outDir, "entrypoint.md"), entrypoint);
|
|
1304
|
-
files.push("entrypoint.md");
|
|
1305
|
-
|
|
1306
|
-
// 8. Generate agent.config.ts
|
|
1307
|
-
const agentConfig = generateAgentConfig(
|
|
1308
|
-
serverInfo,
|
|
1309
|
-
tools,
|
|
1310
|
-
agentPath,
|
|
1311
|
-
sdkImport,
|
|
1312
|
-
visibility,
|
|
1313
|
-
);
|
|
1314
|
-
writeFileSync(join(outDir, "agent.config.ts"), agentConfig);
|
|
1315
|
-
files.push("agent.config.ts");
|
|
1316
|
-
|
|
1317
|
-
// 9. Generate index.ts
|
|
1318
|
-
const index = generateIndex(tools);
|
|
1319
|
-
writeFileSync(join(outDir, "index.ts"), index);
|
|
1320
|
-
files.push("index.ts");
|
|
1321
|
-
|
|
1322
|
-
// 10. Generate CLI
|
|
1323
|
-
if (generateCliFile) {
|
|
1324
|
-
const cli = generateCli(serverInfo, tools, agentPath);
|
|
1325
|
-
writeFileSync(join(outDir, "cli.ts"), cli);
|
|
1326
|
-
files.push("cli.ts");
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
// 11. Generate manifest (for `agents-sdk use`)
|
|
1330
|
-
const manifest = generateManifest(options.server, serverInfo, tools, agentPath, oauth);
|
|
1331
|
-
writeFileSync(join(outDir, ".codegen-manifest.json"), manifest);
|
|
1332
|
-
files.push(".codegen-manifest.json");
|
|
1333
|
-
|
|
1334
|
-
return {
|
|
1335
|
-
outDir,
|
|
1336
|
-
serverInfo,
|
|
1337
|
-
toolCount: tools.length,
|
|
1338
|
-
toolFiles,
|
|
1339
|
-
files,
|
|
1340
|
-
...(oauth ? { oauth } : {}),
|
|
1341
|
-
};
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
// ============================================
|
|
1345
|
-
// Use: execute a tool on a codegenned agent
|
|
1346
|
-
// ============================================
|
|
1347
|
-
|
|
1348
|
-
/**
|
|
1349
|
-
* Execute a tool on a previously codegenned agent by reconnecting
|
|
1350
|
-
* to the MCP server and calling the tool.
|
|
1351
|
-
*/
|
|
1352
|
-
export async function useAgent(options: {
|
|
1353
|
-
/** Path to the generated agent directory (contains .codegen-manifest.json) */
|
|
1354
|
-
agentDir: string;
|
|
1355
|
-
/** Tool name to execute */
|
|
1356
|
-
tool: string;
|
|
1357
|
-
/** Tool parameters */
|
|
1358
|
-
params?: Record<string, unknown>;
|
|
1359
|
-
}): Promise<unknown> {
|
|
1360
|
-
const manifestPath = join(
|
|
1361
|
-
resolve(options.agentDir),
|
|
1362
|
-
".codegen-manifest.json",
|
|
1363
|
-
);
|
|
1364
|
-
|
|
1365
|
-
if (!existsSync(manifestPath)) {
|
|
1366
|
-
throw new Error(
|
|
1367
|
-
`No codegen manifest found at ${manifestPath}. Run codegen first.`,
|
|
1368
|
-
);
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
const manifest: CodegenManifest = JSON.parse(
|
|
1372
|
-
readFileSync(manifestPath, "utf-8"),
|
|
1373
|
-
);
|
|
1374
|
-
|
|
1375
|
-
// Verify tool exists
|
|
1376
|
-
const toolDef = manifest.tools.find((t) => t.name === options.tool);
|
|
1377
|
-
if (!toolDef) {
|
|
1378
|
-
const available = manifest.tools.map((t) => t.name).join(", ");
|
|
1379
|
-
throw new Error(
|
|
1380
|
-
`Unknown tool '${options.tool}'. Available: ${available}`,
|
|
1381
|
-
);
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
// Connect to server and call tool
|
|
1385
|
-
const transport = parseServerSource(manifest.serverSource);
|
|
1386
|
-
|
|
1387
|
-
try {
|
|
1388
|
-
await transport.send("initialize", {
|
|
1389
|
-
protocolVersion: LATEST_PROTOCOL_VERSION,
|
|
1390
|
-
capabilities: {},
|
|
1391
|
-
clientInfo: { name: "agents-sdk-use", version: "1.0.0" },
|
|
1392
|
-
});
|
|
1393
|
-
|
|
1394
|
-
await transport.notify("notifications/initialized");
|
|
1395
|
-
|
|
1396
|
-
const result = await transport.send("tools/call", {
|
|
1397
|
-
name: options.tool,
|
|
1398
|
-
arguments: options.params ?? {},
|
|
1399
|
-
});
|
|
1400
|
-
|
|
1401
|
-
return result;
|
|
1402
|
-
} finally {
|
|
1403
|
-
await transport.close();
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
/**
|
|
1408
|
-
* List tools available on a codegenned agent.
|
|
1409
|
-
*/
|
|
1410
|
-
export function listAgentTools(agentDir: string): CodegenManifest["tools"] {
|
|
1411
|
-
const manifestPath = join(resolve(agentDir), ".codegen-manifest.json");
|
|
1412
|
-
|
|
1413
|
-
if (!existsSync(manifestPath)) {
|
|
1414
|
-
throw new Error(
|
|
1415
|
-
`No codegen manifest found at ${manifestPath}. Run codegen first.`,
|
|
1416
|
-
);
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
const manifest: CodegenManifest = JSON.parse(
|
|
1420
|
-
readFileSync(manifestPath, "utf-8"),
|
|
1421
|
-
);
|
|
1422
|
-
return manifest.tools;
|
|
1423
|
-
}
|