apps 1.0.0-beta.2 → 1.0.0-beta.4

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 (41) hide show
  1. package/dist/authoring/index.d.ts +254 -37
  2. package/dist/authoring/index.js +11 -1
  3. package/dist/{chunk-OGCPVWAZ.js → chunk-2DVKSPQH.js} +134 -13
  4. package/dist/{chunk-NFLNA2SC.js → chunk-4OZFMCTT.js} +7 -1
  5. package/dist/{chunk-LWKQDDLZ.js → chunk-535V5K64.js} +61 -4
  6. package/dist/chunk-DP6UJCMX.js +27 -0
  7. package/dist/chunk-QA72XJZU.js +41 -0
  8. package/dist/chunk-S5KFP4JQ.js +20 -0
  9. package/dist/{chunk-646MS7DD.js → chunk-XYT6GZ7M.js} +7 -7
  10. package/dist/http/index.d.ts +9 -0
  11. package/dist/http/index.js +6 -0
  12. package/dist/index.d.ts +3 -0
  13. package/dist/index.js +23 -1
  14. package/dist/manifest/contract.d.ts +8 -0
  15. package/dist/manifest/index.js +3 -3
  16. package/dist/mcp/client.d.ts +9 -0
  17. package/dist/mcp/index.d.ts +11 -0
  18. package/dist/mcp/index.js +7 -0
  19. package/dist/openapi/index.d.ts +14 -0
  20. package/dist/openapi/index.js +7 -0
  21. package/dist/openapi/mapper.d.ts +7 -0
  22. package/dist/runner/index.js +29 -15
  23. package/dist/runner/protocol.d.ts +18 -5
  24. package/dist/runner/protocol.js +5 -3
  25. package/dist/runtime/index.d.ts +42 -5
  26. package/dist/runtime/index.js +3 -1
  27. package/dist/workerd/index.d.ts +1 -1
  28. package/dist/workerd/index.js +1 -1
  29. package/package.json +13 -6
  30. package/src/authoring/index.ts +395 -49
  31. package/src/http/index.ts +26 -0
  32. package/src/index.ts +3 -0
  33. package/src/manifest/contract.ts +9 -0
  34. package/src/mcp/client.ts +59 -0
  35. package/src/mcp/index.ts +42 -0
  36. package/src/openapi/index.ts +70 -0
  37. package/src/openapi/mapper.ts +25 -0
  38. package/src/runner/index.ts +35 -12
  39. package/src/runner/protocol.ts +32 -7
  40. package/src/runtime/index.ts +216 -25
  41. package/src/workerd/index.ts +2 -0
@@ -3,6 +3,18 @@
3
3
  * Authored modules record plain claims without I/O or user context; the
4
4
  * manifest loader owns execution, validation, and effects.
5
5
  */
6
+ /** A single opaque credential with RFC 6750 bearer semantics. */
7
+ export interface BearerMethod {
8
+ readonly kind: "bearer";
9
+ readonly label: string;
10
+ readonly help?: string;
11
+ }
12
+ /** An RFC 7617 username/password pair. */
13
+ export interface BasicMethod {
14
+ readonly kind: "basic";
15
+ readonly label: string;
16
+ readonly help?: string;
17
+ }
6
18
  export interface SecretsMethod<F extends Readonly<Record<string, SecretField>> = Readonly<Record<string, SecretField>>> {
7
19
  readonly kind: "secrets";
8
20
  readonly label: string;
@@ -17,7 +29,7 @@ export interface SecretField {
17
29
  readonly secret: boolean;
18
30
  }
19
31
  /**
20
- * An oauth auth method is a secrets-shaped auth method with acquisition strategies
32
+ * An oauth auth method is a bearer method with acquisition strategies
21
33
  * layered on: the material contract is always {token, refresh_token?,
22
34
  * expires_at?}, and the host fills it by the best strategy it can offer
23
35
  * — authorization-code ceremony, device-code ceremony, or paste. Paste
@@ -41,7 +53,15 @@ export interface OAuthMethod {
41
53
  * (audience-bound tokens, DPoP). Stated, never inferred. */
42
54
  readonly paste?: false;
43
55
  }
44
- export type AuthMethod = SecretsMethod | OAuthMethod;
56
+ export type AuthMethod = BearerMethod | BasicMethod | SecretsMethod | OAuthMethod;
57
+ export declare const bearer: (opts: {
58
+ readonly label: string;
59
+ readonly help?: string;
60
+ }) => BearerMethod;
61
+ export declare const basic: (opts: {
62
+ readonly label: string;
63
+ readonly help?: string;
64
+ }) => BasicMethod;
45
65
  export declare const secrets: <const F extends Readonly<Record<string, SecretField>>>(opts: {
46
66
  readonly label: string;
47
67
  /** Markdown shown under this auth method on the connect page, for example:
@@ -82,55 +102,195 @@ export interface RequirementSpec {
82
102
  readonly description?: string;
83
103
  }
84
104
  declare const RequirementKey: unique symbol;
105
+ declare const ServiceDeclKey: unique symbol;
106
+ declare const RequirementPhantom: unique symbol;
107
+ /** Requirement arity, tracked at the type level so handler contexts
108
+ * know whether a role arrives as one entry, maybe-one, or a list. */
109
+ export type RequirementArity = "one" | "optional" | "many";
85
110
  /**
86
111
  * A requirement value in a tool's `requirements` record. A Service IS
87
112
  * a requirement (bare value = arity "one"); .optional()/.array()
88
113
  * return terminal Requirements so nonsense chains do not typecheck.
114
+ * The phantom property is never assigned — it exists so auth-compose
115
+ * callbacks and handler contexts can recover the service's grants, an
116
+ * auth-door pin, and the arity at the type level.
89
117
  */
90
- export interface Requirement<D extends ServiceDecl = ServiceDecl> {
118
+ export interface Requirement<D extends ServiceDecl = ServiceDecl, G extends string | undefined = string | undefined, A extends RequirementArity = RequirementArity> {
91
119
  readonly [RequirementKey]: RequirementSpec;
92
- describe(text: string): Requirement<D>;
93
- }
94
- export interface Service<D extends ServiceDecl = ServiceDecl> extends Requirement<D> {
95
- readonly decl: D;
96
- optional(): Requirement<D>;
97
- array(): Requirement<D>;
98
- /** Pin one auth method: this tool supports only the named grant. */
99
- grant(name: keyof NonNullable<D["auth"]> & string): Service<D>;
120
+ readonly [RequirementPhantom]?: {
121
+ readonly decl: D;
122
+ readonly pinned: G;
123
+ readonly arity: A;
124
+ };
125
+ describe(text: string): Requirement<D, G, A>;
126
+ }
127
+ /**
128
+ * A grant-pinned requirement: what `service.auth.<name>` returns. A
129
+ * deliberately narrower type than Service — it can shape arity and
130
+ * describe itself, but carries no `auth` (pinning twice is nonsense)
131
+ * and no declaration access. Pinning is property access because the
132
+ * grants are static facts of the declaration: `github.auth.pat`
133
+ * autocompletes and a wrong name fails compilation.
134
+ */
135
+ export interface PinnedRequirement<D extends ServiceDecl = ServiceDecl, N extends string = string> extends Requirement<D, N, "one"> {
136
+ optional(): Requirement<D, N, "optional">;
137
+ array(): Requirement<D, N, "many">;
138
+ describe(text: string): PinnedRequirement<D, N>;
139
+ }
140
+ /** The grant-pinned doors of a service, keyed by declared auth method. */
141
+ export type ServiceAuth<D extends ServiceDecl> = {
142
+ readonly [N in keyof NonNullable<D["auth"]> & string]: PinnedRequirement<D, N>;
143
+ };
144
+ export interface Service<D extends ServiceDecl = ServiceDecl> extends Requirement<D, undefined, "one"> {
145
+ /** Symbol-keyed so author IntelliSense shows only authoring verbs;
146
+ * the declaration is plumbing the manifest reads at collection. */
147
+ readonly [ServiceDeclKey]: D;
148
+ /** Pin one auth method: `github.auth.pat` is this tool supporting
149
+ * only that grant. */
150
+ readonly auth: ServiceAuth<D>;
151
+ optional(): Requirement<D, undefined, "optional">;
152
+ array(): Requirement<D, undefined, "many">;
100
153
  describe(text: string): Service<D>;
101
154
  }
102
155
  /** A publish-time authoring problem accumulated without interrupting defines. */
103
156
  export interface Problem {
104
157
  readonly message: string;
105
158
  }
106
- /**
107
- * A dynamic tool-set claim: tools discovered from a live surface (an
108
- * MCP server's tool list, an OpenAPI document) rather than authored one
109
- * file at a time. The claim is recorded at publish time; discovery runs
110
- * later, per connection — an app with zero static tools and one claim
111
- * saves green, and listing never blocks on an upstream.
112
- */
113
- export type DynamicClaimSpec = {
114
- readonly kind: "mcp";
115
- readonly requirement: RequirementSpec;
116
- /** MCP endpoint URL. Absent = the service origin at its root path. */
117
- readonly endpoint?: string;
159
+ /** One connected role, discriminated by the declared auth method kind. */
160
+ export type AuthRoleEntry<D extends ServiceDecl> = {
161
+ [N in keyof NonNullable<D["auth"]> & string]: NonNullable<D["auth"]>[N] extends SecretsMethod<infer F> ? {
162
+ readonly grant: N;
163
+ readonly kind: "secrets";
164
+ readonly secrets: {
165
+ readonly [K in keyof F & string]: string;
166
+ };
167
+ } : NonNullable<D["auth"]>[N] extends BasicMethod ? {
168
+ readonly grant: N;
169
+ readonly kind: "basic";
170
+ readonly username: string;
171
+ readonly password: string;
172
+ } : {
173
+ readonly grant: N;
174
+ readonly kind: "bearer" | "oauth";
175
+ readonly token: string;
176
+ };
177
+ }[keyof NonNullable<D["auth"]> & string];
178
+ /** Kind-discriminated credential material accepted by surface helpers. */
179
+ export type RoleMaterial = {
180
+ readonly kind: "bearer" | "oauth";
181
+ readonly token: string;
182
+ } | {
183
+ readonly kind: "basic";
184
+ readonly username: string;
185
+ readonly password: string;
118
186
  } | {
119
- readonly kind: "openapi";
120
- readonly requirement: RequirementSpec;
121
- /** OpenAPI document: an absolute URL or an app-relative path. */
122
- readonly spec: string;
187
+ readonly kind: "secrets";
188
+ readonly secrets: Readonly<Record<string, string>>;
189
+ };
190
+ /** Recover the connected role type produced by a service requirement. */
191
+ export type RoleOf<S, G extends string | undefined = undefined> = S extends Requirement<infer D, infer Pin> ? [G] extends [string] ? Extract<AuthRoleEntry<D>, {
192
+ grant: G;
193
+ }> : Pin extends string ? Extract<AuthRoleEntry<D>, {
194
+ grant: Pin;
195
+ }> : AuthRoleEntry<D> : never;
196
+ /** Role record received by an authored authentication placement callback. */
197
+ export type AuthRoles<Reqs extends Readonly<Record<string, Requirement>>> = {
198
+ readonly [K in keyof Reqs]: NonNullable<Reqs[K][typeof RequirementPhantom]> extends {
199
+ readonly decl: infer D extends ServiceDecl;
200
+ readonly pinned: infer G;
201
+ } ? G extends string ? Extract<AuthRoleEntry<D>, {
202
+ grant: G;
203
+ }> : AuthRoleEntry<D> : never;
123
204
  };
205
+ /** Request placement returned by an authored dynamic auth callback. */
206
+ export interface AuthPlacement {
207
+ readonly headers?: HeadersInit;
208
+ readonly searchParams?: URLSearchParams | Readonly<Record<string, string>>;
209
+ }
210
+ export type AuthComposeFn<Reqs extends Readonly<Record<string, Requirement>> = never> = (roles: AuthRoles<Reqs>) => AuthPlacement | undefined | void;
211
+ export type HasSecretsGrant<D extends ServiceDecl> = true extends (NonNullable<D["auth"]>[keyof NonNullable<D["auth"]>] extends infer M ? M extends SecretsMethod ? true : false : never) ? true : false;
212
+ /** Whether an OpenAPI wrapper must state credential placement explicitly. */
213
+ export type RequiresAuthCompose<Reqs extends Readonly<Record<string, Requirement>>> = true extends {
214
+ [K in keyof Reqs]: NonNullable<Reqs[K][typeof RequirementPhantom]> extends {
215
+ readonly decl: infer D extends ServiceDecl;
216
+ readonly pinned: infer G;
217
+ } ? G extends keyof NonNullable<D["auth"]> & string ? NonNullable<D["auth"]>[G] extends SecretsMethod ? true : false : HasSecretsGrant<D> : never;
218
+ }[keyof Reqs] ? true : false;
219
+ /** Record a wrapper-specific publish problem on the active authored file. @internal */
220
+ export declare const recordAuthoringProblem: (message: string) => void;
221
+ /** Operation curation for a dynamic tool set. Exactly one of allow/block. */
222
+ export type OperationSelection = {
223
+ readonly allow: readonly string[];
224
+ readonly block?: undefined;
225
+ } | {
226
+ readonly block: readonly string[];
227
+ readonly allow?: undefined;
228
+ };
229
+ /** One tool emitted by a dynamic discovery pass. */
230
+ export interface DiscoveredToolSpec {
231
+ readonly name: string;
232
+ readonly description: string;
233
+ readonly input: SchemaLike;
234
+ readonly annotations?: ToolAnnotations;
235
+ readonly detail?: unknown;
236
+ }
237
+ /**
238
+ * The generic authored shape of a dynamic pair: ctx is inferred from
239
+ * the requirements record exactly as for static tools. Discovered
240
+ * inputs are runtime data (the schema arrives from the upstream at
241
+ * discover time), so `call`'s input is honestly `unknown` — the
242
+ * runtime validates against the discovered descriptor before calling.
243
+ */
244
+ export interface DynamicToolsDecl<Reqs extends Readonly<Record<string, Requirement>> | undefined = Readonly<Record<string, Requirement>> | undefined> {
245
+ readonly description: string;
246
+ readonly requirements?: Reqs;
247
+ discover(args: {
248
+ readonly ctx: HandlerCtx<Reqs>;
249
+ }): Promise<readonly DiscoveredToolSpec[]>;
250
+ call(args: {
251
+ readonly tool: {
252
+ readonly name: string;
253
+ readonly detail: unknown;
254
+ };
255
+ readonly input: unknown;
256
+ readonly ctx: HandlerCtx<Reqs>;
257
+ }): Promise<unknown>;
258
+ }
259
+ /**
260
+ * The erased registry row of a dynamic pair — same storage boundary
261
+ * as ToolSpec. App code: runs where handlers run (in-process locally,
262
+ * inside the isolate on workerd) and places credentials itself from
263
+ * ctx, like any handler. `discover` interrogates the tool source;
264
+ * `call` executes one discovered tool from the identity and detail
265
+ * discover emitted.
266
+ */
267
+ export interface DynamicToolsSpec {
268
+ readonly description: string;
269
+ readonly requirements?: Readonly<Record<string, Requirement>>;
270
+ discover(args: never): Promise<readonly DiscoveredToolSpec[]>;
271
+ call(args: never): Promise<unknown>;
272
+ }
124
273
  declare const DynamicKey: unique symbol;
125
- /** The value fromMCP/fromOpenAPI return: an opaque claim handle a tool
126
- * file default-exports. The spec is undefined when the claim was
274
+ /** The value defineDynamicTools returns: an opaque handle a tool file
275
+ * default-exports. The spec is undefined when the definition was
127
276
  * invalid (a problem was recorded instead). */
128
277
  export interface DynamicToolSet {
129
- readonly [DynamicKey]: DynamicClaimSpec | undefined;
278
+ readonly [DynamicKey]: DynamicToolsSpec | undefined;
130
279
  }
131
- /** A registered dynamic claim and its source attribution. @internal */
132
- export interface RegisteredDynamic {
133
- readonly spec: DynamicClaimSpec;
280
+ /** Reads a dynamic spec from a tool file's default export. @internal */
281
+ export declare const dynamicSpecOf: (value: unknown) => DynamicToolsSpec | undefined;
282
+ /**
283
+ * The dynamic-tools primitive: an author-written discover/call pair
284
+ * whose tool list is interrogated at runtime, per connection, instead
285
+ * of authored one file at a time. The file's name becomes the address
286
+ * segment (appKey/<stem>/<toolName>); tools land in the catalog after
287
+ * discovery and never block a save. fromMCP/fromOpenAPI are shipped
288
+ * pairs over this same primitive.
289
+ */
290
+ export declare const defineDynamicTools: <const Reqs extends Readonly<Record<string, Requirement>> | undefined = undefined>(decl: DynamicToolsDecl<Reqs>) => DynamicToolSet;
291
+ /** A registered dynamic tool set and its source attribution. @internal */
292
+ export interface DynamicRegistration {
293
+ readonly spec: DynamicToolsSpec;
134
294
  readonly file: string;
135
295
  }
136
296
  /** A registered service declaration and its source attribution. @internal */
@@ -148,7 +308,7 @@ export interface Registry {
148
308
  app: AppManifest | undefined;
149
309
  readonly services: Map<string, RegisteredService>;
150
310
  readonly tools: Map<string, RegisteredTool>;
151
- readonly dynamic: Map<string, RegisteredDynamic>;
311
+ readonly dynamic: Map<string, DynamicRegistration>;
152
312
  readonly problems: Problem[];
153
313
  readonly toolFiles: Map<string, {
154
314
  definitions: number;
@@ -178,27 +338,84 @@ export declare const defineApp: (manifest: {
178
338
  }) => AppManifest;
179
339
  /** Any Standard Schema (zod preferred) or a raw JSON-schema object. */
180
340
  export type SchemaLike = object;
341
+ /**
342
+ * The validated value a Standard Schema produces, recovered from its
343
+ * phantom types (zod 4 and effect Schema both publish them). A raw
344
+ * JSON-schema object carries no phantom, so its input is `unknown` —
345
+ * stated weakness, not silent weakness.
346
+ */
347
+ export type InferSchema<S> = S extends {
348
+ readonly "~standard": {
349
+ readonly types?: infer T;
350
+ };
351
+ } ? NonNullable<T> extends {
352
+ readonly output: infer O;
353
+ } ? O : unknown : unknown;
354
+ /**
355
+ * The capability a handler receives for one connected requirement
356
+ * role: the grant's kind-discriminated material (narrowed by any
357
+ * auth-door pin) plus the spending fetch. Arity shapes the value —
358
+ * `.optional()` may be absent, `.array()` is every connected account.
359
+ */
360
+ export type HandlerRole<R> = R extends Requirement<infer D, infer G, infer A> ? [A] extends ["many"] ? ReadonlyArray<HandlerRoleEntry<D, G>> : [A] extends ["optional"] ? HandlerRoleEntry<D, G> | undefined : HandlerRoleEntry<D, G> : never;
361
+ /** One connected role entry: declared-auth material + capability fetch. */
362
+ export type HandlerRoleEntry<D extends ServiceDecl, G extends string | undefined> = ([G] extends [string] ? Extract<AuthRoleEntry<D>, {
363
+ grant: G;
364
+ }> : AuthRoleEntry<D>) & {
365
+ fetch(path: string, init?: RequestInit): Promise<Response>;
366
+ };
367
+ /** The ctx record a handler receives, keyed by requirement role. */
368
+ export type HandlerCtx<Reqs> = [Reqs] extends [Readonly<Record<string, Requirement>>] ? {
369
+ readonly [K in keyof Reqs]: HandlerRole<Reqs[K]>;
370
+ } : Readonly<Record<never, never>>;
181
371
  export interface ToolAnnotations {
182
372
  readonly readOnly?: boolean;
183
373
  readonly destructive?: boolean;
184
374
  /** Facts, not flow: shortfall surfaces as the scope challenge. */
185
375
  readonly scopes?: readonly string[];
186
376
  }
187
- export interface ToolSpec {
377
+ /**
378
+ * The generic authored shape: input/ctx/output are all inferred — the
379
+ * handler's input from the Standard Schema, ctx from the requirements
380
+ * record (grants, pins, arity), and the return type checked against
381
+ * the output schema when one is stated. The handler takes ONE
382
+ * destructurable argument so unused halves never demand an underscore:
383
+ * `handler: async ({ input }) => ...` / `async ({ ctx }) => ...`.
384
+ */
385
+ export interface ToolDecl<Input extends SchemaLike = SchemaLike, Reqs extends Readonly<Record<string, Requirement>> | undefined = Readonly<Record<string, Requirement>> | undefined, Output extends SchemaLike | undefined = SchemaLike | undefined> {
188
386
  /** Address segment. Single-tool files may omit it (filename). */
387
+ readonly name?: string;
388
+ readonly description: string;
389
+ readonly requirements?: Reqs;
390
+ readonly input: Input;
391
+ readonly output?: Output;
392
+ readonly annotations?: ToolAnnotations;
393
+ handler(args: {
394
+ readonly input: InferSchema<Input>;
395
+ readonly ctx: HandlerCtx<Reqs>;
396
+ }): Promise<Output extends SchemaLike ? InferSchema<Output> : unknown>;
397
+ }
398
+ /**
399
+ * The erased registry row. Registration drops the generics — storage
400
+ * is untyped by design, the same boundary every schema library draws
401
+ * between its typed builders and its runtime representation. The
402
+ * runtime re-establishes safety by validating input before the
403
+ * handler runs and shaping ctx from resolved connections.
404
+ */
405
+ export interface ToolSpec {
189
406
  readonly name?: string;
190
407
  readonly description: string;
191
408
  readonly requirements?: Readonly<Record<string, Requirement>>;
192
409
  readonly input: SchemaLike;
193
410
  readonly output?: SchemaLike;
194
411
  readonly annotations?: ToolAnnotations;
195
- handler(input: never, ctx: never): Promise<unknown>;
412
+ handler(args: never): Promise<unknown>;
196
413
  }
197
414
  declare const ToolKey: unique symbol;
198
415
  export interface Tool {
199
416
  readonly [ToolKey]: ToolSpec;
200
417
  }
201
- export declare const defineTool: (spec: ToolSpec) => Tool;
418
+ export declare const defineTool: <const Input extends SchemaLike, const Reqs extends Readonly<Record<string, Requirement>> | undefined = undefined, const Output extends SchemaLike | undefined = undefined>(decl: ToolDecl<Input, Reqs, Output>) => Tool;
202
419
  export declare const toolSpecOf: (value: unknown) => ToolSpec | undefined;
203
420
  /** A tool file's default export. */
204
421
  export type ToolExport = Tool | ReadonlyArray<Tool>;
@@ -1,23 +1,33 @@
1
1
  import {
2
2
  __enterFile,
3
3
  __leaveFile,
4
+ basic,
5
+ bearer,
4
6
  defineApp,
7
+ defineDynamicTools,
5
8
  defineTool,
9
+ dynamicSpecOf,
6
10
  newRegistry,
7
11
  oauth,
12
+ recordAuthoringProblem,
8
13
  requirementSpecOf,
9
14
  secrets,
10
15
  service,
11
16
  setActiveRegistry,
12
17
  toolSpecOf
13
- } from "../chunk-LWKQDDLZ.js";
18
+ } from "../chunk-535V5K64.js";
14
19
  export {
15
20
  __enterFile,
16
21
  __leaveFile,
22
+ basic,
23
+ bearer,
17
24
  defineApp,
25
+ defineDynamicTools,
18
26
  defineTool,
27
+ dynamicSpecOf,
19
28
  newRegistry,
20
29
  oauth,
30
+ recordAuthoringProblem,
21
31
  requirementSpecOf,
22
32
  secrets,
23
33
  service,
@@ -1,5 +1,9 @@
1
1
  // src/runtime/index.ts
2
- var failure = (reason) => ({ ok: false, reason });
2
+ var failure = (kind, reason) => ({
3
+ ok: false,
4
+ kind,
5
+ reason
6
+ });
3
7
  var errorMessage = (error) => error instanceof Error ? error.message : String(error);
4
8
  var isJsonValue = (value) => {
5
9
  if (value === null || typeof value === "string" || typeof value === "boolean") return true;
@@ -48,6 +52,10 @@ var compileInputSchema = (input, where) => {
48
52
  };
49
53
  var requirementKey = Symbol.for("executor/apps/requirement");
50
54
  var requirementSpecOf = (value) => typeof value === "object" && value !== null && requirementKey in value ? value[requirementKey] : void 0;
55
+ var segmentOf = (file) => {
56
+ const name = file.slice(file.lastIndexOf("/") + 1);
57
+ return name.endsWith(".ts") ? name.slice(0, -3) : name;
58
+ };
51
59
  var canonicalJson = (value) => {
52
60
  const sortKeys = (node) => {
53
61
  if (Array.isArray(node)) return node.map(sortKeys);
@@ -100,6 +108,8 @@ var collectRegistry = (registry) => {
100
108
  for (const [file, summary] of registry.toolFiles) {
101
109
  if (summary.definitions > 1 && summary.hasUnnamedTool)
102
110
  problems.push(`${file}: multi-tool files must name every tool`);
111
+ if (registry.dynamic.has(file))
112
+ problems.push(`${file}: a file claims a dynamic tool set or defines tools, never both`);
103
113
  }
104
114
  const tools = [];
105
115
  if (app !== void 0) {
@@ -128,13 +138,43 @@ var collectRegistry = (registry) => {
128
138
  });
129
139
  }
130
140
  }
141
+ const dynamic = [];
142
+ const seenSegments = /* @__PURE__ */ new Map();
143
+ for (const [file, registered] of registry.dynamic) {
144
+ const segment = segmentOf(file);
145
+ const where = `${file}: dynamic tool set '${segment}'`;
146
+ const existing = seenSegments.get(segment);
147
+ if (existing !== void 0) {
148
+ problems.push(`${where}: segment already claimed by ${existing}`);
149
+ continue;
150
+ }
151
+ seenSegments.set(segment, file);
152
+ const requires = Object.entries(registered.spec.requirements ?? {}).flatMap(([role, requirement]) => {
153
+ const collected = collectRequirement(role, requirement, where);
154
+ return collected === void 0 ? [] : [collected];
155
+ }).sort((left, right) => left.role.localeCompare(right.role));
156
+ dynamic.push({
157
+ segment,
158
+ description: registered.spec.description,
159
+ requires,
160
+ file
161
+ });
162
+ }
131
163
  if (problems.length > 0 || app === void 0) return { problems };
132
164
  const services = [...claims.values()].sort(
133
165
  (left, right) => left.origin.localeCompare(right.origin)
134
166
  );
135
167
  const sortedTools = [...tools].sort((left, right) => left.address.localeCompare(right.address));
168
+ const sortedDynamic = [...dynamic].sort(
169
+ (left, right) => left.segment.localeCompare(right.segment)
170
+ );
136
171
  const appResult = { key: app.key, label: app.label, description: app.description };
137
- const manifest = { app: appResult, services, tools: sortedTools };
172
+ const manifest = {
173
+ app: appResult,
174
+ services,
175
+ tools: sortedTools,
176
+ dynamic: sortedDynamic
177
+ };
138
178
  return { problems: [], manifest, canonical: canonicalJson(manifest) };
139
179
  };
140
180
  var digestOf = async (canonical) => {
@@ -154,8 +194,8 @@ var manifestOf = async (registry) => {
154
194
  };
155
195
  };
156
196
  var standardValidateOf = (input) => "~standard" in input && typeof input["~standard"].validate === "function" ? input["~standard"].validate : void 0;
157
- var parseInput = async (spec, input) => {
158
- const validate = standardValidateOf(spec.input);
197
+ var parseInput = async (schema, input) => {
198
+ const validate = standardValidateOf(schema);
159
199
  if (validate === void 0)
160
200
  return { ok: false, reason: "input schema does not implement Standard Schema validate" };
161
201
  try {
@@ -170,21 +210,102 @@ var parseInput = async (spec, input) => {
170
210
  return { ok: false, reason: `input validation threw (${errorMessage(error)})` };
171
211
  }
172
212
  };
173
- var callTool = async (registry, name, input, ctx) => {
174
- const registered = registry.tools.get(name);
175
- if (registered === void 0) return failure("unknown tool");
176
- const parsed = await parseInput(registered.spec, input);
177
- if (!parsed.ok) return parsed;
213
+ var splitDynamicAddress = (address) => {
214
+ const slash = address.indexOf("/");
215
+ if (slash <= 0 || slash === address.length - 1) return void 0;
216
+ return { segment: address.slice(0, slash), name: address.slice(slash + 1) };
217
+ };
218
+ var dynamicFor = (registry, segment) => {
219
+ for (const [file, registered] of registry.dynamic)
220
+ if (segmentOf(file) === segment) return registered.spec;
221
+ return void 0;
222
+ };
223
+ var discoveredSpecs = /* @__PURE__ */ new WeakMap();
224
+ var callTool = async (registry, address, input, ctx, detail) => {
225
+ const dynamic = splitDynamicAddress(address);
226
+ if (dynamic === void 0) {
227
+ if (detail !== void 0)
228
+ return failure("handler", "a static tool takes no discovery detail");
229
+ const registered = registry.tools.get(address);
230
+ if (registered === void 0) return failure("handler", "unknown tool");
231
+ const parsed2 = await parseInput(registered.spec.input, input);
232
+ if (!parsed2.ok) return failure("input", parsed2.reason);
233
+ try {
234
+ const handler = registered.spec.handler;
235
+ return { ok: true, value: await handler({ input: parsed2.value, ctx }) };
236
+ } catch (error) {
237
+ return failure("handler", `handler failed (${errorMessage(error)})`);
238
+ }
239
+ }
240
+ if (detail === void 0)
241
+ return failure("handler", "a dynamic tool requires discovery detail");
242
+ const pair = dynamicFor(registry, dynamic.segment);
243
+ if (pair === void 0)
244
+ return failure("handler", `unknown dynamic tool set '${dynamic.segment}'`);
245
+ const discovered = discoveredSpecs.get(registry)?.get(dynamic.segment)?.get(dynamic.name);
246
+ if (discovered === void 0)
247
+ return failure(
248
+ "input",
249
+ `dynamic tool '${dynamic.name}' has no compiled discovery descriptor`
250
+ );
251
+ const parsed = await parseInput(discovered.input, input);
252
+ if (!parsed.ok) return failure("input", parsed.reason);
178
253
  try {
179
- const handler = registered.spec.handler;
180
- return { ok: true, value: await handler(parsed.value, ctx) };
254
+ const call = pair.call;
255
+ return {
256
+ ok: true,
257
+ value: await call({
258
+ tool: { name: dynamic.name, detail },
259
+ input: parsed.value,
260
+ ctx
261
+ })
262
+ };
263
+ } catch (error) {
264
+ return failure("handler", `dynamic call failed (${errorMessage(error)})`);
265
+ }
266
+ };
267
+ var discoverTool = async (registry, segment, ctx) => {
268
+ const pair = dynamicFor(registry, segment);
269
+ if (pair === void 0)
270
+ return { ok: false, reason: `unknown dynamic tool set '${segment}'` };
271
+ discoveredSpecs.get(registry)?.delete(segment);
272
+ let emitted;
273
+ try {
274
+ const discover = pair.discover;
275
+ emitted = await discover({ ctx });
181
276
  } catch (error) {
182
- return failure(`handler failed (${errorMessage(error)})`);
277
+ return { ok: false, reason: `discover failed (${errorMessage(error)})` };
278
+ }
279
+ const tools = [];
280
+ const live = /* @__PURE__ */ new Map();
281
+ for (const tool of emitted) {
282
+ if (live.has(tool.name))
283
+ return { ok: false, reason: `discover emitted duplicate tool '${tool.name}'` };
284
+ const compiled = compileInputSchema(tool.input, `${segment}/${tool.name}`);
285
+ if ("problem" in compiled) return { ok: false, reason: compiled.problem };
286
+ live.set(tool.name, tool);
287
+ tools.push({
288
+ segment,
289
+ name: tool.name,
290
+ descriptor: {
291
+ description: tool.description,
292
+ inputSchema: compiled.value,
293
+ annotations: {
294
+ ...tool.annotations?.readOnly === void 0 ? {} : { readOnly: tool.annotations.readOnly },
295
+ ...tool.annotations?.destructive === void 0 ? {} : { destructive: tool.annotations.destructive }
296
+ }
297
+ },
298
+ detail: tool.detail ?? null
299
+ });
183
300
  }
301
+ const registryDiscoveries = discoveredSpecs.get(registry) ?? /* @__PURE__ */ new Map();
302
+ registryDiscoveries.set(segment, live);
303
+ discoveredSpecs.set(registry, registryDiscoveries);
304
+ return { ok: true, value: tools.sort((left, right) => left.name.localeCompare(right.name)) };
184
305
  };
185
- var discoverTool = async (_registry, _name, _binding) => failure("no dynamic tools yet");
186
306
 
187
307
  export {
308
+ compileInputSchema,
188
309
  manifestOf,
189
310
  callTool,
190
311
  discoverTool
@@ -5,7 +5,13 @@ var CallError = class extends Data.TaggedError("CallError") {
5
5
  return `${this.target}: ${this.reason}`;
6
6
  }
7
7
  };
8
+ var DiscoverError = class extends Data.TaggedError("DiscoverError") {
9
+ get message() {
10
+ return `${this.segment}: ${this.reason}`;
11
+ }
12
+ };
8
13
 
9
14
  export {
10
- CallError
15
+ CallError,
16
+ DiscoverError
11
17
  };