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.
- package/common/index.ts +10 -0
- package/common/mcpExposure.ts +106 -0
- package/constant/immerify.ts +1 -1
- package/dictionary/DictionaryLookup.ts +30 -0
- package/dictionary/dictInfo.ts +77 -0
- package/dictionary/index.ts +1 -0
- package/dictionary/locale.ts +16 -2
- package/document/index.ts +1 -0
- package/document/noDocumentError.ts +12 -0
- package/fetch/client/fetchClient.ts +3 -0
- package/fetch/fetchType/endpointFetch.type.ts +10 -4
- package/package.json +1 -1
- package/server/akanApp.ts +32 -0
- package/server/akanServer.ts +154 -5
- package/server/devtools/signalSerializer.ts +2 -2
- package/server/devtools/types.ts +2 -2
- package/server/mcp/McpAuth.ts +177 -0
- package/server/mcp/McpDispatcher.ts +245 -0
- package/server/mcp/McpEventStream.ts +76 -0
- package/server/mcp/McpExecutionContext.ts +105 -0
- package/server/mcp/McpRouter.ts +604 -0
- package/server/mcp/index.ts +5 -0
- package/server/resolver/database.resolver.ts +5 -4
- package/server/resolver/signal.resolver.ts +10 -0
- package/server/subRouteIndexDocument.tsx +61 -0
- package/server/systemPageDocument.tsx +2 -2
- package/server/systemPages.tsx +49 -16
- package/server/webRouter.ts +27 -1
- package/service/predefinedAdaptor/database.adaptor.ts +4 -3
- package/signal/agent/AgentCatalogue.ts +165 -0
- package/signal/agent/index.ts +1 -0
- package/signal/endpointInfo.ts +38 -2
- package/signal/guard.ts +11 -1
- package/signal/guards.ts +3 -1
- package/signal/index.ts +3 -0
- package/signal/mcp/McpDocument.ts +322 -0
- package/signal/mcp/McpProgress.ts +106 -0
- package/signal/mcp/McpUriTemplate.ts +85 -0
- package/signal/mcp/Msg.ts +396 -0
- package/signal/mcp/index.ts +5 -0
- package/signal/mcp/mcpProtocol.ts +120 -0
- package/signal/openapi/openapi.ts +32 -171
- package/signal/schema/JsonSchemaBuilder.ts +211 -0
- package/signal/schema/index.ts +1 -0
- package/signal/serializer/fetch.serializer.ts +3 -0
- package/signal/signalContext.ts +45 -5
- package/signal/slice.ts +15 -2
- package/signal/types.ts +33 -1
- package/store/action.ts +1 -1
- package/store/storeInstance.ts +1 -1
- package/types/common/index.d.ts +1 -0
- package/types/common/mcpExposure.d.ts +69 -0
- package/types/dictionary/DictionaryLookup.d.ts +14 -0
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictInfo.d.ts +41 -11
- package/types/dictionary/dictionary.d.ts +8 -8
- package/types/dictionary/index.d.ts +1 -0
- package/types/dictionary/locale.d.ts +9 -1
- package/types/document/index.d.ts +1 -0
- package/types/document/noDocumentError.d.ts +12 -0
- package/types/fetch/fetchType/endpointFetch.type.d.ts +5 -3
- package/types/server/akanApp.d.ts +6 -0
- package/types/server/akanServer.d.ts +43 -0
- package/types/server/devtools/types.d.ts +2 -2
- package/types/server/mcp/McpAuth.d.ts +62 -0
- package/types/server/mcp/McpDispatcher.d.ts +54 -0
- package/types/server/mcp/McpEventStream.d.ts +18 -0
- package/types/server/mcp/McpExecutionContext.d.ts +44 -0
- package/types/server/mcp/McpRouter.d.ts +68 -0
- package/types/server/mcp/index.d.ts +5 -0
- package/types/server/mcp.d.ts +1 -0
- package/types/server/subRouteIndexDocument.d.ts +8 -0
- package/types/server/systemPageDocument.d.ts +1 -0
- package/types/server/systemPages.d.ts +5 -0
- package/types/signal/agent/AgentCatalogue.d.ts +100 -0
- package/types/signal/agent/index.d.ts +1 -0
- package/types/signal/agent.d.ts +1 -0
- package/types/signal/endpointInfo.d.ts +21 -3
- package/types/signal/guard.d.ts +10 -0
- package/types/signal/guards.d.ts +3 -1
- package/types/signal/index.d.ts +3 -0
- package/types/signal/mcp/McpDocument.d.ts +76 -0
- package/types/signal/mcp/McpProgress.d.ts +40 -0
- package/types/signal/mcp/McpUriTemplate.d.ts +26 -0
- package/types/signal/mcp/Msg.d.ts +144 -0
- package/types/signal/mcp/index.d.ts +5 -0
- package/types/signal/mcp/mcpProtocol.d.ts +107 -0
- package/types/signal/mcp.d.ts +1 -0
- package/types/signal/openapi/openapi.d.ts +3 -3
- package/types/signal/schema/JsonSchemaBuilder.d.ts +47 -0
- package/types/signal/schema/index.d.ts +1 -0
- package/types/signal/schema.d.ts +1 -0
- package/types/signal/signalContext.d.ts +16 -1
- package/types/signal/slice.d.ts +3 -1
- package/types/signal/types.d.ts +31 -1
- package/types/ui/Badge.d.ts +1 -1
- package/types/ui/Button.d.ts +1 -1
- package/types/ui/Signal/RestApi.d.ts +3 -2
- package/types/ui/Signal/style.d.ts +3 -0
- package/ui/Badge.tsx +1 -1
- package/ui/Button.tsx +1 -1
- package/ui/DatePicker.tsx +1 -1
- package/ui/Signal/RestApi.tsx +57 -17
- package/ui/Signal/WebSocket.tsx +1 -1
- 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
|
+
};
|
package/constant/immerify.ts
CHANGED
|
@@ -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 &&
|
|
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
|
+
}
|
package/dictionary/dictInfo.ts
CHANGED
|
@@ -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");
|
package/dictionary/index.ts
CHANGED
package/dictionary/locale.ts
CHANGED
|
@@ -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>
|
|
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>
|
|
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
|
@@ -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 "
|
|
62
|
-
?
|
|
63
|
-
:
|
|
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
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",
|