akanjs 3.0.0-alpha.68 → 3.0.0-alpha.69
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/mcpExposure.ts +14 -1
- package/dictionary/base.dictionary.ts +18 -0
- package/fetch/client/fetchClient.ts +12 -0
- package/fetch/client/httpClient.ts +77 -9
- package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/package.json +2 -2
- package/server/akanApp.ts +22 -9
- package/server/mcp/McpRouter.ts +28 -2
- package/signal/mcp/McpDocument.ts +43 -3
- package/signal/mcp/McpUriTemplate.ts +1 -6
- package/signal/serializer/fetch.serializer.ts +14 -0
- package/signal/slice.ts +32 -1
- package/signal/types.ts +31 -0
- package/types/common/mcpExposure.d.ts +5 -1
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +3 -3
- package/types/server/mcp/McpRouter.d.ts +2 -0
- package/types/signal/mcp/McpDocument.d.ts +19 -0
- package/types/signal/mcp/McpUriTemplate.d.ts +0 -1
- package/types/signal/slice.d.ts +25 -0
- package/types/signal/types.d.ts +30 -0
- package/types/ui/System/Common.d.ts +1 -1
- package/ui/Signal/Doc.tsx +1 -1
- package/ui/Signal/RestApi.tsx +1 -1
- package/ui/System/Common.tsx +1 -1
package/common/mcpExposure.ts
CHANGED
|
@@ -16,9 +16,13 @@ export interface McpExposureEndpoint {
|
|
|
16
16
|
args: { name: string; refName: string; type: string; arrDepth?: number; nullable?: boolean }[];
|
|
17
17
|
guards?: string[];
|
|
18
18
|
fileUpload?: boolean;
|
|
19
|
+
mcp?: boolean;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export interface McpExposureOption {
|
|
23
|
+
/** The model the endpoint belongs to and the name it is published under: one rule reads the key, not the shape. */
|
|
24
|
+
refName: string;
|
|
25
|
+
key: string;
|
|
22
26
|
/**
|
|
23
27
|
* The read-only deployment valve, which is server configuration. The browser explorer cannot know it and so
|
|
24
28
|
* badges what the code decided; the boot log is where a read-only deployment says what it dropped.
|
|
@@ -56,10 +60,19 @@ export const mcpHintsOf = (key: string, endpoint: { type: string }) => {
|
|
|
56
60
|
};
|
|
57
61
|
|
|
58
62
|
/** The sentence explaining why this endpoint is not in the catalogue, or `null` when it is. */
|
|
59
|
-
export const mcpRefusalOf = (
|
|
63
|
+
export const mcpRefusalOf = (
|
|
64
|
+
endpoint: McpExposureEndpoint,
|
|
65
|
+
{ refName, key, readOnly }: McpExposureOption,
|
|
66
|
+
): string | null => {
|
|
67
|
+
|
|
68
|
+
if (endpoint.mcp === false)
|
|
69
|
+
return "it declares `mcp: false`, so it is deliberately off the agent shelf. HTTP still serves it.";
|
|
60
70
|
|
|
61
71
|
if (!endpoint.guards?.length)
|
|
62
72
|
return "it declares no guards, and exposure follows them — write `guards: [Public]` if anonymous access is the intent.";
|
|
73
|
+
|
|
74
|
+
if (key === `light${capitalize(refName)}`)
|
|
75
|
+
return `it reads the same document as \`${refName}\` in a smaller shape — call \`${refName}\` instead.`;
|
|
63
76
|
if (endpoint.type === "prompt") return mcpPromptRefusalOf(endpoint);
|
|
64
77
|
if (endpoint.type === "pubsub" || endpoint.type === "message")
|
|
65
78
|
return `\`${endpoint.type}\` rides the websocket, and its internal arguments read a socket an MCP request does not have.`;
|
|
@@ -27,6 +27,24 @@ export const baseDictionary = serviceDictionary(["en", "ko"])
|
|
|
27
27
|
})),
|
|
28
28
|
pubsubPing: fn(["Pubsub Ping", "Pubsub Ping"]),
|
|
29
29
|
}))
|
|
30
|
+
.error({
|
|
31
|
+
serverUnreachable: [
|
|
32
|
+
"Cannot reach the server. Check your connection and try again.",
|
|
33
|
+
"서버에 연결할 수 없습니다. 네트워크 상태를 확인한 뒤 다시 시도해주세요.",
|
|
34
|
+
],
|
|
35
|
+
serverUnavailable: [
|
|
36
|
+
"The server is restarting. Please try again in a moment.",
|
|
37
|
+
"서버가 재시작 중입니다. 잠시 후 다시 시도해주세요.",
|
|
38
|
+
],
|
|
39
|
+
gatewayTimeout: [
|
|
40
|
+
"The server took too long to answer. Please try again in a moment.",
|
|
41
|
+
"서버 응답이 지연되고 있습니다. 잠시 후 다시 시도해주세요.",
|
|
42
|
+
],
|
|
43
|
+
unexpectedResponse: [
|
|
44
|
+
"The server returned an unexpected response ({status}).",
|
|
45
|
+
"서버가 예상하지 못한 응답을 보냈습니다. ({status})",
|
|
46
|
+
],
|
|
47
|
+
})
|
|
30
48
|
.translate({
|
|
31
49
|
somethingWrong: ["Something's wrong!", "문제가 생겼어요!"],
|
|
32
50
|
connecting: ["Connecting...", "연결 중..."],
|
|
@@ -443,6 +443,9 @@ export class FetchClient {
|
|
|
443
443
|
const createGuards = signal.createGuards ?? signal.cruGuards;
|
|
444
444
|
const updateGuards = signal.updateGuards ?? signal.cruGuards;
|
|
445
445
|
const removeGuards = signal.removeGuards ?? signal.cruGuards;
|
|
446
|
+
|
|
447
|
+
const mcp = (verb: keyof NonNullable<SerializedSignal["mcp"]>) =>
|
|
448
|
+
signal.mcp?.[verb] === false ? { mcp: false as const } : {};
|
|
446
449
|
const endpoint: { [key: string]: SerializedEndpoint } = {};
|
|
447
450
|
if (signal.getGuards) {
|
|
448
451
|
endpoint[names.model] = {
|
|
@@ -450,12 +453,14 @@ export class FetchClient {
|
|
|
450
453
|
args: [{ type: "param", name: names.modelId, refName: "ID" }],
|
|
451
454
|
returns: { refName, modelType: "full" },
|
|
452
455
|
guards: signal.getGuards,
|
|
456
|
+
...mcp("get"),
|
|
453
457
|
};
|
|
454
458
|
endpoint[names.lightModel] = {
|
|
455
459
|
type: "query",
|
|
456
460
|
args: [{ type: "param", name: names.modelId, refName: "ID" }],
|
|
457
461
|
returns: { refName, modelType: "light" },
|
|
458
462
|
guards: signal.getGuards,
|
|
463
|
+
...mcp("get"),
|
|
459
464
|
};
|
|
460
465
|
}
|
|
461
466
|
if (createGuards) {
|
|
@@ -464,6 +469,7 @@ export class FetchClient {
|
|
|
464
469
|
args: [{ type: "body", name: "data", refName, modelType: "input" }],
|
|
465
470
|
returns: { refName, modelType: "full" },
|
|
466
471
|
guards: createGuards,
|
|
472
|
+
...mcp("create"),
|
|
467
473
|
};
|
|
468
474
|
}
|
|
469
475
|
if (updateGuards) {
|
|
@@ -475,6 +481,7 @@ export class FetchClient {
|
|
|
475
481
|
],
|
|
476
482
|
returns: { refName, modelType: "full" },
|
|
477
483
|
guards: updateGuards,
|
|
484
|
+
...mcp("update"),
|
|
478
485
|
};
|
|
479
486
|
}
|
|
480
487
|
if (removeGuards) {
|
|
@@ -483,6 +490,7 @@ export class FetchClient {
|
|
|
483
490
|
args: [{ type: "param", name: names.modelId, refName: "ID" }],
|
|
484
491
|
returns: { refName, modelType: "full" },
|
|
485
492
|
guards: removeGuards,
|
|
493
|
+
...mcp("remove"),
|
|
486
494
|
};
|
|
487
495
|
}
|
|
488
496
|
return endpoint;
|
|
@@ -602,18 +610,22 @@ export class FetchClient {
|
|
|
602
610
|
list: `${refName}List${capSuffix}`,
|
|
603
611
|
insight: `${refName}Insight${capSuffix}`,
|
|
604
612
|
};
|
|
613
|
+
|
|
614
|
+
const mcp = slice.mcp === false ? { mcp: false as const } : {};
|
|
605
615
|
const endpoint: { [key: string]: SerializedEndpoint } = {
|
|
606
616
|
[names.list]: {
|
|
607
617
|
type: "query",
|
|
608
618
|
args: [...slice.args, ...FetchClient.paginationArgs],
|
|
609
619
|
returns: { refName, modelType: "light", arrDepth: 1 },
|
|
610
620
|
guards: slice.guards,
|
|
621
|
+
...mcp,
|
|
611
622
|
},
|
|
612
623
|
[names.insight]: {
|
|
613
624
|
type: "query",
|
|
614
625
|
args: [...slice.args],
|
|
615
626
|
returns: { refName, modelType: "insight" },
|
|
616
627
|
guards: slice.guards,
|
|
628
|
+
...mcp,
|
|
617
629
|
},
|
|
618
630
|
};
|
|
619
631
|
return endpoint;
|
|
@@ -27,6 +27,19 @@ interface FetchOptions {
|
|
|
27
27
|
baseUrl?: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
const jsonContentType = /^application\/(?:[\w.+-]+\+)?json\b/i;
|
|
31
|
+
|
|
32
|
+
const transportErrorKeyMap = {
|
|
33
|
+
408: "base.error.gatewayTimeout",
|
|
34
|
+
502: "base.error.serverUnavailable",
|
|
35
|
+
503: "base.error.serverUnavailable",
|
|
36
|
+
504: "base.error.gatewayTimeout",
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
const serverUnreachableKey = "base.error.serverUnreachable";
|
|
40
|
+
const unexpectedResponseKey = "base.error.unexpectedResponse";
|
|
41
|
+
const transportDetailLimit = 200;
|
|
42
|
+
|
|
30
43
|
export class HttpClient {
|
|
31
44
|
readonly baseUrl: string;
|
|
32
45
|
constructor(
|
|
@@ -42,11 +55,17 @@ export class HttpClient {
|
|
|
42
55
|
#resolveBaseUrl(baseUrl?: string) {
|
|
43
56
|
return (baseUrl ?? this.baseUrl).replace(/\/$/, "");
|
|
44
57
|
}
|
|
58
|
+
#resolveUrl(url: string, options: FetchOptions) {
|
|
59
|
+
return `${this.#resolveBaseUrl(options.baseUrl)}${url}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static #makeHeaders(headers: Record<string, string>, options: FetchOptions) {
|
|
63
|
+
return { Accept: "application/json", ...headers, ...options.headers };
|
|
64
|
+
}
|
|
45
65
|
async get<Returns = unknown>(url: string, options: FetchOptions = {}): Promise<Returns> {
|
|
46
|
-
|
|
47
|
-
headers: { "Content-Type": "application/json",
|
|
66
|
+
return await this.#request<Returns>(this.#resolveUrl(url, options), {
|
|
67
|
+
headers: HttpClient.#makeHeaders({ "Content-Type": "application/json" }, options),
|
|
48
68
|
});
|
|
49
|
-
return await this.#readJsonResponse<Returns>(res);
|
|
50
69
|
}
|
|
51
70
|
#makeReqContent(data: FormData | Record<string, unknown>): { body: BodyInit; headers: Record<string, string> } {
|
|
52
71
|
|
|
@@ -60,12 +79,11 @@ export class HttpClient {
|
|
|
60
79
|
options: FetchOptions = {},
|
|
61
80
|
): Promise<Returns> {
|
|
62
81
|
const { body, headers } = this.#makeReqContent(data);
|
|
63
|
-
|
|
82
|
+
return await this.#request<Returns>(this.#resolveUrl(url, options), {
|
|
64
83
|
method,
|
|
65
84
|
body,
|
|
66
|
-
headers:
|
|
85
|
+
headers: HttpClient.#makeHeaders(headers, options),
|
|
67
86
|
});
|
|
68
|
-
return await this.#readJsonResponse<Returns>(res);
|
|
69
87
|
}
|
|
70
88
|
async put<Returns = unknown>(
|
|
71
89
|
url: string,
|
|
@@ -82,19 +100,69 @@ export class HttpClient {
|
|
|
82
100
|
return await this.send<Returns>("POST", url, data, options);
|
|
83
101
|
}
|
|
84
102
|
async delete<Returns = unknown>(url: string, options: FetchOptions = {}): Promise<Returns> {
|
|
85
|
-
|
|
103
|
+
return await this.#request<Returns>(this.#resolveUrl(url, options), {
|
|
86
104
|
method: "DELETE",
|
|
87
|
-
headers: { "Content-Type": "application/json",
|
|
105
|
+
headers: HttpClient.#makeHeaders({ "Content-Type": "application/json" }, options),
|
|
88
106
|
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async #request<Returns>(url: string, init: RequestInit): Promise<Returns> {
|
|
110
|
+
const res = await this.#fetch(url, init);
|
|
89
111
|
return await this.#readJsonResponse<Returns>(res);
|
|
90
112
|
}
|
|
91
113
|
|
|
114
|
+
/**
|
|
115
|
+
* `fetch` rejects only when no response arrived at all — a refused connection, a DNS failure, a socket
|
|
116
|
+
* dropped mid-flight. That is the server being unreachable, not an error this API reported, so it is
|
|
117
|
+
* restored as one rather than surfacing the runtime's own `TypeError: Failed to fetch`.
|
|
118
|
+
*/
|
|
119
|
+
async #fetch(url: string, init: RequestInit) {
|
|
120
|
+
try {
|
|
121
|
+
return await fetch(url, init);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error instanceof Error && error.name === "AbortError") throw error;
|
|
124
|
+
throw this.#restoreError({ error: serverUnreachableKey, details: String(error) }, 503);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
92
128
|
async #readJsonResponse<Returns>(res: Response): Promise<Returns> {
|
|
93
|
-
const body = await res
|
|
129
|
+
const body = await this.#readBody(res);
|
|
94
130
|
if (res.ok) return body as Returns;
|
|
95
131
|
throw this.#restoreError(body, res.status);
|
|
96
132
|
}
|
|
97
133
|
|
|
134
|
+
/**
|
|
135
|
+
* A proxy answers a restarting upstream with a page of its own — nginx's `504 Gateway Time-out` HTML,
|
|
136
|
+
* the federation gateway's plain-text 503. That body is not this API's, so parsing it would surface the
|
|
137
|
+
* parser's complaint (`Unexpected token '<'`) instead of the fact that the server is down.
|
|
138
|
+
*/
|
|
139
|
+
async #readBody(res: Response) {
|
|
140
|
+
if (jsonContentType.test(res.headers.get("content-type") ?? "")) {
|
|
141
|
+
try {
|
|
142
|
+
return (await res.json()) as unknown;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
throw this.#transportError(res.status, String(error));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const raw = await res.text();
|
|
148
|
+
const parsed = HttpClient.#parseJson(raw);
|
|
149
|
+
if (parsed === undefined) throw this.#transportError(res.status, raw);
|
|
150
|
+
return parsed;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
static #parseJson(raw: string): unknown {
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(raw) as unknown;
|
|
156
|
+
} catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#transportError(status: number, detail: string): RestoredError {
|
|
162
|
+
const error = transportErrorKeyMap[status as keyof typeof transportErrorKeyMap] ?? unexpectedResponseKey;
|
|
163
|
+
return this.#restoreError({ error, data: { status }, details: detail.slice(0, transportDetailLimit) }, status);
|
|
164
|
+
}
|
|
165
|
+
|
|
98
166
|
#restoreError(body: unknown, fallbackStatusCode: number): RestoredError {
|
|
99
167
|
const payload =
|
|
100
168
|
body && typeof body === "object" && "error" in body
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/server/akanApp.ts
CHANGED
|
@@ -780,7 +780,7 @@ export class AkanApp {
|
|
|
780
780
|
`Child ${child.idx}/${child.role} upstream is unreachable (${child.upstream.socketPath}); restarting`,
|
|
781
781
|
);
|
|
782
782
|
this.#scheduleChildRestart(child, child.proc, "upstream-open-failed");
|
|
783
|
-
return
|
|
783
|
+
return AkanApp.#unavailableResponse(req, "Federation child upstream is unreachable; restarting");
|
|
784
784
|
}
|
|
785
785
|
throw error;
|
|
786
786
|
} finally {
|
|
@@ -802,13 +802,13 @@ export class AkanApp {
|
|
|
802
802
|
*/
|
|
803
803
|
async #pickReadyFederationChild(req: Request): Promise<ChildState | null> {
|
|
804
804
|
const ready = this.#pickFederationChild();
|
|
805
|
-
if (ready) return ready;
|
|
805
|
+
if (ready?.upstream) return ready;
|
|
806
806
|
const deadline = performance.now() + this.#upstreamWaitMs;
|
|
807
807
|
while (performance.now() < deadline) {
|
|
808
808
|
if (this.#stopping || req.signal.aborted || this.#getCrashLoopDetail()) return null;
|
|
809
809
|
await Bun.sleep(50);
|
|
810
810
|
const child = this.#pickFederationChild();
|
|
811
|
-
if (child) return child;
|
|
811
|
+
if (child?.upstream) return child;
|
|
812
812
|
}
|
|
813
813
|
return null;
|
|
814
814
|
}
|
|
@@ -844,13 +844,26 @@ export class AkanApp {
|
|
|
844
844
|
text: "No healthy federation child is ready",
|
|
845
845
|
note: "A replica is booting or restarting — this page reloads itself as soon as it answers.",
|
|
846
846
|
};
|
|
847
|
-
if (
|
|
848
|
-
return new Response(page
|
|
847
|
+
if (req.headers.get("accept")?.includes("text/html")) {
|
|
848
|
+
return new Response(AkanApp.#statusPageHtml(page), {
|
|
849
|
+
status: 503,
|
|
850
|
+
headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" },
|
|
851
|
+
});
|
|
849
852
|
}
|
|
850
|
-
return
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
853
|
+
return AkanApp.#unavailableResponse(req, page.text);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* An API caller asks for JSON, so it is answered with the payload `HttpClient` restores into an `Err` —
|
|
858
|
+
* a bare-text 503 reaches the browser as a JSON parse error naming the page body instead of the outage.
|
|
859
|
+
*/
|
|
860
|
+
static #unavailableResponse(req: Request, detail: string): Response {
|
|
861
|
+
const headers = { "cache-control": "no-store" };
|
|
862
|
+
if (!req.headers.get("accept")?.includes("application/json")) return new Response(detail, { status: 503, headers });
|
|
863
|
+
return Response.json(
|
|
864
|
+
{ error: "base.error.serverUnavailable", statusCode: 503, data: { status: 503 }, details: detail },
|
|
865
|
+
{ status: 503, headers },
|
|
866
|
+
);
|
|
854
867
|
}
|
|
855
868
|
|
|
856
869
|
static #statusPageHtml({ heading, detail, note }: { heading: string; detail: string; note: string }) {
|
package/server/mcp/McpRouter.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type McpExposedEndpoint,
|
|
15
15
|
type McpJsonRpcRequest,
|
|
16
16
|
McpProgress,
|
|
17
|
+
type McpSignalCost,
|
|
17
18
|
type McpToolResult,
|
|
18
19
|
} from "../../signal/mcp";
|
|
19
20
|
import type { MiddlewareCls } from "../../signal/middleware";
|
|
@@ -94,6 +95,8 @@ const discoverCache: McpCacheHint = { ttlMs: 3_600_000, cacheScope: "public" };
|
|
|
94
95
|
*/
|
|
95
96
|
export class McpRouter {
|
|
96
97
|
static readonly logger = new Logger("McpRouter");
|
|
98
|
+
/** Past this a listing is a meaningful slice of a model's window, which is where it becomes worth saying so. */
|
|
99
|
+
static readonly listingWarnBytes = 100 * 1024;
|
|
97
100
|
|
|
98
101
|
readonly #props: McpRouterProps;
|
|
99
102
|
readonly #dispatcher: McpDispatcher;
|
|
@@ -138,9 +141,18 @@ export class McpRouter {
|
|
|
138
141
|
*/
|
|
139
142
|
report() {
|
|
140
143
|
try {
|
|
141
|
-
const
|
|
144
|
+
const document = this.#getDocument();
|
|
145
|
+
const { tools, prompts, resourceTemplates, refusals, undescribed } = document;
|
|
146
|
+
const cost = document.listingCost;
|
|
142
147
|
const counts = `tools=${tools.length} prompts=${prompts.length} resourceTemplates=${resourceTemplates.length}`;
|
|
143
|
-
|
|
148
|
+
const readOnly = this.#props.readOnly ? " (read-only deployment)" : "";
|
|
149
|
+
McpRouter.logger.debug(`MCP catalogue: ${counts}${readOnly} · listing ${McpRouter.#kb(cost.bytes)}`);
|
|
150
|
+
if (cost.bySignal.length) McpRouter.logger.debug(`MCP catalogue cost: ${McpRouter.#costLine(cost.bySignal)}`);
|
|
151
|
+
|
|
152
|
+
if (cost.bytes > McpRouter.listingWarnBytes)
|
|
153
|
+
McpRouter.logger.warn(
|
|
154
|
+
`MCP listing is ${McpRouter.#kb(cost.bytes)}, which every agent that connects pays before its first turn. Narrow it with \`mcp: false\` on an endpoint or the \`mcp\` map on \`slice()\`.`,
|
|
155
|
+
);
|
|
144
156
|
|
|
145
157
|
if (!tools.length && !prompts.length)
|
|
146
158
|
McpRouter.logger.warn(
|
|
@@ -157,6 +169,20 @@ export class McpRouter {
|
|
|
157
169
|
}
|
|
158
170
|
}
|
|
159
171
|
|
|
172
|
+
static #kb(bytes: number) {
|
|
173
|
+
return bytes < 1024 ? `${bytes}B` : `${Math.round(bytes / 1024)}KB`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The heaviest signals first, capped: a fleet of forty models would otherwise wrap the line off the screen. */
|
|
177
|
+
static #costLine(bySignal: McpSignalCost[]) {
|
|
178
|
+
const shown = bySignal.slice(0, 8);
|
|
179
|
+
const rest = bySignal.length - shown.length;
|
|
180
|
+
const line = shown
|
|
181
|
+
.map(({ refName, entries, bytes }) => `${refName} ${entries}/${McpRouter.#kb(bytes)}`)
|
|
182
|
+
.join(" · ");
|
|
183
|
+
return rest > 0 ? `${line} · +${rest} more` : line;
|
|
184
|
+
}
|
|
185
|
+
|
|
160
186
|
/** Rebuilding per request would re-derive every tool schema on a list agents poll; the set is fixed at boot. */
|
|
161
187
|
#getDocument() {
|
|
162
188
|
if (this.#document) return this.#document;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isMcpDescribableArg, mcpHintsOf, mcpRefusalOf } from "akanjs/common";
|
|
2
2
|
import { FetchClient } from "akanjs/fetch";
|
|
3
3
|
import { type AgentCandidate, AgentCatalogue, type AgentRefusal, type AgentUndescribed } from "../agent";
|
|
4
4
|
import { type JsonSchema, JsonSchemaBuilder } from "../schema";
|
|
@@ -30,6 +30,18 @@ export interface McpExposedEndpoint {
|
|
|
30
30
|
export type McpRefusal = AgentRefusal;
|
|
31
31
|
export type McpUndescribed = AgentUndescribed;
|
|
32
32
|
|
|
33
|
+
/** What one signal contributes to a listing, so a catalogue that grew can say where it grew. */
|
|
34
|
+
export interface McpSignalCost {
|
|
35
|
+
refName: string;
|
|
36
|
+
entries: number;
|
|
37
|
+
bytes: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface McpListingCost {
|
|
41
|
+
bytes: number;
|
|
42
|
+
bySignal: McpSignalCost[];
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
/**
|
|
34
46
|
* Turns the serialized signal registry into the three MCP catalogues and answers the lookups `tools/call` and
|
|
35
47
|
* `resources/read` need. Pure: no IO and no DI — the sibling of `createOpenApiDocument`.
|
|
@@ -62,6 +74,7 @@ export class McpDocument {
|
|
|
62
74
|
readonly #byPromptName = new Map<string, { exposed: McpExposedEndpoint; prompt: McpPrompt }>();
|
|
63
75
|
/** Keyed by endpoint key: what is addressable, and by exactly which uri. */
|
|
64
76
|
readonly #templates = new Map<string, string>();
|
|
77
|
+
#cost: McpListingCost | null = null;
|
|
65
78
|
|
|
66
79
|
constructor(serializedSignal: Record<string, SerializedSignal>, options: McpDocumentOptions = {}) {
|
|
67
80
|
this.#options = options;
|
|
@@ -82,6 +95,30 @@ export class McpDocument {
|
|
|
82
95
|
this.undescribed = this.#catalogue.undescribed;
|
|
83
96
|
}
|
|
84
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Roughly what a `tools/list` plus `prompts/list` costs the caller, and which signals it went to.
|
|
100
|
+
*
|
|
101
|
+
* Worth reporting because the number is nobody's intuition: MCP has no shared component section and forbids a
|
|
102
|
+
* `$ref` across entries, so every entry inlines the full schema of every model it mentions — a plain 21-field
|
|
103
|
+
* model with one named slice ships 12KB across its eight entries, three quarters of it the same four schemas
|
|
104
|
+
* repeated. A catalogue is re-sent whole to every agent that connects, before its first turn.
|
|
105
|
+
*/
|
|
106
|
+
get listingCost(): McpListingCost {
|
|
107
|
+
if (this.#cost) return this.#cost;
|
|
108
|
+
const bySignal = new Map<string, McpSignalCost>();
|
|
109
|
+
const add = (refName: string, entry: unknown) => {
|
|
110
|
+
const cost = bySignal.get(refName) ?? { refName, entries: 0, bytes: 0 };
|
|
111
|
+
cost.entries += 1;
|
|
112
|
+
cost.bytes += JSON.stringify(entry).length;
|
|
113
|
+
bySignal.set(refName, cost);
|
|
114
|
+
};
|
|
115
|
+
for (const tool of this.tools) add(this.#byToolName.get(tool.name)?.refName ?? tool.name, tool);
|
|
116
|
+
for (const prompt of this.prompts) add(this.#byPromptName.get(prompt.name)?.exposed.refName ?? prompt.name, prompt);
|
|
117
|
+
const costs = [...bySignal.values()].sort((a, b) => b.bytes - a.bytes);
|
|
118
|
+
this.#cost = { bytes: costs.reduce((sum, cost) => sum + cost.bytes, 0), bySignal: costs };
|
|
119
|
+
return this.#cost;
|
|
120
|
+
}
|
|
121
|
+
|
|
85
122
|
findTool(name: string): McpExposedEndpoint | undefined {
|
|
86
123
|
return this.#byToolName.get(name);
|
|
87
124
|
}
|
|
@@ -132,7 +169,11 @@ export class McpDocument {
|
|
|
132
169
|
endpoint: candidate.endpoint,
|
|
133
170
|
};
|
|
134
171
|
|
|
135
|
-
const reason = mcpRefusalOf(item.endpoint, {
|
|
172
|
+
const reason = mcpRefusalOf(item.endpoint, {
|
|
173
|
+
refName: item.refName,
|
|
174
|
+
key: item.key,
|
|
175
|
+
readOnly: this.#options.readOnly,
|
|
176
|
+
});
|
|
136
177
|
if (reason) {
|
|
137
178
|
this.#catalogue.refuse(item.key, reason);
|
|
138
179
|
continue;
|
|
@@ -273,7 +314,6 @@ export class McpDocument {
|
|
|
273
314
|
|
|
274
315
|
static #uriTemplate(refName: string, key: string, endpoint: SerializedEndpoint) {
|
|
275
316
|
if (key === refName) return McpUriTemplate.model(refName);
|
|
276
|
-
if (key === `light${capitalize(refName)}`) return McpUriTemplate.light(refName);
|
|
277
317
|
const listPrefix = `${refName}List`;
|
|
278
318
|
if (!key.startsWith(listPrefix)) return undefined;
|
|
279
319
|
const suffix = key.slice(listPrefix.length);
|
|
@@ -15,14 +15,11 @@ export interface McpResourceTarget {
|
|
|
15
15
|
export class McpUriTemplate {
|
|
16
16
|
static readonly scheme = "akan";
|
|
17
17
|
/** Reserved second segment: a model id may never take one of these values, and none is a valid ObjectId. */
|
|
18
|
-
static readonly #reserved = new Set(["
|
|
18
|
+
static readonly #reserved = new Set(["list"]);
|
|
19
19
|
|
|
20
20
|
static model(refName: string) {
|
|
21
21
|
return `${McpUriTemplate.scheme}://${refName}/{${refName}Id}`;
|
|
22
22
|
}
|
|
23
|
-
static light(refName: string) {
|
|
24
|
-
return `${McpUriTemplate.scheme}://${refName}/light/{${refName}Id}`;
|
|
25
|
-
}
|
|
26
23
|
/**
|
|
27
24
|
* The model's own unfiltered list is the bare `…/list`, never `…/list/<token>`. A named slice occupies the
|
|
28
25
|
* third segment, and a slice key is an author-chosen identifier — so any token put there for the root list
|
|
@@ -51,8 +48,6 @@ export class McpUriTemplate {
|
|
|
51
48
|
return { endpointKey: refName, args: { [`${refName}Id`]: second } };
|
|
52
49
|
if (segments.length === 2 && second === "list")
|
|
53
50
|
return { endpointKey: `${refName}List`, args: McpUriTemplate.#searchArgs(search) };
|
|
54
|
-
if (segments.length === 3 && second === "light" && third)
|
|
55
|
-
return { endpointKey: `light${capitalize(refName)}`, args: { [`${refName}Id`]: third } };
|
|
56
51
|
if (segments.length === 3 && second === "list" && third)
|
|
57
52
|
return { endpointKey: `${refName}List${capitalize(third)}`, args: McpUriTemplate.#searchArgs(search) };
|
|
58
53
|
return null;
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
SerializedFilter,
|
|
14
14
|
SerializedReturns,
|
|
15
15
|
SerializedSignal,
|
|
16
|
+
SerializedSignalMcp,
|
|
16
17
|
SerializedSlice,
|
|
17
18
|
SliceCls,
|
|
18
19
|
SliceInfo,
|
|
@@ -69,6 +70,7 @@ export class FetchSerializer {
|
|
|
69
70
|
...(endpointInfo.signalOption.method ? { method: endpointInfo.signalOption.method } : {}),
|
|
70
71
|
...(endpointInfo.signalOption.fileUpload ? { fileUpload: true } : {}),
|
|
71
72
|
...(guards?.length ? { guards } : {}),
|
|
73
|
+
...(endpointInfo.signalOption.mcp === false ? { mcp: false as const } : {}),
|
|
72
74
|
};
|
|
73
75
|
}
|
|
74
76
|
|
|
@@ -107,6 +109,7 @@ export class FetchSerializer {
|
|
|
107
109
|
args: sliceInfo.args.map(FetchSerializer.#serializeArg),
|
|
108
110
|
...(sliceInfo.signalOption.path ? { path: sliceInfo.signalOption.path } : {}),
|
|
109
111
|
...(guards?.length ? { guards } : {}),
|
|
112
|
+
...(sliceInfo.signalOption.mcp === false ? { mcp: false as const } : {}),
|
|
110
113
|
};
|
|
111
114
|
}
|
|
112
115
|
|
|
@@ -146,10 +149,21 @@ export class FetchSerializer {
|
|
|
146
149
|
...(sliceCls.removeGuards !== sliceCls.cruGuards && sliceCls.removeGuards.filter((g) => g.name !== "None").length
|
|
147
150
|
? { removeGuards: sliceCls.removeGuards.map((g) => g.name) }
|
|
148
151
|
: {}),
|
|
152
|
+
...FetchSerializer.#serializeSliceMcp(sliceCls),
|
|
149
153
|
endpoint,
|
|
150
154
|
};
|
|
151
155
|
}
|
|
152
156
|
|
|
157
|
+
/** Only the verbs kept off the shelf travel: `true` is the default, so emitting it would grow every payload. */
|
|
158
|
+
static #serializeSliceMcp(sliceCls: SliceCls): { mcp?: SerializedSignalMcp } {
|
|
159
|
+
const mcp = Object.fromEntries(
|
|
160
|
+
Object.entries(sliceCls.mcp ?? {})
|
|
161
|
+
.filter(([, published]) => !published)
|
|
162
|
+
.map(([verb]) => [verb, false]),
|
|
163
|
+
) as SerializedSignalMcp;
|
|
164
|
+
return Object.keys(mcp).length ? { mcp } : {};
|
|
165
|
+
}
|
|
166
|
+
|
|
153
167
|
static serializeServiceSignal(endpointCls: EndpointCls): SerializedSignal {
|
|
154
168
|
const endpointMeta = endpointCls[ENDPOINT_META] as { [key: string]: EndpointInfo };
|
|
155
169
|
const endpoint: { [key: string]: SerializedEndpoint } = {};
|
package/signal/slice.ts
CHANGED
|
@@ -38,8 +38,17 @@ export type SliceCls<
|
|
|
38
38
|
createGuards: GuardCls[];
|
|
39
39
|
updateGuards: GuardCls[];
|
|
40
40
|
removeGuards: GuardCls[];
|
|
41
|
+
mcp: ResolvedSliceMcp;
|
|
41
42
|
};
|
|
42
43
|
|
|
44
|
+
/** The generated verbs, resolved to a plain answer each. `root` is not here: it rides on the root slice itself. */
|
|
45
|
+
export interface ResolvedSliceMcp {
|
|
46
|
+
get: boolean;
|
|
47
|
+
create: boolean;
|
|
48
|
+
update: boolean;
|
|
49
|
+
remove: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
43
52
|
interface RootSliceOption {
|
|
44
53
|
guards?: {
|
|
45
54
|
root?: GuardCls | GuardCls[];
|
|
@@ -49,6 +58,18 @@ interface RootSliceOption {
|
|
|
49
58
|
update?: GuardCls | GuardCls[];
|
|
50
59
|
remove?: GuardCls | GuardCls[];
|
|
51
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* Which of the entries this call generates belong on an agent's shelf. Keyed exactly like `guards` above and
|
|
63
|
+
* scoped to exactly what `guards` governs — the root slice and the generated CRUD — so `false` here narrows the
|
|
64
|
+
* catalogue and nothing else. `create`/`update`/`remove` fall back to `cru` the same way their guards do, and
|
|
65
|
+
* the bare `false` is every key at once.
|
|
66
|
+
*
|
|
67
|
+
* A named slice and a custom endpoint declare their own `mcp` in their own signal option, exactly as they
|
|
68
|
+
* declare their own guards. This is curation, not authorization: HTTP is unchanged.
|
|
69
|
+
*/
|
|
70
|
+
mcp?:
|
|
71
|
+
| boolean
|
|
72
|
+
| { root?: boolean; get?: boolean; cru?: boolean; create?: boolean; update?: boolean; remove?: boolean };
|
|
52
73
|
prefix?: string;
|
|
53
74
|
}
|
|
54
75
|
|
|
@@ -131,6 +152,15 @@ export function slice<
|
|
|
131
152
|
const createGuards = option.guards?.create ? toGuards(option.guards.create) : cruGuards;
|
|
132
153
|
const updateGuards = option.guards?.update ? toGuards(option.guards.update) : cruGuards;
|
|
133
154
|
const removeGuards = option.guards?.remove ? toGuards(option.guards.remove) : cruGuards;
|
|
155
|
+
const mcpOption =
|
|
156
|
+
typeof option.mcp === "boolean" ? { root: option.mcp, get: option.mcp, cru: option.mcp } : option.mcp;
|
|
157
|
+
const cruMcp = mcpOption?.cru !== false;
|
|
158
|
+
const mcp: ResolvedSliceMcp = {
|
|
159
|
+
get: mcpOption?.get !== false,
|
|
160
|
+
create: mcpOption?.create ?? cruMcp,
|
|
161
|
+
update: mcpOption?.update ?? cruMcp,
|
|
162
|
+
remove: mcpOption?.remove ?? cruMcp,
|
|
163
|
+
};
|
|
134
164
|
const srvKeys = [
|
|
135
165
|
...new Set([...Object.keys(srv.srvMap), ...libSlices.flatMap((libSlice) => Object.keys(libSlice.srv.srvMap))]),
|
|
136
166
|
];
|
|
@@ -144,10 +174,11 @@ export function slice<
|
|
|
144
174
|
static createGuards = createGuards;
|
|
145
175
|
static updateGuards = updateGuards;
|
|
146
176
|
static removeGuards = removeGuards;
|
|
177
|
+
static mcp = mcp;
|
|
147
178
|
static [SLICE_META] = Object.assign(
|
|
148
179
|
{
|
|
149
180
|
|
|
150
|
-
[""]: init({ guards: rootGuards })
|
|
181
|
+
[""]: init({ guards: rootGuards, ...(mcpOption?.root === false ? { mcp: false } : {}) })
|
|
151
182
|
.search<"queryKey", string>("queryKey", String)
|
|
152
183
|
.search<"args", unknown[]>("args", Any)
|
|
153
184
|
.exec((queryKey, args) => {
|
package/signal/types.ts
CHANGED
|
@@ -85,6 +85,17 @@ export interface SignalOption<Response = any, Nullable extends boolean = false,
|
|
|
85
85
|
method?: HttpMutationMethod;
|
|
86
86
|
/** Marks this mutation as the framework file-upload endpoint (see resolveFileUploadCapability). */
|
|
87
87
|
fileUpload?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Whether this endpoint belongs on an agent's shelf. `true` — the default — publishes it to MCP subject to the
|
|
90
|
+
* guard and shape rules; `false` keeps it out of the catalogue entirely.
|
|
91
|
+
*
|
|
92
|
+
* This is curation, not authorization: HTTP serves the endpoint exactly as before, and the guards are still the
|
|
93
|
+
* only thing deciding who may call it. Write it where an endpoint is perfectly guarded and still has no business
|
|
94
|
+
* on a shelf — a step of a UI-driven state machine (`requestPhoneCodeForSignin`), or a read a model would only
|
|
95
|
+
* ever call by mistake. Every catalogue entry carries the model schemas it mentions, so one endpoint dropped
|
|
96
|
+
* here is kilobytes off every `tools/list`.
|
|
97
|
+
*/
|
|
98
|
+
mcp?: boolean;
|
|
88
99
|
/**
|
|
89
100
|
* What a `pubsub(Binary)` does when a subscriber cannot keep up. `"coalesce"` (the default) keeps only the
|
|
90
101
|
* newest frame per room, which is what a telemetry or video stream wants — an old frame is worthless once a
|
|
@@ -108,6 +119,13 @@ interface SerializedSignalOption {
|
|
|
108
119
|
guards?: string[];
|
|
109
120
|
method?: HttpMutationMethod;
|
|
110
121
|
fileUpload?: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Only ever `false`, and only when something declared it: `true` is the default, so serializing it would ship a
|
|
124
|
+
* field per endpoint that says nothing. Resolved here rather than left to the reader because the browser API
|
|
125
|
+
* explorer has guard *names* and no classes, and a second implementation of the rule would eventually disagree
|
|
126
|
+
* with the catalogue.
|
|
127
|
+
*/
|
|
128
|
+
mcp?: false;
|
|
111
129
|
}
|
|
112
130
|
export interface SerializedSlice extends SerializedSignalOption {}
|
|
113
131
|
|
|
@@ -152,6 +170,19 @@ export interface SerializedSignal {
|
|
|
152
170
|
createGuards?: string[];
|
|
153
171
|
updateGuards?: string[];
|
|
154
172
|
removeGuards?: string[];
|
|
173
|
+
/**
|
|
174
|
+
* Which generated CRUD verbs this model keeps off the agent shelf. Carried on the signal because the endpoints
|
|
175
|
+
* it names do not exist until `FetchClient.getBaseEndpoint` synthesizes them. Only the `false` keys travel.
|
|
176
|
+
*/
|
|
177
|
+
mcp?: SerializedSignalMcp;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Keyed by generated verb, mirroring the `guards` map `slice()` takes. The root slice's own flag rides on `slice[""]`. */
|
|
181
|
+
export interface SerializedSignalMcp {
|
|
182
|
+
get?: false;
|
|
183
|
+
create?: false;
|
|
184
|
+
update?: false;
|
|
185
|
+
remove?: false;
|
|
155
186
|
}
|
|
156
187
|
|
|
157
188
|
export type SignalType = "restapi" | "websocket";
|
|
@@ -22,8 +22,12 @@ export interface McpExposureEndpoint {
|
|
|
22
22
|
}[];
|
|
23
23
|
guards?: string[];
|
|
24
24
|
fileUpload?: boolean;
|
|
25
|
+
mcp?: boolean;
|
|
25
26
|
}
|
|
26
27
|
export interface McpExposureOption {
|
|
28
|
+
/** The model the endpoint belongs to and the name it is published under: one rule reads the key, not the shape. */
|
|
29
|
+
refName: string;
|
|
30
|
+
key: string;
|
|
27
31
|
/**
|
|
28
32
|
* The read-only deployment valve, which is server configuration. The browser explorer cannot know it and so
|
|
29
33
|
* badges what the code decided; the boot log is where a read-only deployment says what it dropped.
|
|
@@ -50,7 +54,7 @@ export declare const mcpHintsOf: (key: string, endpoint: {
|
|
|
50
54
|
openWorldHint: boolean;
|
|
51
55
|
};
|
|
52
56
|
/** The sentence explaining why this endpoint is not in the catalogue, or `null` when it is. */
|
|
53
|
-
export declare const mcpRefusalOf: (endpoint: McpExposureEndpoint, { readOnly }
|
|
57
|
+
export declare const mcpRefusalOf: (endpoint: McpExposureEndpoint, { refName, key, readOnly }: McpExposureOption) => string | null;
|
|
54
58
|
/**
|
|
55
59
|
* A prompt is a read exposed on the same terms as a query, so every rejection here is one thing: an argument
|
|
56
60
|
* `prompts/get` cannot carry. Its `arguments` is a flat string map — one string per name, and no schema beside it
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const baseDictionary: import("./dictInfo.d.ts").ServiceDictInfo<[string, string], "ping" | "pingBody" | "pingParam" | "pingQuery" | "wsPing" | "pubsubPing",
|
|
1
|
+
export declare const baseDictionary: import("./dictInfo.d.ts").ServiceDictInfo<[string, string], "ping" | "pingBody" | "pingParam" | "pingQuery" | "wsPing" | "pubsubPing", "serverUnreachable" | "serverUnavailable" | "gatewayTimeout" | "unexpectedResponse", "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view">;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { AgentEndpoint, AgentTurn, BaseEndpoint } from "akanjs/signal";
|
|
2
2
|
export declare const dictionary: {
|
|
3
|
-
base: import("./locale.d.ts").DictModule<import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view">,
|
|
3
|
+
base: import("./locale.d.ts").DictModule<import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view">, "base.error.gatewayTimeout" | "base.error.serverUnavailable" | "base.error.serverUnreachable" | "base.error.unexpectedResponse">;
|
|
4
4
|
agentTurn: import("./locale.d.ts").DictModule<import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc`, never>;
|
|
5
5
|
agent: import("./locale.d.ts").DictModule<import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, "agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed">;
|
|
6
6
|
};
|
|
7
|
-
export declare const Err: import("./trans.d.ts").ErrConstructor<"agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed">, translate: (lang: "en" | "ko" | "zhChs" | "zhCht" | "ja", key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, data?: import("./trans.d.ts").TranslationData) => string, msg: {
|
|
7
|
+
export declare const Err: import("./trans.d.ts").ErrConstructor<"base.error.gatewayTimeout" | "base.error.serverUnavailable" | "base.error.serverUnreachable" | "base.error.unexpectedResponse" | "agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed">, translate: (lang: "en" | "ko" | "zhChs" | "zhCht" | "ja", key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, data?: import("./trans.d.ts").TranslationData) => string, msg: {
|
|
8
8
|
info: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
|
|
9
9
|
success: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
|
|
10
10
|
error: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
|
|
11
11
|
warning: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
|
|
12
12
|
loading: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
|
|
13
|
-
}, getDictionary: (lang: "en" | "ko" | "zhChs" | "zhCht" | "ja") => object, getAllDictionary: () => import("./trans.d.ts").RootDictionary, __Dict_Key__: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, __Error_Key__: "agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed";
|
|
13
|
+
}, getDictionary: (lang: "en" | "ko" | "zhChs" | "zhCht" | "ja") => object, getAllDictionary: () => import("./trans.d.ts").RootDictionary, __Dict_Key__: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, __Error_Key__: "base.error.gatewayTimeout" | "base.error.serverUnavailable" | "base.error.serverUnreachable" | "base.error.unexpectedResponse" | "agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed";
|
|
@@ -41,6 +41,8 @@ export interface McpRouterProps {
|
|
|
41
41
|
export declare class McpRouter {
|
|
42
42
|
#private;
|
|
43
43
|
static readonly logger: Logger;
|
|
44
|
+
/** Past this a listing is a meaningful slice of a model's window, which is where it becomes worth saying so. */
|
|
45
|
+
static readonly listingWarnBytes: number;
|
|
44
46
|
constructor(props: McpRouterProps);
|
|
45
47
|
createRoutes(): HttpRoutes;
|
|
46
48
|
/**
|
|
@@ -22,6 +22,16 @@ export interface McpExposedEndpoint {
|
|
|
22
22
|
*/
|
|
23
23
|
export type McpRefusal = AgentRefusal;
|
|
24
24
|
export type McpUndescribed = AgentUndescribed;
|
|
25
|
+
/** What one signal contributes to a listing, so a catalogue that grew can say where it grew. */
|
|
26
|
+
export interface McpSignalCost {
|
|
27
|
+
refName: string;
|
|
28
|
+
entries: number;
|
|
29
|
+
bytes: number;
|
|
30
|
+
}
|
|
31
|
+
export interface McpListingCost {
|
|
32
|
+
bytes: number;
|
|
33
|
+
bySignal: McpSignalCost[];
|
|
34
|
+
}
|
|
25
35
|
/**
|
|
26
36
|
* Turns the serialized signal registry into the three MCP catalogues and answers the lookups `tools/call` and
|
|
27
37
|
* `resources/read` need. Pure: no IO and no DI — the sibling of `createOpenApiDocument`.
|
|
@@ -46,6 +56,15 @@ export declare class McpDocument {
|
|
|
46
56
|
/** What is published with no description of its own, which is the field a model picks a tool by. */
|
|
47
57
|
readonly undescribed: McpUndescribed[];
|
|
48
58
|
constructor(serializedSignal: Record<string, SerializedSignal>, options?: McpDocumentOptions);
|
|
59
|
+
/**
|
|
60
|
+
* Roughly what a `tools/list` plus `prompts/list` costs the caller, and which signals it went to.
|
|
61
|
+
*
|
|
62
|
+
* Worth reporting because the number is nobody's intuition: MCP has no shared component section and forbids a
|
|
63
|
+
* `$ref` across entries, so every entry inlines the full schema of every model it mentions — a plain 21-field
|
|
64
|
+
* model with one named slice ships 12KB across its eight entries, three quarters of it the same four schemas
|
|
65
|
+
* repeated. A catalogue is re-sent whole to every agent that connects, before its first turn.
|
|
66
|
+
*/
|
|
67
|
+
get listingCost(): McpListingCost;
|
|
49
68
|
findTool(name: string): McpExposedEndpoint | undefined;
|
|
50
69
|
/** Returns the catalogue entry alongside the endpoint: `prompts/get` validates against the published one. */
|
|
51
70
|
findPrompt(name: string): {
|
|
@@ -13,7 +13,6 @@ export declare class McpUriTemplate {
|
|
|
13
13
|
#private;
|
|
14
14
|
static readonly scheme = "akan";
|
|
15
15
|
static model(refName: string): string;
|
|
16
|
-
static light(refName: string): string;
|
|
17
16
|
/**
|
|
18
17
|
* The model's own unfiltered list is the bare `…/list`, never `…/list/<token>`. A named slice occupies the
|
|
19
18
|
* third segment, and a slice key is an author-chosen identifier — so any token put there for the root list
|
package/types/signal/slice.d.ts
CHANGED
|
@@ -29,7 +29,15 @@ export type SliceCls<SrvModule extends ServiceModel = ServiceModel, SliceInfoObj
|
|
|
29
29
|
createGuards: GuardCls[];
|
|
30
30
|
updateGuards: GuardCls[];
|
|
31
31
|
removeGuards: GuardCls[];
|
|
32
|
+
mcp: ResolvedSliceMcp;
|
|
32
33
|
};
|
|
34
|
+
/** The generated verbs, resolved to a plain answer each. `root` is not here: it rides on the root slice itself. */
|
|
35
|
+
export interface ResolvedSliceMcp {
|
|
36
|
+
get: boolean;
|
|
37
|
+
create: boolean;
|
|
38
|
+
update: boolean;
|
|
39
|
+
remove: boolean;
|
|
40
|
+
}
|
|
33
41
|
interface RootSliceOption {
|
|
34
42
|
guards?: {
|
|
35
43
|
root?: GuardCls | GuardCls[];
|
|
@@ -39,6 +47,23 @@ interface RootSliceOption {
|
|
|
39
47
|
update?: GuardCls | GuardCls[];
|
|
40
48
|
remove?: GuardCls | GuardCls[];
|
|
41
49
|
};
|
|
50
|
+
/**
|
|
51
|
+
* Which of the entries this call generates belong on an agent's shelf. Keyed exactly like `guards` above and
|
|
52
|
+
* scoped to exactly what `guards` governs — the root slice and the generated CRUD — so `false` here narrows the
|
|
53
|
+
* catalogue and nothing else. `create`/`update`/`remove` fall back to `cru` the same way their guards do, and
|
|
54
|
+
* the bare `false` is every key at once.
|
|
55
|
+
*
|
|
56
|
+
* A named slice and a custom endpoint declare their own `mcp` in their own signal option, exactly as they
|
|
57
|
+
* declare their own guards. This is curation, not authorization: HTTP is unchanged.
|
|
58
|
+
*/
|
|
59
|
+
mcp?: boolean | {
|
|
60
|
+
root?: boolean;
|
|
61
|
+
get?: boolean;
|
|
62
|
+
cru?: boolean;
|
|
63
|
+
create?: boolean;
|
|
64
|
+
update?: boolean;
|
|
65
|
+
remove?: boolean;
|
|
66
|
+
};
|
|
42
67
|
prefix?: string;
|
|
43
68
|
}
|
|
44
69
|
type RootSliceQueryKey<Filter extends FilterInstance> = Extract<keyof Filter["query"], string>;
|
package/types/signal/types.d.ts
CHANGED
|
@@ -74,6 +74,17 @@ export interface SignalOption<Response = any, Nullable extends boolean = false,
|
|
|
74
74
|
method?: HttpMutationMethod;
|
|
75
75
|
/** Marks this mutation as the framework file-upload endpoint (see resolveFileUploadCapability). */
|
|
76
76
|
fileUpload?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Whether this endpoint belongs on an agent's shelf. `true` — the default — publishes it to MCP subject to the
|
|
79
|
+
* guard and shape rules; `false` keeps it out of the catalogue entirely.
|
|
80
|
+
*
|
|
81
|
+
* This is curation, not authorization: HTTP serves the endpoint exactly as before, and the guards are still the
|
|
82
|
+
* only thing deciding who may call it. Write it where an endpoint is perfectly guarded and still has no business
|
|
83
|
+
* on a shelf — a step of a UI-driven state machine (`requestPhoneCodeForSignin`), or a read a model would only
|
|
84
|
+
* ever call by mistake. Every catalogue entry carries the model schemas it mentions, so one endpoint dropped
|
|
85
|
+
* here is kilobytes off every `tools/list`.
|
|
86
|
+
*/
|
|
87
|
+
mcp?: boolean;
|
|
77
88
|
/**
|
|
78
89
|
* What a `pubsub(Binary)` does when a subscriber cannot keep up. `"coalesce"` (the default) keeps only the
|
|
79
90
|
* newest frame per room, which is what a telemetry or video stream wants — an old frame is worthless once a
|
|
@@ -95,6 +106,13 @@ interface SerializedSignalOption {
|
|
|
95
106
|
guards?: string[];
|
|
96
107
|
method?: HttpMutationMethod;
|
|
97
108
|
fileUpload?: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Only ever `false`, and only when something declared it: `true` is the default, so serializing it would ship a
|
|
111
|
+
* field per endpoint that says nothing. Resolved here rather than left to the reader because the browser API
|
|
112
|
+
* explorer has guard *names* and no classes, and a second implementation of the rule would eventually disagree
|
|
113
|
+
* with the catalogue.
|
|
114
|
+
*/
|
|
115
|
+
mcp?: false;
|
|
98
116
|
}
|
|
99
117
|
export interface SerializedSlice extends SerializedSignalOption {
|
|
100
118
|
}
|
|
@@ -144,6 +162,18 @@ export interface SerializedSignal {
|
|
|
144
162
|
createGuards?: string[];
|
|
145
163
|
updateGuards?: string[];
|
|
146
164
|
removeGuards?: string[];
|
|
165
|
+
/**
|
|
166
|
+
* Which generated CRUD verbs this model keeps off the agent shelf. Carried on the signal because the endpoints
|
|
167
|
+
* it names do not exist until `FetchClient.getBaseEndpoint` synthesizes them. Only the `false` keys travel.
|
|
168
|
+
*/
|
|
169
|
+
mcp?: SerializedSignalMcp;
|
|
170
|
+
}
|
|
171
|
+
/** Keyed by generated verb, mirroring the `guards` map `slice()` takes. The root slice's own flag rides on `slice[""]`. */
|
|
172
|
+
export interface SerializedSignalMcp {
|
|
173
|
+
get?: false;
|
|
174
|
+
create?: false;
|
|
175
|
+
update?: false;
|
|
176
|
+
remove?: false;
|
|
147
177
|
}
|
|
148
178
|
export type SignalType = "restapi" | "websocket";
|
|
149
179
|
export type WebsocketReqData = {
|
|
@@ -40,7 +40,7 @@ export interface ProviderProps {
|
|
|
40
40
|
/** Root route component used by CSR page loading. */
|
|
41
41
|
of: (props: unknown) => ReactNode | null;
|
|
42
42
|
}
|
|
43
|
-
export declare const Common: () =>
|
|
43
|
+
export declare const Common: () => null;
|
|
44
44
|
export declare function ManifestLink({ manifest }: {
|
|
45
45
|
manifest?: WebAppManifest;
|
|
46
46
|
}): import("react/jsx-runtime").JSX.Element | null;
|
package/ui/Signal/Doc.tsx
CHANGED
|
@@ -247,7 +247,7 @@ const Zone = ({ refName, fetch, openAll }: ZoneProps) => {
|
|
|
247
247
|
const desc = dictText(l, `${refName}.modelDesc`);
|
|
248
248
|
const entries = endpointEntriesOf(refName, fetch);
|
|
249
249
|
const wsEntries = entries.filter(({ endpoint }) => isWsEndpoint(endpoint));
|
|
250
|
-
const mcpEntries = entries.filter(({ endpoint }) => !mcpRefusalOf(endpoint));
|
|
250
|
+
const mcpEntries = entries.filter(({ key, endpoint }) => !mcpRefusalOf(endpoint, { refName, key }));
|
|
251
251
|
return (
|
|
252
252
|
<div className="flex break-after-page flex-col gap-6">
|
|
253
253
|
<div className="flex flex-col gap-1">
|
package/ui/Signal/RestApi.tsx
CHANGED
|
@@ -110,7 +110,7 @@ const RestApiEndpoint = ({
|
|
|
110
110
|
const [viewStatus, setViewStatus] = useState<"doc" | "test">("doc");
|
|
111
111
|
const path = FetchClient.makeHttpUrl(endpointKey, endpoint, signalPrefix, new Map());
|
|
112
112
|
|
|
113
|
-
const mcpRefusal = mcpRefusalOf(endpoint);
|
|
113
|
+
const mcpRefusal = mcpRefusalOf(endpoint, { refName, key: endpointKey });
|
|
114
114
|
const guards = guardsOf(endpoint);
|
|
115
115
|
const label = dictText(l, `${refName}.signal.${endpointKey}`);
|
|
116
116
|
const desc = dictText(l, `${refName}.signal.${endpointKey}.desc`);
|