@llui/agent 0.0.52 → 0.0.53

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.
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod';
2
+ export interface McpForwardedToolDescriptor {
3
+ kind: 'forward';
4
+ name: string;
5
+ description: string;
6
+ schema: z.ZodObject<z.ZodRawShape>;
7
+ /** LAP endpoint path relative to the base path, e.g. '/observe'. */
8
+ lapPath: string;
9
+ }
10
+ export interface McpMetaToolDescriptor {
11
+ kind: 'meta';
12
+ name: string;
13
+ description: string;
14
+ schema: z.ZodObject<z.ZodRawShape>;
15
+ }
16
+ export type McpToolDescriptor = McpForwardedToolDescriptor | McpMetaToolDescriptor;
17
+ export declare const DISCONNECT_SESSION_DESCRIPTOR: McpMetaToolDescriptor;
18
+ export declare const FORWARDED_TOOL_DESCRIPTORS: McpForwardedToolDescriptor[];
19
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/mcp/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAcvB,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,SAAS,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAA;IAClC,oEAAoE;IACpE,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAA;CACnC;AAED,MAAM,MAAM,iBAAiB,GAAG,0BAA0B,GAAG,qBAAqB,CAAA;AAElF,eAAO,MAAM,6BAA6B,EAAE,qBAK3C,CAAA;AAED,eAAO,MAAM,0BAA0B,EAAE,0BAA0B,EAmLlE,CAAA"}
@@ -0,0 +1,176 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Shared MCP tool catalogue for the LLui agent. Consumed by both:
4
+ * - `@llui/agent` server-side MCP router (routes to LAP handlers internally)
5
+ * - `llui-agent` bridge (forwards to remote LAP over HTTP)
6
+ *
7
+ * `connect_session` is intentionally absent — the two surfaces have
8
+ * different signatures (bridge needs `url` + `token`; server only needs
9
+ * `token`). Each defines its own.
10
+ */
11
+ const empty = z.object({});
12
+ export const DISCONNECT_SESSION_DESCRIPTOR = {
13
+ kind: 'meta',
14
+ name: 'disconnect_session',
15
+ description: 'Clear the session binding. Subsequent tool calls will fail until reconnected.',
16
+ schema: empty,
17
+ };
18
+ export const FORWARDED_TOOL_DESCRIPTORS = [
19
+ {
20
+ kind: 'forward',
21
+ name: 'observe',
22
+ description: 'Unified snapshot — returns {state, actions, description, context} in one call. Use this as the default "what can I see, what can I do" read; prefer it over describe_app + get_state + list_actions. Typical flow: observe → send_message → (repeat). The response includes the static app description (name, version, msgSchema, docs) on every call so first-time callers do not need a separate describe_app.',
23
+ schema: empty,
24
+ lapPath: '/observe',
25
+ },
26
+ {
27
+ kind: 'forward',
28
+ name: 'describe_app',
29
+ description: "Return the bound app's name, version, state/message schemas, annotations, and static docs. Legacy — prefer `observe`, which includes this as `description`.",
30
+ schema: empty,
31
+ lapPath: '/describe',
32
+ },
33
+ {
34
+ kind: 'forward',
35
+ name: 'get_state',
36
+ description: 'Return the current app state. Optional `path` (JSON-pointer) to narrow the slice. Legacy for full-state reads — prefer `observe`. Still useful for scoped reads via JSON pointer.',
37
+ schema: z.object({
38
+ path: z.string().optional().describe('Optional JSON-pointer, e.g. "/user/name"'),
39
+ }),
40
+ lapPath: '/state',
41
+ },
42
+ {
43
+ kind: 'forward',
44
+ name: 'query_state',
45
+ description: 'Read a single slice of state via JSON-pointer path. Returns `{found: true, value}` on hit or `{found: false, detail}` on miss (missing key, walking through null, etc.). Cheaper than `observe` when checking one field. Path syntax: `""` (whole state), `"/auth/user"`, `"/items/0/id"`, `"/key~1with~1slash"` (escaped `/`), `"/key~0tilde"` (escaped `~`).',
46
+ schema: z.object({
47
+ path: z.string().describe('JSON-pointer (RFC 6901) — `/auth/user` or `""` for whole state'),
48
+ }),
49
+ lapPath: '/query-state',
50
+ },
51
+ {
52
+ kind: 'forward',
53
+ name: 'describe_recent_actions',
54
+ description: 'Return the most recent log entries for this session (newest first). Each `dispatched` entry includes a `stateDiff` showing what changed. Useful for self-correction over multi-step flows — read your own past dispatches without re-querying full state. Filter by `kind` (e.g. `"dispatched"`) to skip read-only entries.',
55
+ schema: z.object({
56
+ n: z.number().int().positive().optional().describe('How many entries to return (default 10)'),
57
+ kind: z
58
+ .string()
59
+ .optional()
60
+ .describe('Filter to a specific kind (e.g. "dispatched", "read", "error")'),
61
+ }),
62
+ lapPath: '/recent-actions',
63
+ },
64
+ {
65
+ kind: 'forward',
66
+ name: 'would_dispatch',
67
+ description: 'Predict what dispatching `msg` would do without committing it. Runs the reducer in isolation against current state and returns `{stateDiff, effects}`. Effects are listed but NOT executed — the cloud is not hit, analytics do not fire. Use this to weigh a candidate action before sending: "if I dispatch X, will it change Y?" Pure-reducer assumption: if the reducer branches on Date.now() / localStorage / random, prediction drifts from real dispatch by exactly that impurity.',
68
+ schema: z.object({
69
+ msg: z
70
+ .object({ type: z.string() })
71
+ .passthrough()
72
+ .describe('The candidate message; must have a `type` string'),
73
+ }),
74
+ lapPath: '/would-dispatch',
75
+ },
76
+ {
77
+ kind: 'forward',
78
+ name: 'list_actions',
79
+ description: 'Return the currently-affordable actions: visible UI bindings plus agent-affordable registry entries, filtered by annotation gates. Legacy — prefer `observe`, which includes this as `actions`.',
80
+ schema: empty,
81
+ lapPath: '/actions',
82
+ },
83
+ {
84
+ kind: 'forward',
85
+ name: 'send_message',
86
+ description: 'Dispatch a message to the app. Blocks by default until the message queue goes idle (drain semantics — captures http/delay/debounce round-trips that feed back as messages). Returns {status, stateDiff, actions, drain} on dispatched, {status: "pending-confirmation", confirmId} when the variant is @requiresConfirm, or {status: "rejected", reason} on validation failures. By default the response carries `stateDiff` (a JSON-Patch-shaped delta) and not the full post-state — apply the diff to the snapshot you got from `connect`/`observe`. Pass `includeState: true` if you want the full snapshot back (rare; expensive on bandwidth and context for large states). `drain.timedOut: true` means the 5s cap was hit while messages were still arriving — follow up with `observe` to resync. `actions` in the response reflects the new state, so you normally do not need a separate `observe` after a send.',
87
+ schema: z.object({
88
+ msg: z
89
+ .object({ type: z.string() })
90
+ .passthrough()
91
+ .describe('The message to dispatch; must have a `type` string'),
92
+ reason: z
93
+ .string()
94
+ .optional()
95
+ .describe('User-facing rationale (required for confirm-gated variants)'),
96
+ waitFor: z
97
+ .enum(['drained', 'idle', 'none'])
98
+ .optional()
99
+ .describe('"drained" (default) waits for the message queue to go idle; "idle" flushes the update cycle only (no async effects); "none" is fire-and-forget.'),
100
+ drainQuietMs: z
101
+ .number()
102
+ .optional()
103
+ .describe('Quiescence window for waitFor:"drained". Drain completes when no commit fires for this many ms. Default 100.'),
104
+ timeoutMs: z
105
+ .number()
106
+ .optional()
107
+ .describe('Hard cap on total wait. Default 5000. For waitFor:"drained", this bounds how long the drain loop runs; for pending-confirmation, how long to wait for user approval.'),
108
+ includeState: z
109
+ .boolean()
110
+ .optional()
111
+ .describe('Include the full post-drain `stateAfter` snapshot in the response. Default false — `stateDiff` is what callers normally need, and resending the full state on every dispatch wastes bandwidth and context. Set true only when you need a fresh snapshot back (e.g., after a long-running effect that may have produced changes the diff misses).'),
112
+ }),
113
+ lapPath: '/message',
114
+ },
115
+ {
116
+ kind: 'forward',
117
+ name: 'get_confirm_result',
118
+ description: 'Poll a pending-confirmation by confirmId. Returns confirmed / rejected / still-pending.',
119
+ schema: z.object({
120
+ confirmId: z.string(),
121
+ timeoutMs: z.number().optional(),
122
+ }),
123
+ lapPath: '/confirm-result',
124
+ },
125
+ {
126
+ kind: 'forward',
127
+ name: 'wait_for_change',
128
+ description: 'Long-poll for a state change. Returns changed / timeout. Specialized — use for external state pushes (WebSocket messages, timers) that arrive while Claude is idle. For the normal send-then-read loop, `send_message` with `waitFor:"drained"` already waits for effect round-trips.',
129
+ schema: z.object({
130
+ path: z
131
+ .string()
132
+ .optional()
133
+ .describe('Optional JSON-pointer to narrow which state changes trigger resolution'),
134
+ timeoutMs: z.number().optional(),
135
+ }),
136
+ lapPath: '/wait',
137
+ },
138
+ {
139
+ kind: 'forward',
140
+ name: 'narrate',
141
+ description: 'Push a one-line prose update into the in-app activity feed without dispatching a Msg. Use BEFORE long-running actions ("Looking at your cart now…", "Calling the pricing API — should take ~2s"), to surface inferred reasoning ("I notice you have an unsaved draft in `notes` — assuming you want to keep it"), or to acknowledge user input before acting on it. Keep narration single-line and present-tense; the user reads it in the activity panel inline with your dispatched actions. The text shows up as a `narrate`-kind entry — distinct from `dispatched` and `read`. Returns { ok: true } once the host runtime has the entry.',
142
+ schema: z.object({
143
+ text: z.string().describe('The narration prose, ideally one sentence. Required.'),
144
+ intent: z
145
+ .string()
146
+ .optional()
147
+ .describe('Short label shown next to the narration (e.g. "Thinking", "Notice", "Plan"). Defaults to "Agent narrated".'),
148
+ }),
149
+ lapPath: '/narrate',
150
+ },
151
+ {
152
+ kind: 'forward',
153
+ name: 'query_dom',
154
+ description: 'Read elements tagged with data-agent="<name>" in the rendered UI.',
155
+ schema: z.object({
156
+ name: z.string(),
157
+ multiple: z.boolean().optional(),
158
+ }),
159
+ lapPath: '/query-dom',
160
+ },
161
+ {
162
+ kind: 'forward',
163
+ name: 'describe_visible_content',
164
+ description: 'Return a structured outline of the currently-visible data-agent-tagged subtrees.',
165
+ schema: empty,
166
+ lapPath: '/describe-visible',
167
+ },
168
+ {
169
+ kind: 'forward',
170
+ name: 'describe_context',
171
+ description: 'Return the current per-state narrative docs (agentContext) — what the user is trying to do right now.',
172
+ schema: empty,
173
+ lapPath: '/context',
174
+ },
175
+ ];
176
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../../src/mcp/tools.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from 'zod'\n\n/**\n * Shared MCP tool catalogue for the LLui agent. Consumed by both:\n * - `@llui/agent` server-side MCP router (routes to LAP handlers internally)\n * - `llui-agent` bridge (forwards to remote LAP over HTTP)\n *\n * `connect_session` is intentionally absent — the two surfaces have\n * different signatures (bridge needs `url` + `token`; server only needs\n * `token`). Each defines its own.\n */\n\nconst empty = z.object({})\n\nexport interface McpForwardedToolDescriptor {\n kind: 'forward'\n name: string\n description: string\n schema: z.ZodObject<z.ZodRawShape>\n /** LAP endpoint path relative to the base path, e.g. '/observe'. */\n lapPath: string\n}\n\nexport interface McpMetaToolDescriptor {\n kind: 'meta'\n name: string\n description: string\n schema: z.ZodObject<z.ZodRawShape>\n}\n\nexport type McpToolDescriptor = McpForwardedToolDescriptor | McpMetaToolDescriptor\n\nexport const DISCONNECT_SESSION_DESCRIPTOR: McpMetaToolDescriptor = {\n kind: 'meta',\n name: 'disconnect_session',\n description: 'Clear the session binding. Subsequent tool calls will fail until reconnected.',\n schema: empty,\n}\n\nexport const FORWARDED_TOOL_DESCRIPTORS: McpForwardedToolDescriptor[] = [\n {\n kind: 'forward',\n name: 'observe',\n description:\n 'Unified snapshot — returns {state, actions, description, context} in one call. Use this as the default \"what can I see, what can I do\" read; prefer it over describe_app + get_state + list_actions. Typical flow: observe → send_message → (repeat). The response includes the static app description (name, version, msgSchema, docs) on every call so first-time callers do not need a separate describe_app.',\n schema: empty,\n lapPath: '/observe',\n },\n {\n kind: 'forward',\n name: 'describe_app',\n description:\n \"Return the bound app's name, version, state/message schemas, annotations, and static docs. Legacy — prefer `observe`, which includes this as `description`.\",\n schema: empty,\n lapPath: '/describe',\n },\n {\n kind: 'forward',\n name: 'get_state',\n description:\n 'Return the current app state. Optional `path` (JSON-pointer) to narrow the slice. Legacy for full-state reads — prefer `observe`. Still useful for scoped reads via JSON pointer.',\n schema: z.object({\n path: z.string().optional().describe('Optional JSON-pointer, e.g. \"/user/name\"'),\n }),\n lapPath: '/state',\n },\n {\n kind: 'forward',\n name: 'query_state',\n description:\n 'Read a single slice of state via JSON-pointer path. Returns `{found: true, value}` on hit or `{found: false, detail}` on miss (missing key, walking through null, etc.). Cheaper than `observe` when checking one field. Path syntax: `\"\"` (whole state), `\"/auth/user\"`, `\"/items/0/id\"`, `\"/key~1with~1slash\"` (escaped `/`), `\"/key~0tilde\"` (escaped `~`).',\n schema: z.object({\n path: z.string().describe('JSON-pointer (RFC 6901) — `/auth/user` or `\"\"` for whole state'),\n }),\n lapPath: '/query-state',\n },\n {\n kind: 'forward',\n name: 'describe_recent_actions',\n description:\n 'Return the most recent log entries for this session (newest first). Each `dispatched` entry includes a `stateDiff` showing what changed. Useful for self-correction over multi-step flows — read your own past dispatches without re-querying full state. Filter by `kind` (e.g. `\"dispatched\"`) to skip read-only entries.',\n schema: z.object({\n n: z.number().int().positive().optional().describe('How many entries to return (default 10)'),\n kind: z\n .string()\n .optional()\n .describe('Filter to a specific kind (e.g. \"dispatched\", \"read\", \"error\")'),\n }),\n lapPath: '/recent-actions',\n },\n {\n kind: 'forward',\n name: 'would_dispatch',\n description:\n 'Predict what dispatching `msg` would do without committing it. Runs the reducer in isolation against current state and returns `{stateDiff, effects}`. Effects are listed but NOT executed — the cloud is not hit, analytics do not fire. Use this to weigh a candidate action before sending: \"if I dispatch X, will it change Y?\" Pure-reducer assumption: if the reducer branches on Date.now() / localStorage / random, prediction drifts from real dispatch by exactly that impurity.',\n schema: z.object({\n msg: z\n .object({ type: z.string() })\n .passthrough()\n .describe('The candidate message; must have a `type` string'),\n }),\n lapPath: '/would-dispatch',\n },\n {\n kind: 'forward',\n name: 'list_actions',\n description:\n 'Return the currently-affordable actions: visible UI bindings plus agent-affordable registry entries, filtered by annotation gates. Legacy — prefer `observe`, which includes this as `actions`.',\n schema: empty,\n lapPath: '/actions',\n },\n {\n kind: 'forward',\n name: 'send_message',\n description:\n 'Dispatch a message to the app. Blocks by default until the message queue goes idle (drain semantics — captures http/delay/debounce round-trips that feed back as messages). Returns {status, stateDiff, actions, drain} on dispatched, {status: \"pending-confirmation\", confirmId} when the variant is @requiresConfirm, or {status: \"rejected\", reason} on validation failures. By default the response carries `stateDiff` (a JSON-Patch-shaped delta) and not the full post-state — apply the diff to the snapshot you got from `connect`/`observe`. Pass `includeState: true` if you want the full snapshot back (rare; expensive on bandwidth and context for large states). `drain.timedOut: true` means the 5s cap was hit while messages were still arriving — follow up with `observe` to resync. `actions` in the response reflects the new state, so you normally do not need a separate `observe` after a send.',\n schema: z.object({\n msg: z\n .object({ type: z.string() })\n .passthrough()\n .describe('The message to dispatch; must have a `type` string'),\n reason: z\n .string()\n .optional()\n .describe('User-facing rationale (required for confirm-gated variants)'),\n waitFor: z\n .enum(['drained', 'idle', 'none'])\n .optional()\n .describe(\n '\"drained\" (default) waits for the message queue to go idle; \"idle\" flushes the update cycle only (no async effects); \"none\" is fire-and-forget.',\n ),\n drainQuietMs: z\n .number()\n .optional()\n .describe(\n 'Quiescence window for waitFor:\"drained\". Drain completes when no commit fires for this many ms. Default 100.',\n ),\n timeoutMs: z\n .number()\n .optional()\n .describe(\n 'Hard cap on total wait. Default 5000. For waitFor:\"drained\", this bounds how long the drain loop runs; for pending-confirmation, how long to wait for user approval.',\n ),\n includeState: z\n .boolean()\n .optional()\n .describe(\n 'Include the full post-drain `stateAfter` snapshot in the response. Default false — `stateDiff` is what callers normally need, and resending the full state on every dispatch wastes bandwidth and context. Set true only when you need a fresh snapshot back (e.g., after a long-running effect that may have produced changes the diff misses).',\n ),\n }),\n lapPath: '/message',\n },\n {\n kind: 'forward',\n name: 'get_confirm_result',\n description:\n 'Poll a pending-confirmation by confirmId. Returns confirmed / rejected / still-pending.',\n schema: z.object({\n confirmId: z.string(),\n timeoutMs: z.number().optional(),\n }),\n lapPath: '/confirm-result',\n },\n {\n kind: 'forward',\n name: 'wait_for_change',\n description:\n 'Long-poll for a state change. Returns changed / timeout. Specialized — use for external state pushes (WebSocket messages, timers) that arrive while Claude is idle. For the normal send-then-read loop, `send_message` with `waitFor:\"drained\"` already waits for effect round-trips.',\n schema: z.object({\n path: z\n .string()\n .optional()\n .describe('Optional JSON-pointer to narrow which state changes trigger resolution'),\n timeoutMs: z.number().optional(),\n }),\n lapPath: '/wait',\n },\n {\n kind: 'forward',\n name: 'narrate',\n description:\n 'Push a one-line prose update into the in-app activity feed without dispatching a Msg. Use BEFORE long-running actions (\"Looking at your cart now…\", \"Calling the pricing API — should take ~2s\"), to surface inferred reasoning (\"I notice you have an unsaved draft in `notes` — assuming you want to keep it\"), or to acknowledge user input before acting on it. Keep narration single-line and present-tense; the user reads it in the activity panel inline with your dispatched actions. The text shows up as a `narrate`-kind entry — distinct from `dispatched` and `read`. Returns { ok: true } once the host runtime has the entry.',\n schema: z.object({\n text: z.string().describe('The narration prose, ideally one sentence. Required.'),\n intent: z\n .string()\n .optional()\n .describe(\n 'Short label shown next to the narration (e.g. \"Thinking\", \"Notice\", \"Plan\"). Defaults to \"Agent narrated\".',\n ),\n }),\n lapPath: '/narrate',\n },\n {\n kind: 'forward',\n name: 'query_dom',\n description: 'Read elements tagged with data-agent=\"<name>\" in the rendered UI.',\n schema: z.object({\n name: z.string(),\n multiple: z.boolean().optional(),\n }),\n lapPath: '/query-dom',\n },\n {\n kind: 'forward',\n name: 'describe_visible_content',\n description: 'Return a structured outline of the currently-visible data-agent-tagged subtrees.',\n schema: empty,\n lapPath: '/describe-visible',\n },\n {\n kind: 'forward',\n name: 'describe_context',\n description:\n 'Return the current per-state narrative docs (agentContext) — what the user is trying to do right now.',\n schema: empty,\n lapPath: '/context',\n },\n]\n"]}
@@ -46,7 +46,16 @@
46
46
  * See `./worker.ts` for `routeToAgentDO` and the full wiring.
47
47
  */
48
48
  import type { CoreOptions, AgentCoreHandle } from '../core.js';
49
- export type DurableObjectOptions = Omit<CoreOptions, 'registry'>;
49
+ import type { McpRouterOptions } from '../mcp/router.js';
50
+ export type DurableObjectOptions = Omit<CoreOptions, 'registry'> & {
51
+ /**
52
+ * Enable the server-side MCP endpoint at `/agent/mcp` (or a custom
53
+ * path). Pass `true` for all defaults, or an `McpRouterOptions`
54
+ * object to customise path, server name, and connect_session
55
+ * description.
56
+ */
57
+ mcp?: boolean | McpRouterOptions;
58
+ };
50
59
  /**
51
60
  * Agent server instance scoped to a single Durable Object. All
52
61
  * pairing state lives in the DO's in-process memory — which is safe
@@ -54,11 +63,13 @@ export type DurableObjectOptions = Omit<CoreOptions, 'registry'>;
54
63
  * one-shot Worker isolate.
55
64
  *
56
65
  * Users instantiate one of these inside their DO class's constructor
57
- * and delegate `fetch` to `agent.fetch(req)`. LAP HTTP routes and
58
- * WebSocket upgrades both flow through this single entry.
66
+ * and delegate `fetch` to `agent.fetch(req)`. LAP HTTP routes,
67
+ * WebSocket upgrades, and the optional MCP endpoint all flow through
68
+ * this single entry.
59
69
  */
60
70
  export declare class AgentPairingDurableObject {
61
71
  readonly agent: AgentCoreHandle;
72
+ private readonly mcpRouter;
62
73
  constructor(opts: DurableObjectOptions);
63
74
  fetch(req: Request): Promise<Response>;
64
75
  }
@@ -1 +1 @@
1
- {"version":3,"file":"durable-object.d.ts","sourceRoot":"","sources":["../../../src/server/cloudflare/durable-object.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAI9D,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,CAAA;AAEhE;;;;;;;;;GASG;AACH,qBAAa,yBAAyB;IACpC,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAA;gBAEnB,IAAI,EAAE,oBAAoB;IAIhC,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;CAgB7C"}
1
+ {"version":3,"file":"durable-object.d.ts","sourceRoot":"","sources":["../../../src/server/cloudflare/durable-object.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAG9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAGxD,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,GAAG;IACjE;;;;;OAKG;IACH,GAAG,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAA;CACjC,CAAA;AAED;;;;;;;;;;GAUG;AACH,qBAAa,yBAAyB;IACpC,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAA;IAC/B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqD;gBAEnE,IAAI,EAAE,oBAAoB;IAehC,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;CAsB7C"}
@@ -1,5 +1,6 @@
1
1
  import { createLluiAgentCore } from '../core.js';
2
2
  import { handleCloudflareUpgrade } from '../web/upgrade.js';
3
+ import { createMcpRouter } from '../mcp/router.js';
3
4
  /**
4
5
  * Agent server instance scoped to a single Durable Object. All
5
6
  * pairing state lives in the DO's in-process memory — which is safe
@@ -7,16 +8,33 @@ import { handleCloudflareUpgrade } from '../web/upgrade.js';
7
8
  * one-shot Worker isolate.
8
9
  *
9
10
  * Users instantiate one of these inside their DO class's constructor
10
- * and delegate `fetch` to `agent.fetch(req)`. LAP HTTP routes and
11
- * WebSocket upgrades both flow through this single entry.
11
+ * and delegate `fetch` to `agent.fetch(req)`. LAP HTTP routes,
12
+ * WebSocket upgrades, and the optional MCP endpoint all flow through
13
+ * this single entry.
12
14
  */
13
15
  export class AgentPairingDurableObject {
14
16
  agent;
17
+ mcpRouter;
15
18
  constructor(opts) {
16
- this.agent = createLluiAgentCore(opts);
19
+ const { mcp, ...coreOpts } = opts;
20
+ this.agent = createLluiAgentCore(coreOpts);
21
+ if (mcp) {
22
+ const mcpOpts = mcp === true ? {} : mcp;
23
+ const lapBasePath = coreOpts.lapBasePath ?? '/agent/lap/v1';
24
+ this.mcpRouter = createMcpRouter({ coreRouter: this.agent.router, tokenStore: this.agent.tokenStore, lapBasePath }, mcpOpts);
25
+ }
26
+ else {
27
+ this.mcpRouter = null;
28
+ }
17
29
  }
18
30
  async fetch(req) {
19
31
  const url = new URL(req.url);
32
+ // MCP endpoint takes priority when enabled.
33
+ if (this.mcpRouter) {
34
+ const mcpRes = await this.mcpRouter(req);
35
+ if (mcpRes)
36
+ return mcpRes;
37
+ }
20
38
  // LAP routes (/agent/lap/v1/*, /agent/*). `router` returns null
21
39
  // for non-matching paths so we can fall through to the upgrade.
22
40
  const lapRes = await this.agent.router(req);
@@ -1 +1 @@
1
- {"version":3,"file":"durable-object.js","sourceRoot":"","sources":["../../../src/server/cloudflare/durable-object.ts"],"names":[],"mappings":"AAgDA,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAChD,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAA;AAI3D;;;;;;;;;GASG;AACH,MAAM,OAAO,yBAAyB;IAC3B,KAAK,CAAiB;IAE/B,YAAY,IAA0B;QACpC,IAAI,CAAC,KAAK,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAA;IACxC,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,GAAY;QACtB,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAE5B,gEAAgE;QAChE,gEAAgE;QAChE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAC3C,IAAI,MAAM;YAAE,OAAO,MAAM,CAAA;QAEzB,iEAAiE;QACjE,sBAAsB;QACtB,IAAI,GAAG,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;YACjC,OAAO,uBAAuB,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;QACjD,CAAC;QAED,OAAO,IAAI,QAAQ,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IACnD,CAAC;CACF","sourcesContent":["/**\n * Durable Object helper for hosting the agent pairing + LAP surface\n * on Cloudflare Workers. One DO instance owns one `tid` — its\n * in-memory `PairingRegistry` survives across Worker isolate\n * invocations because the DO IS persistent.\n *\n * This file exports a class designed to be composed into a real\n * Durable Object in the user's Worker project. We intentionally\n * don't subclass `DurableObject` from `@cloudflare/workers-types` —\n * that dependency belongs to the user's project, not ours. Users\n * wrap an instance of `AgentPairingDurableObject` in their own DO\n * class and forward `fetch` to it.\n *\n * Usage in a Worker project:\n *\n * ```ts\n * // worker.ts\n * import { AgentPairingDurableObject } from '@llui/agent/server/cloudflare'\n *\n * export class AgentDO {\n * private agent: AgentPairingDurableObject\n * constructor(_state: DurableObjectState, env: Env) {\n * // Tokens are opaque (see token.ts) — no signing key needed.\n * this.agent = new AgentPairingDurableObject({})\n * }\n * fetch(req: Request): Promise<Response> {\n * return this.agent.fetch(req)\n * }\n * }\n *\n * export default {\n * async fetch(req: Request, env: Env): Promise<Response> {\n * // routeToAgentDO now takes a `resolveTid` callback — typically\n * // a fetch to the root DO's token-resolution endpoint, or a\n * // const stub when you don't shard by tid.\n * return routeToAgentDO(req, env.AGENT_DO, async (token) => {\n * const stub = env.AGENT_DO.get(env.AGENT_DO.idFromName('__root'))\n * const r = await stub.fetch(`http://internal/__resolve?token=${encodeURIComponent(token)}`)\n * const body = (await r.json()) as { tid: string | null }\n * return body.tid\n * })\n * },\n * }\n * ```\n *\n * See `./worker.ts` for `routeToAgentDO` and the full wiring.\n */\nimport type { CoreOptions, AgentCoreHandle } from '../core.js'\nimport { createLluiAgentCore } from '../core.js'\nimport { handleCloudflareUpgrade } from '../web/upgrade.js'\n\nexport type DurableObjectOptions = Omit<CoreOptions, 'registry'>\n\n/**\n * Agent server instance scoped to a single Durable Object. All\n * pairing state lives in the DO's in-process memory — which is safe\n * here because the DO is a persistent addressable entity, not a\n * one-shot Worker isolate.\n *\n * Users instantiate one of these inside their DO class's constructor\n * and delegate `fetch` to `agent.fetch(req)`. LAP HTTP routes and\n * WebSocket upgrades both flow through this single entry.\n */\nexport class AgentPairingDurableObject {\n readonly agent: AgentCoreHandle\n\n constructor(opts: DurableObjectOptions) {\n this.agent = createLluiAgentCore(opts)\n }\n\n async fetch(req: Request): Promise<Response> {\n const url = new URL(req.url)\n\n // LAP routes (/agent/lap/v1/*, /agent/*). `router` returns null\n // for non-matching paths so we can fall through to the upgrade.\n const lapRes = await this.agent.router(req)\n if (lapRes) return lapRes\n\n // WebSocket upgrade — uses `WebSocketPair`, which only exists in\n // Cloudflare Workers.\n if (url.pathname === '/agent/ws') {\n return handleCloudflareUpgrade(req, this.agent)\n }\n\n return new Response('Not Found', { status: 404 })\n }\n}\n"]}
1
+ {"version":3,"file":"durable-object.js","sourceRoot":"","sources":["../../../src/server/cloudflare/durable-object.ts"],"names":[],"mappings":"AAgDA,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAChD,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAA;AAE3D,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAYlD;;;;;;;;;;GAUG;AACH,MAAM,OAAO,yBAAyB;IAC3B,KAAK,CAAiB;IACd,SAAS,CAAqD;IAE/E,YAAY,IAA0B;QACpC,MAAM,EAAE,GAAG,EAAE,GAAG,QAAQ,EAAE,GAAG,IAAI,CAAA;QACjC,IAAI,CAAC,KAAK,GAAG,mBAAmB,CAAC,QAAQ,CAAC,CAAA;QAC1C,IAAI,GAAG,EAAE,CAAC;YACR,MAAM,OAAO,GAAG,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAA;YACvC,MAAM,WAAW,GAAG,QAAQ,CAAC,WAAW,IAAI,eAAe,CAAA;YAC3D,IAAI,CAAC,SAAS,GAAG,eAAe,CAC9B,EAAE,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,WAAW,EAAE,EACjF,OAAO,CACR,CAAA;QACH,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,SAAS,GAAG,IAAI,CAAA;QACvB,CAAC;IACH,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,GAAY;QACtB,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAE5B,4CAA4C;QAC5C,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACnB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAA;YACxC,IAAI,MAAM;gBAAE,OAAO,MAAM,CAAA;QAC3B,CAAC;QAED,gEAAgE;QAChE,gEAAgE;QAChE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAC3C,IAAI,MAAM;YAAE,OAAO,MAAM,CAAA;QAEzB,iEAAiE;QACjE,sBAAsB;QACtB,IAAI,GAAG,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;YACjC,OAAO,uBAAuB,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;QACjD,CAAC;QAED,OAAO,IAAI,QAAQ,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IACnD,CAAC;CACF","sourcesContent":["/**\n * Durable Object helper for hosting the agent pairing + LAP surface\n * on Cloudflare Workers. One DO instance owns one `tid` — its\n * in-memory `PairingRegistry` survives across Worker isolate\n * invocations because the DO IS persistent.\n *\n * This file exports a class designed to be composed into a real\n * Durable Object in the user's Worker project. We intentionally\n * don't subclass `DurableObject` from `@cloudflare/workers-types` —\n * that dependency belongs to the user's project, not ours. Users\n * wrap an instance of `AgentPairingDurableObject` in their own DO\n * class and forward `fetch` to it.\n *\n * Usage in a Worker project:\n *\n * ```ts\n * // worker.ts\n * import { AgentPairingDurableObject } from '@llui/agent/server/cloudflare'\n *\n * export class AgentDO {\n * private agent: AgentPairingDurableObject\n * constructor(_state: DurableObjectState, env: Env) {\n * // Tokens are opaque (see token.ts) — no signing key needed.\n * this.agent = new AgentPairingDurableObject({})\n * }\n * fetch(req: Request): Promise<Response> {\n * return this.agent.fetch(req)\n * }\n * }\n *\n * export default {\n * async fetch(req: Request, env: Env): Promise<Response> {\n * // routeToAgentDO now takes a `resolveTid` callback — typically\n * // a fetch to the root DO's token-resolution endpoint, or a\n * // const stub when you don't shard by tid.\n * return routeToAgentDO(req, env.AGENT_DO, async (token) => {\n * const stub = env.AGENT_DO.get(env.AGENT_DO.idFromName('__root'))\n * const r = await stub.fetch(`http://internal/__resolve?token=${encodeURIComponent(token)}`)\n * const body = (await r.json()) as { tid: string | null }\n * return body.tid\n * })\n * },\n * }\n * ```\n *\n * See `./worker.ts` for `routeToAgentDO` and the full wiring.\n */\nimport type { CoreOptions, AgentCoreHandle } from '../core.js'\nimport { createLluiAgentCore } from '../core.js'\nimport { handleCloudflareUpgrade } from '../web/upgrade.js'\nimport type { McpRouterOptions } from '../mcp/router.js'\nimport { createMcpRouter } from '../mcp/router.js'\n\nexport type DurableObjectOptions = Omit<CoreOptions, 'registry'> & {\n /**\n * Enable the server-side MCP endpoint at `/agent/mcp` (or a custom\n * path). Pass `true` for all defaults, or an `McpRouterOptions`\n * object to customise path, server name, and connect_session\n * description.\n */\n mcp?: boolean | McpRouterOptions\n}\n\n/**\n * Agent server instance scoped to a single Durable Object. All\n * pairing state lives in the DO's in-process memory — which is safe\n * here because the DO is a persistent addressable entity, not a\n * one-shot Worker isolate.\n *\n * Users instantiate one of these inside their DO class's constructor\n * and delegate `fetch` to `agent.fetch(req)`. LAP HTTP routes,\n * WebSocket upgrades, and the optional MCP endpoint all flow through\n * this single entry.\n */\nexport class AgentPairingDurableObject {\n readonly agent: AgentCoreHandle\n private readonly mcpRouter: ((req: Request) => Promise<Response | null>) | null\n\n constructor(opts: DurableObjectOptions) {\n const { mcp, ...coreOpts } = opts\n this.agent = createLluiAgentCore(coreOpts)\n if (mcp) {\n const mcpOpts = mcp === true ? {} : mcp\n const lapBasePath = coreOpts.lapBasePath ?? '/agent/lap/v1'\n this.mcpRouter = createMcpRouter(\n { coreRouter: this.agent.router, tokenStore: this.agent.tokenStore, lapBasePath },\n mcpOpts,\n )\n } else {\n this.mcpRouter = null\n }\n }\n\n async fetch(req: Request): Promise<Response> {\n const url = new URL(req.url)\n\n // MCP endpoint takes priority when enabled.\n if (this.mcpRouter) {\n const mcpRes = await this.mcpRouter(req)\n if (mcpRes) return mcpRes\n }\n\n // LAP routes (/agent/lap/v1/*, /agent/*). `router` returns null\n // for non-matching paths so we can fall through to the upgrade.\n const lapRes = await this.agent.router(req)\n if (lapRes) return lapRes\n\n // WebSocket upgrade — uses `WebSocketPair`, which only exists in\n // Cloudflare Workers.\n if (url.pathname === '/agent/ws') {\n return handleCloudflareUpgrade(req, this.agent)\n }\n\n return new Response('Not Found', { status: 404 })\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/server/factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAIpE;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,GAAE,aAAkB,GAAG,iBAAiB,CAiBjF"}
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/server/factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAKpE;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,GAAE,aAAkB,GAAG,iBAAiB,CAiCjF"}
@@ -1,5 +1,6 @@
1
1
  import { createLluiAgentCore } from './core.js';
2
2
  import { createWsUpgradeHandler } from './ws/upgrade.js';
3
+ import { createMcpRouter } from './mcp/router.js';
3
4
  /**
4
5
  * Node adapter. Wraps the runtime-neutral core with a Node-specific
5
6
  * `wsUpgrade` handler that uses the `ws` library. Imports `ws`
@@ -16,8 +17,20 @@ export function createLluiAgentServer(opts = {}) {
16
17
  registry: core.registry,
17
18
  auditSink: core.auditSink,
18
19
  });
20
+ const lapBasePath = opts.lapBasePath ?? '/agent/lap/v1';
21
+ let router = core.router;
22
+ if (opts.mcp) {
23
+ const mcpOpts = opts.mcp === true ? {} : opts.mcp;
24
+ const mcpRouter = createMcpRouter({ coreRouter: core.router, tokenStore: core.tokenStore, lapBasePath }, mcpOpts);
25
+ router = async (req) => {
26
+ const mcpRes = await mcpRouter(req);
27
+ if (mcpRes)
28
+ return mcpRes;
29
+ return core.router(req);
30
+ };
31
+ }
19
32
  return {
20
- router: core.router,
33
+ router,
21
34
  wsUpgrade,
22
35
  registry: core.registry,
23
36
  tokenStore: core.tokenStore,
@@ -1 +1 @@
1
- {"version":3,"file":"factory.js","sourceRoot":"","sources":["../../src/server/factory.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA;AAC/C,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AAExD;;;;;;;;GAQG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAsB,EAAE;IAC5D,MAAM,IAAI,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAA;IAEtC,MAAM,SAAS,GAAG,sBAAsB,CAAC;QACvC,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,SAAS,EAAE,IAAI,CAAC,SAAS;KAC1B,CAAC,CAAA;IAEF,OAAO;QACL,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,SAAS;QACT,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;KACxC,CAAA;AACH,CAAC","sourcesContent":["import type { ServerOptions, AgentServerHandle } from './options.js'\nimport { createLluiAgentCore } from './core.js'\nimport { createWsUpgradeHandler } from './ws/upgrade.js'\n\n/**\n * Node adapter. Wraps the runtime-neutral core with a Node-specific\n * `wsUpgrade` handler that uses the `ws` library. Imports `ws`\n * eagerly, so this module only works where `ws` is available — use\n * `@llui/agent/server/web` for Cloudflare Workers, Deno, or other\n * WHATWG runtimes.\n *\n * Spec §10.1, §10.4.\n */\nexport function createLluiAgentServer(opts: ServerOptions = {}): AgentServerHandle {\n const core = createLluiAgentCore(opts)\n\n const wsUpgrade = createWsUpgradeHandler({\n tokenStore: core.tokenStore,\n registry: core.registry,\n auditSink: core.auditSink,\n })\n\n return {\n router: core.router,\n wsUpgrade,\n registry: core.registry,\n tokenStore: core.tokenStore,\n auditSink: core.auditSink,\n acceptConnection: core.acceptConnection,\n }\n}\n"]}
1
+ {"version":3,"file":"factory.js","sourceRoot":"","sources":["../../src/server/factory.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA;AAC/C,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEjD;;;;;;;;GAQG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAsB,EAAE;IAC5D,MAAM,IAAI,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAA;IAEtC,MAAM,SAAS,GAAG,sBAAsB,CAAC;QACvC,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,SAAS,EAAE,IAAI,CAAC,SAAS;KAC1B,CAAC,CAAA;IAEF,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,eAAe,CAAA;IAEvD,IAAI,MAAM,GAAG,IAAI,CAAC,MAAM,CAAA;IACxB,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAA;QACjD,MAAM,SAAS,GAAG,eAAe,CAC/B,EAAE,UAAU,EAAE,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,WAAW,EAAE,EACrE,OAAO,CACR,CAAA;QACD,MAAM,GAAG,KAAK,EAAE,GAAG,EAAE,EAAE;YACrB,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,CAAA;YACnC,IAAI,MAAM;gBAAE,OAAO,MAAM,CAAA;YACzB,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACzB,CAAC,CAAA;IACH,CAAC;IAED,OAAO;QACL,MAAM;QACN,SAAS;QACT,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;KACxC,CAAA;AACH,CAAC","sourcesContent":["import type { ServerOptions, AgentServerHandle } from './options.js'\nimport { createLluiAgentCore } from './core.js'\nimport { createWsUpgradeHandler } from './ws/upgrade.js'\nimport { createMcpRouter } from './mcp/router.js'\n\n/**\n * Node adapter. Wraps the runtime-neutral core with a Node-specific\n * `wsUpgrade` handler that uses the `ws` library. Imports `ws`\n * eagerly, so this module only works where `ws` is available — use\n * `@llui/agent/server/web` for Cloudflare Workers, Deno, or other\n * WHATWG runtimes.\n *\n * Spec §10.1, §10.4.\n */\nexport function createLluiAgentServer(opts: ServerOptions = {}): AgentServerHandle {\n const core = createLluiAgentCore(opts)\n\n const wsUpgrade = createWsUpgradeHandler({\n tokenStore: core.tokenStore,\n registry: core.registry,\n auditSink: core.auditSink,\n })\n\n const lapBasePath = opts.lapBasePath ?? '/agent/lap/v1'\n\n let router = core.router\n if (opts.mcp) {\n const mcpOpts = opts.mcp === true ? {} : opts.mcp\n const mcpRouter = createMcpRouter(\n { coreRouter: core.router, tokenStore: core.tokenStore, lapBasePath },\n mcpOpts,\n )\n router = async (req) => {\n const mcpRes = await mcpRouter(req)\n if (mcpRes) return mcpRes\n return core.router(req)\n }\n }\n\n return {\n router,\n wsUpgrade,\n registry: core.registry,\n tokenStore: core.tokenStore,\n auditSink: core.auditSink,\n acceptConnection: core.acceptConnection,\n }\n}\n"]}
@@ -0,0 +1,26 @@
1
+ import type { TokenStore } from '../token-store.js';
2
+ export type McpRouterOptions = {
3
+ /** Path prefix for the MCP endpoint. Default: '/agent/mcp'. */
4
+ path?: string;
5
+ /** MCP server name shown in Claude Desktop. Default: 'agent'. */
6
+ serverName?: string;
7
+ /** MCP server version string. Default: '1'. */
8
+ serverVersion?: string;
9
+ /** Description for the connect_session tool. */
10
+ connectDescription?: string;
11
+ };
12
+ export type McpRouterDeps = {
13
+ coreRouter: (req: Request) => Promise<Response | null>;
14
+ tokenStore: TokenStore;
15
+ lapBasePath: string;
16
+ };
17
+ /**
18
+ * Build a WHATWG-compatible MCP router that mounts at `opts.path`.
19
+ * Integrates into the agent core's fetch-style router by prepending
20
+ * this function's result in the request chain.
21
+ *
22
+ * Uses `WebStandardStreamableHTTPServerTransport` (WHATWG, runtime-
23
+ * neutral) rather than the Node-only `StreamableHTTPServerTransport`.
24
+ */
25
+ export declare function createMcpRouter(deps: McpRouterDeps, opts?: McpRouterOptions): (req: Request) => Promise<Response | null>;
26
+ //# sourceMappingURL=router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../../src/server/mcp/router.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAEnD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,+CAA+C;IAC/C,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,gDAAgD;IAChD,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAA;IACtD,UAAU,EAAE,UAAU,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAOD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,aAAa,EACnB,IAAI,GAAE,gBAAqB,GAC1B,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CA0E5C"}
@@ -0,0 +1,81 @@
1
+ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
2
+ import { McpSessionMap } from './session-map.js';
3
+ import { createAgentMcpServer } from './server.js';
4
+ const DEFAULT_CONNECT_DESCRIPTION = 'Connect to the app. Call once per chat when the user pastes a token from the app connect panel. ' +
5
+ 'Returns {state, actions, description, context} so you can start acting immediately — ' +
6
+ 'no separate observe call needed on the first turn.';
7
+ /**
8
+ * Build a WHATWG-compatible MCP router that mounts at `opts.path`.
9
+ * Integrates into the agent core's fetch-style router by prepending
10
+ * this function's result in the request chain.
11
+ *
12
+ * Uses `WebStandardStreamableHTTPServerTransport` (WHATWG, runtime-
13
+ * neutral) rather than the Node-only `StreamableHTTPServerTransport`.
14
+ */
15
+ export function createMcpRouter(deps, opts = {}) {
16
+ const mcpPath = opts.path ?? '/agent/mcp';
17
+ const serverName = opts.serverName ?? 'agent';
18
+ const serverVersion = opts.serverVersion ?? '1';
19
+ const connectDescription = opts.connectDescription ?? DEFAULT_CONNECT_DESCRIPTION;
20
+ const lapBasePath = deps.lapBasePath;
21
+ const sessionMap = new McpSessionMap();
22
+ // mcp-session-id → active transport. Populated on initialize,
23
+ // cleaned up on DELETE or transport close.
24
+ const transports = new Map();
25
+ return async (req) => {
26
+ const url = new URL(req.url);
27
+ if (!url.pathname.startsWith(mcpPath))
28
+ return null;
29
+ const sessionHeader = req.headers.get('mcp-session-id');
30
+ // ── Existing session ───────────────────────────────────────────
31
+ if (sessionHeader) {
32
+ const transport = transports.get(sessionHeader);
33
+ if (!transport) {
34
+ // Unknown session ID — reject so the client can reinitialize.
35
+ return new Response(JSON.stringify({ error: 'session not found' }), {
36
+ status: 404,
37
+ headers: { 'content-type': 'application/json' },
38
+ });
39
+ }
40
+ return transport.handleRequest(req);
41
+ }
42
+ // ── New session (no mcp-session-id) ───────────────────────────
43
+ // Only POST (initialize) should arrive without a session ID.
44
+ if (req.method !== 'POST') {
45
+ return new Response(JSON.stringify({ error: 'mcp-session-id required' }), {
46
+ status: 400,
47
+ headers: { 'content-type': 'application/json' },
48
+ });
49
+ }
50
+ const transport = new WebStandardStreamableHTTPServerTransport({
51
+ sessionIdGenerator: () => crypto.randomUUID(),
52
+ onsessioninitialized: (id) => {
53
+ transports.set(id, transport);
54
+ },
55
+ onsessionclosed: (id) => {
56
+ transports.delete(id);
57
+ sessionMap.delete(id);
58
+ },
59
+ });
60
+ transport.onclose = () => {
61
+ const id = transport.sessionId;
62
+ if (id) {
63
+ transports.delete(id);
64
+ sessionMap.delete(id);
65
+ }
66
+ };
67
+ const mcpServer = createAgentMcpServer({
68
+ coreRouter: deps.coreRouter,
69
+ tokenStore: deps.tokenStore,
70
+ sessionMap,
71
+ getSessionId: () => transport.sessionId,
72
+ lapBasePath,
73
+ serverName,
74
+ serverVersion,
75
+ connectDescription,
76
+ });
77
+ await mcpServer.connect(transport);
78
+ return transport.handleRequest(req);
79
+ };
80
+ }
81
+ //# sourceMappingURL=router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.js","sourceRoot":"","sources":["../../../src/server/mcp/router.ts"],"names":[],"mappings":"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","sourcesContent":["import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'\nimport { McpSessionMap } from './session-map.js'\nimport { createAgentMcpServer } from './server.js'\nimport type { TokenStore } from '../token-store.js'\n\nexport type McpRouterOptions = {\n /** Path prefix for the MCP endpoint. Default: '/agent/mcp'. */\n path?: string\n /** MCP server name shown in Claude Desktop. Default: 'agent'. */\n serverName?: string\n /** MCP server version string. Default: '1'. */\n serverVersion?: string\n /** Description for the connect_session tool. */\n connectDescription?: string\n}\n\nexport type McpRouterDeps = {\n coreRouter: (req: Request) => Promise<Response | null>\n tokenStore: TokenStore\n lapBasePath: string\n}\n\nconst DEFAULT_CONNECT_DESCRIPTION =\n 'Connect to the app. Call once per chat when the user pastes a token from the app connect panel. ' +\n 'Returns {state, actions, description, context} so you can start acting immediately — ' +\n 'no separate observe call needed on the first turn.'\n\n/**\n * Build a WHATWG-compatible MCP router that mounts at `opts.path`.\n * Integrates into the agent core's fetch-style router by prepending\n * this function's result in the request chain.\n *\n * Uses `WebStandardStreamableHTTPServerTransport` (WHATWG, runtime-\n * neutral) rather than the Node-only `StreamableHTTPServerTransport`.\n */\nexport function createMcpRouter(\n deps: McpRouterDeps,\n opts: McpRouterOptions = {},\n): (req: Request) => Promise<Response | null> {\n const mcpPath = opts.path ?? '/agent/mcp'\n const serverName = opts.serverName ?? 'agent'\n const serverVersion = opts.serverVersion ?? '1'\n const connectDescription = opts.connectDescription ?? DEFAULT_CONNECT_DESCRIPTION\n const lapBasePath = deps.lapBasePath\n\n const sessionMap = new McpSessionMap()\n\n // mcp-session-id → active transport. Populated on initialize,\n // cleaned up on DELETE or transport close.\n const transports = new Map<string, WebStandardStreamableHTTPServerTransport>()\n\n return async (req: Request): Promise<Response | null> => {\n const url = new URL(req.url)\n if (!url.pathname.startsWith(mcpPath)) return null\n\n const sessionHeader = req.headers.get('mcp-session-id')\n\n // ── Existing session ───────────────────────────────────────────\n if (sessionHeader) {\n const transport = transports.get(sessionHeader)\n if (!transport) {\n // Unknown session ID — reject so the client can reinitialize.\n return new Response(JSON.stringify({ error: 'session not found' }), {\n status: 404,\n headers: { 'content-type': 'application/json' },\n })\n }\n return transport.handleRequest(req)\n }\n\n // ── New session (no mcp-session-id) ───────────────────────────\n // Only POST (initialize) should arrive without a session ID.\n if (req.method !== 'POST') {\n return new Response(JSON.stringify({ error: 'mcp-session-id required' }), {\n status: 400,\n headers: { 'content-type': 'application/json' },\n })\n }\n\n const transport = new WebStandardStreamableHTTPServerTransport({\n sessionIdGenerator: () => crypto.randomUUID(),\n onsessioninitialized: (id) => {\n transports.set(id, transport)\n },\n onsessionclosed: (id) => {\n transports.delete(id)\n sessionMap.delete(id)\n },\n })\n\n transport.onclose = () => {\n const id = transport.sessionId\n if (id) {\n transports.delete(id)\n sessionMap.delete(id)\n }\n }\n\n const mcpServer = createAgentMcpServer({\n coreRouter: deps.coreRouter,\n tokenStore: deps.tokenStore,\n sessionMap,\n getSessionId: () => transport.sessionId,\n lapBasePath,\n serverName,\n serverVersion,\n connectDescription,\n })\n\n await mcpServer.connect(transport)\n return transport.handleRequest(req)\n }\n}\n"]}
@@ -0,0 +1,26 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { McpSessionMap } from './session-map.js';
3
+ import type { TokenStore } from '../token-store.js';
4
+ export type McpServerDeps = {
5
+ /** WHATWG router from the agent core — used to call LAP endpoints internally. */
6
+ coreRouter: (req: Request) => Promise<Response | null>;
7
+ tokenStore: TokenStore;
8
+ sessionMap: McpSessionMap;
9
+ /**
10
+ * Returns the MCP session ID for this server instance. Called lazily so
11
+ * the transport can assign the ID during `initialize` before any tool
12
+ * handler fires.
13
+ */
14
+ getSessionId: () => string | undefined;
15
+ lapBasePath: string;
16
+ serverName: string;
17
+ serverVersion: string;
18
+ connectDescription: string;
19
+ };
20
+ /**
21
+ * Build one `McpServer` instance for a single MCP session. Tool handlers
22
+ * call LAP endpoints via synthetic WHATWG Requests routed through
23
+ * `coreRouter` — no extra HTTP round-trip to localhost needed.
24
+ */
25
+ export declare function createAgentMcpServer(deps: McpServerDeps): McpServer;
26
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../src/server/mcp/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAQnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAGnD,MAAM,MAAM,aAAa,GAAG;IAC1B,iFAAiF;IACjF,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAA;IACtD,UAAU,EAAE,UAAU,CAAA;IACtB,UAAU,EAAE,aAAa,CAAA;IACzB;;;;OAIG;IACH,YAAY,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;IACtC,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;IACrB,kBAAkB,EAAE,MAAM,CAAA;CAC3B,CAAA;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS,CAmEnE"}
@@ -0,0 +1,116 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { FORWARDED_TOOL_DESCRIPTORS, DISCONNECT_SESSION_DESCRIPTOR, } from '../../mcp/tools.js';
4
+ import { verifyAndReadTid } from '../lap/describe.js';
5
+ /**
6
+ * Build one `McpServer` instance for a single MCP session. Tool handlers
7
+ * call LAP endpoints via synthetic WHATWG Requests routed through
8
+ * `coreRouter` — no extra HTTP round-trip to localhost needed.
9
+ */
10
+ export function createAgentMcpServer(deps) {
11
+ const server = new McpServer({ name: deps.serverName, version: deps.serverVersion }, { capabilities: { tools: {} } });
12
+ // ── connect_session ────────────────────────────────────────────────
13
+ server.registerTool('connect_session', {
14
+ description: deps.connectDescription,
15
+ inputSchema: z.object({
16
+ token: z.string().describe('Bearer token from the app connect panel'),
17
+ }).shape,
18
+ }, async ({ token }) => {
19
+ // Verify the token and extract the tid. We re-use the existing
20
+ // LAP auth helper by constructing a minimal synthetic Request.
21
+ const authReq = new Request('http://local/auth', {
22
+ headers: { authorization: `Bearer ${token}` },
23
+ });
24
+ const auth = await verifyAndReadTid(authReq, deps.tokenStore);
25
+ if (!auth.ok) {
26
+ return errorResult(auth.code === 'auth-failed'
27
+ ? 'Token is invalid or expired. Ask the user to copy a fresh token from the app.'
28
+ : `Auth failed: ${auth.code}`);
29
+ }
30
+ const sessionId = deps.getSessionId();
31
+ if (!sessionId)
32
+ return errorResult('MCP session not yet initialized — retry in a moment.');
33
+ deps.sessionMap.set(sessionId, { tid: auth.tid, token });
34
+ // Prefetch the initial observe bundle — same reason the bridge does
35
+ // this: Claude gets state + actions + description + context in one
36
+ // call, avoiding a follow-up round-trip.
37
+ const result = await lapCall(deps.coreRouter, token, deps.lapBasePath, '/observe', {});
38
+ if (!result.ok) {
39
+ deps.sessionMap.delete(sessionId);
40
+ return errorResult(`connect_session: observe failed — ${result.error}`);
41
+ }
42
+ return okResult({ status: 'connected', ...result.body });
43
+ });
44
+ // ── disconnect_session ─────────────────────────────────────────────
45
+ server.registerTool(DISCONNECT_SESSION_DESCRIPTOR.name, {
46
+ description: DISCONNECT_SESSION_DESCRIPTOR.description,
47
+ inputSchema: DISCONNECT_SESSION_DESCRIPTOR.schema.shape,
48
+ }, async () => {
49
+ const sessionId = deps.getSessionId();
50
+ if (sessionId)
51
+ deps.sessionMap.delete(sessionId);
52
+ return okResult({ status: 'disconnected' });
53
+ });
54
+ // ── forwarded tools ────────────────────────────────────────────────
55
+ for (const desc of FORWARDED_TOOL_DESCRIPTORS) {
56
+ registerForwardedTool(server, deps, desc);
57
+ }
58
+ return server;
59
+ }
60
+ function registerForwardedTool(server, deps, desc) {
61
+ server.registerTool(desc.name, { description: desc.description, inputSchema: desc.schema.shape }, async (args) => {
62
+ const sessionId = deps.getSessionId();
63
+ const session = sessionId ? deps.sessionMap.get(sessionId) : null;
64
+ if (!session) {
65
+ return errorResult('Not connected — ask the user to copy the token from the app connect panel ' +
66
+ 'and call connect_session with it.');
67
+ }
68
+ const result = await lapCall(deps.coreRouter, session.token, deps.lapBasePath, desc.lapPath, (args ?? {}));
69
+ if (!result.ok)
70
+ return errorResult(`${desc.name}: ${result.error}`);
71
+ return okResult(result.body);
72
+ });
73
+ }
74
+ /**
75
+ * Call a LAP endpoint internally by constructing a synthetic WHATWG
76
+ * Request and routing it through the agent core's router. No actual
77
+ * HTTP round-trip — the router handles it in-process.
78
+ */
79
+ async function lapCall(coreRouter, token, lapBasePath, lapPath, body) {
80
+ const req = new Request(`http://local${lapBasePath}${lapPath}`, {
81
+ method: 'POST',
82
+ headers: {
83
+ authorization: `Bearer ${token}`,
84
+ 'content-type': 'application/json',
85
+ },
86
+ body: JSON.stringify(body),
87
+ });
88
+ try {
89
+ const res = await coreRouter(req);
90
+ if (!res)
91
+ return { ok: false, error: `no handler for ${lapPath}` };
92
+ const payload = (await res.json());
93
+ if (!res.ok || payload.error) {
94
+ const code = payload.error?.code ?? res.status;
95
+ const detail = payload.error?.detail ? ` — ${payload.error.detail}` : '';
96
+ return { ok: false, error: `status=${res.status} code=${code}${detail}` };
97
+ }
98
+ return { ok: true, body: payload };
99
+ }
100
+ catch (e) {
101
+ return { ok: false, error: String(e) };
102
+ }
103
+ }
104
+ function okResult(body) {
105
+ return {
106
+ structuredContent: body,
107
+ content: [{ type: 'text', text: JSON.stringify(body) }],
108
+ };
109
+ }
110
+ function errorResult(msg) {
111
+ return {
112
+ content: [{ type: 'text', text: msg }],
113
+ isError: true,
114
+ };
115
+ }
116
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../../src/server/mcp/server.ts"],"names":[],"mappings":"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","sourcesContent":["import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'\nimport { z } from 'zod'\nimport {\n FORWARDED_TOOL_DESCRIPTORS,\n DISCONNECT_SESSION_DESCRIPTOR,\n type McpForwardedToolDescriptor,\n} from '../../mcp/tools.js'\nimport type { McpSessionMap } from './session-map.js'\nimport type { TokenStore } from '../token-store.js'\nimport { verifyAndReadTid } from '../lap/describe.js'\n\nexport type McpServerDeps = {\n /** WHATWG router from the agent core — used to call LAP endpoints internally. */\n coreRouter: (req: Request) => Promise<Response | null>\n tokenStore: TokenStore\n sessionMap: McpSessionMap\n /**\n * Returns the MCP session ID for this server instance. Called lazily so\n * the transport can assign the ID during `initialize` before any tool\n * handler fires.\n */\n getSessionId: () => string | undefined\n lapBasePath: string\n serverName: string\n serverVersion: string\n connectDescription: string\n}\n\n/**\n * Build one `McpServer` instance for a single MCP session. Tool handlers\n * call LAP endpoints via synthetic WHATWG Requests routed through\n * `coreRouter` — no extra HTTP round-trip to localhost needed.\n */\nexport function createAgentMcpServer(deps: McpServerDeps): McpServer {\n const server = new McpServer(\n { name: deps.serverName, version: deps.serverVersion },\n { capabilities: { tools: {} } },\n )\n\n // ── connect_session ────────────────────────────────────────────────\n server.registerTool(\n 'connect_session',\n {\n description: deps.connectDescription,\n inputSchema: z.object({\n token: z.string().describe('Bearer token from the app connect panel'),\n }).shape,\n },\n async ({ token }) => {\n // Verify the token and extract the tid. We re-use the existing\n // LAP auth helper by constructing a minimal synthetic Request.\n const authReq = new Request('http://local/auth', {\n headers: { authorization: `Bearer ${token}` },\n })\n const auth = await verifyAndReadTid(authReq, deps.tokenStore)\n if (!auth.ok) {\n return errorResult(\n auth.code === 'auth-failed'\n ? 'Token is invalid or expired. Ask the user to copy a fresh token from the app.'\n : `Auth failed: ${auth.code}`,\n )\n }\n\n const sessionId = deps.getSessionId()\n if (!sessionId) return errorResult('MCP session not yet initialized — retry in a moment.')\n\n deps.sessionMap.set(sessionId, { tid: auth.tid, token })\n\n // Prefetch the initial observe bundle — same reason the bridge does\n // this: Claude gets state + actions + description + context in one\n // call, avoiding a follow-up round-trip.\n const result = await lapCall(deps.coreRouter, token, deps.lapBasePath, '/observe', {})\n if (!result.ok) {\n deps.sessionMap.delete(sessionId)\n return errorResult(`connect_session: observe failed — ${result.error}`)\n }\n return okResult({ status: 'connected', ...(result.body as object) })\n },\n )\n\n // ── disconnect_session ─────────────────────────────────────────────\n server.registerTool(\n DISCONNECT_SESSION_DESCRIPTOR.name,\n {\n description: DISCONNECT_SESSION_DESCRIPTOR.description,\n inputSchema: DISCONNECT_SESSION_DESCRIPTOR.schema.shape,\n },\n async () => {\n const sessionId = deps.getSessionId()\n if (sessionId) deps.sessionMap.delete(sessionId)\n return okResult({ status: 'disconnected' })\n },\n )\n\n // ── forwarded tools ────────────────────────────────────────────────\n for (const desc of FORWARDED_TOOL_DESCRIPTORS) {\n registerForwardedTool(server, deps, desc)\n }\n\n return server\n}\n\nfunction registerForwardedTool(\n server: McpServer,\n deps: McpServerDeps,\n desc: McpForwardedToolDescriptor,\n): void {\n server.registerTool(\n desc.name,\n { description: desc.description, inputSchema: desc.schema.shape },\n async (args) => {\n const sessionId = deps.getSessionId()\n const session = sessionId ? deps.sessionMap.get(sessionId) : null\n if (!session) {\n return errorResult(\n 'Not connected — ask the user to copy the token from the app connect panel ' +\n 'and call connect_session with it.',\n )\n }\n const result = await lapCall(\n deps.coreRouter,\n session.token,\n deps.lapBasePath,\n desc.lapPath,\n (args ?? {}) as Record<string, unknown>,\n )\n if (!result.ok) return errorResult(`${desc.name}: ${result.error}`)\n return okResult(result.body)\n },\n )\n}\n\n/**\n * Call a LAP endpoint internally by constructing a synthetic WHATWG\n * Request and routing it through the agent core's router. No actual\n * HTTP round-trip — the router handles it in-process.\n */\nasync function lapCall(\n coreRouter: (req: Request) => Promise<Response | null>,\n token: string,\n lapBasePath: string,\n lapPath: string,\n body: Record<string, unknown>,\n): Promise<{ ok: true; body: unknown } | { ok: false; error: string }> {\n const req = new Request(`http://local${lapBasePath}${lapPath}`, {\n method: 'POST',\n headers: {\n authorization: `Bearer ${token}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify(body),\n })\n try {\n const res = await coreRouter(req)\n if (!res) return { ok: false, error: `no handler for ${lapPath}` }\n const payload = (await res.json()) as { error?: { code: string; detail?: string } }\n if (!res.ok || payload.error) {\n const code = payload.error?.code ?? res.status\n const detail = payload.error?.detail ? ` — ${payload.error.detail}` : ''\n return { ok: false, error: `status=${res.status} code=${code}${detail}` }\n }\n return { ok: true, body: payload }\n } catch (e) {\n return { ok: false, error: String(e) }\n }\n}\n\nfunction okResult(body: unknown): CallToolResult {\n return {\n structuredContent: body as Record<string, unknown>,\n content: [{ type: 'text', text: JSON.stringify(body) }],\n }\n}\n\nfunction errorResult(msg: string): CallToolResult {\n return {\n content: [{ type: 'text', text: msg }],\n isError: true,\n }\n}\n"]}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Per-MCP-session binding. Populated by `connect_session` and read by
3
+ * every forwarded tool handler. Keyed by the SDK-assigned MCP session ID
4
+ * (`mcp-session-id` response header / request header).
5
+ */
6
+ export type McpSession = {
7
+ /** Token record ID resolved at connect_session time. */
8
+ tid: string;
9
+ /** Bearer token — used to construct synthetic LAP requests. */
10
+ token: string;
11
+ };
12
+ export declare class McpSessionMap {
13
+ private map;
14
+ set(mcpSessionId: string, session: McpSession): void;
15
+ get(mcpSessionId: string): McpSession | null;
16
+ delete(mcpSessionId: string): void;
17
+ }
18
+ //# sourceMappingURL=session-map.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-map.d.ts","sourceRoot":"","sources":["../../../src/server/mcp/session-map.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,wDAAwD;IACxD,GAAG,EAAE,MAAM,CAAA;IACX,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,GAAG,CAAgC;IAE3C,GAAG,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,IAAI;IAIpD,GAAG,CAAC,YAAY,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;IAI5C,MAAM,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI;CAGnC"}
@@ -0,0 +1,13 @@
1
+ export class McpSessionMap {
2
+ map = new Map();
3
+ set(mcpSessionId, session) {
4
+ this.map.set(mcpSessionId, session);
5
+ }
6
+ get(mcpSessionId) {
7
+ return this.map.get(mcpSessionId) ?? null;
8
+ }
9
+ delete(mcpSessionId) {
10
+ this.map.delete(mcpSessionId);
11
+ }
12
+ }
13
+ //# sourceMappingURL=session-map.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-map.js","sourceRoot":"","sources":["../../../src/server/mcp/session-map.ts"],"names":[],"mappings":"AAYA,MAAM,OAAO,aAAa;IAChB,GAAG,GAAG,IAAI,GAAG,EAAsB,CAAA;IAE3C,GAAG,CAAC,YAAoB,EAAE,OAAmB;QAC3C,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,OAAO,CAAC,CAAA;IACrC,CAAC;IAED,GAAG,CAAC,YAAoB;QACtB,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,IAAI,CAAA;IAC3C,CAAC;IAED,MAAM,CAAC,YAAoB;QACzB,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;IAC/B,CAAC;CACF","sourcesContent":["/**\n * Per-MCP-session binding. Populated by `connect_session` and read by\n * every forwarded tool handler. Keyed by the SDK-assigned MCP session ID\n * (`mcp-session-id` response header / request header).\n */\nexport type McpSession = {\n /** Token record ID resolved at connect_session time. */\n tid: string\n /** Bearer token — used to construct synthetic LAP requests. */\n token: string\n}\n\nexport class McpSessionMap {\n private map = new Map<string, McpSession>()\n\n set(mcpSessionId: string, session: McpSession): void {\n this.map.set(mcpSessionId, session)\n }\n\n get(mcpSessionId: string): McpSession | null {\n return this.map.get(mcpSessionId) ?? null\n }\n\n delete(mcpSessionId: string): void {\n this.map.delete(mcpSessionId)\n }\n}\n"]}
@@ -7,6 +7,8 @@ import type { RateLimiter } from './rate-limit.js';
7
7
  import type { PairingRegistry } from './ws/pairing-registry.js';
8
8
  import type { AcceptResult } from './core.js';
9
9
  import type { PairingConnection } from './ws/pairing-registry.js';
10
+ import type { McpRouterOptions } from './mcp/router.js';
11
+ export type { McpRouterOptions };
10
12
  /**
11
13
  * Options accepted by `createLluiAgentServer`. All values are
12
14
  * optional and fall back to in-memory defaults. See spec §10.1.
@@ -33,6 +35,17 @@ export type ServerOptions = {
33
35
  slidingTtlMs?: number;
34
36
  /** Allowed origins for the HTTP surface (CORS). Empty = any. */
35
37
  corsOrigins?: readonly string[];
38
+ /**
39
+ * Enable the server-side MCP endpoint at `/agent/mcp` (or a custom
40
+ * path). When set, Claude Desktop can connect directly to the app
41
+ * backend without installing the `llui-agent` bridge — the user pastes
42
+ * the token via `connect_session` in chat, same flow as the bridge but
43
+ * no separate process required.
44
+ *
45
+ * Pass `true` to use all defaults, or an `McpRouterOptions` object to
46
+ * customise the path, server name, and connect_session description.
47
+ */
48
+ mcp?: boolean | McpRouterOptions;
36
49
  };
37
50
  /**
38
51
  * Value returned by `createLluiAgentServer`. `router` matches any
@@ -1 +1 @@
1
- {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/server/options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAC/D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAC7C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAEjE;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,wDAAwD;IACxD,UAAU,CAAC,EAAE,UAAU,CAAA;IAEvB,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,gBAAgB,CAAA;IAEnC,kDAAkD;IAClD,SAAS,CAAC,EAAE,SAAS,CAAA;IAErB,qEAAqE;IACrE,WAAW,CAAC,EAAE,WAAW,CAAA;IAEzB,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAA;IAEpB,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAA;IAEvB,yDAAyD;IACzD,YAAY,CAAC,EAAE,MAAM,CAAA;IAErB,gEAAgE;IAChE,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAChC,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAA;IAClD;;;;;;OAMG;IACH,SAAS,EAAE,CAAC,GAAG,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAChF,oEAAoE;IACpE,QAAQ,EAAE,eAAe,CAAA;IACzB,8BAA8B;IAC9B,UAAU,EAAE,UAAU,CAAA;IACtB,6BAA6B;IAC7B,SAAS,EAAE,SAAS,CAAA;IACpB;;;;;;OAMG;IACH,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,YAAY,CAAC,CAAA;CACpF,CAAA"}
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/server/options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAC/D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAC7C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AACjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAEvD,YAAY,EAAE,gBAAgB,EAAE,CAAA;AAEhC;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,wDAAwD;IACxD,UAAU,CAAC,EAAE,UAAU,CAAA;IAEvB,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,gBAAgB,CAAA;IAEnC,kDAAkD;IAClD,SAAS,CAAC,EAAE,SAAS,CAAA;IAErB,qEAAqE;IACrE,WAAW,CAAC,EAAE,WAAW,CAAA;IAEzB,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAA;IAEpB,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAA;IAEvB,yDAAyD;IACzD,YAAY,CAAC,EAAE,MAAM,CAAA;IAErB,gEAAgE;IAChE,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAE/B;;;;;;;;;OASG;IACH,GAAG,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAA;CACjC,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAA;IAClD;;;;;;OAMG;IACH,SAAS,EAAE,CAAC,GAAG,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAChF,oEAAoE;IACpE,QAAQ,EAAE,eAAe,CAAA;IACzB,8BAA8B;IAC9B,UAAU,EAAE,UAAU,CAAA;IACtB,6BAA6B;IAC7B,SAAS,EAAE,SAAS,CAAA;IACpB;;;;;;OAMG;IACH,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,YAAY,CAAC,CAAA;CACpF,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"options.js","sourceRoot":"","sources":["../../src/server/options.ts"],"names":[],"mappings":"","sourcesContent":["import type { IncomingMessage } from 'node:http'\nimport type { Duplex } from 'node:stream'\nimport type { TokenStore } from './token-store.js'\nimport type { IdentityResolver } from './identity.js'\nimport type { AuditSink } from './audit.js'\nimport type { RateLimiter } from './rate-limit.js'\nimport type { PairingRegistry } from './ws/pairing-registry.js'\nimport type { AcceptResult } from './core.js'\nimport type { PairingConnection } from './ws/pairing-registry.js'\n\n/**\n * Options accepted by `createLluiAgentServer`. All values are\n * optional and fall back to in-memory defaults. See spec §10.1.\n *\n * Pre-0.0.35 this required a `signingKey` for HMAC-signed JWT tokens.\n * The new opaque-token scheme (token.ts) doesn't sign anything — the\n * server stores the SHA-256 hash and looks tokens up. The option is\n * gone; existing config that passed `signingKey` should drop it.\n */\nexport type ServerOptions = {\n /** Token store. Defaults to an `InMemoryTokenStore`. */\n tokenStore?: TokenStore\n\n /** Identity resolver. Defaults to anonymous (always null). */\n identityResolver?: IdentityResolver\n\n /** Audit sink. Defaults to `consoleAuditSink`. */\n auditSink?: AuditSink\n\n /** Rate limiter. Defaults to `defaultRateLimiter` with 30/minute. */\n rateLimiter?: RateLimiter\n\n /** Base path prefix for LAP endpoints. Defaults to `/agent/lap/v1`. */\n lapBasePath?: string\n\n /** Pairing grace window after a tab closes, in ms. Default 15 min. */\n pairingGraceMs?: number\n\n /** Sliding TTL for active tokens, in ms. Default 1 h. */\n slidingTtlMs?: number\n\n /** Allowed origins for the HTTP surface (CORS). Empty = any. */\n corsOrigins?: readonly string[]\n}\n\n/**\n * Value returned by `createLluiAgentServer`. `router` matches any\n * `/agent/*` request and returns a Response (or null to fall through).\n * `wsUpgrade` handles Node HTTP upgrade events for `/agent/ws`.\n */\nexport type AgentServerHandle = {\n router: (req: Request) => Promise<Response | null>\n /**\n * Handles Node HTTP upgrade events for `/agent/ws`. Returns a Promise\n * because token verification uses WebCrypto (async). Node's\n * `server.on('upgrade', handler)` fires the handler without awaiting,\n * which is fine — the handler writes errors directly to the socket\n * and never throws back to the caller.\n */\n wsUpgrade: (req: IncomingMessage, socket: Duplex, head: Buffer) => Promise<void>\n /** The pairing registry. Runtime-neutral adapters may access it. */\n registry: PairingRegistry\n /** The active token store. */\n tokenStore: TokenStore\n /** The active audit sink. */\n auditSink: AuditSink\n /**\n * Runtime-neutral WebSocket acceptance primitive. Validates a token\n * and registers a `PairingConnection` with the registry. The Node\n * `wsUpgrade` above calls this internally; web-runtime adapters\n * (`@llui/agent/server/web`) use it after accepting a WebSocket via\n * their native API.\n */\n acceptConnection: (token: string, conn: PairingConnection) => Promise<AcceptResult>\n}\n"]}
1
+ {"version":3,"file":"options.js","sourceRoot":"","sources":["../../src/server/options.ts"],"names":[],"mappings":"","sourcesContent":["import type { IncomingMessage } from 'node:http'\nimport type { Duplex } from 'node:stream'\nimport type { TokenStore } from './token-store.js'\nimport type { IdentityResolver } from './identity.js'\nimport type { AuditSink } from './audit.js'\nimport type { RateLimiter } from './rate-limit.js'\nimport type { PairingRegistry } from './ws/pairing-registry.js'\nimport type { AcceptResult } from './core.js'\nimport type { PairingConnection } from './ws/pairing-registry.js'\nimport type { McpRouterOptions } from './mcp/router.js'\n\nexport type { McpRouterOptions }\n\n/**\n * Options accepted by `createLluiAgentServer`. All values are\n * optional and fall back to in-memory defaults. See spec §10.1.\n *\n * Pre-0.0.35 this required a `signingKey` for HMAC-signed JWT tokens.\n * The new opaque-token scheme (token.ts) doesn't sign anything — the\n * server stores the SHA-256 hash and looks tokens up. The option is\n * gone; existing config that passed `signingKey` should drop it.\n */\nexport type ServerOptions = {\n /** Token store. Defaults to an `InMemoryTokenStore`. */\n tokenStore?: TokenStore\n\n /** Identity resolver. Defaults to anonymous (always null). */\n identityResolver?: IdentityResolver\n\n /** Audit sink. Defaults to `consoleAuditSink`. */\n auditSink?: AuditSink\n\n /** Rate limiter. Defaults to `defaultRateLimiter` with 30/minute. */\n rateLimiter?: RateLimiter\n\n /** Base path prefix for LAP endpoints. Defaults to `/agent/lap/v1`. */\n lapBasePath?: string\n\n /** Pairing grace window after a tab closes, in ms. Default 15 min. */\n pairingGraceMs?: number\n\n /** Sliding TTL for active tokens, in ms. Default 1 h. */\n slidingTtlMs?: number\n\n /** Allowed origins for the HTTP surface (CORS). Empty = any. */\n corsOrigins?: readonly string[]\n\n /**\n * Enable the server-side MCP endpoint at `/agent/mcp` (or a custom\n * path). When set, Claude Desktop can connect directly to the app\n * backend without installing the `llui-agent` bridge — the user pastes\n * the token via `connect_session` in chat, same flow as the bridge but\n * no separate process required.\n *\n * Pass `true` to use all defaults, or an `McpRouterOptions` object to\n * customise the path, server name, and connect_session description.\n */\n mcp?: boolean | McpRouterOptions\n}\n\n/**\n * Value returned by `createLluiAgentServer`. `router` matches any\n * `/agent/*` request and returns a Response (or null to fall through).\n * `wsUpgrade` handles Node HTTP upgrade events for `/agent/ws`.\n */\nexport type AgentServerHandle = {\n router: (req: Request) => Promise<Response | null>\n /**\n * Handles Node HTTP upgrade events for `/agent/ws`. Returns a Promise\n * because token verification uses WebCrypto (async). Node's\n * `server.on('upgrade', handler)` fires the handler without awaiting,\n * which is fine — the handler writes errors directly to the socket\n * and never throws back to the caller.\n */\n wsUpgrade: (req: IncomingMessage, socket: Duplex, head: Buffer) => Promise<void>\n /** The pairing registry. Runtime-neutral adapters may access it. */\n registry: PairingRegistry\n /** The active token store. */\n tokenStore: TokenStore\n /** The active audit sink. */\n auditSink: AuditSink\n /**\n * Runtime-neutral WebSocket acceptance primitive. Validates a token\n * and registers a `PairingConnection` with the registry. The Node\n * `wsUpgrade` above calls this internally; web-runtime adapters\n * (`@llui/agent/server/web`) use it after accepting a WebSocket via\n * their native API.\n */\n acceptConnection: (token: string, conn: PairingConnection) => Promise<AcceptResult>\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llui/agent",
3
- "version": "0.0.52",
3
+ "version": "0.0.53",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "exports": {
@@ -32,6 +32,10 @@
32
32
  "types": "./dist/codecs.d.ts",
33
33
  "import": "./dist/codecs.js"
34
34
  },
35
+ "./mcp/tools": {
36
+ "types": "./dist/mcp/tools.d.ts",
37
+ "import": "./dist/mcp/tools.js"
38
+ },
35
39
  "./styles/agent-panel.css": "./styles/agent-panel.css"
36
40
  },
37
41
  "files": [
@@ -39,7 +43,9 @@
39
43
  "styles"
40
44
  ],
41
45
  "dependencies": {
42
- "ws": "^8.18.0"
46
+ "@modelcontextprotocol/sdk": "^1.29.0",
47
+ "ws": "^8.18.0",
48
+ "zod": "^4.0.0"
43
49
  },
44
50
  "peerDependencies": {
45
51
  "@llui/dom": "^0.0.37"