@slashfi/agents-sdk 0.20.0 → 0.22.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/src/client.ts ADDED
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Agent Client
3
+ *
4
+ * Creates a typed client from a SerializedAgentDefinition.
5
+ * The client spawns (or connects to) the MCP server and proxies
6
+ * tool calls via JSON-RPC.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { createClient } from '@slashfi/agents-sdk';
11
+ * import definition from './agents/notion/definition.json';
12
+ *
13
+ * const client = createClient(definition, {
14
+ * env: { NOTION_TOKEN: process.env.NOTION_TOKEN },
15
+ * });
16
+ *
17
+ * const result = await client.call('API-post-search', { query: 'meeting notes' });
18
+ * await client.close();
19
+ * ```
20
+ */
21
+
22
+ import { type ChildProcess, spawn } from "node:child_process";
23
+ import type {
24
+ SerializedAgentDefinition,
25
+ SerializedTool,
26
+ } from "./serialized.js";
27
+
28
+ // ============================================
29
+ // Client Options
30
+ // ============================================
31
+
32
+ export interface CreateClientOptions {
33
+ /** Extra environment variables for the MCP server process */
34
+ env?: Record<string, string | undefined>;
35
+ /** Override the server command (defaults to definition.serverSource) */
36
+ serverCommand?: string;
37
+ /** Timeout for tool calls in ms (default: 30000) */
38
+ timeout?: number;
39
+ }
40
+
41
+ // ============================================
42
+ // Agent Client
43
+ // ============================================
44
+
45
+ export interface AgentClient {
46
+ /** The definition this client was created from */
47
+ readonly definition: SerializedAgentDefinition;
48
+
49
+ /** List available tools */
50
+ tools(): SerializedTool[];
51
+
52
+ /** Call a tool by name */
53
+ call(toolName: string, input?: Record<string, unknown>): Promise<unknown>;
54
+
55
+ /** Check if connected */
56
+ isConnected(): boolean;
57
+
58
+ /** Close the MCP server connection */
59
+ close(): void;
60
+ }
61
+
62
+ // ============================================
63
+ // MCP Stdio Transport
64
+ // ============================================
65
+
66
+ interface PendingRequest {
67
+ resolve: (value: unknown) => void;
68
+ reject: (error: Error) => void;
69
+ timer: ReturnType<typeof setTimeout>;
70
+ }
71
+
72
+ class McpStdioClient implements AgentClient {
73
+ readonly definition: SerializedAgentDefinition;
74
+ private proc: ChildProcess | null = null;
75
+ private messageId = 0;
76
+ private buffer = "";
77
+ private pending = new Map<number, PendingRequest>();
78
+ private initialized = false;
79
+ private initPromise: Promise<void> | null = null;
80
+ private serverCommand: string;
81
+ private env: Record<string, string | undefined>;
82
+ private timeout: number;
83
+
84
+ constructor(
85
+ definition: SerializedAgentDefinition,
86
+ options: CreateClientOptions = {},
87
+ ) {
88
+ this.definition = definition;
89
+ this.serverCommand = options.serverCommand ?? definition.serverSource ?? "";
90
+ this.env = options.env ?? {};
91
+ this.timeout = options.timeout ?? 30000;
92
+
93
+ if (!this.serverCommand) {
94
+ throw new Error(
95
+ `No server command for agent "${definition.path}". Set serverSource in the definition or pass serverCommand in options.`,
96
+ );
97
+ }
98
+ }
99
+
100
+ tools(): SerializedTool[] {
101
+ return this.definition.tools;
102
+ }
103
+
104
+ isConnected(): boolean {
105
+ return this.proc !== null && !this.proc.killed;
106
+ }
107
+
108
+ async call(
109
+ toolName: string,
110
+ input: Record<string, unknown> = {},
111
+ ): Promise<unknown> {
112
+ // Validate tool exists
113
+ const tool = this.definition.tools.find((t) => t.name === toolName);
114
+ if (!tool) {
115
+ const available = this.definition.tools.map((t) => t.name).join(", ");
116
+ throw new Error(`Tool "${toolName}" not found. Available: ${available}`);
117
+ }
118
+
119
+ // Ensure connected + initialized
120
+ await this.ensureInitialized();
121
+
122
+ // Send tools/call
123
+ const result = await this.sendRequest("tools/call", {
124
+ name: toolName,
125
+ arguments: input,
126
+ });
127
+
128
+ // MCP tools/call returns { content: [{ type, text }] }
129
+ const resultObj = result as Record<string, unknown> | null;
130
+ if (resultObj && typeof resultObj === "object" && "content" in resultObj) {
131
+ const content = resultObj.content;
132
+ if (Array.isArray(content) && content.length > 0) {
133
+ const first = content[0];
134
+ if (first.type === "text") {
135
+ try {
136
+ return JSON.parse(first.text);
137
+ } catch {
138
+ return first.text;
139
+ }
140
+ }
141
+ return first;
142
+ }
143
+ }
144
+ return result;
145
+ }
146
+
147
+ close(): void {
148
+ if (this.proc) {
149
+ this.proc.kill();
150
+ this.proc = null;
151
+ }
152
+ this.initialized = false;
153
+ this.initPromise = null;
154
+ // Reject all pending
155
+ for (const req of Array.from(this.pending.values())) {
156
+ clearTimeout(req.timer);
157
+ req.reject(new Error("Client closed"));
158
+ }
159
+ this.pending.clear();
160
+ }
161
+
162
+ // ── Private ──
163
+
164
+ private async ensureInitialized(): Promise<void> {
165
+ if (this.initialized) return;
166
+ if (this.initPromise) return this.initPromise;
167
+
168
+ this.initPromise = this.connect();
169
+ await this.initPromise;
170
+ this.initialized = true;
171
+ }
172
+
173
+ private async connect(): Promise<void> {
174
+ const parts = this.serverCommand.split(/\s+/);
175
+ this.proc = spawn(parts[0], parts.slice(1), {
176
+ stdio: ["pipe", "pipe", "pipe"],
177
+ env: { ...process.env, ...this.env },
178
+ });
179
+
180
+ this.proc.stdout?.on("data", (chunk: Buffer) => this.onData(chunk));
181
+ this.proc.on("error", (err) => {
182
+ for (const req of Array.from(this.pending.values())) {
183
+ clearTimeout(req.timer);
184
+ req.reject(err);
185
+ }
186
+ this.pending.clear();
187
+ });
188
+
189
+ // Initialize handshake
190
+ await this.sendRequest("initialize", {
191
+ protocolVersion: "2024-11-05",
192
+ capabilities: {},
193
+ clientInfo: { name: "agents-sdk-client", version: "1.0.0" },
194
+ });
195
+
196
+ // Send initialized notification
197
+ this.sendNotification("notifications/initialized");
198
+ }
199
+
200
+ private sendRequest(
201
+ method: string,
202
+ params: Record<string, unknown> = {},
203
+ ): Promise<unknown> {
204
+ return new Promise((resolve, reject) => {
205
+ const id = ++this.messageId;
206
+ const timer = setTimeout(() => {
207
+ this.pending.delete(id);
208
+ reject(new Error(`Timeout after ${this.timeout}ms calling ${method}`));
209
+ }, this.timeout);
210
+
211
+ this.pending.set(id, { resolve, reject, timer });
212
+
213
+ const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params });
214
+ this.proc?.stdin?.write(`${msg}\n`);
215
+ });
216
+ }
217
+
218
+ private sendNotification(
219
+ method: string,
220
+ params?: Record<string, unknown>,
221
+ ): void {
222
+ const msg = JSON.stringify({
223
+ jsonrpc: "2.0",
224
+ method,
225
+ ...(params ? { params } : {}),
226
+ });
227
+ this.proc?.stdin?.write(`${msg}\n`);
228
+ }
229
+
230
+ private onData(chunk: Buffer): void {
231
+ this.buffer += chunk.toString();
232
+ const lines = this.buffer.split("\n");
233
+ this.buffer = lines.pop() || "";
234
+
235
+ for (const line of lines) {
236
+ const trimmed = line.trim();
237
+ if (!trimmed) continue;
238
+ try {
239
+ const parsed = JSON.parse(trimmed);
240
+ if (parsed.id != null && this.pending.has(parsed.id)) {
241
+ const req = this.pending.get(parsed.id);
242
+ if (!req) continue;
243
+ this.pending.delete(parsed.id);
244
+ clearTimeout(req.timer);
245
+ if (parsed.error) {
246
+ req.reject(new Error(JSON.stringify(parsed.error)));
247
+ } else {
248
+ req.resolve(parsed.result);
249
+ }
250
+ }
251
+ } catch {
252
+ // ignore non-JSON lines (stderr leakage, etc.)
253
+ }
254
+ }
255
+ }
256
+ }
257
+
258
+ // ============================================
259
+ // Factory
260
+ // ============================================
261
+
262
+ /**
263
+ * Create an agent client from a serialized definition.
264
+ *
265
+ * The client lazily spawns the MCP server on first call and
266
+ * proxies tool invocations via JSON-RPC over stdio.
267
+ */
268
+ export function createClient(
269
+ definition: SerializedAgentDefinition,
270
+ options?: CreateClientOptions,
271
+ ): AgentClient {
272
+ return new McpStdioClient(definition, options);
273
+ }
package/src/index.ts CHANGED
@@ -294,3 +294,49 @@ export type {
294
294
  McpTransport,
295
295
  ServerSource,
296
296
  } from "./codegen.js";
297
+
298
+ // ============================================
299
+ // Serialized Agent Definitions
300
+ // ============================================
301
+
302
+ export { serializeAgent, serializeTool } from "./serialized.js";
303
+ export type {
304
+ SerializedAgentDefinition,
305
+ SerializedTool,
306
+ } from "./serialized.js";
307
+
308
+ // ============================================
309
+ // Agent Client
310
+ // ============================================
311
+
312
+ export { createClient } from "./client.js";
313
+ export type {
314
+ AgentClient,
315
+ CreateClientOptions,
316
+ } from "./client.js";
317
+
318
+ // ============================================
319
+ // JSONC Parser
320
+ // ============================================
321
+
322
+ export { parseJsonc, readJsoncFile } from "./jsonc.js";
323
+
324
+ // ============================================
325
+ // Pack & Publish
326
+ // ============================================
327
+
328
+ export { pack, publish } from "./pack.js";
329
+ export type {
330
+ PackOptions,
331
+ PackResult,
332
+ PublishOptions,
333
+ VersionMeta,
334
+ VersionChanges,
335
+ } from "./pack.js";
336
+
337
+ // ============================================
338
+ // Introspect
339
+ // ============================================
340
+
341
+ export { introspectMcp } from "./introspect.js";
342
+ export type { IntrospectOptions } from "./introspect.js";
@@ -0,0 +1,171 @@
1
+ /**
2
+ * MCP Introspection
3
+ *
4
+ * Connects to an MCP server, introspects its tools,
5
+ * and outputs a SerializedAgentDefinition (agent.json).
6
+ *
7
+ * Deduplicates shared $defs across tools.
8
+ */
9
+
10
+ import { spawn } from "node:child_process";
11
+ import { mkdirSync, writeFileSync } from "node:fs";
12
+ import { dirname, resolve } from "node:path";
13
+
14
+ export interface IntrospectOptions {
15
+ server: string;
16
+ name: string;
17
+ out?: string;
18
+ env?: Record<string, string>;
19
+ }
20
+
21
+ function deduplicateDefs(tools: Record<string, unknown>[]): {
22
+ tools: Record<string, unknown>[];
23
+ sharedDefs: Record<string, unknown>;
24
+ } {
25
+ const defsByContent = new Map<string, { name: string; schema: unknown }>();
26
+
27
+ for (const tool of tools) {
28
+ const schema = tool.inputSchema as Record<string, unknown> | undefined;
29
+ const defs = schema?.$defs;
30
+ if (!defs || typeof defs !== "object") continue;
31
+ for (const [defName, defSchema] of Object.entries(
32
+ defs as Record<string, unknown>,
33
+ )) {
34
+ const key = JSON.stringify(defSchema);
35
+ if (!defsByContent.has(key)) {
36
+ defsByContent.set(key, { name: defName, schema: defSchema });
37
+ }
38
+ }
39
+ }
40
+
41
+ const sharedDefs: Record<string, unknown> = {};
42
+ for (const { name, schema } of Array.from(defsByContent.values())) {
43
+ sharedDefs[name] = schema;
44
+ }
45
+
46
+ const cleanedTools = tools.map((t) => {
47
+ const schema = { ...(t.inputSchema as Record<string, unknown>) };
48
+ schema.$defs = undefined;
49
+ return { ...t, inputSchema: schema };
50
+ });
51
+
52
+ return { tools: cleanedTools, sharedDefs };
53
+ }
54
+
55
+ export async function introspectMcp(options: IntrospectOptions): Promise<void> {
56
+ const { server, name, out } = options;
57
+
58
+ const parts = server.split(/\s+/);
59
+ const proc = spawn(parts[0], parts.slice(1), {
60
+ stdio: ["pipe", "pipe", "pipe"],
61
+ env: { ...process.env, ...options.env },
62
+ });
63
+
64
+ let buffer = "";
65
+ let messageId = 0;
66
+
67
+ function sendJsonRpc(
68
+ method: string,
69
+ params: Record<string, unknown> = {},
70
+ ): number {
71
+ const id = ++messageId;
72
+ const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params });
73
+ proc.stdin?.write(`${msg}\n`);
74
+ return id;
75
+ }
76
+
77
+ function waitForResponse(targetId: number): Promise<Record<string, unknown>> {
78
+ return new Promise((res, reject) => {
79
+ const timeout = setTimeout(() => reject(new Error("Timeout")), 30000);
80
+ const handler = (chunk: Buffer) => {
81
+ buffer += chunk.toString();
82
+ const lines = buffer.split("\n");
83
+ buffer = lines.pop() || "";
84
+ for (const line of lines) {
85
+ const trimmed = line.trim();
86
+ if (!trimmed) continue;
87
+ try {
88
+ const parsed = JSON.parse(trimmed);
89
+ if (parsed.id === targetId) {
90
+ clearTimeout(timeout);
91
+ proc.stdout?.removeListener("data", handler);
92
+ if (parsed.error) reject(new Error(JSON.stringify(parsed.error)));
93
+ else res(parsed.result);
94
+ }
95
+ } catch {
96
+ /* ignore non-JSON */
97
+ }
98
+ }
99
+ };
100
+ proc.stdout?.on("data", handler);
101
+ });
102
+ }
103
+
104
+ try {
105
+ console.log(`Connecting to MCP server: ${server}`);
106
+
107
+ const initId = sendJsonRpc("initialize", {
108
+ protocolVersion: "2024-11-05",
109
+ capabilities: {},
110
+ clientInfo: { name: "adk-introspect", version: "1.0.0" },
111
+ });
112
+ const initResult = (await waitForResponse(initId)) as Record<
113
+ string,
114
+ unknown
115
+ >;
116
+ const serverInfo = initResult.serverInfo as
117
+ | Record<string, string>
118
+ | undefined;
119
+ console.log(`Server: ${serverInfo?.name} v${serverInfo?.version}`);
120
+
121
+ proc.stdin?.write(
122
+ `${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })}\n`,
123
+ );
124
+
125
+ const toolsId = sendJsonRpc("tools/list", {});
126
+ const toolsResult = (await waitForResponse(toolsId)) as Record<
127
+ string,
128
+ unknown
129
+ >;
130
+ const rawTools = (
131
+ (toolsResult.tools || []) as Record<string, unknown>[]
132
+ ).map((t) => ({
133
+ name: t.name as string,
134
+ description: (t.description as string) || "",
135
+ inputSchema: (t.inputSchema as Record<string, unknown>) || {
136
+ type: "object",
137
+ properties: {},
138
+ },
139
+ }));
140
+ console.log(`Discovered ${rawTools.length} tools`);
141
+
142
+ const { tools, sharedDefs } = deduplicateDefs(rawTools);
143
+ const defsCount = Object.keys(sharedDefs).length;
144
+ if (defsCount > 0) {
145
+ console.log(`Hoisted ${defsCount} shared $defs`);
146
+ }
147
+
148
+ const definition: Record<string, unknown> = {
149
+ path: name,
150
+ name: serverInfo?.name || name,
151
+ description: `Agent for ${serverInfo?.name || name}`,
152
+ version: serverInfo?.version || "1.0.0",
153
+ visibility: "public",
154
+ serverSource: server,
155
+ serverInfo,
156
+ ...(defsCount > 0 ? { $defs: sharedDefs } : {}),
157
+ tools,
158
+ generatedAt: new Date().toISOString(),
159
+ sdkVersion: "0.22.0",
160
+ };
161
+
162
+ const outPath = out || `./${name}.json`;
163
+ const resolved = resolve(outPath);
164
+ mkdirSync(dirname(resolved), { recursive: true });
165
+ writeFileSync(resolved, `${JSON.stringify(definition, null, 2)}\n`);
166
+ const sizeKB = (JSON.stringify(definition).length / 1024).toFixed(1);
167
+ console.log(`\nWrote ${resolved} (${sizeKB}KB, ${tools.length} tools)`);
168
+ } finally {
169
+ proc.kill();
170
+ }
171
+ }
package/src/jsonc.ts ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * JSONC Parser
3
+ *
4
+ * Parses JSON with comments (single-line and multi-line)
5
+ * so agent.json files can be annotated.
6
+ */
7
+
8
+ import { readFileSync } from "node:fs";
9
+
10
+ /**
11
+ * Strip comments from JSONC content and parse as JSON.
12
+ */
13
+ export function parseJsonc(content: string): unknown {
14
+ let result = "";
15
+ let i = 0;
16
+ let inString = false;
17
+ let stringChar = "";
18
+
19
+ while (i < content.length) {
20
+ if (inString) {
21
+ if (content[i] === "\\" && i + 1 < content.length) {
22
+ result += content[i] + content[i + 1];
23
+ i += 2;
24
+ continue;
25
+ }
26
+ if (content[i] === stringChar) {
27
+ inString = false;
28
+ }
29
+ result += content[i];
30
+ i++;
31
+ continue;
32
+ }
33
+
34
+ if (content[i] === '"') {
35
+ inString = true;
36
+ stringChar = content[i];
37
+ result += content[i];
38
+ i++;
39
+ continue;
40
+ }
41
+
42
+ // Single-line comment
43
+ if (
44
+ content[i] === "/" &&
45
+ i + 1 < content.length &&
46
+ content[i + 1] === "/"
47
+ ) {
48
+ while (i < content.length && content[i] !== "\n") i++;
49
+ continue;
50
+ }
51
+
52
+ // Multi-line comment
53
+ if (
54
+ content[i] === "/" &&
55
+ i + 1 < content.length &&
56
+ content[i + 1] === "*"
57
+ ) {
58
+ i += 2;
59
+ while (
60
+ i + 1 < content.length &&
61
+ !(content[i] === "*" && content[i + 1] === "/")
62
+ )
63
+ i++;
64
+ i += 2;
65
+ continue;
66
+ }
67
+
68
+ result += content[i];
69
+ i++;
70
+ }
71
+
72
+ // Handle trailing commas
73
+ const cleaned = result.replace(/,\s*([}\]])/g, "$1");
74
+ return JSON.parse(cleaned);
75
+ }
76
+
77
+ /**
78
+ * Read and parse a JSONC file.
79
+ */
80
+ export function readJsoncFile(path: string): unknown {
81
+ const content = readFileSync(path, "utf-8");
82
+ return parseJsonc(content);
83
+ }