akanjs 3.0.0-alpha.10 → 3.0.0-alpha.11

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 (105) hide show
  1. package/common/index.ts +10 -0
  2. package/common/mcpExposure.ts +106 -0
  3. package/constant/immerify.ts +1 -1
  4. package/dictionary/DictionaryLookup.ts +30 -0
  5. package/dictionary/dictInfo.ts +77 -0
  6. package/dictionary/index.ts +1 -0
  7. package/dictionary/locale.ts +16 -2
  8. package/document/index.ts +1 -0
  9. package/document/noDocumentError.ts +12 -0
  10. package/fetch/client/fetchClient.ts +3 -0
  11. package/fetch/fetchType/endpointFetch.type.ts +10 -4
  12. package/package.json +1 -1
  13. package/server/akanApp.ts +32 -0
  14. package/server/akanServer.ts +154 -5
  15. package/server/devtools/signalSerializer.ts +2 -2
  16. package/server/devtools/types.ts +2 -2
  17. package/server/mcp/McpAuth.ts +177 -0
  18. package/server/mcp/McpDispatcher.ts +245 -0
  19. package/server/mcp/McpEventStream.ts +76 -0
  20. package/server/mcp/McpExecutionContext.ts +105 -0
  21. package/server/mcp/McpRouter.ts +604 -0
  22. package/server/mcp/index.ts +5 -0
  23. package/server/resolver/database.resolver.ts +5 -4
  24. package/server/resolver/signal.resolver.ts +10 -0
  25. package/server/subRouteIndexDocument.tsx +61 -0
  26. package/server/systemPageDocument.tsx +2 -2
  27. package/server/systemPages.tsx +49 -16
  28. package/server/webRouter.ts +27 -1
  29. package/service/predefinedAdaptor/database.adaptor.ts +4 -3
  30. package/signal/agent/AgentCatalogue.ts +165 -0
  31. package/signal/agent/index.ts +1 -0
  32. package/signal/endpointInfo.ts +38 -2
  33. package/signal/guard.ts +11 -1
  34. package/signal/guards.ts +3 -1
  35. package/signal/index.ts +3 -0
  36. package/signal/mcp/McpDocument.ts +322 -0
  37. package/signal/mcp/McpProgress.ts +106 -0
  38. package/signal/mcp/McpUriTemplate.ts +85 -0
  39. package/signal/mcp/Msg.ts +396 -0
  40. package/signal/mcp/index.ts +5 -0
  41. package/signal/mcp/mcpProtocol.ts +120 -0
  42. package/signal/openapi/openapi.ts +32 -171
  43. package/signal/schema/JsonSchemaBuilder.ts +211 -0
  44. package/signal/schema/index.ts +1 -0
  45. package/signal/serializer/fetch.serializer.ts +3 -0
  46. package/signal/signalContext.ts +45 -5
  47. package/signal/slice.ts +15 -2
  48. package/signal/types.ts +33 -1
  49. package/store/action.ts +1 -1
  50. package/store/storeInstance.ts +1 -1
  51. package/types/common/index.d.ts +1 -0
  52. package/types/common/mcpExposure.d.ts +69 -0
  53. package/types/dictionary/DictionaryLookup.d.ts +14 -0
  54. package/types/dictionary/base.dictionary.d.ts +1 -1
  55. package/types/dictionary/dictInfo.d.ts +41 -11
  56. package/types/dictionary/dictionary.d.ts +8 -8
  57. package/types/dictionary/index.d.ts +1 -0
  58. package/types/dictionary/locale.d.ts +9 -1
  59. package/types/document/index.d.ts +1 -0
  60. package/types/document/noDocumentError.d.ts +12 -0
  61. package/types/fetch/fetchType/endpointFetch.type.d.ts +5 -3
  62. package/types/server/akanApp.d.ts +6 -0
  63. package/types/server/akanServer.d.ts +43 -0
  64. package/types/server/devtools/types.d.ts +2 -2
  65. package/types/server/mcp/McpAuth.d.ts +62 -0
  66. package/types/server/mcp/McpDispatcher.d.ts +54 -0
  67. package/types/server/mcp/McpEventStream.d.ts +18 -0
  68. package/types/server/mcp/McpExecutionContext.d.ts +44 -0
  69. package/types/server/mcp/McpRouter.d.ts +68 -0
  70. package/types/server/mcp/index.d.ts +5 -0
  71. package/types/server/mcp.d.ts +1 -0
  72. package/types/server/subRouteIndexDocument.d.ts +8 -0
  73. package/types/server/systemPageDocument.d.ts +1 -0
  74. package/types/server/systemPages.d.ts +5 -0
  75. package/types/signal/agent/AgentCatalogue.d.ts +100 -0
  76. package/types/signal/agent/index.d.ts +1 -0
  77. package/types/signal/agent.d.ts +1 -0
  78. package/types/signal/endpointInfo.d.ts +21 -3
  79. package/types/signal/guard.d.ts +10 -0
  80. package/types/signal/guards.d.ts +3 -1
  81. package/types/signal/index.d.ts +3 -0
  82. package/types/signal/mcp/McpDocument.d.ts +76 -0
  83. package/types/signal/mcp/McpProgress.d.ts +40 -0
  84. package/types/signal/mcp/McpUriTemplate.d.ts +26 -0
  85. package/types/signal/mcp/Msg.d.ts +144 -0
  86. package/types/signal/mcp/index.d.ts +5 -0
  87. package/types/signal/mcp/mcpProtocol.d.ts +107 -0
  88. package/types/signal/mcp.d.ts +1 -0
  89. package/types/signal/openapi/openapi.d.ts +3 -3
  90. package/types/signal/schema/JsonSchemaBuilder.d.ts +47 -0
  91. package/types/signal/schema/index.d.ts +1 -0
  92. package/types/signal/schema.d.ts +1 -0
  93. package/types/signal/signalContext.d.ts +16 -1
  94. package/types/signal/slice.d.ts +3 -1
  95. package/types/signal/types.d.ts +31 -1
  96. package/types/ui/Badge.d.ts +1 -1
  97. package/types/ui/Button.d.ts +1 -1
  98. package/types/ui/Signal/RestApi.d.ts +3 -2
  99. package/types/ui/Signal/style.d.ts +3 -0
  100. package/ui/Badge.tsx +1 -1
  101. package/ui/Button.tsx +1 -1
  102. package/ui/DatePicker.tsx +1 -1
  103. package/ui/Signal/RestApi.tsx +57 -17
  104. package/ui/Signal/WebSocket.tsx +1 -1
  105. package/ui/Signal/style.ts +6 -1
package/common/index.ts CHANGED
@@ -27,6 +27,16 @@ export {
27
27
  resolveAkanI18nConfig,
28
28
  } from "./localeConfig";
29
29
  export { lowerlize } from "./lowerlize";
30
+ export {
31
+ isMcpDescribableArg,
32
+ type McpExposureEndpoint,
33
+ type McpExposureHints,
34
+ type McpExposureOption,
35
+ mcpBaseVerbOf,
36
+ mcpHintsOf,
37
+ mcpPromptRefusalOf,
38
+ mcpRefusalOf,
39
+ } from "./mcpExposure";
30
40
  export { mergeVersion } from "./mergeVersion";
31
41
  export { objectify } from "./objectify";
32
42
  export { pathGet } from "./pathGet";
@@ -0,0 +1,106 @@
1
+ import { capitalize } from "./capitalize";
2
+
3
+ /**
4
+ * What MCP says about one endpoint: whether it is published, why it is not, and the hints it carries.
5
+ *
6
+ * Structurally typed like the rest of `common/` so the same rules answer on both sides — the server builds its
7
+ * catalogue from them and the browser API explorer badges an endpoint from them. A second implementation would
8
+ * eventually disagree, and an audit surface that disagrees with the catalogue is worse than none.
9
+ *
10
+ * Every rejection returns the sentence an author reads, at boot in the server log and in the explorer. Fail-closed
11
+ * with no reason is what left a deliberate `mcp: { expose: true }` vanishing with nowhere to look but the source.
12
+ */
13
+ export interface McpExposureEndpoint {
14
+ type: string;
15
+ returns: { refName: string };
16
+ args: { name: string; refName: string; type: string; arrDepth?: number; nullable?: boolean }[];
17
+ guards?: string[];
18
+ fileUpload?: boolean;
19
+ }
20
+
21
+ export interface McpExposureOption {
22
+ /**
23
+ * The read-only deployment valve, which is server configuration. The browser explorer cannot know it and so
24
+ * badges what the code decided; the boot log is where a read-only deployment says what it dropped.
25
+ */
26
+ readOnly?: boolean;
27
+ }
28
+
29
+ /** The author's overrides for the hints derived below. */
30
+ export interface McpExposureHints {
31
+ readOnly?: boolean;
32
+ destructive?: boolean;
33
+ idempotent?: boolean;
34
+ }
35
+
36
+ /** `Any` publishes as the empty schema, which tells a model nothing — so it is left out rather than described. */
37
+ export const isMcpDescribableArg = (arg: { refName: string }) => arg.refName !== "Any";
38
+
39
+ /**
40
+ * Which slice-level verb opts in each CRUD endpoint a model generates. They carry no signal option of their own,
41
+ * so `mcp: { get: true }` on the slice is the only place their exposure can be written.
42
+ */
43
+ export const mcpBaseVerbOf = (refName: string, key: string) => {
44
+ const cap = capitalize(refName);
45
+ if (key === refName || key === `light${cap}`) return "get" as const;
46
+ if (key === `create${cap}`) return "create" as const;
47
+ if (key === `update${cap}`) return "update" as const;
48
+ if (key === `remove${cap}`) return "remove" as const;
49
+ return null;
50
+ };
51
+
52
+ /**
53
+ * The hints a client renders beside a tool. Hints only — clients are told to distrust them, so they inform a UI
54
+ * and never stand in for a guard. `openWorldHint` is always false: every endpoint reaches this app's own
55
+ * database, not the wider internet.
56
+ */
57
+ export const mcpHintsOf = (key: string, endpoint: { type: string }, mcp: McpExposureHints = {}) => {
58
+ const readOnly = mcp.readOnly ?? endpoint.type === "query";
59
+ const destructive = mcp.destructive ?? (!readOnly && /^(remove|delete)/.test(key));
60
+ return {
61
+ readOnlyHint: readOnly,
62
+ destructiveHint: destructive,
63
+ idempotentHint: mcp.idempotent ?? (readOnly || /^(set|update)/.test(key)),
64
+ openWorldHint: false,
65
+ };
66
+ };
67
+
68
+ /** The sentence explaining why this endpoint is not in the catalogue, or `null` when it is. */
69
+ export const mcpRefusalOf = (endpoint: McpExposureEndpoint, { readOnly }: McpExposureOption = {}): string | null => {
70
+ if (endpoint.type === "prompt") return mcpPromptRefusalOf(endpoint);
71
+ if (endpoint.type === "pubsub" || endpoint.type === "message")
72
+ return `\`${endpoint.type}\` rides the websocket, and its internal arguments read a socket an MCP request does not have.`;
73
+ if (readOnly && endpoint.type !== "query")
74
+ return "this deployment is read-only, which drops every endpoint that is not a query.";
75
+ if (endpoint.returns.refName === "Any" || endpoint.returns.refName === "Upload")
76
+ return `a return typed \`${endpoint.returns.refName}\` cannot be described to a model.`;
77
+ if (endpoint.fileUpload || endpoint.args.some((arg) => arg.refName === "Upload"))
78
+ return "a file upload has no MCP representation.";
79
+ if (endpoint.type === "mutation" && !endpoint.guards?.some((name) => name !== "Public"))
80
+ return "a mutation needs a real guard — `[Public]` is having none, spelled out.";
81
+ const opaque = endpoint.args.find((arg) => !isMcpDescribableArg(arg) && arg.type !== "search" && !arg.nullable);
82
+ if (opaque)
83
+ return `its required argument \`${opaque.name}\` is typed \`Any\`, which is left out of the published schema — expose a named filter slice instead.`;
84
+ return null;
85
+ };
86
+
87
+ /**
88
+ * A prompt is a read exposed on the same terms as a query, so every rejection here is one thing: an argument
89
+ * `prompts/get` cannot carry. Its `arguments` is a flat string map — one string per name, and no schema beside it
90
+ * — which rules out the argument *kinds* the builder already refuses and, just as surely, two argument *types* it
91
+ * accepts. A tool escapes both because it publishes a real JSON Schema.
92
+ */
93
+ export const mcpPromptRefusalOf = (endpoint: McpExposureEndpoint): string | null => {
94
+ const carried = endpoint.args.find((arg) => arg.type === "body" || arg.type === "msg" || arg.type === "room");
95
+ if (carried)
96
+ return `a prompt's arguments travel as a flat string map, so its \`${carried.type}\` argument \`${carried.name}\` cannot be carried.`;
97
+
98
+ const list = endpoint.args.find((arg) => arg.arrDepth);
99
+ if (list)
100
+ return `a prompt argument is one string, so its list argument \`${list.name}\` could never carry more than one value.`;
101
+
102
+ const opaque = endpoint.args.find((arg) => !isMcpDescribableArg(arg));
103
+ if (opaque)
104
+ return `its argument \`${opaque.name}\` is typed \`Any\`, and a prompt has no schema in which to describe one.`;
105
+ return null;
106
+ };
@@ -9,7 +9,7 @@ export const immerify = <T extends object>(modelRef: ConstantModelRef, objOrArr:
9
9
  }) as Record<string, unknown>;
10
10
  const objRecord = objOrArr as Record<string, unknown>;
11
11
  Object.entries(modelRef[FIELD_META]).forEach(([key, field]) => {
12
- if (field.isScalar && field.isClass && !!objRecord[key])
12
+ if (field.isScalar && field.isClass && objRecord[key])
13
13
  immeredObj[key] = immerify(field.modelRef, objRecord[key] as object);
14
14
  });
15
15
  return immeredObj as T;
@@ -0,0 +1,30 @@
1
+ import { pathGet } from "akanjs/common";
2
+ import { DictionaryRegistry } from "./dictionaryRegistry";
3
+ import type { DictionaryNode } from "./trans";
4
+
5
+ /**
6
+ * Reads translated text out of the merged dictionary tree by dotted key (`user.signal.createUser.arg.data.desc`).
7
+ *
8
+ * Unlike `translate`, a missing key resolves to `undefined` rather than echoing the key back: callers here are
9
+ * document generators (OpenAPI, MCP) that must omit an absent description instead of emitting `"user.signal.…"`
10
+ * as if it were prose. The merged root is snapshotted once per instance, so build a fresh one per document.
11
+ */
12
+ export class DictionaryLookup {
13
+ readonly language: string;
14
+ readonly #models: Record<string, DictionaryNode>;
15
+ constructor(language?: string) {
16
+ const root = DictionaryRegistry.getRoot();
17
+ this.language = language && root[language] ? language : (Object.keys(root).at(0) ?? "en");
18
+ this.#models = root[this.language] ?? {};
19
+ }
20
+
21
+ /** A bare refName names the model node itself, which holds no text of its own — its label is `<refName>.modelName`. */
22
+ text(key: string): string | undefined {
23
+ const [refName, ...rest] = key.split(".");
24
+ if (!refName) return undefined;
25
+ const model = this.#models[refName];
26
+ const node = (rest.length ? pathGet(rest.join("."), model) : model) as { t?: unknown } | null;
27
+ const text = node?.t;
28
+ return typeof text === "string" && text.length ? text : undefined;
29
+ }
30
+ }
@@ -56,6 +56,17 @@ type DictEndpointShape<Endpoint> =
56
56
  : Endpoint extends Record<string, EndpointInfo>
57
57
  ? EndpointCompactShape<Endpoint>
58
58
  : Record<never, never>;
59
+ /**
60
+ * The actions of a store, which is every method on it that dispatches.
61
+ *
62
+ * Derived from the shape rather than from a marker like `ENDPOINT_DICT_SHAPE`, because a store's custom actions
63
+ * are declared on the subclass — after `store()` has already returned, so there is nothing for it to stamp. What
64
+ * makes the shape readable anyway is that `st.do.<action>()` is typed `void`: an action returns nothing, so the
65
+ * void-returning methods are exactly the actions and `get` / `pick` / `slice` fall out on their own.
66
+ */
67
+ type DictStoreShape<Store> = {
68
+ [K in keyof Store as Store[K] extends (...args: never[]) => void | Promise<void> ? K & string : never]: true;
69
+ };
59
70
  type DictArgNames<ArgNames> = ArgNames extends readonly string[] ? ArgNames[number] : never;
60
71
  type DictFilterQuery<Filter> = DictFilterShape<Filter>["query"];
61
72
  type DictFilterSort<Filter> = DictFilterShape<Filter>["sort"];
@@ -140,6 +151,7 @@ export class ModelDictInfo<
140
151
  BaseSignalKey extends string = never,
141
152
  SliceKey extends string = "",
142
153
  EndpointKey extends string = never,
154
+ StoreKey extends string = never,
143
155
  ErrorKey extends string = never,
144
156
  EtcKey extends string = never,
145
157
  > {
@@ -245,6 +257,9 @@ export class ModelDictInfo<
245
257
  endpointDictionary: { [K in EndpointKey]: FunctionTranslation<Languages> } = {} as {
246
258
  [K in EndpointKey]: FunctionTranslation<Languages>;
247
259
  };
260
+ storeDictionary: { [K in StoreKey]: FieldTranslation<Languages> } = {} as {
261
+ [K in StoreKey]: FieldTranslation<Languages>;
262
+ };
248
263
  errorDictionary: { [K in ErrorKey]: Languages } = {} as {
249
264
  [K in ErrorKey]: Languages;
250
265
  };
@@ -278,6 +293,7 @@ export class ModelDictInfo<
278
293
  BaseSignalKey,
279
294
  SliceKey,
280
295
  EndpointKey,
296
+ StoreKey,
281
297
  ErrorKey,
282
298
  EtcKey
283
299
  >;
@@ -302,6 +318,7 @@ export class ModelDictInfo<
302
318
  BaseSignalKey,
303
319
  SliceKey,
304
320
  EndpointKey,
321
+ StoreKey,
305
322
  ErrorKey,
306
323
  EtcKey
307
324
  >;
@@ -327,6 +344,7 @@ export class ModelDictInfo<
327
344
  BaseSignalKey,
328
345
  SliceKey,
329
346
  EndpointKey,
347
+ StoreKey,
330
348
  ErrorKey,
331
349
  EtcKey
332
350
  >;
@@ -351,6 +369,7 @@ export class ModelDictInfo<
351
369
  BaseSignalKey,
352
370
  SliceKey,
353
371
  EndpointKey,
372
+ StoreKey,
354
373
  ErrorKey,
355
374
  EtcKey
356
375
  >;
@@ -375,6 +394,7 @@ export class ModelDictInfo<
375
394
  BaseSignalKey,
376
395
  SliceKey,
377
396
  EndpointKey,
397
+ StoreKey,
378
398
  ErrorKey,
379
399
  EtcKey
380
400
  >;
@@ -400,6 +420,7 @@ export class ModelDictInfo<
400
420
  BaseSignalKey,
401
421
  keyof DictSliceShape<Slice> & string,
402
422
  EndpointKey,
423
+ StoreKey,
403
424
  ErrorKey,
404
425
  EtcKey
405
426
  >;
@@ -425,6 +446,41 @@ export class ModelDictInfo<
425
446
  BaseSignalKey,
426
447
  SliceKey,
427
448
  keyof DictEndpointShape<Endpoint> & string,
449
+ StoreKey,
450
+ ErrorKey,
451
+ EtcKey
452
+ >;
453
+ }
454
+ /**
455
+ * What a store's custom actions are called, in the words a person would use.
456
+ *
457
+ * Optional, and the only stage that is: an action whose name matches the endpoint it wraps already reads as
458
+ * that endpoint's `.desc()`, which is most of them — the house naming rule (`st.do.X` reads the same as
459
+ * `fetch.X`) is what makes that true. This stage is for the rest, where inheriting the endpoint's words would
460
+ * be actively wrong: nine `getSummaryListIn*` actions that all call one endpoint, or `logout` over
461
+ * `signoutUser`, where the store name is the verb a user would say and the endpoint name is the verb the API
462
+ * has. `akan quality scan` names those and no others.
463
+ *
464
+ * Labels only, no `.arg()`. Parameter names are not in a class's type the way an endpoint builder's are, and an
465
+ * action mostly takes none anyway — its data comes from the form state the user already filled in.
466
+ */
467
+ store<Store>(
468
+ translate: (t: (trans: Languages) => FieldTranslation<Languages>) => Partial<{
469
+ [K in keyof DictStoreShape<Store>]: FieldTranslation<Languages>;
470
+ }>,
471
+ ) {
472
+ Object.assign(this.storeDictionary, translate(FieldTranslation.translate));
473
+ return this as unknown as ModelDictInfo<
474
+ Languages,
475
+ ModelKey,
476
+ InsightKey,
477
+ QueryKey,
478
+ SortKey,
479
+ EnumKey,
480
+ BaseSignalKey,
481
+ SliceKey,
482
+ EndpointKey,
483
+ StoreKey | (keyof DictStoreShape<Store> & string),
428
484
  ErrorKey,
429
485
  EtcKey
430
486
  >;
@@ -441,6 +497,7 @@ export class ModelDictInfo<
441
497
  BaseSignalKey,
442
498
  SliceKey,
443
499
  EndpointKey,
500
+ StoreKey,
444
501
  ErrorKey | (keyof ErrorDict & string),
445
502
  EtcKey
446
503
  >;
@@ -457,6 +514,7 @@ export class ModelDictInfo<
457
514
  BaseSignalKey,
458
515
  SliceKey,
459
516
  EndpointKey,
517
+ StoreKey,
460
518
  ErrorKey,
461
519
  EtcKey | (keyof EtcDict & string)
462
520
  >;
@@ -473,6 +531,7 @@ export class ModelDictInfo<
473
531
  GetBaseSignalKey<RefName>,
474
532
  SliceKey,
475
533
  EndpointKey,
534
+ StoreKey,
476
535
  ErrorKey,
477
536
  EtcKey
478
537
  >;
@@ -496,6 +555,7 @@ export class ModelDictInfo<
496
555
  this.#registerBaseSignalToRoot(refName, rootDict);
497
556
  this.#registerSliceToRoot(refName, rootDict);
498
557
  this.#registerEndpointToRoot(refName, rootDict);
558
+ this.#registerStoreToRoot(refName, rootDict);
499
559
  this.#registerErrorToRoot(refName, rootDict);
500
560
  this.#registerModelToRoot(refName, rootDict);
501
561
  this.#registerEtcToRoot(refName, rootDict);
@@ -777,6 +837,23 @@ export class ModelDictInfo<
777
837
  });
778
838
  });
779
839
  }
840
+ /** Under its own `store` node rather than beside `signal`, because the two hold the same key by design. */
841
+ #registerStoreToRoot(refName: string, rootDict: RootDictionary) {
842
+ this.languages.forEach((language) => {
843
+ ensureNode(getRootModelNode(rootDict, language, refName), "store");
844
+ });
845
+ Object.entries(this.storeDictionary as { [key: string]: FieldTranslation<Languages> }).forEach(([key, value]) => {
846
+ value.trans.forEach((t, idx) => {
847
+ ensureNode(ensureNode(getTranslatedRootModelNode(rootDict, this.languages, idx, refName), "store"), key).t = t;
848
+ });
849
+ value.descTrans?.forEach((t, idx) => {
850
+ ensureNode(
851
+ ensureNode(ensureNode(getTranslatedRootModelNode(rootDict, this.languages, idx, refName), "store"), key),
852
+ "desc",
853
+ ).t = t;
854
+ });
855
+ });
856
+ }
780
857
  #registerErrorToRoot(refName: string, rootDict: RootDictionary) {
781
858
  this.languages.forEach((language) => {
782
859
  ensureNode(getRootModelNode(rootDict, language, refName), "error");
@@ -1,3 +1,4 @@
1
+ export * from "./DictionaryLookup";
1
2
  export * from "./dictInfo";
2
3
  export {
3
4
  __Dict_Key__,
@@ -168,6 +168,14 @@ export type ModelTrans<
168
168
  }>;
169
169
  error: { [K in ErrorKey]: Trans };
170
170
  } & { [K in EtcKey]: Trans };
171
+ /**
172
+ * Store action labels, keyed off the resolved `.store()` keys rather than off the store class.
173
+ *
174
+ * The endpoint half needs the class because it reads argument names out of it. A store entry is a label and a
175
+ * description with no arguments to name, so the keys the stage already resolved are the whole of it — which is
176
+ * also why a generated `dict.ts` passes no store type at all.
177
+ */
178
+ export type StoreTranslatorKey<T extends string, StoreKey extends string> = `${T}.store.${StoreKey}${"" | ".desc"}`;
171
179
  export type ModelTranslatorKey<T extends string, Model, Insight, Filter, Slice, Endpoint, EtcKey extends string> =
172
180
  | `${T}.modelName`
173
181
  | `${T}.modelDesc`
@@ -234,11 +242,14 @@ export const registerModelTrans = <
234
242
  infer _BaseSignalKey,
235
243
  infer _SliceKey,
236
244
  infer _EndpointKey,
245
+ infer StoreKey,
237
246
  infer ErrorKey,
238
247
  infer EtcKey
239
248
  >
240
249
  ? DictModule<
241
- ModelTranslatorKey<RefName, Model, Insight, Filter, Slice, Endpoint, EtcKey> | EnumTranslatorKey<EnumKey>,
250
+ | ModelTranslatorKey<RefName, Model, Insight, Filter, Slice, Endpoint, EtcKey>
251
+ | EnumTranslatorKey<EnumKey>
252
+ | StoreTranslatorKey<RefName, StoreKey>,
242
253
  `${RefName}.error.${ErrorKey}`
243
254
  >
244
255
  : never => {
@@ -252,11 +263,14 @@ export const registerModelTrans = <
252
263
  infer _BaseSignalKey,
253
264
  infer _SliceKey,
254
265
  infer _EndpointKey,
266
+ infer StoreKey,
255
267
  infer ErrorKey,
256
268
  infer EtcKey
257
269
  >
258
270
  ? DictModule<
259
- ModelTranslatorKey<RefName, Model, Insight, Filter, Slice, Endpoint, EtcKey> | EnumTranslatorKey<EnumKey>,
271
+ | ModelTranslatorKey<RefName, Model, Insight, Filter, Slice, Endpoint, EtcKey>
272
+ | EnumTranslatorKey<EnumKey>
273
+ | StoreTranslatorKey<RefName, StoreKey>,
260
274
  `${RefName}.error.${ErrorKey}`
261
275
  >
262
276
  : never;
package/document/index.ts CHANGED
@@ -7,5 +7,6 @@ export * from "./documentSchema";
7
7
  export * from "./filterMeta";
8
8
  export * from "./into";
9
9
  export * from "./loaderInfo";
10
+ export * from "./noDocumentError";
10
11
  export * from "./schema";
11
12
  export * from "./types";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * A `pick` or a `get` that matched nothing. Typed so a caller can tell "you asked for something that is not
3
+ * there" from "we broke" — an agent-facing transport in particular must not log a stack and answer "the server
4
+ * failed" every time a model guesses an id.
5
+ *
6
+ * The message is unchanged from the bare `Error` this replaces, because callers already match on it, and the
7
+ * `statusCode` does not reach the HTTP layer: `isExceptionLike` wants a `toJSON` too, so a missing document
8
+ * stays a 500 there until someone decides framework-wide that it should be a 404.
9
+ */
10
+ export class NoDocumentError extends Error {
11
+ readonly statusCode = 404;
12
+ }
@@ -246,6 +246,8 @@ export class FetchClient {
246
246
  const parseReturn = this.#makeReturnParser(endpoint.returns);
247
247
  const { bodyArgs, uploadArgs } = FetchClient.classifyHttpArgs(endpoint.args);
248
248
  switch (endpoint.type) {
249
+
250
+ case "prompt":
249
251
  case "query": {
250
252
  const queryFn = async (...argData: unknown[]) => {
251
253
  const args = argData.slice(0, argLength);
@@ -286,6 +288,7 @@ export class FetchClient {
286
288
  }
287
289
  #registerEndpoint(key: string, endpoint: SerializedEndpoint, prefix?: string) {
288
290
  switch (endpoint.type) {
291
+ case "prompt":
289
292
  case "query": {
290
293
  this.#setHandlerFactory(key, () => this.#makeHttpFn(key, endpoint, prefix));
291
294
  return;
@@ -7,6 +7,7 @@ import type {
7
7
  EndpInfoReqType,
8
8
  EndpointCls,
9
9
  EndpointInfo,
10
+ PromptMessage,
10
11
  SlceCnstFull,
11
12
  SlceCnstInsight,
12
13
  SlceCnstLight,
@@ -40,6 +41,9 @@ type QueryOrMutationFetchFn<E, SlceCls extends SliceCls | never> = (
40
41
  ...args: [...EndpInfoArgs<E>, fetchPolicy?: FetchPolicy]
41
42
  ) => Promise<EndpInfoReturns<E, SlceCls>>;
42
43
 
44
+ /** Typed off `PromptResult` rather than the endpoint's return ref, which is the `Any` carrier a prompt rides on. */
45
+ type PromptFetchFn<E> = (...args: [...EndpInfoArgs<E>, fetchPolicy?: FetchPolicy]) => Promise<PromptMessage[]>;
46
+
43
47
  type MessageEmitFn<E> = (...args: EndpInfoArgs<E>) => void;
44
48
 
45
49
  type MessageListenFn<E, SlceCls extends SliceCls | never> = (
@@ -58,12 +62,14 @@ type PubsubSubscribeFn<E, SlceCls extends SliceCls | never> = (
58
62
  type PrimaryFetchFn<E, SlceCls extends SliceCls | never> =
59
63
  EndpInfoReqType<E> extends "query" | "mutation"
60
64
  ? QueryOrMutationFetchFn<E, SlceCls>
61
- : EndpInfoReqType<E> extends "message"
62
- ? MessageEmitFn<E>
63
- : never;
65
+ : EndpInfoReqType<E> extends "prompt"
66
+ ? PromptFetchFn<E>
67
+ : EndpInfoReqType<E> extends "message"
68
+ ? MessageEmitFn<E>
69
+ : never;
64
70
 
65
71
  type PrimaryFetchType<EInfoObj extends { [key: string]: EndpointInfo }, SlceCls extends SliceCls | never> = {
66
- [K in keyof EInfoObj as EndpInfoReqType<EInfoObj[K]> extends "query" | "mutation" | "message"
72
+ [K in keyof EInfoObj as EndpInfoReqType<EInfoObj[K]> extends "query" | "mutation" | "prompt" | "message"
67
73
  ? K
68
74
  : never]: PrimaryFetchFn<EInfoObj[K], SlceCls>;
69
75
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-alpha.10",
3
+ "version": "3.0.0-alpha.11",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
package/server/akanApp.ts CHANGED
@@ -6,6 +6,7 @@ import { Logger } from "akanjs/common";
6
6
  import type { AkanChildRole, AkanChildStatus, AkanIpcMessage, AkanMetricsReport, AkanUpstream } from "akanjs/service";
7
7
  import { isTraceEnabled } from "akanjs/signal";
8
8
  import { makeAkanChildProxyHeaders } from "./akanAppHeaders";
9
+ import type { McpServerOption } from "./akanServer";
9
10
  import type { BuilderCsrReq, BuilderCsrRes, BuilderMessage, BuilderReq, BuilderRes } from "./artifact";
10
11
  import { resolveEncodedSidecar } from "./assetEncoding";
11
12
  import { isPortInUseError } from "./lifecycle/portInUse";
@@ -54,6 +55,11 @@ export interface AkanAppOptions {
54
55
  port?: number;
55
56
  wsBasePort?: number;
56
57
  openapi?: boolean;
58
+ /**
59
+ * MCP server config for the children, which are the processes that actually mount `/mcp`. Declared here
60
+ * because `server.ts` is generated and takes no options, so `main.ts` is the only app-authored place left.
61
+ */
62
+ mcp?: boolean | McpServerOption;
57
63
  }
58
64
 
59
65
  interface AkanReplicaConfig {
@@ -84,6 +90,7 @@ export class AkanApp {
84
90
  readonly #port: number;
85
91
  readonly #wsBasePort: number;
86
92
  readonly #openapi?: boolean;
93
+ readonly #mcpEnv: Record<string, string>;
87
94
  readonly #children = new Map<number, ChildState>();
88
95
  readonly #roomChildren = new Map<string, Set<number>>();
89
96
  readonly #childRooms = new Map<number, Set<string>>();
@@ -125,6 +132,30 @@ export class AkanApp {
125
132
  this.#port = Number(resolvedOptions.port ?? process.env.PORT ?? 8282);
126
133
  this.#wsBasePort = Number(resolvedOptions.wsBasePort ?? process.env.AKAN_WS_BASE_PORT ?? this.#port + 10_000);
127
134
  this.#openapi = resolvedOptions.openapi;
135
+ this.#mcpEnv = AkanApp.#toMcpEnv(resolvedOptions.mcp);
136
+ }
137
+
138
+ /**
139
+ * `McpServerOption`, spelled as the environment a child is spawned with — the only channel the gateway has to
140
+ * a process it starts, and the same one a deployment configures MCP through when it sets nothing in code.
141
+ */
142
+ static #toMcpEnv(mcp: boolean | McpServerOption | undefined): Record<string, string> {
143
+ if (mcp === undefined) return {};
144
+ if (typeof mcp === "boolean") return { AKAN_MCP: String(mcp) };
145
+ const { enabled, readOnly, path: mcpPath, version, instructions, allowedOrigins, pageSize, language, auth } = mcp;
146
+ return {
147
+ AKAN_MCP: String(enabled ?? true),
148
+ ...(readOnly === undefined ? {} : { AKAN_MCP_READONLY: String(readOnly) }),
149
+ ...(mcpPath ? { AKAN_MCP_PATH: mcpPath } : {}),
150
+ ...(version ? { AKAN_MCP_VERSION: version } : {}),
151
+ ...(instructions ? { AKAN_MCP_INSTRUCTIONS: instructions } : {}),
152
+ ...(allowedOrigins?.length ? { AKAN_MCP_ALLOWED_ORIGINS: allowedOrigins.join(",") } : {}),
153
+ ...(pageSize === undefined ? {} : { AKAN_MCP_PAGE_SIZE: String(pageSize) }),
154
+ ...(language ? { AKAN_MCP_LANGUAGE: language } : {}),
155
+ ...(auth?.authorizationServers?.length ? { AKAN_MCP_AUTH_SERVERS: auth.authorizationServers.join(",") } : {}),
156
+ ...(auth?.scopes?.length ? { AKAN_MCP_SCOPES: auth.scopes.join(",") } : {}),
157
+ ...(auth?.resource ? { AKAN_MCP_RESOURCE: auth.resource } : {}),
158
+ };
128
159
  }
129
160
 
130
161
  static #resolveServerPath(serverPath: string) {
@@ -289,6 +320,7 @@ export class AkanApp {
289
320
  AKAN_CHILD_SOCKET: upstream.http.socketPath,
290
321
  AKAN_CHILD_WS_PORT: upstream.ws ? String(upstream.ws.port) : "",
291
322
  ...(this.#openapi === undefined ? {} : { AKAN_OPENAPI: this.#openapi ? "true" : "false" }),
323
+ ...this.#mcpEnv,
292
324
  },
293
325
  ipc: (message) => this.#handleMessage(idx, message as AkanIpcMessage, proc),
294
326
  stdout: "pipe",