@theoremjs/web 0.0.0-stage → 0.3.4-next.375.ga2d1878
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/LICENSE +21 -0
- package/README.md +493 -2
- package/client/audio-level.d.ts +5 -0
- package/client/audio-level.js +24 -0
- package/client/chat.d.ts +144 -0
- package/client/chat.js +760 -0
- package/client/composer-drawer.d.ts +19 -0
- package/client/composer-drawer.js +15 -0
- package/client/composer-hints.d.ts +25 -0
- package/client/composer-hints.js +20 -0
- package/client/composer-primary.d.ts +18 -0
- package/client/composer-primary.js +29 -0
- package/client/decision-transport.d.ts +70 -0
- package/client/decision-transport.js +175 -0
- package/client/decode-composer-draft.d.ts +11 -0
- package/client/decode-composer-draft.js +12 -0
- package/client/embedding-input.d.ts +84 -0
- package/client/embedding-input.js +194 -0
- package/client/embedding-space.d.ts +82 -0
- package/client/embedding-space.js +389 -0
- package/client/embedding-transport.d.ts +40 -0
- package/client/embedding-transport.js +71 -0
- package/client/encode-composer-draft.d.ts +9 -0
- package/client/encode-composer-draft.js +34 -0
- package/client/encode-files.d.ts +18 -0
- package/client/encode-files.js +54 -0
- package/client/failure.d.ts +29 -0
- package/client/failure.js +34 -0
- package/client/generation-selection.d.ts +15 -0
- package/client/generation-selection.js +24 -0
- package/client/host-transport.d.ts +70 -0
- package/client/host-transport.js +75 -0
- package/client/index.d.ts +37 -0
- package/client/index.js +31 -0
- package/client/live/live-mic-forward.d.ts +30 -0
- package/client/live/live-mic-forward.js +46 -0
- package/client/live/live-page-tool.d.ts +39 -0
- package/client/live/live-page-tool.js +26 -0
- package/client/live/live-state.d.ts +13 -0
- package/client/live/live-state.js +22 -0
- package/client/live/live-tool.d.ts +11 -0
- package/client/live/live-tool.js +1 -0
- package/client/live-client.d.ts +189 -0
- package/client/live-client.js +829 -0
- package/client/live-errors.d.ts +2 -0
- package/client/live-errors.js +9 -0
- package/client/live-messages.d.ts +148 -0
- package/client/live-messages.js +100 -0
- package/client/mic-capture.d.ts +4 -0
- package/client/mic-capture.js +6 -0
- package/client/oauth-popup.d.ts +26 -0
- package/client/oauth-popup.js +30 -0
- package/client/page-dispatch.d.ts +15 -0
- package/client/page-dispatch.js +63 -0
- package/client/pcm-downsample.d.ts +4 -0
- package/client/pcm-downsample.js +45 -0
- package/client/run-commit.d.ts +34 -0
- package/client/run-commit.js +49 -0
- package/client/run-session.d.ts +79 -0
- package/client/run-session.js +207 -0
- package/client/schema-fields.d.ts +63 -0
- package/client/schema-fields.js +199 -0
- package/client/shaped-data.d.ts +118 -0
- package/client/shaped-data.js +343 -0
- package/client/source-citations.d.ts +13 -0
- package/client/source-citations.js +56 -0
- package/client/space-geometry.d.ts +39 -0
- package/client/space-geometry.js +183 -0
- package/client/tool-result.d.ts +78 -0
- package/client/tool-result.js +293 -0
- package/client/tool-resume.d.ts +50 -0
- package/client/tool-resume.js +14 -0
- package/client/trace-feed.d.ts +13 -0
- package/client/trace-feed.js +18 -0
- package/client/transcript-schema.d.ts +10 -0
- package/client/transcript-schema.js +120 -0
- package/client/transport.d.ts +264 -0
- package/client/transport.js +330 -0
- package/client/turn-client.d.ts +44 -0
- package/client/turn-client.js +88 -0
- package/client/wire-line.d.ts +56 -0
- package/client/wire-line.js +90 -0
- package/client/worklets/mic-capture.js +16 -0
- package/package.json +28 -4
- package/server/checkpoint-ownership.d.ts +6 -0
- package/server/checkpoint-ownership.js +42 -0
- package/server/conversation-store.d.ts +92 -0
- package/server/conversation-store.js +131 -0
- package/server/conversation.d.ts +193 -0
- package/server/conversation.js +552 -0
- package/server/credential-store.d.ts +24 -0
- package/server/credential-store.js +14 -0
- package/server/decision-handler.d.ts +42 -0
- package/server/decision-handler.js +46 -0
- package/server/embedding-handler.d.ts +33 -0
- package/server/embedding-handler.js +45 -0
- package/server/handler.d.ts +158 -0
- package/server/handler.js +775 -0
- package/server/json-response.d.ts +2 -0
- package/server/json-response.js +7 -0
- package/server/mcp-handler.d.ts +46 -0
- package/server/mcp-handler.js +312 -0
- package/server/mod.d.ts +27 -0
- package/server/mod.js +25 -0
- package/server/page-dispatch.d.ts +22 -0
- package/server/page-dispatch.js +127 -0
- package/server/request-body.d.ts +2 -0
- package/server/request-body.js +42 -0
- package/server/request-check.d.ts +21 -0
- package/server/request-check.js +49 -0
- package/server/session-authority.d.ts +8 -0
- package/server/session-authority.js +78 -0
- package/server/session-boundary.d.ts +26 -0
- package/server/session-boundary.js +64 -0
- package/server/session-identity.d.ts +16 -0
- package/server/session-identity.js +41 -0
- package/server/session-limits.d.ts +12 -0
- package/server/session-limits.js +59 -0
- package/server/session-store.d.ts +123 -0
- package/server/session-store.js +264 -0
- package/server/single-action.d.ts +21 -0
- package/server/single-action.js +30 -0
- package/server/steer-inbox.d.ts +35 -0
- package/server/steer-inbox.js +52 -0
- package/server/tool-operations.d.ts +6 -0
- package/server/tool-operations.js +132 -0
- package/server/turn-input.d.ts +10 -0
- package/server/turn-input.js +29 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One Web-standard handler that serves a host profile's tools to MCP clients over
|
|
3
|
+
* Streamable HTTP, at one endpoint that takes POST.
|
|
4
|
+
*
|
|
5
|
+
* - Clients on the current revision (`2026-07-28`) send each request with its version and
|
|
6
|
+
* capabilities in `_meta`; nothing is kept between requests.
|
|
7
|
+
* - Clients on an earlier revision open with `initialize`; the handler answers it and serves
|
|
8
|
+
* them the same way, without a session.
|
|
9
|
+
*
|
|
10
|
+
* Every call runs through the kernel as a host call does: the profile's gates, guardrails and
|
|
11
|
+
* trace. The client reads what a model would: the call's guarded text, never its raw output.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import { type ClientTurnOptions } from '@theoremjs/agents/host';
|
|
16
|
+
import { type TheoremHostHandlerOptions } from './handler.ts';
|
|
17
|
+
export type TheoremMcpHandlerOptions = {
|
|
18
|
+
/** Host profile to serve. Registered with the kernel when the handler is created. */
|
|
19
|
+
profile: TheoremHostHandlerOptions['profile'];
|
|
20
|
+
/** The server's version, as clients show it. */
|
|
21
|
+
version: string;
|
|
22
|
+
/** The server's name, as clients show it. Defaults to the profile's id. */
|
|
23
|
+
name?: string;
|
|
24
|
+
/** How a model should use these tools; clients may add it to the model's context. */
|
|
25
|
+
instructions?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Origins a browser page may call from, besides the endpoint's own. A request whose
|
|
28
|
+
* `Origin` is neither is refused with 403, as MCP requires against DNS rebinding.
|
|
29
|
+
*/
|
|
30
|
+
origins?: readonly string[];
|
|
31
|
+
/** Handed to each tool as `ctx.host`. */
|
|
32
|
+
host?: (request: Request) => unknown;
|
|
33
|
+
/** Largest request body read, in bytes. */
|
|
34
|
+
maxRequestBytes?: number;
|
|
35
|
+
/** Who reads this host's replies: forwarded to `clientError` for a failure the handler catches. */
|
|
36
|
+
clientEvents?: ClientTurnOptions;
|
|
37
|
+
/** Called with any error the handler catches, for reporting. */
|
|
38
|
+
onError?: (err: unknown, ctx: {
|
|
39
|
+
request: Request;
|
|
40
|
+
}) => void;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Serves a host profile's tools to MCP clients. Every tool the profile allows must run
|
|
44
|
+
* without a person's approval: an MCP client has no one to ask.
|
|
45
|
+
*/
|
|
46
|
+
export declare function createTheoremMcpHandler(options: TheoremMcpHandlerOptions): (request: Request) => Promise<Response>;
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One Web-standard handler that serves a host profile's tools to MCP clients over
|
|
3
|
+
* Streamable HTTP, at one endpoint that takes POST.
|
|
4
|
+
*
|
|
5
|
+
* - Clients on the current revision (`2026-07-28`) send each request with its version and
|
|
6
|
+
* capabilities in `_meta`; nothing is kept between requests.
|
|
7
|
+
* - Clients on an earlier revision open with `initialize`; the handler answers it and serves
|
|
8
|
+
* them the same way, without a session.
|
|
9
|
+
*
|
|
10
|
+
* Every call runs through the kernel as a host call does: the profile's gates, guardrails and
|
|
11
|
+
* trace. The client reads what a model would: the call's guarded text, never its raw output.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import { invokeTool, labelText, registerProfile, } from '@theoremjs/agents';
|
|
16
|
+
import { caughtStatus, clientError } from '@theoremjs/agents/host';
|
|
17
|
+
import { hostInterface } from "../client/host-transport.js";
|
|
18
|
+
import { defineHostProfile, hostTool } from "./handler.js";
|
|
19
|
+
import { boundedRequestJson, requestByteLimit } from "./request-body.js";
|
|
20
|
+
/** The revision that carries its version on every request. */
|
|
21
|
+
const MODERN_VERSION = '2026-07-28';
|
|
22
|
+
/** Revisions that open with `initialize`, newest first. */
|
|
23
|
+
const LEGACY_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26'];
|
|
24
|
+
const SUPPORTED_VERSIONS = [MODERN_VERSION, ...LEGACY_VERSIONS];
|
|
25
|
+
const META_VERSION = 'io.modelcontextprotocol/protocolVersion';
|
|
26
|
+
const META_CAPABILITIES = 'io.modelcontextprotocol/clientCapabilities';
|
|
27
|
+
const META_SERVER = 'io.modelcontextprotocol/serverInfo';
|
|
28
|
+
/**
|
|
29
|
+
* How long a current-revision client may keep each cacheable result, in milliseconds. Neither
|
|
30
|
+
* holds anything of the caller's, so any cache may share them.
|
|
31
|
+
*/
|
|
32
|
+
const CACHED = {
|
|
33
|
+
'server/discover': 3_600_000,
|
|
34
|
+
'tools/list': 300_000,
|
|
35
|
+
};
|
|
36
|
+
const PARSE_ERROR = -32700;
|
|
37
|
+
const INVALID_REQUEST = -32600;
|
|
38
|
+
const METHOD_NOT_FOUND = -32601;
|
|
39
|
+
const INVALID_PARAMS = -32602;
|
|
40
|
+
const INTERNAL_ERROR = -32603;
|
|
41
|
+
const HEADER_MISMATCH = -32020;
|
|
42
|
+
const UNSUPPORTED_VERSION = -32022;
|
|
43
|
+
class RpcError extends Error {
|
|
44
|
+
status;
|
|
45
|
+
code;
|
|
46
|
+
data;
|
|
47
|
+
constructor(status, code, message, data) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.status = status;
|
|
50
|
+
this.code = code;
|
|
51
|
+
this.data = data;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function reply(status, body) {
|
|
55
|
+
return new Response(JSON.stringify(body), {
|
|
56
|
+
status,
|
|
57
|
+
headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function rpcError(id, error) {
|
|
61
|
+
return reply(error.status, {
|
|
62
|
+
jsonrpc: '2.0',
|
|
63
|
+
id,
|
|
64
|
+
error: {
|
|
65
|
+
code: error.code,
|
|
66
|
+
message: error.message,
|
|
67
|
+
...(error.data !== undefined ? { data: error.data } : {}),
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function isRecord(value) {
|
|
72
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
73
|
+
}
|
|
74
|
+
/** The JSON-RPC message in `body`; one message per POST, never a batch or a response. */
|
|
75
|
+
function rpcRequest(body) {
|
|
76
|
+
if (!isRecord(body) ||
|
|
77
|
+
body.jsonrpc !== '2.0' ||
|
|
78
|
+
typeof body.method !== 'string' ||
|
|
79
|
+
(body.id !== undefined && typeof body.id !== 'string' && typeof body.id !== 'number') ||
|
|
80
|
+
(body.params !== undefined && !isRecord(body.params)))
|
|
81
|
+
// labels-exempt: protocol diagnostic for the MCP client, not end-user copy
|
|
82
|
+
throw new RpcError(400, INVALID_REQUEST, 'Invalid request');
|
|
83
|
+
return {
|
|
84
|
+
...(body.id !== undefined ? { id: body.id } : {}),
|
|
85
|
+
method: body.method,
|
|
86
|
+
params: isRecord(body.params) ? body.params : {},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** A mirrored header's value, decoded from the `=?base64?…?=` form MCP uses for unsafe values. */
|
|
90
|
+
function headerValue(value) {
|
|
91
|
+
const encoded = /^=\?base64\?(.*)\?=$/.exec(value);
|
|
92
|
+
if (!encoded)
|
|
93
|
+
return value;
|
|
94
|
+
const bytes = Uint8Array.from(atob(encoded[1]), (char) => char.charCodeAt(0));
|
|
95
|
+
return new TextDecoder().decode(bytes);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The current revision's request checks: a version this server speaks, the capabilities the
|
|
99
|
+
* client declares, and headers that agree with the body they mirror.
|
|
100
|
+
*/
|
|
101
|
+
function checkModern(request, message, meta) {
|
|
102
|
+
const version = meta[META_VERSION];
|
|
103
|
+
if (version !== MODERN_VERSION)
|
|
104
|
+
// labels-exempt: protocol diagnostic for the MCP client, not end-user copy
|
|
105
|
+
throw new RpcError(400, UNSUPPORTED_VERSION, 'Unsupported protocol version', {
|
|
106
|
+
supported: SUPPORTED_VERSIONS,
|
|
107
|
+
requested: version,
|
|
108
|
+
});
|
|
109
|
+
if (!isRecord(meta[META_CAPABILITIES]))
|
|
110
|
+
// labels-exempt: protocol diagnostic for the MCP client, not end-user copy
|
|
111
|
+
throw new RpcError(400, INVALID_PARAMS, `Missing _meta field ${META_CAPABILITIES}`);
|
|
112
|
+
const mirrored = [
|
|
113
|
+
['mcp-protocol-version', version],
|
|
114
|
+
['mcp-method', message.method],
|
|
115
|
+
];
|
|
116
|
+
if (message.method === 'tools/call')
|
|
117
|
+
mirrored.push(['mcp-name', message.params.name]);
|
|
118
|
+
for (const [name, body] of mirrored) {
|
|
119
|
+
const header = request.headers.get(name);
|
|
120
|
+
if (header === null || headerValue(header) !== body)
|
|
121
|
+
// labels-exempt: protocol diagnostic for the MCP client, not end-user copy
|
|
122
|
+
throw new RpcError(400, HEADER_MISMATCH, `Header mismatch: ${name}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** The legacy revision a client asked for in `initialize`, or the newest one this server speaks. */
|
|
126
|
+
function legacyVersion(params) {
|
|
127
|
+
const asked = params.protocolVersion;
|
|
128
|
+
return LEGACY_VERSIONS.find((version) => version === asked) ?? LEGACY_VERSIONS[0];
|
|
129
|
+
}
|
|
130
|
+
/** What the client reads of one call, from the kernel's events for it. */
|
|
131
|
+
function callResult(events, labels) {
|
|
132
|
+
for (const event of events) {
|
|
133
|
+
if (event.type === 'error')
|
|
134
|
+
return {
|
|
135
|
+
content: [
|
|
136
|
+
{ type: 'text', text: event.error ?? labelText(`error.${event.errorKind}`, {}, labels) },
|
|
137
|
+
],
|
|
138
|
+
isError: true,
|
|
139
|
+
};
|
|
140
|
+
if (event.type !== 'tool' || !('phase' in event.tool))
|
|
141
|
+
continue;
|
|
142
|
+
const tool = event.tool;
|
|
143
|
+
if (tool.phase === 'complete')
|
|
144
|
+
return {
|
|
145
|
+
content: [
|
|
146
|
+
{ type: 'text', text: tool.readBack ?? '' },
|
|
147
|
+
...(tool.parts ?? []).map(partContent),
|
|
148
|
+
],
|
|
149
|
+
isError: false,
|
|
150
|
+
};
|
|
151
|
+
if (tool.phase === 'error')
|
|
152
|
+
return {
|
|
153
|
+
content: [{ type: 'text', text: tool.readBack ?? tool.failure.message }],
|
|
154
|
+
isError: true,
|
|
155
|
+
};
|
|
156
|
+
// why: No one can answer a gate here; the client reads why the call stopped, as a model would.
|
|
157
|
+
if (tool.phase === 'gate')
|
|
158
|
+
return {
|
|
159
|
+
content: [{ type: 'text', text: tool.readBack ?? labelText('error.blocked', {}, labels) }],
|
|
160
|
+
isError: true,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
content: [{ type: 'text', text: labelText('error.internal', {}, labels) }],
|
|
165
|
+
isError: true,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function partContent(part, index) {
|
|
169
|
+
if (part.type === 'text')
|
|
170
|
+
return { type: 'text', text: part.text };
|
|
171
|
+
if ('uri' in part)
|
|
172
|
+
return { type: 'resource_link', uri: part.uri, name: part.uri, mimeType: part.mimeType };
|
|
173
|
+
if (part.type === 'image' || part.type === 'audio')
|
|
174
|
+
return { type: part.type, data: part.data, mimeType: part.mimeType };
|
|
175
|
+
return {
|
|
176
|
+
type: 'resource',
|
|
177
|
+
resource: { uri: `part:${index}`, mimeType: part.mimeType, blob: part.data },
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/** Same origin, or one the host lists; a request with no `Origin` is not from a page. */
|
|
181
|
+
function originAllowed(request, origins) {
|
|
182
|
+
const origin = request.headers.get('origin');
|
|
183
|
+
return origin === null || origin === new URL(request.url).origin || origins.includes(origin);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Serves a host profile's tools to MCP clients. Every tool the profile allows must run
|
|
187
|
+
* without a person's approval: an MCP client has no one to ask.
|
|
188
|
+
*/
|
|
189
|
+
export function createTheoremMcpHandler(options) {
|
|
190
|
+
const host = defineHostProfile('createTheoremMcpHandler', options.profile);
|
|
191
|
+
for (const name of host.tools.allow) {
|
|
192
|
+
const permission = hostTool(name)?.permission;
|
|
193
|
+
if (permission !== undefined && permission !== 'auto') {
|
|
194
|
+
// labels-exempt: builder config error at setup; no user sees it
|
|
195
|
+
throw new Error(`createTheoremMcpHandler serves tools that run without approval; '${name}' has permission '${permission}'.`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const maxBytes = requestByteLimit(options.maxRequestBytes);
|
|
199
|
+
registerProfile(host);
|
|
200
|
+
const serverInfo = { name: options.name ?? host.id, version: options.version };
|
|
201
|
+
const instructions = options.instructions ? { instructions: options.instructions } : {};
|
|
202
|
+
const listed = () => hostInterface(host, hostTool).tools.map((tool) => ({
|
|
203
|
+
name: tool.name,
|
|
204
|
+
description: tool.description,
|
|
205
|
+
inputSchema: tool.inputSchema,
|
|
206
|
+
annotations: {
|
|
207
|
+
readOnlyHint: tool.access === 'read-only',
|
|
208
|
+
...(tool.access === 'destructive' ? { destructiveHint: true } : {}),
|
|
209
|
+
},
|
|
210
|
+
}));
|
|
211
|
+
async function call(request, params) {
|
|
212
|
+
const name = params.name;
|
|
213
|
+
if (typeof name !== 'string' || !listed().some((tool) => tool.name === name))
|
|
214
|
+
// labels-exempt: protocol diagnostic for the MCP client, not end-user copy
|
|
215
|
+
throw new RpcError(200, INVALID_PARAMS, `Unknown tool: ${String(name)}`);
|
|
216
|
+
const events = [];
|
|
217
|
+
for await (const event of invokeTool({
|
|
218
|
+
profile: host.id,
|
|
219
|
+
name,
|
|
220
|
+
input: params.arguments ?? {},
|
|
221
|
+
signal: request.signal,
|
|
222
|
+
host: await options.host?.(request),
|
|
223
|
+
}))
|
|
224
|
+
events.push(event);
|
|
225
|
+
return callResult(events, host.labels);
|
|
226
|
+
}
|
|
227
|
+
async function result(request, message, modern) {
|
|
228
|
+
switch (message.method) {
|
|
229
|
+
case 'server/discover':
|
|
230
|
+
if (!modern)
|
|
231
|
+
break;
|
|
232
|
+
return {
|
|
233
|
+
supportedVersions: SUPPORTED_VERSIONS,
|
|
234
|
+
capabilities: { tools: {} },
|
|
235
|
+
...instructions,
|
|
236
|
+
};
|
|
237
|
+
case 'initialize':
|
|
238
|
+
if (modern)
|
|
239
|
+
break;
|
|
240
|
+
return {
|
|
241
|
+
protocolVersion: legacyVersion(message.params),
|
|
242
|
+
capabilities: { tools: {} },
|
|
243
|
+
serverInfo,
|
|
244
|
+
...instructions,
|
|
245
|
+
};
|
|
246
|
+
case 'ping':
|
|
247
|
+
if (modern)
|
|
248
|
+
break;
|
|
249
|
+
return {};
|
|
250
|
+
case 'tools/list':
|
|
251
|
+
return { tools: listed() };
|
|
252
|
+
case 'tools/call':
|
|
253
|
+
return await call(request, message.params);
|
|
254
|
+
}
|
|
255
|
+
// labels-exempt: protocol diagnostic for the MCP client, not end-user copy
|
|
256
|
+
throw new RpcError(modern ? 404 : 200, METHOD_NOT_FOUND, `Method not found: ${message.method}`);
|
|
257
|
+
}
|
|
258
|
+
return async (request) => {
|
|
259
|
+
if (!originAllowed(request, options.origins ?? []))
|
|
260
|
+
// labels-exempt: protocol diagnostic for the MCP client, not end-user copy
|
|
261
|
+
return rpcError(null, new RpcError(403, INVALID_REQUEST, 'Origin not allowed'));
|
|
262
|
+
if (request.method !== 'POST')
|
|
263
|
+
return new Response(null, { status: 405, headers: { allow: 'POST' } });
|
|
264
|
+
let id = null;
|
|
265
|
+
try {
|
|
266
|
+
let body;
|
|
267
|
+
try {
|
|
268
|
+
body = await boundedRequestJson(request, maxBytes);
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
if (err instanceof SyntaxError)
|
|
272
|
+
// labels-exempt: protocol diagnostic for the MCP client, not end-user copy
|
|
273
|
+
throw new RpcError(400, PARSE_ERROR, 'Parse error');
|
|
274
|
+
throw err;
|
|
275
|
+
}
|
|
276
|
+
const message = rpcRequest(body);
|
|
277
|
+
id = message.id ?? null;
|
|
278
|
+
const meta = isRecord(message.params._meta) ? message.params._meta : {};
|
|
279
|
+
const modern = META_VERSION in meta;
|
|
280
|
+
if (modern)
|
|
281
|
+
checkModern(request, message, meta);
|
|
282
|
+
else if (request.headers.get('mcp-protocol-version') === MODERN_VERSION)
|
|
283
|
+
// labels-exempt: protocol diagnostic for the MCP client, not end-user copy
|
|
284
|
+
throw new RpcError(400, INVALID_PARAMS, `Missing _meta field ${META_VERSION}`);
|
|
285
|
+
// invariant: A notification has no reply; the client needs only to know it arrived.
|
|
286
|
+
if (message.id === undefined)
|
|
287
|
+
return new Response(null, { status: 202 });
|
|
288
|
+
const answered = await result(request, message, modern);
|
|
289
|
+
const ttlMs = CACHED[message.method];
|
|
290
|
+
return reply(200, {
|
|
291
|
+
jsonrpc: '2.0',
|
|
292
|
+
id: message.id,
|
|
293
|
+
result: modern
|
|
294
|
+
? {
|
|
295
|
+
resultType: 'complete',
|
|
296
|
+
...answered,
|
|
297
|
+
...(ttlMs !== undefined ? { ttlMs, cacheScope: 'public' } : {}),
|
|
298
|
+
_meta: { [META_SERVER]: serverInfo },
|
|
299
|
+
}
|
|
300
|
+
: answered,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
if (err instanceof RpcError)
|
|
305
|
+
return rpcError(id, err);
|
|
306
|
+
options.onError?.(err, { request });
|
|
307
|
+
const failure = clientError(err, host.labels, options.clientEvents);
|
|
308
|
+
const code = failure.errorKind === 'request' ? INVALID_REQUEST : INTERNAL_ERROR;
|
|
309
|
+
return rpcError(id, new RpcError(caughtStatus(err), code, failure.error));
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
package/server/mod.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server half of `@theoremjs/web`: serve one profile to the chat UI, a
|
|
3
|
+
* decision profile to the decision UI, or a host profile's tools to the host UI or to MCP clients.
|
|
4
|
+
* `@theoremjs/react/server` re-exports it.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
export { theoremHostCallRequestSchema } from '../client/host-transport.ts';
|
|
9
|
+
export type { LiveClientMessage, LiveOpenMessage } from '../client/live-messages.ts';
|
|
10
|
+
export { theoremConversationRequestSchema, theoremInvokeRequestSchema, theoremReplaySchema, theoremRewindRequestSchema, theoremSteerRequestSchema, theoremTurnRequestSchema, } from '../client/transport.ts';
|
|
11
|
+
export { answerReply, type ConversationHost, type ConversationUse, conversationView, type IssuedWalkAway, messageReply, type OpenedConversation, openConversation, type PostedMessage, postedMessage, type ReplyLine, type ReplyRunner, type ReplyTurn, readerConversationHost, rewindConversation, startConversation, statelessRefusal, type WalkedAwayCall, waitingCalls, } from './conversation.ts';
|
|
12
|
+
export { type ConversationSnapshot, type ConversationWriteResult, createMemoryConversationStore, createSealedConversationStore, emptyConversation, type PausedReply, type SealedConversationStoreOptions, type StoredConversation, storedConversationSchema, type TheoremConversationStore, unknownConversation, } from './conversation-store.ts';
|
|
13
|
+
export { createMemoryCredentialStore, type TheoremCredentialStore, type TheoremCredentials, } from './credential-store.ts';
|
|
14
|
+
export { createTheoremDecisionHandler, type TheoremDecisionHandlerOptions, } from './decision-handler.ts';
|
|
15
|
+
export { createTheoremEmbeddingHandler, embedBodySchema, embedRequest, type TheoremEmbeddingHandlerOptions, } from './embedding-handler.ts';
|
|
16
|
+
export { createTheoremHandler, createTheoremHostHandler, readBody, type TheoremHandlerOptions, type TheoremHostHandlerOptions, type TheoremRequestContext, theoremSession, theoremSessionId, } from './handler.ts';
|
|
17
|
+
export { createTheoremMcpHandler, type TheoremMcpHandlerOptions } from './mcp-handler.ts';
|
|
18
|
+
export { createMemoryPageToolBroker, type PageToolBroker, type PageToolResult, pageToolResultSchema, sessionPageDispatch, } from './page-dispatch.ts';
|
|
19
|
+
export { checkRequest, parseLiveClientMessage, parseLiveOpenMessage } from './request-check.ts';
|
|
20
|
+
export { sessionStateStore } from './session-authority.ts';
|
|
21
|
+
export { createTheoremSessionBoundary, type PreparedLiveOpen, type SessionBoundaryOptions, type TheoremSessionBoundary, } from './session-boundary.ts';
|
|
22
|
+
export type { TheoremSessionLimits } from './session-limits.ts';
|
|
23
|
+
export { resolveSessionLimits } from './session-limits.ts';
|
|
24
|
+
export { createMemorySessionStore, DEFAULT_OPERATION_RETENTION_MS, type MemorySessionStoreOptions, type PendingToolGate, type SessionSnapshot, type SessionToolOperation, type SessionWriteResult, type TheoremSessionState, type TheoremSessionStore, theoremSessionStateSchema, } from './session-store.ts';
|
|
25
|
+
export { createMemorySteerInbox, type SteerInbox, type SteerUnit, steerStage, steerUnitOf, } from './steer-inbox.ts';
|
|
26
|
+
export { sessionToolCoordinator } from './tool-operations.ts';
|
|
27
|
+
export { kernelTurnInput, liveSessionOpen } from './turn-input.ts';
|
package/server/mod.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server half of `@theoremjs/web`: serve one profile to the chat UI, a
|
|
3
|
+
* decision profile to the decision UI, or a host profile's tools to the host UI or to MCP clients.
|
|
4
|
+
* `@theoremjs/react/server` re-exports it.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
export { theoremHostCallRequestSchema } from "../client/host-transport.js";
|
|
9
|
+
export { theoremConversationRequestSchema, theoremInvokeRequestSchema, theoremReplaySchema, theoremRewindRequestSchema, theoremSteerRequestSchema, theoremTurnRequestSchema, } from "../client/transport.js";
|
|
10
|
+
export { answerReply, conversationView, messageReply, openConversation, postedMessage, readerConversationHost, rewindConversation, startConversation, statelessRefusal, waitingCalls, } from "./conversation.js";
|
|
11
|
+
export { createMemoryConversationStore, createSealedConversationStore, emptyConversation, storedConversationSchema, unknownConversation, } from "./conversation-store.js";
|
|
12
|
+
export { createMemoryCredentialStore, } from "./credential-store.js";
|
|
13
|
+
export { createTheoremDecisionHandler, } from "./decision-handler.js";
|
|
14
|
+
export { createTheoremEmbeddingHandler, embedBodySchema, embedRequest, } from "./embedding-handler.js";
|
|
15
|
+
export { createTheoremHandler, createTheoremHostHandler, readBody, theoremSession, theoremSessionId, } from "./handler.js";
|
|
16
|
+
export { createTheoremMcpHandler } from "./mcp-handler.js";
|
|
17
|
+
export { createMemoryPageToolBroker, pageToolResultSchema, sessionPageDispatch, } from "./page-dispatch.js";
|
|
18
|
+
export { checkRequest, parseLiveClientMessage, parseLiveOpenMessage } from "./request-check.js";
|
|
19
|
+
export { sessionStateStore } from "./session-authority.js";
|
|
20
|
+
export { createTheoremSessionBoundary, } from "./session-boundary.js";
|
|
21
|
+
export { resolveSessionLimits } from "./session-limits.js";
|
|
22
|
+
export { createMemorySessionStore, DEFAULT_OPERATION_RETENTION_MS, theoremSessionStateSchema, } from "./session-store.js";
|
|
23
|
+
export { createMemorySteerInbox, steerStage, steerUnitOf, } from "./steer-inbox.js";
|
|
24
|
+
export { sessionToolCoordinator } from "./tool-operations.js";
|
|
25
|
+
export { kernelTurnInput, liveSessionOpen } from "./turn-input.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type PageAnswer, type PageToolExecution, z } from '@theoremjs/agents';
|
|
2
|
+
import type { MutableStore } from './session-authority.ts';
|
|
3
|
+
import type { TheoremSessionState } from './session-store.ts';
|
|
4
|
+
export type PageToolResult = {
|
|
5
|
+
callId: string;
|
|
6
|
+
operationId: string;
|
|
7
|
+
grantId: string;
|
|
8
|
+
page: PageAnswer;
|
|
9
|
+
};
|
|
10
|
+
export declare const pageToolResultSchema: z.ZodType<PageToolResult>;
|
|
11
|
+
export interface PageToolBroker {
|
|
12
|
+
reserve(sessionId: string, grantId: string, signal?: AbortSignal): {
|
|
13
|
+
result: Promise<PageAnswer>;
|
|
14
|
+
cancel(): void;
|
|
15
|
+
};
|
|
16
|
+
deliver(sessionId: string, grantId: string, page: PageAnswer): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export declare function createMemoryPageToolBroker(maxPending?: number): PageToolBroker;
|
|
19
|
+
export declare function sessionPageDispatch(store: MutableStore<TheoremSessionState>, sessionId: string, broker: PageToolBroker, timeoutMs?: number): {
|
|
20
|
+
execution: PageToolExecution;
|
|
21
|
+
answer(result: PageToolResult): Promise<boolean>;
|
|
22
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { jsonValueSchema, labelText, TheoremError, toolInvocationHash, z, } from '@theoremjs/agents';
|
|
2
|
+
export const pageToolResultSchema = z.strictObject({
|
|
3
|
+
callId: z.string().min(1),
|
|
4
|
+
operationId: z.string().min(1).max(256),
|
|
5
|
+
grantId: z.string().min(1),
|
|
6
|
+
page: z.union([
|
|
7
|
+
z.strictObject({ output: jsonValueSchema }),
|
|
8
|
+
z.strictObject({ unanswered: z.literal(true) }),
|
|
9
|
+
z.strictObject({ timedOut: z.literal(true) }),
|
|
10
|
+
]),
|
|
11
|
+
});
|
|
12
|
+
function unavailable() {
|
|
13
|
+
throw new TheoremError('unavailable', labelText('tool.execution_uncertain'), {
|
|
14
|
+
copy: { key: 'tool.execution_uncertain' },
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export function createMemoryPageToolBroker(maxPending = 128) {
|
|
18
|
+
if (!Number.isSafeInteger(maxPending) || maxPending <= 0)
|
|
19
|
+
throw new TheoremError('config', labelText('error.config'));
|
|
20
|
+
const waiting = new Map();
|
|
21
|
+
const key = (sessionId, grantId) => JSON.stringify([sessionId, grantId]);
|
|
22
|
+
return {
|
|
23
|
+
reserve(sessionId, grantId, signal) {
|
|
24
|
+
const id = key(sessionId, grantId);
|
|
25
|
+
if (signal?.aborted || waiting.has(id) || waiting.size >= maxPending)
|
|
26
|
+
unavailable();
|
|
27
|
+
let finish = () => { };
|
|
28
|
+
const result = new Promise((resolve) => {
|
|
29
|
+
finish = resolve;
|
|
30
|
+
});
|
|
31
|
+
const cancel = () => {
|
|
32
|
+
waiting.delete(id);
|
|
33
|
+
signal?.removeEventListener('abort', cancel);
|
|
34
|
+
finish({ timedOut: true });
|
|
35
|
+
};
|
|
36
|
+
waiting.set(id, (answer) => {
|
|
37
|
+
waiting.delete(id);
|
|
38
|
+
signal?.removeEventListener('abort', cancel);
|
|
39
|
+
finish(answer);
|
|
40
|
+
});
|
|
41
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
42
|
+
return { result, cancel };
|
|
43
|
+
},
|
|
44
|
+
deliver(sessionId, grantId, page) {
|
|
45
|
+
const finish = waiting.get(key(sessionId, grantId));
|
|
46
|
+
if (!finish)
|
|
47
|
+
return Promise.reject(new TheoremError('unavailable', labelText('tool.execution_uncertain')));
|
|
48
|
+
finish(page);
|
|
49
|
+
return Promise.resolve();
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function sessionPageDispatch(store, sessionId, broker, timeoutMs = 20_000) {
|
|
54
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647)
|
|
55
|
+
throw new TheoremError('config', labelText('error.config'));
|
|
56
|
+
return {
|
|
57
|
+
execution: {
|
|
58
|
+
async dispatch(claim, signal) {
|
|
59
|
+
if (!claim.invocation)
|
|
60
|
+
unavailable();
|
|
61
|
+
const grantId = crypto.randomUUID();
|
|
62
|
+
const expiresAt = Date.now() + timeoutMs;
|
|
63
|
+
const pending = broker.reserve(sessionId, grantId, signal);
|
|
64
|
+
try {
|
|
65
|
+
await store.mutate(sessionId, (state) => {
|
|
66
|
+
const record = state.operations[claim.operationId];
|
|
67
|
+
if (record?.status !== 'running' ||
|
|
68
|
+
record.claimToken !== claim.claimToken ||
|
|
69
|
+
record.invocationHash !== claim.invocationHash ||
|
|
70
|
+
record.pageGrant)
|
|
71
|
+
unavailable();
|
|
72
|
+
record.pageGrant = { grantId, expiresAt };
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
pending.cancel();
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
const remaining = expiresAt - Date.now();
|
|
80
|
+
if (remaining <= 0 || signal?.aborted) {
|
|
81
|
+
pending.cancel();
|
|
82
|
+
unavailable();
|
|
83
|
+
}
|
|
84
|
+
let expire = () => { };
|
|
85
|
+
const expired = new Promise((resolve) => {
|
|
86
|
+
expire = () => resolve({ timedOut: true });
|
|
87
|
+
});
|
|
88
|
+
const timer = setTimeout(expire, remaining);
|
|
89
|
+
signal?.addEventListener('abort', expire, { once: true });
|
|
90
|
+
return {
|
|
91
|
+
grantId,
|
|
92
|
+
result: Promise.race([pending.result, expired]).finally(() => {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
signal?.removeEventListener('abort', expire);
|
|
95
|
+
pending.cancel();
|
|
96
|
+
}),
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
async answer(raw) {
|
|
101
|
+
const result = pageToolResultSchema.parse(raw);
|
|
102
|
+
const hash = await toolInvocationHash(result.page);
|
|
103
|
+
const duplicate = await store.mutate(sessionId, (state) => {
|
|
104
|
+
const record = state.operations[result.operationId];
|
|
105
|
+
if (!record ||
|
|
106
|
+
record.status === 'ready' ||
|
|
107
|
+
record.callId !== result.callId ||
|
|
108
|
+
record.pageGrant?.grantId !== result.grantId)
|
|
109
|
+
unavailable();
|
|
110
|
+
if (record.pageGrant.resultHash !== undefined) {
|
|
111
|
+
if (record.pageGrant.resultHash !== hash)
|
|
112
|
+
unavailable();
|
|
113
|
+
if (record.status === 'uncertain')
|
|
114
|
+
unavailable();
|
|
115
|
+
return { duplicate: true, settled: record.status === 'settled' };
|
|
116
|
+
}
|
|
117
|
+
if (record.status !== 'running' || Date.now() >= record.pageGrant.expiresAt)
|
|
118
|
+
unavailable();
|
|
119
|
+
record.pageGrant.resultHash = hash;
|
|
120
|
+
return { duplicate: false, settled: false };
|
|
121
|
+
});
|
|
122
|
+
if (!duplicate.duplicate)
|
|
123
|
+
await broker.deliver(sessionId, result.grantId, result.page);
|
|
124
|
+
return duplicate.settled;
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { labelText, TheoremError, throwIfAborted } from '@theoremjs/agents';
|
|
2
|
+
const DEFAULT_REQUEST_BYTES = 16 * 1024 * 1024;
|
|
3
|
+
export function requestByteLimit(bytes = DEFAULT_REQUEST_BYTES) {
|
|
4
|
+
if (!Number.isSafeInteger(bytes) || bytes <= 0) {
|
|
5
|
+
throw new TheoremError('config', labelText('error.config'));
|
|
6
|
+
}
|
|
7
|
+
return bytes;
|
|
8
|
+
}
|
|
9
|
+
export async function boundedRequestJson(request, maxBytes) {
|
|
10
|
+
if (Number(request.headers.get('content-length')) > maxBytes || !request.body) {
|
|
11
|
+
throw new TheoremError('request', labelText('error.request'));
|
|
12
|
+
}
|
|
13
|
+
const reader = request.body.getReader();
|
|
14
|
+
const abort = () => {
|
|
15
|
+
void reader.cancel(request.signal.reason).catch(() => undefined);
|
|
16
|
+
};
|
|
17
|
+
request.signal.addEventListener('abort', abort, { once: true });
|
|
18
|
+
const decoder = new TextDecoder();
|
|
19
|
+
const text = [];
|
|
20
|
+
let bytes = 0;
|
|
21
|
+
try {
|
|
22
|
+
for (;;) {
|
|
23
|
+
throwIfAborted(request.signal);
|
|
24
|
+
const next = await reader.read();
|
|
25
|
+
throwIfAborted(request.signal);
|
|
26
|
+
if (next.done)
|
|
27
|
+
break;
|
|
28
|
+
bytes += next.value.byteLength;
|
|
29
|
+
if (bytes > maxBytes)
|
|
30
|
+
throw new TheoremError('request', labelText('error.request'));
|
|
31
|
+
if (next.value.byteLength)
|
|
32
|
+
text.push(decoder.decode(next.value, { stream: true }));
|
|
33
|
+
}
|
|
34
|
+
text.push(decoder.decode());
|
|
35
|
+
return JSON.parse(text.join(''));
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
request.signal.removeEventListener('abort', abort);
|
|
39
|
+
void reader.cancel().catch(() => undefined);
|
|
40
|
+
reader.releaseLock();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a browser sends a host, checked: the handler's request bodies and a
|
|
3
|
+
* relay's live messages. A malformed one is a `request` error, which names
|
|
4
|
+
* only the paths and codes that broke, never the value.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import { type z } from '@theoremjs/agents';
|
|
9
|
+
import { type LiveClientMessage, type LiveOpenMessage } from '../client/live-messages.ts';
|
|
10
|
+
/** `raw` checked against `schema`; a missing or malformed field is a `request` error. */
|
|
11
|
+
export declare function checkRequest<T>(schema: z.ZodType<T>, raw: unknown, what: string): T;
|
|
12
|
+
/**
|
|
13
|
+
* One text frame from the live client, as a relay reads it: JSON that passes
|
|
14
|
+
* `liveClientMessageSchema`, else a `request` error.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseLiveClientMessage(text: string): LiveClientMessage;
|
|
17
|
+
/**
|
|
18
|
+
* The live client's first frame, as a relay reads it before it opens the
|
|
19
|
+
* session: JSON that passes `liveOpenMessageSchema`, else a `request` error.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseLiveOpenMessage(text: string): LiveOpenMessage;
|