toolnexus 0.1.1 → 0.2.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.
@@ -0,0 +1,609 @@
1
+ /**
2
+ * Built-in tool source (source: "builtin"). The default toolset toolnexus ships
3
+ * so an agent can act with zero custom wiring — opencode's built-ins, ported
4
+ * with identical tool names + input schemas. See ../../SPEC.md §4A.
5
+ *
6
+ * Every tool obeys the uniform Tool/ToolResult contract: a failure is a
7
+ * ToolResult{isError:true}, never a thrown exception across the boundary. Paths
8
+ * resolve relative to the process working directory unless absolute.
9
+ */
10
+ import { spawn } from "node:child_process";
11
+ import fs from "node:fs/promises";
12
+ import { existsSync, readdirSync } from "node:fs";
13
+ import path from "node:path";
14
+ /**
15
+ * Whether the builtin source is on. Default ON. Same precedence as MCP:
16
+ * `disabled:true` wins, else `enabled:false` disables, otherwise enabled.
17
+ */
18
+ export function builtinsEnabled(cfg) {
19
+ if (cfg === undefined)
20
+ return true;
21
+ if (typeof cfg === "boolean")
22
+ return cfg;
23
+ if (cfg.disabled === true)
24
+ return false;
25
+ if (cfg.enabled === false)
26
+ return false;
27
+ return true;
28
+ }
29
+ /**
30
+ * Resolve the active builtin tools for a config. Whole-source-off wins and
31
+ * returns `[]`. Otherwise all ten are on; a `tools` name→bool map drops any
32
+ * tool mapped to `false` (all-on baseline; `true`/absent stay on; unknown names
33
+ * are ignored). SPEC §4A.
34
+ */
35
+ export function selectBuiltins(cfg) {
36
+ if (!builtinsEnabled(cfg))
37
+ return [];
38
+ const map = typeof cfg === "object" ? cfg.tools : undefined;
39
+ const all = createBuiltinTools();
40
+ if (!map)
41
+ return all;
42
+ return all.filter((t) => map[t.name] !== false);
43
+ }
44
+ const err = (output, metadata) => ({ output, isError: true, metadata });
45
+ const ok = (output, metadata) => ({ output, isError: false, metadata });
46
+ const IGNORE_DIRS = new Set(["node_modules", ".git"]);
47
+ function builtin(name, description, inputSchema, run) {
48
+ return {
49
+ name,
50
+ description,
51
+ inputSchema,
52
+ source: "builtin",
53
+ async execute(args, ctx) {
54
+ try {
55
+ return await run(args ?? {}, ctx);
56
+ }
57
+ catch (e) {
58
+ return err(`${name}: ${e instanceof Error ? e.message : String(e)}`);
59
+ }
60
+ },
61
+ };
62
+ }
63
+ // ---------------------------------------------------------------------------
64
+ // glob helpers (shared by grep + glob)
65
+ // ---------------------------------------------------------------------------
66
+ /** Convert a glob (`*`, `**`, `?`) to an anchored RegExp. */
67
+ function globToRegExp(glob) {
68
+ let re = "";
69
+ for (let i = 0; i < glob.length; i++) {
70
+ const c = glob[i];
71
+ if (c === "*") {
72
+ if (glob[i + 1] === "*") {
73
+ re += ".*";
74
+ i++;
75
+ if (glob[i + 1] === "/")
76
+ i++;
77
+ }
78
+ else {
79
+ re += "[^/]*";
80
+ }
81
+ }
82
+ else if (c === "?") {
83
+ re += "[^/]";
84
+ }
85
+ else if ("\\^$.|+()[]{}".includes(c)) {
86
+ re += "\\" + c;
87
+ }
88
+ else {
89
+ re += c;
90
+ }
91
+ }
92
+ return new RegExp("^" + re + "$");
93
+ }
94
+ /** Match a relative path against a glob; slash-less globs test the basename. */
95
+ function matchGlob(rel, glob) {
96
+ const re = globToRegExp(glob);
97
+ if (!glob.includes("/"))
98
+ return re.test(path.basename(rel));
99
+ return re.test(rel);
100
+ }
101
+ /** Recursively list files under `root` (skips node_modules/.git). Returns absolute paths. */
102
+ function walkFiles(root) {
103
+ const out = [];
104
+ const stack = [root];
105
+ while (stack.length) {
106
+ const dir = stack.pop();
107
+ let entries;
108
+ try {
109
+ entries = readdirSync(dir, { withFileTypes: true });
110
+ }
111
+ catch {
112
+ continue;
113
+ }
114
+ for (const entry of entries) {
115
+ const full = path.join(dir, entry.name);
116
+ if (entry.isDirectory()) {
117
+ if (IGNORE_DIRS.has(entry.name))
118
+ continue;
119
+ stack.push(full);
120
+ }
121
+ else if (entry.isFile()) {
122
+ out.push(full);
123
+ }
124
+ }
125
+ }
126
+ return out;
127
+ }
128
+ // ---------------------------------------------------------------------------
129
+ // individual tools
130
+ // ---------------------------------------------------------------------------
131
+ function bashTool() {
132
+ return builtin("bash", "Run a shell command and return its combined stdout+stderr. Non-zero exit is an error.", {
133
+ type: "object",
134
+ properties: {
135
+ command: { type: "string", description: "The shell command to run" },
136
+ workdir: { type: "string", description: "Working directory (default: process cwd)" },
137
+ timeout: { type: "number", description: "Timeout in milliseconds (default 60000)" },
138
+ description: { type: "string", description: "Human-readable description of the command" },
139
+ },
140
+ required: ["command"],
141
+ additionalProperties: false,
142
+ }, (args) => new Promise((resolve) => {
143
+ const command = String(args.command ?? "");
144
+ if (!command)
145
+ return resolve(err("bash: command is required"));
146
+ const workdir = args.workdir ? String(args.workdir) : process.cwd();
147
+ const timeout = typeof args.timeout === "number" ? args.timeout : 60_000;
148
+ const child = spawn(command, { shell: true, cwd: workdir });
149
+ let out = "";
150
+ let timedOut = false;
151
+ const timer = setTimeout(() => {
152
+ timedOut = true;
153
+ child.kill("SIGKILL");
154
+ }, timeout);
155
+ child.stdout?.on("data", (d) => (out += d.toString()));
156
+ child.stderr?.on("data", (d) => (out += d.toString()));
157
+ child.on("error", (e) => {
158
+ clearTimeout(timer);
159
+ resolve(err(`bash: ${e.message}`));
160
+ });
161
+ child.on("close", (code) => {
162
+ clearTimeout(timer);
163
+ if (timedOut)
164
+ return resolve(err(`bash: command timed out after ${timeout}ms\n${out}`));
165
+ if (code !== 0) {
166
+ return resolve(err(`${out}\nbash: command exited with code ${code}`, { exitCode: code }));
167
+ }
168
+ resolve(ok(out, { exitCode: code }));
169
+ });
170
+ }));
171
+ }
172
+ function readTool() {
173
+ return builtin("read", "Read a UTF-8 text file. With offset/limit, return only that line window.", {
174
+ type: "object",
175
+ properties: {
176
+ path: { type: "string", description: "Path to the file to read" },
177
+ offset: { type: "number", description: "1-based line to start from" },
178
+ limit: { type: "number", description: "Maximum number of lines to read" },
179
+ },
180
+ required: ["path"],
181
+ additionalProperties: false,
182
+ }, async (args) => {
183
+ const p = String(args.path ?? "");
184
+ if (!p)
185
+ return err("read: path is required");
186
+ let content;
187
+ try {
188
+ content = await fs.readFile(p, "utf8");
189
+ }
190
+ catch (e) {
191
+ return err(`read: ${e instanceof Error ? e.message : String(e)}`);
192
+ }
193
+ if (args.offset === undefined && args.limit === undefined)
194
+ return ok(content);
195
+ const lines = content.split("\n");
196
+ const offset = typeof args.offset === "number" ? Math.max(1, Math.trunc(args.offset)) : 1;
197
+ const start = offset - 1;
198
+ const limit = typeof args.limit === "number" ? Math.max(0, Math.trunc(args.limit)) : lines.length - start;
199
+ return ok(lines.slice(start, start + limit).join("\n"));
200
+ });
201
+ }
202
+ function writeTool() {
203
+ return builtin("write", "Write content to a file (create/overwrite), creating parent directories.", {
204
+ type: "object",
205
+ properties: {
206
+ path: { type: "string", description: "Path to write to" },
207
+ content: { type: "string", description: "Content to write" },
208
+ },
209
+ required: ["path", "content"],
210
+ additionalProperties: false,
211
+ }, async (args) => {
212
+ const p = String(args.path ?? "");
213
+ if (!p)
214
+ return err("write: path is required");
215
+ const content = typeof args.content === "string" ? args.content : String(args.content ?? "");
216
+ await fs.mkdir(path.dirname(path.resolve(p)), { recursive: true });
217
+ await fs.writeFile(p, content, "utf8");
218
+ const bytes = Buffer.byteLength(content, "utf8");
219
+ return ok(`Wrote ${bytes} bytes to ${p}`, { bytes });
220
+ });
221
+ }
222
+ function editTool() {
223
+ return builtin("edit", "Exact-string replace in a file. Default replaces a single unique occurrence; replaceAll replaces all.", {
224
+ type: "object",
225
+ properties: {
226
+ path: { type: "string", description: "Path to the file to edit" },
227
+ oldString: { type: "string", description: "Exact string to replace" },
228
+ newString: { type: "string", description: "Replacement string" },
229
+ replaceAll: { type: "boolean", description: "Replace all occurrences" },
230
+ },
231
+ required: ["path", "oldString", "newString"],
232
+ additionalProperties: false,
233
+ }, async (args) => {
234
+ const p = String(args.path ?? "");
235
+ if (!p)
236
+ return err("edit: path is required");
237
+ if (typeof args.oldString !== "string" || args.oldString.length === 0) {
238
+ return err("edit: oldString is required");
239
+ }
240
+ const oldString = args.oldString;
241
+ const newString = typeof args.newString === "string" ? args.newString : String(args.newString ?? "");
242
+ let content;
243
+ try {
244
+ content = await fs.readFile(p, "utf8");
245
+ }
246
+ catch (e) {
247
+ return err(`edit: ${e instanceof Error ? e.message : String(e)}`);
248
+ }
249
+ const count = content.split(oldString).length - 1;
250
+ if (count === 0)
251
+ return err(`edit: oldString not found in ${p}`);
252
+ let next;
253
+ if (args.replaceAll === true) {
254
+ next = content.split(oldString).join(newString);
255
+ }
256
+ else {
257
+ if (count > 1)
258
+ return err(`edit: oldString is not unique in ${p} (${count} occurrences); use replaceAll`);
259
+ next = content.replace(oldString, newString);
260
+ }
261
+ await fs.writeFile(p, next, "utf8");
262
+ return ok(`Edited ${p} (${args.replaceAll === true ? count : 1} replacement${(args.replaceAll === true ? count : 1) === 1 ? "" : "s"})`, {
263
+ replacements: args.replaceAll === true ? count : 1,
264
+ });
265
+ });
266
+ }
267
+ function grepTool() {
268
+ return builtin("grep", "Search file contents by regex under a directory. Output is file:line:text matches.", {
269
+ type: "object",
270
+ properties: {
271
+ pattern: { type: "string", description: "Regular expression to search for" },
272
+ path: { type: "string", description: "Directory to search (default: process cwd)" },
273
+ include: { type: "string", description: "Glob filter for file names" },
274
+ limit: { type: "number", description: "Maximum number of matches (default 100)" },
275
+ },
276
+ required: ["pattern"],
277
+ additionalProperties: false,
278
+ }, async (args) => {
279
+ const pattern = String(args.pattern ?? "");
280
+ if (!pattern)
281
+ return err("grep: pattern is required");
282
+ let re;
283
+ try {
284
+ re = new RegExp(pattern);
285
+ }
286
+ catch (e) {
287
+ return err(`grep: invalid regex: ${e instanceof Error ? e.message : String(e)}`);
288
+ }
289
+ const root = args.path ? String(args.path) : process.cwd();
290
+ const include = args.include ? String(args.include) : undefined;
291
+ const limit = typeof args.limit === "number" ? args.limit : 100;
292
+ const matches = [];
293
+ for (const file of walkFiles(root)) {
294
+ if (matches.length >= limit)
295
+ break;
296
+ const rel = path.relative(root, file);
297
+ if (include && !matchGlob(rel, include))
298
+ continue;
299
+ let text;
300
+ try {
301
+ text = await fs.readFile(file, "utf8");
302
+ }
303
+ catch {
304
+ continue;
305
+ }
306
+ const lines = text.split("\n");
307
+ for (let i = 0; i < lines.length; i++) {
308
+ if (matches.length >= limit)
309
+ break;
310
+ if (re.test(lines[i]))
311
+ matches.push(`${file}:${i + 1}:${lines[i]}`);
312
+ }
313
+ }
314
+ return ok(matches.join("\n"), { count: matches.length });
315
+ });
316
+ }
317
+ function globTool() {
318
+ return builtin("glob", "List files matching a glob under a directory. Output is newline-joined relative paths.", {
319
+ type: "object",
320
+ properties: {
321
+ pattern: { type: "string", description: "Glob pattern to match" },
322
+ path: { type: "string", description: "Directory to search (default: process cwd)" },
323
+ limit: { type: "number", description: "Maximum number of results (default 100)" },
324
+ },
325
+ required: ["pattern"],
326
+ additionalProperties: false,
327
+ }, async (args) => {
328
+ const pattern = String(args.pattern ?? "");
329
+ if (!pattern)
330
+ return err("glob: pattern is required");
331
+ const root = args.path ? String(args.path) : process.cwd();
332
+ const limit = typeof args.limit === "number" ? args.limit : 100;
333
+ const found = [];
334
+ for (const file of walkFiles(root)) {
335
+ if (found.length >= limit)
336
+ break;
337
+ const rel = path.relative(root, file);
338
+ if (matchGlob(rel, pattern))
339
+ found.push(rel);
340
+ }
341
+ found.sort();
342
+ return ok(found.slice(0, limit).join("\n"), { count: Math.min(found.length, limit) });
343
+ });
344
+ }
345
+ /** Very light HTML → text: drop scripts/styles + tags, collapse whitespace. */
346
+ function stripHtml(html) {
347
+ return html
348
+ .replace(/<script[\s\S]*?<\/script>/gi, "")
349
+ .replace(/<style[\s\S]*?<\/style>/gi, "")
350
+ .replace(/<[^>]+>/g, "")
351
+ .replace(/[ \t]+\n/g, "\n")
352
+ .replace(/\n{3,}/g, "\n\n")
353
+ .trim();
354
+ }
355
+ function webfetchTool() {
356
+ return builtin("webfetch", "HTTP GET a URL and return its body as text, markdown, or html.", {
357
+ type: "object",
358
+ properties: {
359
+ url: { type: "string", description: "URL to fetch" },
360
+ format: { type: "string", enum: ["text", "markdown", "html"], description: "Response format (default markdown)" },
361
+ timeout: { type: "number", description: "Timeout in seconds (default 30)" },
362
+ },
363
+ required: ["url"],
364
+ additionalProperties: false,
365
+ }, async (args, ctx) => {
366
+ const url = String(args.url ?? "");
367
+ if (!url)
368
+ return err("webfetch: url is required");
369
+ const format = args.format === "text" || args.format === "html" ? args.format : "markdown";
370
+ const timeoutMs = (typeof args.timeout === "number" ? args.timeout : 30) * 1000;
371
+ const controller = new AbortController();
372
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
373
+ if (ctx?.signal)
374
+ ctx.signal.addEventListener("abort", () => controller.abort(), { once: true });
375
+ try {
376
+ const res = await fetch(url, { method: "GET", signal: controller.signal });
377
+ const body = await res.text();
378
+ if (!res.ok)
379
+ return err(`HTTP ${res.status}`, { status: res.status });
380
+ const output = format === "html" ? body : stripHtml(body);
381
+ return ok(output, { status: res.status, format });
382
+ }
383
+ catch (e) {
384
+ return err(`webfetch: ${e instanceof Error ? e.message : String(e)}`);
385
+ }
386
+ finally {
387
+ clearTimeout(timer);
388
+ }
389
+ });
390
+ }
391
+ function questionTool() {
392
+ return builtin("question", "Ask the host one or more questions. Returns the questions as structured output for the host to answer.", {
393
+ type: "object",
394
+ properties: {
395
+ questions: {
396
+ type: "array",
397
+ description: "Questions to ask",
398
+ items: {
399
+ type: "object",
400
+ properties: {
401
+ question: { type: "string" },
402
+ header: { type: "string" },
403
+ options: { type: "array", items: { type: "string" } },
404
+ multiple: { type: "boolean" },
405
+ },
406
+ required: ["question"],
407
+ },
408
+ },
409
+ },
410
+ required: ["questions"],
411
+ additionalProperties: false,
412
+ }, async (args) => {
413
+ const questions = Array.isArray(args.questions) ? args.questions : [];
414
+ return ok(JSON.stringify(questions), { questions });
415
+ });
416
+ }
417
+ function todowriteTool() {
418
+ return builtin("todowrite", "Replace the session todo list. Returns the rendered list.", {
419
+ type: "object",
420
+ properties: {
421
+ todos: {
422
+ type: "array",
423
+ description: "The full todo list to store",
424
+ items: {
425
+ type: "object",
426
+ properties: {
427
+ id: { type: "string" },
428
+ text: { type: "string" },
429
+ completed: { type: "boolean" },
430
+ },
431
+ required: ["id", "text", "completed"],
432
+ },
433
+ },
434
+ },
435
+ required: ["todos"],
436
+ additionalProperties: false,
437
+ }, async (args) => {
438
+ const todos = Array.isArray(args.todos) ? args.todos : [];
439
+ const rendered = todos.map((t) => `[${t.completed ? "x" : " "}] ${String(t.text ?? "")}`).join("\n");
440
+ return ok(rendered || "(no todos)", { todos });
441
+ });
442
+ }
443
+ const FILE_MARKER = /^\*\*\* (Add|Update|Delete) File: (.+)$/;
444
+ function parsePatch(patchText) {
445
+ const lines = patchText.split("\n");
446
+ let i = 0;
447
+ while (i < lines.length && lines[i].trim() === "")
448
+ i++;
449
+ if (lines[i]?.trim() !== "*** Begin Patch")
450
+ throw new Error("missing '*** Begin Patch'");
451
+ i++;
452
+ const ops = [];
453
+ while (i < lines.length) {
454
+ const line = lines[i];
455
+ if (line.trim() === "*** End Patch")
456
+ return ops;
457
+ if (line.trim() === "") {
458
+ i++;
459
+ continue;
460
+ }
461
+ const m = FILE_MARKER.exec(line);
462
+ if (!m)
463
+ throw new Error(`unexpected line: ${line}`);
464
+ const kind = m[1];
465
+ const p = m[2].trim();
466
+ i++;
467
+ const body = [];
468
+ while (i < lines.length && lines[i].trim() !== "*** End Patch" && !FILE_MARKER.test(lines[i])) {
469
+ body.push(lines[i]);
470
+ i++;
471
+ }
472
+ if (kind === "Add") {
473
+ const content = body.map((l) => (l.startsWith("+") ? l.slice(1) : l)).join("\n");
474
+ ops.push({ type: "add", path: p, content });
475
+ }
476
+ else if (kind === "Delete") {
477
+ ops.push({ type: "delete", path: p });
478
+ }
479
+ else {
480
+ ops.push({ type: "update", path: p, body });
481
+ }
482
+ }
483
+ throw new Error("missing '*** End Patch'");
484
+ }
485
+ /** Apply an Update hunk-body to file content, returning the new content or throwing on a non-match. */
486
+ function applyUpdate(content, body) {
487
+ // split into hunks by @@ markers; body with no @@ is a single hunk.
488
+ const hunks = [];
489
+ let cur = [];
490
+ for (const l of body) {
491
+ if (l.startsWith("@@")) {
492
+ if (cur.length)
493
+ hunks.push(cur);
494
+ cur = [];
495
+ }
496
+ else {
497
+ cur.push(l);
498
+ }
499
+ }
500
+ if (cur.length)
501
+ hunks.push(cur);
502
+ let result = content;
503
+ for (const hunk of hunks) {
504
+ const oldLines = [];
505
+ const newLines = [];
506
+ for (const l of hunk) {
507
+ if (l.startsWith("-"))
508
+ oldLines.push(l.slice(1));
509
+ else if (l.startsWith("+"))
510
+ newLines.push(l.slice(1));
511
+ else if (l.startsWith(" ")) {
512
+ oldLines.push(l.slice(1));
513
+ newLines.push(l.slice(1));
514
+ }
515
+ else {
516
+ oldLines.push(l);
517
+ newLines.push(l);
518
+ }
519
+ }
520
+ const oldBlock = oldLines.join("\n");
521
+ const newBlock = newLines.join("\n");
522
+ if (oldBlock.length > 0) {
523
+ if (!result.includes(oldBlock))
524
+ throw new Error("hunk does not match file contents");
525
+ result = result.replace(oldBlock, newBlock);
526
+ }
527
+ else {
528
+ // pure insertion with no context — append.
529
+ result = result + (result.endsWith("\n") || result === "" ? "" : "\n") + newBlock;
530
+ }
531
+ }
532
+ return result;
533
+ }
534
+ function applyPatchTool() {
535
+ return builtin("apply_patch", "Apply a patch (Begin/End Patch grammar: Add/Update/Delete File). Atomic — a non-matching hunk aborts with no writes.", {
536
+ type: "object",
537
+ properties: {
538
+ patchText: { type: "string", description: "The patch text in Begin/End Patch format" },
539
+ },
540
+ required: ["patchText"],
541
+ additionalProperties: false,
542
+ }, async (args) => {
543
+ const patchText = String(args.patchText ?? "");
544
+ if (!patchText)
545
+ return err("apply_patch: patchText is required");
546
+ let ops;
547
+ try {
548
+ ops = parsePatch(patchText);
549
+ }
550
+ catch (e) {
551
+ return err(`apply_patch: ${e instanceof Error ? e.message : String(e)}`);
552
+ }
553
+ // Stage every write/delete first; only touch the filesystem once all hunks apply.
554
+ const writes = [];
555
+ const deletes = [];
556
+ try {
557
+ for (const op of ops) {
558
+ if (op.type === "add") {
559
+ if (existsSync(op.path))
560
+ throw new Error(`file already exists: ${op.path}`);
561
+ writes.push({ path: op.path, content: op.content });
562
+ }
563
+ else if (op.type === "delete") {
564
+ if (!existsSync(op.path))
565
+ throw new Error(`file not found: ${op.path}`);
566
+ deletes.push(op.path);
567
+ }
568
+ else {
569
+ const content = await fs.readFile(op.path, "utf8");
570
+ writes.push({ path: op.path, content: applyUpdate(content, op.body) });
571
+ }
572
+ }
573
+ }
574
+ catch (e) {
575
+ return err(`apply_patch: ${e instanceof Error ? e.message : String(e)}`);
576
+ }
577
+ for (const w of writes) {
578
+ await fs.mkdir(path.dirname(path.resolve(w.path)), { recursive: true });
579
+ await fs.writeFile(w.path, w.content, "utf8");
580
+ }
581
+ for (const d of deletes) {
582
+ await fs.rm(d, { force: true });
583
+ }
584
+ return ok(`Applied patch: ${ops.length} file operation${ops.length === 1 ? "" : "s"}`, {
585
+ added: ops.filter((o) => o.type === "add").length,
586
+ updated: ops.filter((o) => o.type === "update").length,
587
+ deleted: ops.filter((o) => o.type === "delete").length,
588
+ });
589
+ });
590
+ }
591
+ /**
592
+ * Build the ten built-in tools (each source:"builtin"). The order is fixed
593
+ * for parity: bash, read, write, edit, grep, glob, webfetch, question,
594
+ * apply_patch, todowrite.
595
+ */
596
+ export function createBuiltinTools() {
597
+ return [
598
+ bashTool(),
599
+ readTool(),
600
+ writeTool(),
601
+ editTool(),
602
+ grepTool(),
603
+ globTool(),
604
+ webfetchTool(),
605
+ questionTool(),
606
+ applyPatchTool(),
607
+ todowriteTool(),
608
+ ];
609
+ }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  export * from "./types.js";
2
2
  export * from "./mcp.js";
3
3
  export * from "./skill.js";
4
+ export * from "./builtin.js";
4
5
  export * from "./native.js";
5
6
  export * from "./http.js";
7
+ export * from "./a2a.js";
8
+ export * from "./serve.js";
6
9
  export * from "./adapters.js";
7
10
  export * from "./toolkit.js";
8
11
  export * from "./client.js";
package/dist/index.js CHANGED
@@ -1,8 +1,11 @@
1
1
  export * from "./types.js";
2
2
  export * from "./mcp.js";
3
3
  export * from "./skill.js";
4
+ export * from "./builtin.js";
4
5
  export * from "./native.js";
5
6
  export * from "./http.js";
7
+ export * from "./a2a.js";
8
+ export * from "./serve.js";
6
9
  export * from "./adapters.js";
7
10
  export * from "./toolkit.js";
8
11
  export * from "./client.js";
package/dist/mcp.js CHANGED
@@ -28,7 +28,13 @@ function isEnabled(cfg) {
28
28
  /** Accept a path, a raw object, or an object wrapped under mcpServers/servers/mcp. */
29
29
  export function parseMcpConfig(input) {
30
30
  const raw = typeof input === "string" ? JSON.parse(readFileSync(input, "utf8")) : input;
31
- const servers = raw.mcpServers ?? raw.servers ?? raw.mcp ?? raw;
31
+ const wrapped = raw.mcpServers ?? raw.servers ?? raw.mcp;
32
+ if (wrapped !== undefined)
33
+ return wrapped;
34
+ // Bare map of servers — but strip sibling top-level config keys (builtins/
35
+ // agents/a2a) so they are not mistaken for MCP servers when no wrapper key
36
+ // is present.
37
+ const { builtins: _builtins, agents: _agents, a2a: _a2a, ...servers } = raw;
32
38
  return servers;
33
39
  }
34
40
  /**