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
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Tool scheduling (SPEC §8 "Tool scheduling"): the calls of one model response run in emitted
3
+ * order as consecutive runs — safe calls overlap under a cap, each unsafe call runs alone.
4
+ */
5
+ import type { Tool } from "./types.js"
6
+
7
+ /** The cap when neither the option nor TOOLNEXUS_MAX_TOOL_CONCURRENCY sets one. */
8
+ export const DEFAULT_MAX_TOOL_CONCURRENCY = 10
9
+
10
+ /** option > env TOOLNEXUS_MAX_TOOL_CONCURRENCY > 10. Non-positive or non-numeric values fall back. */
11
+ export function resolveMaxToolConcurrency(option?: number): number {
12
+ if (typeof option === "number" && Number.isInteger(option) && option > 0) return option
13
+ const raw = typeof process !== "undefined" ? process.env.TOOLNEXUS_MAX_TOOL_CONCURRENCY : undefined
14
+ if (raw !== undefined && /^\s*[+]?\d+\s*$/.test(raw)) {
15
+ const n = Number.parseInt(raw, 10)
16
+ if (n > 0) return n
17
+ }
18
+ return DEFAULT_MAX_TOOL_CONCURRENCY
19
+ }
20
+
21
+ /** Whether a call to `tool` with `args` may run alongside other safe calls. The tool's own
22
+ * predicate wins; otherwise only a tool whose hints say readOnly is safe. */
23
+ export function isConcurrencySafe(tool: Tool, args: Record<string, unknown>): boolean {
24
+ if (tool.concurrencySafe) return tool.concurrencySafe(args)
25
+ return tool.hints?.readOnly === true
26
+ }
27
+
28
+ /** Runs `fn` over `items` in emitted order: safe items overlap (at most `cap`), an unsafe item
29
+ * waits for everything before it and runs alone. Results keep item order. The first failure
30
+ * stops new items from starting and is rethrown once the running ones settle. */
31
+ export async function scheduleCalls<T, R>(
32
+ items: T[],
33
+ safe: (item: T) => boolean,
34
+ cap: number,
35
+ fn: (item: T) => Promise<R>,
36
+ ): Promise<R[]> {
37
+ const out = new Array<R>(items.length)
38
+ const running = new Set<Promise<void>>()
39
+ let failed: { e: unknown } | undefined
40
+ for (let i = 0; i < items.length && !failed; i++) {
41
+ const item = items[i]
42
+ if (!safe(item)) {
43
+ await Promise.all(running)
44
+ if (failed) break
45
+ try {
46
+ out[i] = await fn(item)
47
+ } catch (e) {
48
+ failed = { e }
49
+ }
50
+ continue
51
+ }
52
+ while (running.size >= cap) await Promise.race(running)
53
+ if (failed) break
54
+ const p: Promise<void> = fn(item).then(
55
+ (r) => {
56
+ out[i] = r
57
+ },
58
+ (e) => {
59
+ if (!failed) failed = { e }
60
+ },
61
+ ).finally(() => running.delete(p))
62
+ running.add(p)
63
+ }
64
+ await Promise.all(running)
65
+ if (failed) throw failed.e
66
+ return out
67
+ }
package/src/toolkit.ts CHANGED
@@ -8,9 +8,11 @@ import { loadSkills, type SkillDef, type SkillSource } from "./skill.js"
8
8
  import { selectBuiltins, type BuiltinsConfig } from "./builtin.js"
9
9
  import { agentTools, parseAgentsConfig, type Agent } from "./a2a.js"
10
10
  import { startA2AServer, type A2AConfig, type OnTask, type ServeHandle } from "./serve.js"
11
- import { type MCPServeConfig, type OnCall } from "./mcpserve.js"
11
+ import { serveLocalMcp, type LocalMcpServer, type MCPServeConfig, type OnCall } from "./mcpserve.js"
12
+ import { createMcpHandler, type McpHandler, type McpHandlerConfig } from "./mcphandler.js"
12
13
  import { toAnthropic, toGemini, toOpenAI } from "./adapters.js"
13
14
  import type { Client } from "./client.js"
15
+ import type { Media } from "./media.js"
14
16
 
15
17
  export interface ToolkitOptions {
16
18
  mcpConfig?: string | object
@@ -31,6 +33,8 @@ export interface ToolkitOptions {
31
33
  /** Sibling-file sample cap: 0 ⇒ default 10, n>0 ⇒ cap, -1 ⇒ omit <skill_files> (§3, S5). */
32
34
  skillSampleLimit?: number
33
35
  extraTools?: Tool[]
36
+ /** Media generation (§7G): registers `media.tools()` (source "media"). */
37
+ media?: Media
34
38
  /** Built-in tools (§4A). On by default. false | { disabled:true } | { enabled:false } ⇒ off. */
35
39
  builtins?: BuiltinsConfig
36
40
  /** Remote A2A agents — each advertised skill becomes a tool (source:"a2a"). */
@@ -45,6 +49,10 @@ export interface ToolkitOptions {
45
49
  /** §2 Gap 3. Bounds the MCP load: each connect/list is capped by the server timeout and the
46
50
  * whole load aborts promptly if this signal fires. Omit ⇒ unbounded as before. */
47
51
  signal?: AbortSignal
52
+ /** §2A.1a where MCP OAuth tokens live (default: in memory, per load). */
53
+ tokenStore?: import("./mcp2026/auth.js").TokenStore
54
+ /** §2A.1a how `auto` MCP OAuth opens the authorization URL (default: the OS opener). */
55
+ openUrl?: (url: string) => unknown | Promise<unknown>
48
56
  }
49
57
 
50
58
  export class Toolkit {
@@ -80,12 +88,13 @@ export class Toolkit {
80
88
  }
81
89
 
82
90
  static async create(opts: ToolkitOptions = {}): Promise<Toolkit> {
83
- const mcp = opts.mcpConfig !== undefined ? await loadMcp(opts.mcpConfig, { waitFor: opts.waitFor, signal: opts.signal }) : undefined
91
+ const mcp = opts.mcpConfig !== undefined ? await loadMcp(opts.mcpConfig, { waitFor: opts.waitFor, signal: opts.signal, tokenStore: opts.tokenStore, openUrl: opts.openUrl, disableTools: opts.disableTools }) : undefined
84
92
  // Skills come from directories, in-memory data, and/or a lazy provider (§3,
85
93
  // S1). The provider is resolved here (create() is async) and merged with the
86
94
  // data list; a provider failure is isolated so other sources still load.
87
95
  let skill: SkillSource | undefined
88
- if (opts.skillsDir !== undefined || opts.skills !== undefined || opts.skillProvider !== undefined) {
96
+ const mcpSkills = mcp?.skills ?? []
97
+ if (opts.skillsDir !== undefined || opts.skills !== undefined || opts.skillProvider !== undefined || mcpSkills.length > 0) {
89
98
  let providerDefs: SkillDef[] = []
90
99
  if (opts.skillProvider) {
91
100
  try {
@@ -95,7 +104,8 @@ export class Toolkit {
95
104
  providerDefs = []
96
105
  }
97
106
  }
98
- const dataDefs = [...(opts.skills ?? []), ...providerDefs]
107
+ // §2.7 remote skills (MCP skills extension) join the same inventory, after local ones.
108
+ const dataDefs = [...(opts.skills ?? []), ...providerDefs, ...mcpSkills]
99
109
  // disableSkills is sugar over a skillsFilter drop-list: fold each name in as false
100
110
  // WITHOUT overriding an explicit skillsFilter entry (explicit entries win).
101
111
  const filter =
@@ -137,7 +147,7 @@ export class Toolkit {
137
147
  }),
138
148
  ),
139
149
  )
140
- const agentToolsFlat = agentToolLists.flat()
150
+ const agentToolsFlat = [...agentToolLists.flat(), ...(opts.media ? opts.media.tools() : [])]
141
151
 
142
152
  // A2A inbound profile: a top-level `a2a` block on a parsed config object
143
153
  // (mirrors builtins/agents). serve() prefers its inline `a2a` over this.
@@ -218,6 +228,26 @@ export class Toolkit {
218
228
  })
219
229
  }
220
230
 
231
+ /**
232
+ * §7C.1 (ADR 0039). An embeddable streamable-HTTP MCP server as a Node request listener
233
+ * `(req, res) => void` (+ `notifyToolsChanged()`), to mount on the host's own server/router
234
+ * behind its own TLS, CORS and OAuth. Every policy is a hook in `cfg`; `serve()` is unchanged.
235
+ * Without `cfg.listTools` it exposes this toolkit's tools.
236
+ */
237
+ mcpHandler(cfg: McpHandlerConfig = {}): McpHandler {
238
+ return createMcpHandler(() => this.tools(), cfg)
239
+ }
240
+
241
+ /**
242
+ * SPEC §8C. Serves this toolkit for a local agent to call: the §7C MCP mapping (tool names
243
+ * verbatim, `skill` stays `skill`, optional `config` filter) on `127.0.0.1:<ephemeral>/mcp`
244
+ * behind a fresh 64-hex bearer token. The caller builds whatever `mcpServers` entry its agent
245
+ * accepts from `{url, token}`; `close()` stops it (idempotent).
246
+ */
247
+ asMcpServer(config?: MCPServeConfig): Promise<LocalMcpServer> {
248
+ return serveLocalMcp(this.tools(), config)
249
+ }
250
+
221
251
  async execute(name: string, args: Record<string, unknown>, ctx?: ToolContext): Promise<ToolResult> {
222
252
  const tool = this.byName.get(name)
223
253
  if (!tool) {
package/src/types.ts CHANGED
@@ -12,7 +12,7 @@ export type JSONSchema = {
12
12
 
13
13
  import type { ContentPart } from "./content.js"
14
14
 
15
- export type ToolSource = "mcp" | "skill" | "builtin" | "native" | "http" | "a2a" | "custom"
15
+ export type ToolSource = "mcp" | "skill" | "builtin" | "native" | "http" | "a2a" | "media" | "custom"
16
16
 
17
17
  export interface ToolResult {
18
18
  output: string
@@ -25,6 +25,22 @@ export interface ToolResult {
25
25
  */
26
26
  parts?: ContentPart[]
27
27
  metadata?: Record<string, unknown>
28
+ /**
29
+ * Machine-readable result (MCP `structuredContent`, SPEC §1). Absent ⇒ byte-identical wire.
30
+ * When set and `output` is empty, MCP serve emits the JSON text of `structured` as the text block.
31
+ */
32
+ structured?: unknown
33
+ /** Protocol metadata for the wire result (MCP `_meta`, SPEC §1). Absent ⇒ omitted. */
34
+ meta?: Record<string, unknown>
35
+ }
36
+
37
+ /** MCP tool annotations (SPEC §1, §7C.1), sent verbatim on an MCP `tools/list` entry. */
38
+ export interface ToolAnnotations {
39
+ title?: string
40
+ readOnlyHint?: boolean
41
+ destructiveHint?: boolean
42
+ idempotentHint?: boolean
43
+ openWorldHint?: boolean
28
44
  }
29
45
 
30
46
  export interface ToolContext {
@@ -35,6 +51,9 @@ export interface ToolContext {
35
51
  /** The model's id for this tool call, set by the client loop (§4C). A message's calls run
36
52
  * concurrently, so this is how a tool tells its sibling calls apart. Absent outside the loop. */
37
53
  callId?: string
54
+ /** Set by `toolkit.mcpHandler` (§7C.1): the inbound MCP request this call serves. Read it with
55
+ * `identityFrom(ctx)` / `requestFrom(ctx)` / `mcpSessionIdFrom(ctx)` / `mcpRequestMetaFrom(ctx)`. */
56
+ mcp?: import("./mcphandler.js").McpContext
38
57
  }
39
58
 
40
59
  /**
@@ -130,6 +149,13 @@ export interface Tool {
130
149
  /** What the tool may do (§4C). Builtins always carry hints; MCP tools carry them when the
131
150
  * server sends annotations. Advisory: a server can mislabel its tools. */
132
151
  hints?: ToolHints
152
+ /** Decides per call whether this tool may run alongside other safe calls of the same model
153
+ * response (SPEC §8 "Tool scheduling"). Absent ⇒ safe only when `hints.readOnly`. */
154
+ concurrencySafe?: (args: Record<string, unknown>) => boolean
155
+ /** JSON Schema of `ToolResult.structured` (SPEC §1); absent ⇒ omitted everywhere. */
156
+ outputSchema?: Record<string, unknown>
157
+ /** MCP annotations (§7C.1), verbatim on the MCP wire; absent ⇒ derived from `hints`, else omitted. */
158
+ annotations?: ToolAnnotations
133
159
  }
134
160
 
135
161
  export type McpStatus = "connected" | "disabled" | "failed"