toolnexus 0.23.0 → 0.25.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.
Files changed (65) hide show
  1. package/README.md +139 -1
  2. package/dist/acp.d.ts +37 -6
  3. package/dist/acp.js +239 -52
  4. package/dist/agents/loop.js +4 -1
  5. package/dist/agents/runtime.js +4 -1
  6. package/dist/client.d.ts +55 -9
  7. package/dist/client.js +154 -41
  8. package/dist/index.d.ts +4 -0
  9. package/dist/index.js +4 -0
  10. package/dist/mcp.d.ts +32 -7
  11. package/dist/mcp.js +125 -36
  12. package/dist/mcp2026/auth.d.ts +137 -0
  13. package/dist/mcp2026/auth.js +446 -0
  14. package/dist/mcp2026/client.d.ts +48 -0
  15. package/dist/mcp2026/client.js +160 -0
  16. package/dist/mcp2026/protocol.d.ts +54 -0
  17. package/dist/mcp2026/protocol.js +113 -0
  18. package/dist/mcp2026/skills.d.ts +5 -0
  19. package/dist/mcp2026/skills.js +25 -0
  20. package/dist/mcp2026/source.d.ts +86 -0
  21. package/dist/mcp2026/source.js +372 -0
  22. package/dist/mcp2026/stdio.d.ts +24 -0
  23. package/dist/mcp2026/stdio.js +99 -0
  24. package/dist/mcp2026/toolsearch.d.ts +5 -0
  25. package/dist/mcp2026/toolsearch.js +58 -0
  26. package/dist/mcphandler.d.ts +169 -0
  27. package/dist/mcphandler.js +1145 -0
  28. package/dist/mcpheader.d.ts +5 -0
  29. package/dist/mcpheader.js +18 -0
  30. package/dist/mcpserve.d.ts +24 -15
  31. package/dist/mcpserve.js +105 -2
  32. package/dist/mcpserverauth.d.ts +48 -0
  33. package/dist/mcpserverauth.js +203 -0
  34. package/dist/media.d.ts +126 -0
  35. package/dist/media.js +540 -0
  36. package/dist/skill.d.ts +21 -0
  37. package/dist/skill.js +44 -0
  38. package/dist/toolconcurrency.d.ts +16 -0
  39. package/dist/toolconcurrency.js +59 -0
  40. package/dist/toolkit.d.ts +23 -1
  41. package/dist/toolkit.js +26 -4
  42. package/dist/types.d.ts +26 -1
  43. package/package.json +5 -3
  44. package/src/acp.ts +258 -55
  45. package/src/agents/loop.ts +4 -1
  46. package/src/agents/runtime.ts +4 -1
  47. package/src/client.ts +196 -48
  48. package/src/index.ts +4 -0
  49. package/src/mcp.ts +162 -43
  50. package/src/mcp2026/auth.ts +473 -0
  51. package/src/mcp2026/client.ts +161 -0
  52. package/src/mcp2026/protocol.ts +135 -0
  53. package/src/mcp2026/skills.ts +22 -0
  54. package/src/mcp2026/source.ts +407 -0
  55. package/src/mcp2026/stdio.ts +102 -0
  56. package/src/mcp2026/toolsearch.ts +56 -0
  57. package/src/mcphandler.ts +1252 -0
  58. package/src/mcpheader.ts +19 -0
  59. package/src/mcpserve.ts +115 -2
  60. package/src/mcpserverauth.ts +216 -0
  61. package/src/media.ts +604 -0
  62. package/src/skill.ts +50 -0
  63. package/src/toolconcurrency.ts +67 -0
  64. package/src/toolkit.ts +35 -5
  65. package/src/types.ts +27 -1
package/dist/client.d.ts CHANGED
@@ -7,7 +7,7 @@ import type { Toolkit } from "./toolkit.js";
7
7
  import type { ToolHints, ToolResult, Request, Answer } from "./types.js";
8
8
  import { type PromptCacheOptions } from "./promptcache.js";
9
9
  import { type ToolTier, type ToolTierOptions } from "./tooltiers.js";
10
- import type { PromptInput, UnsupportedPartMode } from "./content.js";
10
+ import type { ContentPart, PromptInput, UnsupportedPartMode } from "./content.js";
11
11
  import type { TranslateRequest, TranslateResult } from "./translate.js";
12
12
  export type ClientStyle = "openai" | "anthropic";
13
13
  export interface ClientOptions {
@@ -26,6 +26,10 @@ export interface ClientOptions {
26
26
  headers?: Record<string, string>;
27
27
  systemPrompt?: string;
28
28
  maxTurns?: number;
29
+ /** Cap on concurrency-safe tool calls of one response running at once (SPEC §8 "Tool
30
+ * scheduling"). Absent ⇒ env `TOOLNEXUS_MAX_TOOL_CONCURRENCY` when a positive integer, else 10.
31
+ * Unsafe calls (anything not read-only) always run alone. */
32
+ maxToolConcurrency?: number;
29
33
  hooks?: Hooks;
30
34
  /** Retries on transient LLM errors (`429`/`500`/`502`/`503`/`504`/`529` + network). Default 2.
31
35
  * Widen the status set with `retryableStatuses`. */
@@ -299,7 +303,19 @@ export interface ToolCallRecord {
299
303
  output: string;
300
304
  isError: boolean;
301
305
  metadata?: Record<string, unknown>;
306
+ /** §8: the ToolResult's parts — present iff the result had parts. */
307
+ parts?: ContentPart[];
302
308
  }
309
+ /** §8 RunResult.files entry: a non-text part a tool call produced this run. */
310
+ export type RunFile = {
311
+ type: "image" | "file" | "audio";
312
+ mimeType: string;
313
+ url?: string;
314
+ data?: string;
315
+ name?: string;
316
+ from: string;
317
+ callId: string;
318
+ };
303
319
  /** Token usage, summed across every LLM round trip in the run. */
304
320
  export interface Usage {
305
321
  promptTokens: number;
@@ -319,6 +335,8 @@ export interface RunResult {
319
335
  messages: any[];
320
336
  /** Every tool call made, with its output, error flag, and metadata. */
321
337
  toolCalls: ToolCallRecord[];
338
+ /** Every non-text part produced by tool calls this run, in call order (§8). Always present. */
339
+ files: RunFile[];
322
340
  /** Total number of tool calls (= toolCalls.length). */
323
341
  toolCallCount: number;
324
342
  /** Number of LLM round trips. */
@@ -340,12 +358,21 @@ export interface RunResult {
340
358
  export type StreamEvent = {
341
359
  type: "text";
342
360
  delta: string;
343
- } | {
361
+ }
362
+ /** Model reasoning narration (e.g. an ACP agent's `agent_thought_chunk`). Never part of
363
+ * the answer text or the RunResult. */
364
+ | {
365
+ type: "thought";
366
+ delta: string;
367
+ via?: "acp";
368
+ }
369
+ /** `via: "acp"` marks a tool the ACP agent ran itself: reported, never executed by the loop. */
370
+ | {
344
371
  type: "tool_call";
345
372
  id: string;
346
373
  name: string;
347
374
  args: Record<string, unknown>;
348
- via?: "call_tool";
375
+ via?: "call_tool" | "acp";
349
376
  } | {
350
377
  type: "tool_result";
351
378
  id: string;
@@ -353,7 +380,7 @@ export type StreamEvent = {
353
380
  output: string;
354
381
  isError: boolean;
355
382
  metadata?: Record<string, unknown>;
356
- via?: "call_tool";
383
+ via?: "call_tool" | "acp";
357
384
  } | {
358
385
  type: "usage";
359
386
  usage: Usage;
@@ -491,6 +518,10 @@ export declare class Client {
491
518
  /** Run one tool through beforeTool/afterTool hooks (mutate args, short-circuit, transform result). */
492
519
  private runTool;
493
520
  private runToolDirect;
521
+ /** Runs one response's calls under the tool schedule (SPEC §8 "Tool scheduling"). */
522
+ private schedule;
523
+ /** One call's safety; looks through `call_tool` to the tool it names. Unknown ⇒ unsafe. */
524
+ private callSafe;
494
525
  /** ToolTiers `call_tool {name, arguments}` -> the real call; undefined when tiers are off. */
495
526
  private unwrapCall;
496
527
  /** The RunResult.toolCalls entry: a `call_tool` call is recorded as the real tool with its
@@ -560,11 +591,22 @@ export interface InProcessResponse {
560
591
  totalTokens?: number;
561
592
  };
562
593
  }
594
+ /** A streaming in-process model: `emit` each event as it happens — `text` deltas become
595
+ * the streamed answer as they arrive; any other event (thought, an agent's own tool_call /
596
+ * tool_result) goes straight to the stream consumer and is never executed by the loop.
597
+ * Return the whole message when done (its `toolCalls`, if any, are what the loop runs). */
598
+ export type InProcessGenerateStream = (request: InProcessRequest, emit: (event: StreamEvent) => void) => InProcessResponse | Promise<InProcessResponse>;
599
+ type InProcessGenerate = (request: InProcessRequest) => InProcessResponse | Promise<InProcessResponse>;
563
600
  /** Options for {@link createInProcessClient}: every client option EXCEPT the three
564
- * that describe a wire — `baseUrl`, `apiKey` and `style` — plus `generate`. */
601
+ * that describe a wire — `baseUrl`, `apiKey` and `style` — plus `generate` and/or
602
+ * `generateStream` (at least one). */
565
603
  export type InProcessOptions = Omit<ClientOptions, "baseUrl" | "apiKey" | "style" | "fetch"> & {
566
604
  /** Your model. Receives the assembled request, returns one assistant message. */
567
- generate: (request: InProcessRequest) => InProcessResponse | Promise<InProcessResponse>;
605
+ generate?: InProcessGenerate;
606
+ /** Your model, streaming for real. With it, `stream()` is incremental; without it a
607
+ * `stream()` on a plain `generate` is refused. When both are set, `generate` serves
608
+ * non-streaming calls; alone, it serves them too (with a no-op `emit`). */
609
+ generateStream?: InProcessGenerateStream;
568
610
  };
569
611
  /**
570
612
  * Build a `fetch`-shaped adapter over a semantic `generate` function (ADR 0030):
@@ -574,9 +616,11 @@ export type InProcessOptions = Omit<ClientOptions, "baseUrl" | "apiKey" | "style
574
616
  * or the agent runtime's `inProcess` option) goes through this ONE function, so there
575
617
  * is no second copy of the request/response assembly to drift.
576
618
  *
577
- * Streaming is refused rather than faked — see the thrown error.
619
+ * Streaming with only a plain `generate` is refused rather than faked — see the thrown
620
+ * error. With `generateStream` it streams for real: each text event is written to the
621
+ * SSE body the moment it is emitted.
578
622
  */
579
- export declare function createInProcessFetch(generate: (request: InProcessRequest) => InProcessResponse | Promise<InProcessResponse>): typeof fetch;
623
+ export declare function createInProcessFetch(generate?: InProcessGenerate, generateStream?: InProcessGenerateStream): typeof fetch;
580
624
  /**
581
625
  * A client backed by a model running IN THIS PROCESS — no server, no socket, and no
582
626
  * HTTP types to construct.
@@ -587,7 +631,8 @@ export declare function createInProcessFetch(generate: (request: InProcessReques
587
631
  * The `fetch`-shaped adapter itself is {@link createInProcessFetch} — this function
588
632
  * is just that adapter wired into a `Client`.
589
633
  *
590
- * Streaming is refused rather than faked — see {@link createInProcessFetch}.
634
+ * Streaming is refused rather than faked for a plain `generate`; with `generateStream`
635
+ * it streams for real — see {@link createInProcessFetch}.
591
636
  */
592
637
  export declare function createInProcessClient(opts: InProcessOptions): Client;
593
638
  /** Stateful multi-turn conversation: each send() continues the same transcript (memory). */
@@ -603,3 +648,4 @@ export declare class Conversation {
603
648
  /** Reset the conversation memory. */
604
649
  reset(): void;
605
650
  }
651
+ export {};
package/dist/client.js CHANGED
@@ -3,6 +3,7 @@ import { toOpenAI, toAnthropic } from "./adapters.js";
3
3
  import { applyPromptCache } from "./promptcache.js";
4
4
  import { buildToolPlan, toolSearch, unwrapCallTool } from "./tooltiers.js";
5
5
  import { isRetryableStatus, retryAfterMs } from "./retry.js";
6
+ import { isConcurrencySafe, resolveMaxToolConcurrency, scheduleCalls } from "./toolconcurrency.js";
6
7
  import { toOpenAIWire, toAnthropicWire, checkPromptParts } from "./wire.js";
7
8
  import { finishReasonFor, argsString, openAIMessagesToAnthropic, openAIToolsToAnthropic, openAIToolChoiceToAnthropic, hasSystemMessage, } from "./translate.js";
8
9
  /**
@@ -106,6 +107,20 @@ function delay(ms, signal) {
106
107
  signal.addEventListener("abort", () => { clearTimeout(t); reject(signal.reason ?? new Error("aborted")); }, { once: true });
107
108
  });
108
109
  }
110
+ /** Model tool-call id of each record, kept off the record itself (§8 shape has no id). */
111
+ const callIds = new WeakMap();
112
+ /** RunResult.files: every non-text part of every tool call, in call order (§8). */
113
+ function runFiles(toolCalls) {
114
+ const out = [];
115
+ for (const r of toolCalls) {
116
+ for (const p of r.parts ?? []) {
117
+ if (p.type === "text")
118
+ continue;
119
+ out.push({ ...p, from: r.name, callId: callIds.get(r) ?? "" });
120
+ }
121
+ }
122
+ return out;
123
+ }
109
124
  function addUsage(acc, raw, style) {
110
125
  if (!raw)
111
126
  return;
@@ -718,15 +733,34 @@ export class Client {
718
733
  this.emitTool(name, source, result, t0);
719
734
  return { args: a, result };
720
735
  }
736
+ /** Runs one response's calls under the tool schedule (SPEC §8 "Tool scheduling"). */
737
+ schedule(toolkit, items, key, fn) {
738
+ return scheduleCalls(items, (item) => {
739
+ const [name, args] = key(item);
740
+ return this.callSafe(toolkit, name, args);
741
+ }, resolveMaxToolConcurrency(this.opts.maxToolConcurrency), fn);
742
+ }
743
+ /** One call's safety; looks through `call_tool` to the tool it names. Unknown ⇒ unsafe. */
744
+ callSafe(toolkit, name, args) {
745
+ if (this.opts.toolTiers && name === "tool_search")
746
+ return true;
747
+ const inner = this.unwrapCall(name, args ?? {});
748
+ const tool = toolkit?.get(inner ? inner.name : name);
749
+ return tool ? isConcurrencySafe(tool, inner ? inner.args : (args ?? {})) : false;
750
+ }
721
751
  /** ToolTiers `call_tool {name, arguments}` -> the real call; undefined when tiers are off. */
722
752
  unwrapCall(name, args) {
723
753
  return this.opts.toolTiers ? unwrapCallTool(name, args) : undefined;
724
754
  }
725
755
  /** The RunResult.toolCalls entry: a `call_tool` call is recorded as the real tool with its
726
756
  * inner arguments, the name hooks and stream events see (the transcript keeps the wrapper). */
727
- toolCallRecord(name, args, result) {
757
+ toolCallRecord(name, args, result, callId) {
728
758
  const inner = this.unwrapCall(name, args);
729
- return { name: inner ? inner.name : name, args: inner ? inner.args : args, output: result.output, isError: result.isError, metadata: result.metadata };
759
+ const rec = { name: inner ? inner.name : name, args: inner ? inner.args : args, output: result.output, isError: result.isError, metadata: result.metadata };
760
+ if (result.parts?.length)
761
+ rec.parts = result.parts;
762
+ callIds.set(rec, callId ?? "");
763
+ return rec;
730
764
  }
731
765
  /** The stream's tool_call event, naming the real tool for a `call_tool` call. */
732
766
  callEvent(id, name, args) {
@@ -886,7 +920,7 @@ export class Client {
886
920
  return this.endRun(runStart, lastModel, msg.content ?? "", messages, toolCalls, turns, usage);
887
921
  // execute all tool calls in this turn concurrently (true parallel tool calling)
888
922
  done.clear();
889
- const settled = await untilAborted(signal, Promise.all(calls.map(async (call) => {
923
+ const settled = await untilAborted(signal, this.schedule(toolkit, calls, (call) => [call.function.name, safeJson(call.function.arguments)], async (call) => {
890
924
  let { args, result } = await this.runTool(toolkit, call.function.name, safeJson(call.function.arguments), call.id, turn, signal);
891
925
  let halted;
892
926
  const req = pendingOf(result);
@@ -898,12 +932,12 @@ export class Client {
898
932
  if (!halted && call.id)
899
933
  done.set(call.id, result);
900
934
  return { call, args, result, halted };
901
- })));
935
+ }));
902
936
  // Record in tool-call order; on the FIRST durable halt, surface it and stop — deterministic,
903
937
  // and the later suspensions' placeholder results never enter the transcript (they re-suspend
904
938
  // on resume). Mirrors the streaming path.
905
939
  for (const s of settled) {
906
- toolCalls.push(this.toolCallRecord(s.call.function.name, s.args, s.result));
940
+ toolCalls.push(this.toolCallRecord(s.call.function.name, s.args, s.result, s.call.id));
907
941
  messages.push(toolMessage(s.call.id, s.call.function.name, s.result));
908
942
  if (s.halted)
909
943
  return this.pendingRun(runStart, lastModel, s.halted, messages, toolCalls, turns, usage);
@@ -919,7 +953,7 @@ export class Client {
919
953
  }
920
954
  result(model, text, messages, toolCalls, turns, usage, exhausted = false) {
921
955
  const incomplete = exhausted && text === "";
922
- const r = { text, messages, toolCalls, toolCallCount: toolCalls.length, turns, usage, model, status: incomplete ? "incomplete" : "done" };
956
+ const r = { text, messages, toolCalls, files: runFiles(toolCalls), toolCallCount: toolCalls.length, turns, usage, model, status: incomplete ? "incomplete" : "done" };
923
957
  if (incomplete)
924
958
  r.limit = "maxTurns";
925
959
  return r;
@@ -927,7 +961,7 @@ export class Client {
927
961
  /** §10: a run halted because a tool suspended and no `waitFor` was configured. */
928
962
  pendingRun(runStart, model, request, messages, toolCalls, turns, usage) {
929
963
  this.emit({ event: "run", model, turns, toolCalls: toolCalls.length, totalTokens: usage.totalTokens, ms: Date.now() - runStart });
930
- return { text: request.prompt, messages, toolCalls, toolCallCount: toolCalls.length, turns, usage, model, status: "pending", pending: request };
964
+ return { text: request.prompt, messages, toolCalls, files: runFiles(toolCalls), toolCallCount: toolCalls.length, turns, usage, model, status: "pending", pending: request };
931
965
  }
932
966
  /**
933
967
  * §10: resolve a suspended tool result. Calls `waitFor(request)`, and on `ok` re-executes the
@@ -1008,7 +1042,7 @@ export class Client {
1008
1042
  }
1009
1043
  // execute all tool_use blocks in this turn concurrently (true parallel tool calling)
1010
1044
  done.clear();
1011
- const settled = await untilAborted(signal, Promise.all(uses.map(async (use) => {
1045
+ const settled = await untilAborted(signal, this.schedule(toolkit, uses, (use) => [use.name, use.input ?? {}], async (use) => {
1012
1046
  let { args, result } = await this.runTool(toolkit, use.name, use.input ?? {}, use.id, turn, signal);
1013
1047
  let halted;
1014
1048
  const req = pendingOf(result);
@@ -1020,14 +1054,14 @@ export class Client {
1020
1054
  if (!halted && use.id)
1021
1055
  done.set(use.id, result);
1022
1056
  return { use, args, result, halted };
1023
- })));
1057
+ }));
1024
1058
  // Record in tool-call order; on the FIRST durable halt, include that tool_result block, stop
1025
1059
  // building the content, and surface it — deterministic (G3), later suspensions re-suspend on
1026
1060
  // resume. Mirrors the OpenAI path and the streaming loops.
1027
1061
  const content = [];
1028
1062
  let halted;
1029
1063
  for (const s of settled) {
1030
- toolCalls.push(this.toolCallRecord(s.use.name, s.args, s.result));
1064
+ toolCalls.push(this.toolCallRecord(s.use.name, s.args, s.result, s.use.id));
1031
1065
  content.push(toolResultBlock(s.use.id, s.result));
1032
1066
  if (s.halted) {
1033
1067
  halted = s.halted;
@@ -1098,6 +1132,13 @@ export class Client {
1098
1132
  if (payload === "[DONE]")
1099
1133
  break;
1100
1134
  const j = safeParse(payload);
1135
+ // A non-text event from an in-process streaming generate rides in-band so it keeps
1136
+ // its order and arrives as it happens; it goes straight to the consumer and is never
1137
+ // executed. Honoured only on the in-process sentinel URL, never from a real server.
1138
+ if (j && j[IN_PROCESS_SIDE_KEY] && this.opts.baseUrl === IN_PROCESS_BASE_URL) {
1139
+ yield j[IN_PROCESS_SIDE_KEY];
1140
+ continue;
1141
+ }
1101
1142
  if (j?.usage)
1102
1143
  addUsage(usage, j.usage, "openai");
1103
1144
  const choice = j?.choices?.[0];
@@ -1136,12 +1177,12 @@ export class Client {
1136
1177
  for (const c of calls)
1137
1178
  yield this.callEvent(c.id, c.name, safeJson(c.args));
1138
1179
  done.clear();
1139
- const settled = await untilAborted(signal, Promise.all(calls.map(async (c) => {
1180
+ const settled = await untilAborted(signal, this.schedule(toolkit, calls, (c) => [c.name, safeJson(c.args)], async (c) => {
1140
1181
  const r = await this.runTool(toolkit, c.name, safeJson(c.args), c.id, turn, signal);
1141
1182
  if (!pendingOf(r.result) && c.id)
1142
1183
  done.set(c.id, r.result);
1143
1184
  return r;
1144
- })));
1185
+ }));
1145
1186
  for (let i = 0; i < calls.length; i++) {
1146
1187
  const c = calls[i];
1147
1188
  let { args, result } = settled[i];
@@ -1151,13 +1192,13 @@ export class Client {
1151
1192
  const r = await this.resolvePending(toolkit, c.name, args, req, c.id, turn, signal);
1152
1193
  result = r.result;
1153
1194
  if (r.halted) {
1154
- toolCalls.push(this.toolCallRecord(c.name, args, result));
1195
+ toolCalls.push(this.toolCallRecord(c.name, args, result, c.id));
1155
1196
  messages.push(toolMessage(c.id, c.name, result));
1156
1197
  yield { type: "done", result: this.pendingRun(runStart, lastModel, r.halted, messages, toolCalls, turns, usage) };
1157
1198
  return;
1158
1199
  }
1159
1200
  }
1160
- toolCalls.push(this.toolCallRecord(c.name, args, result));
1201
+ toolCalls.push(this.toolCallRecord(c.name, args, result, c.id));
1161
1202
  messages.push(toolMessage(c.id, c.name, result));
1162
1203
  yield this.resultEvent(c.id, c.name, safeJson(c.args), result);
1163
1204
  }
@@ -1256,12 +1297,12 @@ export class Client {
1256
1297
  for (const u of uses)
1257
1298
  yield this.callEvent(u.id, u.name, u.input);
1258
1299
  done.clear();
1259
- const settled = await untilAborted(signal, Promise.all(uses.map(async (u) => {
1300
+ const settled = await untilAborted(signal, this.schedule(toolkit, uses, (u) => [u.name, u.input ?? {}], async (u) => {
1260
1301
  const r = await this.runTool(toolkit, u.name, u.input ?? {}, u.id, turn, signal);
1261
1302
  if (!pendingOf(r.result) && u.id)
1262
1303
  done.set(u.id, r.result);
1263
1304
  return r;
1264
- })));
1305
+ }));
1265
1306
  const results = [];
1266
1307
  for (let i = 0; i < uses.length; i++) {
1267
1308
  const u = uses[i];
@@ -1272,14 +1313,14 @@ export class Client {
1272
1313
  const r = await this.resolvePending(toolkit, u.name, args, req, u.id, turn, signal);
1273
1314
  result = r.result;
1274
1315
  if (r.halted) {
1275
- toolCalls.push(this.toolCallRecord(u.name, args, result));
1316
+ toolCalls.push(this.toolCallRecord(u.name, args, result, u.id));
1276
1317
  results.push(toolResultBlock(u.id, result));
1277
1318
  messages.push({ role: "user", content: results });
1278
1319
  yield { type: "done", result: this.pendingRun(runStart, lastModel, r.halted, messages, toolCalls, turns, usage) };
1279
1320
  return;
1280
1321
  }
1281
1322
  }
1282
- toolCalls.push(this.toolCallRecord(u.name, args, result));
1323
+ toolCalls.push(this.toolCallRecord(u.name, args, result, u.id));
1283
1324
  results.push(toolResultBlock(u.id, result));
1284
1325
  yield this.resultEvent(u.id, u.name, u.input, result);
1285
1326
  }
@@ -1302,6 +1343,9 @@ export function createClient(opts) {
1302
1343
  * it must be syntactically valid. `.invalid` is reserved by RFC 2606 precisely so a
1303
1344
  * name can never resolve. */
1304
1345
  const IN_PROCESS_BASE_URL = "http://in-process.invalid/v1";
1346
+ /** The in-band SSE key that carries a non-text stream event from an in-process
1347
+ * streaming generate to the consumer (honoured only on {@link IN_PROCESS_BASE_URL}). */
1348
+ const IN_PROCESS_SIDE_KEY = "toolnexus_event";
1305
1349
  /**
1306
1350
  * Build a `fetch`-shaped adapter over a semantic `generate` function (ADR 0030):
1307
1351
  * the reusable piece of {@link createInProcessClient} — assembles the wire request,
@@ -1310,14 +1354,19 @@ const IN_PROCESS_BASE_URL = "http://in-process.invalid/v1";
1310
1354
  * or the agent runtime's `inProcess` option) goes through this ONE function, so there
1311
1355
  * is no second copy of the request/response assembly to drift.
1312
1356
  *
1313
- * Streaming is refused rather than faked — see the thrown error.
1357
+ * Streaming with only a plain `generate` is refused rather than faked — see the thrown
1358
+ * error. With `generateStream` it streams for real: each text event is written to the
1359
+ * SSE body the moment it is emitted.
1314
1360
  */
1315
- export function createInProcessFetch(generate) {
1316
- if (typeof generate !== "function") {
1317
- throw new Error("toolnexus: createInProcessFetch requires a `generate` function");
1361
+ export function createInProcessFetch(generate, generateStream) {
1362
+ if (typeof generate !== "function" && typeof generateStream !== "function") {
1363
+ throw new Error("toolnexus: createInProcessFetch requires a `generate` (or `generateStream`) function");
1318
1364
  }
1319
1365
  const inProcessFetch = async (_url, init) => {
1320
1366
  const body = JSON.parse(String(init?.body ?? "{}"));
1367
+ const request = { messages: body.messages ?? [], tools: body.tools, model: body.model, body };
1368
+ if (body.stream && generateStream)
1369
+ return inProcessStream(generateStream, request);
1321
1370
  // A generate() returns a whole answer, so a stream would be one chunk pretending
1322
1371
  // to be many. Content and delta-count assertions cannot tell that from a real
1323
1372
  // stream, so refuse instead of degrading silently.
@@ -1325,24 +1374,10 @@ export function createInProcessFetch(generate) {
1325
1374
  throw new Error("toolnexus: createInProcessClient does not support streaming — `generate` returns a " +
1326
1375
  "complete answer. Use the non-streaming run, or supply a `fetch`/transport that streams.");
1327
1376
  }
1328
- const answer = await generate({
1329
- messages: body.messages ?? [],
1330
- tools: body.tools,
1331
- model: body.model,
1332
- body,
1333
- });
1377
+ const answer = generate ? await generate(request) : await generateStream(request, () => { });
1334
1378
  const message = { role: "assistant" };
1335
1379
  if (answer.toolCalls && answer.toolCalls.length > 0) {
1336
- message.tool_calls = answer.toolCalls.map((c, i) => ({
1337
- id: c.id ?? `call_${i}`,
1338
- type: "function",
1339
- function: {
1340
- name: c.name,
1341
- // A string is already encoded; anything else is encoded here so a model
1342
- // author never has to think about the wire.
1343
- arguments: typeof c.arguments === "string" ? c.arguments : JSON.stringify(c.arguments ?? {}),
1344
- },
1345
- }));
1380
+ message.tool_calls = wireToolCalls(answer.toolCalls, false);
1346
1381
  }
1347
1382
  else {
1348
1383
  message.content = answer.content ?? "";
@@ -1361,6 +1396,80 @@ export function createInProcessFetch(generate) {
1361
1396
  };
1362
1397
  return inProcessFetch;
1363
1398
  }
1399
+ function wireToolCalls(calls, indexed) {
1400
+ return calls.map((c, i) => ({
1401
+ ...(indexed ? { index: i } : {}),
1402
+ id: c.id ?? `call_${i}`,
1403
+ type: "function",
1404
+ function: {
1405
+ name: c.name,
1406
+ // A string is already encoded; anything else is encoded here so a model
1407
+ // author never has to think about the wire.
1408
+ arguments: typeof c.arguments === "string" ? c.arguments : JSON.stringify(c.arguments ?? {}),
1409
+ },
1410
+ }));
1411
+ }
1412
+ /** Runs a streaming generate and answers with an OpenAI SSE body written AS EVENTS ARRIVE:
1413
+ * text → a content delta; any other event → an in-band side chunk for the consumer; then
1414
+ * (tool calls as one delta | the content as one delta when no text was emitted), the
1415
+ * finish_reason chunk, usage, and [DONE]. */
1416
+ function inProcessStream(generateStream, request) {
1417
+ const enc = new TextEncoder();
1418
+ let open = true;
1419
+ const sse = new ReadableStream({
1420
+ start(controller) {
1421
+ const chunk = (v) => {
1422
+ if (open)
1423
+ controller.enqueue(enc.encode(`data: ${JSON.stringify(v)}\n\n`));
1424
+ };
1425
+ let sawText = false;
1426
+ void (async () => {
1427
+ try {
1428
+ const answer = await generateStream(request, (ev) => {
1429
+ if (!ev || typeof ev !== "object")
1430
+ return;
1431
+ if (ev.type === "text") {
1432
+ if (ev.delta) {
1433
+ sawText = true;
1434
+ chunk({ choices: [{ index: 0, delta: { content: ev.delta } }] });
1435
+ }
1436
+ return;
1437
+ }
1438
+ chunk({ [IN_PROCESS_SIDE_KEY]: ev });
1439
+ });
1440
+ let finish = "stop";
1441
+ if (answer.toolCalls && answer.toolCalls.length > 0) {
1442
+ chunk({ choices: [{ index: 0, delta: { tool_calls: wireToolCalls(answer.toolCalls, true) } }] });
1443
+ finish = "tool_calls";
1444
+ }
1445
+ else if (!sawText && answer.content) {
1446
+ chunk({ choices: [{ index: 0, delta: { content: answer.content } }] });
1447
+ }
1448
+ chunk({ choices: [{ index: 0, delta: {}, finish_reason: finish }] });
1449
+ const u = answer.usage;
1450
+ if (u) {
1451
+ const p = u.promptTokens ?? 0, c = u.completionTokens ?? 0;
1452
+ chunk({ choices: [], usage: { prompt_tokens: p, completion_tokens: c, total_tokens: u.totalTokens || p + c } });
1453
+ }
1454
+ if (open)
1455
+ controller.enqueue(enc.encode("data: [DONE]\n\n"));
1456
+ open = false;
1457
+ controller.close();
1458
+ }
1459
+ catch (e) {
1460
+ if (open) {
1461
+ open = false;
1462
+ controller.error(e);
1463
+ }
1464
+ }
1465
+ })();
1466
+ },
1467
+ cancel() {
1468
+ open = false;
1469
+ },
1470
+ });
1471
+ return new Response(sse, { status: 200, headers: { "content-type": "text/event-stream" } });
1472
+ }
1364
1473
  /**
1365
1474
  * A client backed by a model running IN THIS PROCESS — no server, no socket, and no
1366
1475
  * HTTP types to construct.
@@ -1371,11 +1480,15 @@ export function createInProcessFetch(generate) {
1371
1480
  * The `fetch`-shaped adapter itself is {@link createInProcessFetch} — this function
1372
1481
  * is just that adapter wired into a `Client`.
1373
1482
  *
1374
- * Streaming is refused rather than faked — see {@link createInProcessFetch}.
1483
+ * Streaming is refused rather than faked for a plain `generate`; with `generateStream`
1484
+ * it streams for real — see {@link createInProcessFetch}.
1375
1485
  */
1376
1486
  export function createInProcessClient(opts) {
1377
- const { generate, ...rest } = opts;
1378
- const inProcessFetch = createInProcessFetch(generate);
1487
+ const { generate, generateStream, ...rest } = opts;
1488
+ if (typeof generate !== "function" && typeof generateStream !== "function") {
1489
+ throw new Error("toolnexus: createInProcessClient requires a `generate` (or `generateStream`) function");
1490
+ }
1491
+ const inProcessFetch = createInProcessFetch(generate, generateStream);
1379
1492
  return new Client({
1380
1493
  // Retries default to ZERO here, unlike a network client. There is no wire, so
1381
1494
  // there is no transient failure to ride out: whatever `generate` throws will
package/dist/index.d.ts CHANGED
@@ -10,6 +10,8 @@ export * from "./a2a.js";
10
10
  export * from "./acp.js";
11
11
  export * from "./serve.js";
12
12
  export * from "./mcpserve.js";
13
+ export * from "./mcphandler.js";
14
+ export * from "./mcpserverauth.js";
13
15
  export * from "./adapters.js";
14
16
  export * from "./translate.js";
15
17
  export * from "./toolkit.js";
@@ -22,5 +24,7 @@ export * from "./batteries.js";
22
24
  export { State, context, questionMap, ask, gate, decide, applyGate, applyPolicy, answersOf, staticClassifier, Tape, DEFAULT_BANDS, type NamedQuestion, type Band, type Bands, type Answers, type Rule, type GateOutcome, type Policy, } from "./judge.js";
23
25
  export * from "./promptcache.js";
24
26
  export * from "./tooltiers.js";
27
+ export * from "./toolconcurrency.js";
25
28
  export * from "./models.js";
26
29
  export * from "./builtin_extras.js";
30
+ export * from "./media.js";
package/dist/index.js CHANGED
@@ -10,6 +10,8 @@ export * from "./a2a.js";
10
10
  export * from "./acp.js";
11
11
  export * from "./serve.js";
12
12
  export * from "./mcpserve.js";
13
+ export * from "./mcphandler.js";
14
+ export * from "./mcpserverauth.js";
13
15
  export * from "./adapters.js";
14
16
  export * from "./translate.js";
15
17
  export * from "./toolkit.js";
@@ -25,5 +27,7 @@ export * from "./batteries.js";
25
27
  export { State, context, questionMap, ask, gate, decide, applyGate, applyPolicy, answersOf, staticClassifier, Tape, DEFAULT_BANDS, } from "./judge.js";
26
28
  export * from "./promptcache.js";
27
29
  export * from "./tooltiers.js";
30
+ export * from "./toolconcurrency.js";
28
31
  export * from "./models.js";
29
32
  export * from "./builtin_extras.js";
33
+ export * from "./media.js";
package/dist/mcp.d.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import type { ElicitRequest, ElicitResult } from "@modelcontextprotocol/sdk/types.js";
2
- import { type JSONSchema, type Tool, type ToolHints, type McpStatus, type Request, type Answer } from "./types.js";
2
+ import { type JSONSchema, type Tool, type ToolHints, type ToolResult, type McpStatus, type Request, type Answer } from "./types.js";
3
3
  import { type ContentPart } from "./content.js";
4
+ import type { SkillDef } from "./skill.js";
5
+ import { type AuthConfig } from "./mcp2026/source.js";
6
+ import { type TokenStore } from "./mcp2026/auth.js";
7
+ export { Token, MemoryTokenStore, type TokenStore } from "./mcp2026/auth.js";
8
+ export type { AuthConfig } from "./mcp2026/source.js";
4
9
  /** Map an MCP `elicitation/create` request onto a §10 `Request`. */
5
10
  export declare function elicitationToRequest(params: ElicitRequest["params"]): Request;
6
11
  /** Map a resolved §10 `Answer` back onto an MCP `ElicitResult`. ok→accept; declined→decline; else→cancel. */
@@ -18,6 +23,10 @@ export interface LocalServer {
18
23
  * builtins/skills filter (nil/empty ⇒ all; ≥1 true ⇒ allowlist; only-false ⇒ drop-list; unknown
19
24
  * ⇒ ignore+warn). Applied to the listed defs before sanitize/prefix. */
20
25
  tools?: Record<string, boolean>;
26
+ /** §2.8 lazy tool search: expose `<server>_search_tools` + `<server>_call_tool` instead of every tool. */
27
+ toolSearch?: boolean;
28
+ /** §2.8 what this server is for (shown by search_tools). */
29
+ description?: string;
21
30
  }
22
31
  export interface RemoteServer {
23
32
  type?: "remote";
@@ -28,6 +37,11 @@ export interface RemoteServer {
28
37
  timeout?: number;
29
38
  /** §2 Gap 7. Per-server tool allowlist (see LocalServer.tools). */
30
39
  tools?: Record<string, boolean>;
40
+ /** §2.8 lazy tool search (see LocalServer.toolSearch). */
41
+ toolSearch?: boolean;
42
+ description?: string;
43
+ /** §2A OAuth 2.1 client settings. Absent ⇒ a 401 still triggers the flow (CIMD if the AS supports it). */
44
+ auth?: AuthConfig;
31
45
  }
32
46
  export type ServerConfig = LocalServer | RemoteServer;
33
47
  export type McpConfig = Record<string, ServerConfig>;
@@ -46,6 +60,8 @@ export declare function expandEnvHeaders(headers?: Record<string, string>): Reco
46
60
  * takes it there.
47
61
  */
48
62
  export declare function mcpContentToParts(content: unknown): ContentPart[] | undefined;
63
+ /** §2 `tools/call` result → ToolResult (shared by the 2026 path and the frozen fallback). */
64
+ export declare function mapCallToolResult(result: any, server: string): ToolResult;
49
65
  export interface McpSource {
50
66
  tools: Tool[];
51
67
  status: Record<string, McpStatus>;
@@ -56,6 +72,8 @@ export interface McpSource {
56
72
  * source is closed.
57
73
  */
58
74
  resourceTools(): Tool[];
75
+ /** §2.7 skills served by 2026 servers (skills extension); the toolkit adds them to the `skill` tool. */
76
+ skills: SkillDef[];
59
77
  }
60
78
  /** One server's outcome, reported by `loadMcp`'s `onServer` the moment that server finishes. */
61
79
  export interface McpServerResult {
@@ -75,12 +93,21 @@ export declare function mcpHints(a: {
75
93
  destructiveHint?: boolean;
76
94
  openWorldHint?: boolean;
77
95
  } | undefined): ToolHints | undefined;
96
+ /** Options shared by loadMcp / listMcpTools for the 2026 path and OAuth (§2A.1a). */
97
+ export interface McpLoadOptions {
98
+ waitFor?: (request: Request) => Promise<Answer>;
99
+ signal?: AbortSignal;
100
+ /** Where OAuth tokens live (default: in memory, per load). */
101
+ tokenStore?: TokenStore;
102
+ /** How `auto` OAuth mode opens the authorization URL (default: the OS opener). */
103
+ openUrl?: (url: string) => unknown | Promise<unknown>;
104
+ /** Final exposed names to drop; on a tool-search server they are also hidden from search/call. */
105
+ disableTools?: string[];
106
+ }
78
107
  /** Connect to every enabled server, list + convert tools. Failures are isolated.
79
108
  * §2 Gap 3: an optional `signal` bounds each connect/list by the server timeout and aborts the
80
109
  * whole load promptly on parent cancellation. */
81
- export declare function loadMcp(input: string | object, opts?: {
82
- waitFor?: (request: Request) => Promise<Answer>;
83
- signal?: AbortSignal;
110
+ export declare function loadMcp(input: string | object, opts?: McpLoadOptions & {
84
111
  /**
85
112
  * Called once per server the moment it is connected, failed or found disabled (SPEC §4C),
86
113
  * so a host can register a fast server's tools while a slow one is still connecting.
@@ -105,6 +132,4 @@ export interface McpInventory {
105
132
  * §2 Gap 6. Connect to every enabled server, list tool definitions, and DISCONNECT before
106
133
  * returning — no toolkit, no execute wiring, nothing left running. Failure isolation as in loadMcp.
107
134
  */
108
- export declare function listMcpTools(input: string | object, opts?: {
109
- signal?: AbortSignal;
110
- }): Promise<McpInventory>;
135
+ export declare function listMcpTools(input: string | object, opts?: McpLoadOptions): Promise<McpInventory>;