llui-agent 0.0.1 → 0.0.4

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # llui-agent
2
2
 
3
- MCP bridge for the [LLui Agent Protocol](../../docs/superpowers/specs/2026-04-19-llui-agent-design.md). Install once into your LLM client; paste a `/llui-connect <url> <token>` into any Claude conversation to bind it to a running LLui app.
3
+ MCP bridge for the [LLui Agent Protocol](../../docs/superpowers/specs/2026-04-19-llui-agent-design.md). Install once into your LLM client; paste the connect snippet from any LLui app to bind the conversation to that app.
4
4
 
5
5
  ## Install (Claude Desktop)
6
6
 
@@ -17,18 +17,58 @@ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) o
17
17
  }
18
18
  ```
19
19
 
20
- Restart Claude Desktop. The 11 LLui tools (`llui_connect_session`, `llui_disconnect_session`, `describe_app`, `get_state`, `list_actions`, `send_message`, `get_confirm_result`, `wait_for_change`, `query_dom`, `describe_visible_content`, `describe_context`) and the `/llui-connect` prompt now appear in Claude.
20
+ Restart Claude Desktop. The 11 LLui tools (`llui_connect_session`, `llui_disconnect_session`, `describe_app`, `get_state`, `list_actions`, `send_message`, `get_confirm_result`, `wait_for_change`, `query_dom`, `describe_visible_content`, `describe_context`) now appear. Desktop exposes the bundled `llui-connect` MCP prompt as a slash command — see "Slash shortcuts" below.
21
+
22
+ ## Install (Claude Code CLI)
23
+
24
+ ```bash
25
+ claude mcp add --transport stdio llui -- npx -y llui-agent
26
+ ```
27
+
28
+ For local development against unpublished bridge code, point at the built CLI instead:
29
+
30
+ ```bash
31
+ claude mcp add --transport stdio llui -- node /absolute/path/to/llui/packages/agent-bridge/dist/cli.js
32
+ ```
33
+
34
+ Run `/mcp` inside CC to confirm the server connected (or start a new session). The same 11 tools become available.
35
+
36
+ > **If you run CC in auto mode** (`permissions.defaultMode: "auto"` in `~/.claude/settings.json`), the auto-classifier silently rejects unrecognized MCP tools the first time they're called — Claude reports "tool was rejected" but no UI prompt is shown. Add the bridge's tools to your allowlist once so subsequent calls go through:
37
+ >
38
+ > ```jsonc
39
+ > // ~/.claude/settings.json
40
+ > {
41
+ > "permissions": {
42
+ > "allow": [
43
+ > "mcp__llui__*", // replace `llui` with the name you used in `claude mcp add`
44
+ > ],
45
+ > },
46
+ > }
47
+ > ```
48
+ >
49
+ > Users on `defaultMode: "default"` or `"ask"` instead get a permission prompt on the first call and don't need this allowlist entry.
21
50
 
22
51
  ## Use
23
52
 
24
- Open any LLui app that's built with `@llui/agent/client`. Click "Connect with Claude" in the app. Copy the generated `/llui-connect <url> <token>` string into Claude. Claude will now talk to that specific app instance.
53
+ Open any LLui app built with `@llui/agent/client`. Click "Connect with Claude" in the app and copy the generated snippet. Paste it into Claude — the snippet is a natural-language instruction containing the URL and token. Claude reads it and calls `llui_connect_session` to bind. The same snippet works in Desktop and CC.
54
+
55
+ Each Claude chat is bound to ONE LLui app at a time. To switch, ask Claude to call `llui_disconnect_session` and paste a new snippet.
56
+
57
+ ## Slash shortcuts (optional)
58
+
59
+ The bridge registers an MCP prompt named `llui-connect`. Both clients surface it as a slash command, but the namespacing differs:
60
+
61
+ | Client | Shortcut |
62
+ | --------------- | ------------------------------------------------- |
63
+ | Claude Desktop | `/llui-connect <url> <token>` |
64
+ | Claude Code CLI | `/mcp__<server-name>__llui-connect <url> <token>` |
25
65
 
26
- Each Claude chat is bound to ONE LLui app at a time. To switch, run `/llui-disconnect` or start a new chat.
66
+ The `<server-name>` in CC is whatever you passed to `claude mcp add` `llui` if you used the command above. Power-user shortcut only; the natural-language snippet from the app works the same in either client and doesn't depend on the server-name choice.
27
67
 
28
68
  ## How it works
29
69
 
30
- 1. Your LLui app mints a per-browser-session token and shows a `/llui-connect` string.
31
- 2. You paste into Claude — the bridge records `{url, token}` for this chat.
70
+ 1. Your LLui app mints a per-browser-session token and renders a connect snippet — a one-line instruction containing the LAP URL and the bearer token.
71
+ 2. You paste into Claude — Claude reads the snippet, calls `llui_connect_session`, and the bridge records `{url, token}` for this chat.
32
72
  3. The bridge pings `POST {url}/describe` to validate and cache the app's schema.
33
73
  4. Subsequent Claude tool calls (`get_state`, `send_message`, etc.) forward to `{url}/<path>` with your token as a Bearer.
34
74
  5. Sensitive actions (`@requiresConfirm` in the app's code) route through a confirmation prompt that only the user can approve.
package/dist/bridge.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js';
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { BindingMap } from './binding.js';
3
3
  export type BridgeDeps = {
4
4
  /** Injectable for tests. */
@@ -10,5 +10,18 @@ export type BridgeDeps = {
10
10
  /** Package version — set from package.json at boot. */
11
11
  version: string;
12
12
  };
13
+ /**
14
+ * Builds the bridge's MCP server using the high-level `McpServer`
15
+ * registrars. Each tool's Zod schema (declared once in `tools.ts`)
16
+ * drives both runtime input validation and the JSON Schema published
17
+ * to `tools/list` — eliminating the hand-written-schema-vs-handler
18
+ * drift that the low-level `setRequestHandler` pattern is prone to.
19
+ *
20
+ * Forwarded tools (`kind: 'forward'`) share a generic forwarder that
21
+ * looks up the binding, dispatches to LAP, and caches description
22
+ * payloads where applicable. The two meta tools
23
+ * (`llui_connect_session`, `llui_disconnect_session`) carry custom
24
+ * handlers that mutate the BindingMap directly.
25
+ */
13
26
  export declare function createBridgeServer(deps: BridgeDeps): McpServer;
14
27
  //# sourceMappingURL=bridge.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,2CAA2C,CAAA;AAQ/E,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAKzC,MAAM,MAAM,UAAU,GAAG;IACvB,4BAA4B;IAC5B,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;IACpB,0GAA0G;IAC1G,SAAS,EAAE,MAAM,CAAA;IACjB,uDAAuD;IACvD,QAAQ,EAAE,UAAU,CAAA;IACpB,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,SAAS,CAwE9D"}
1
+ {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAGnE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAKzC,MAAM,MAAM,UAAU,GAAG;IACvB,4BAA4B;IAC5B,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;IACpB,0GAA0G;IAC1G,SAAS,EAAE,MAAM,CAAA;IACjB,uDAAuD;IACvD,QAAQ,EAAE,UAAU,CAAA;IACpB,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,SAAS,CAa9D"}
package/dist/bridge.js CHANGED
@@ -1,67 +1,118 @@
1
- import { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js';
2
- import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
3
- import { TOOLS, TOOL_TO_LAP_PATH } from './tools.js';
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { TOOL_DESCRIPTORS } from './tools.js';
4
3
  import { BindingMap } from './binding.js';
5
4
  import { forwardLap } from './forwarder.js';
6
5
  import { registerPrompts } from './prompts.js';
6
+ /**
7
+ * Builds the bridge's MCP server using the high-level `McpServer`
8
+ * registrars. Each tool's Zod schema (declared once in `tools.ts`)
9
+ * drives both runtime input validation and the JSON Schema published
10
+ * to `tools/list` — eliminating the hand-written-schema-vs-handler
11
+ * drift that the low-level `setRequestHandler` pattern is prone to.
12
+ *
13
+ * Forwarded tools (`kind: 'forward'`) share a generic forwarder that
14
+ * looks up the binding, dispatches to LAP, and caches description
15
+ * payloads where applicable. The two meta tools
16
+ * (`llui_connect_session`, `llui_disconnect_session`) carry custom
17
+ * handlers that mutate the BindingMap directly.
18
+ */
7
19
  export function createBridgeServer(deps) {
8
20
  const server = new McpServer({ name: 'llui-agent', version: deps.version }, { capabilities: { tools: {}, prompts: {} } });
9
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
10
- tools: TOOLS,
11
- }));
12
- server.setRequestHandler(CallToolRequestSchema, async (req) => {
13
- const { name, arguments: args = {} } = req.params;
14
- if (name === 'llui_connect_session') {
15
- const { url, token } = args;
16
- if (typeof url !== 'string' || typeof token !== 'string') {
17
- return errorResult('invalid: url and token required');
18
- }
19
- deps.bindings.set(deps.sessionId, url, token);
20
- // Validate immediately by pinging /describe
21
- const res = await forwardLap(url, token, '/describe', {}, { fetch: deps.fetch });
22
- if (!res.ok) {
23
- deps.bindings.clear(deps.sessionId);
24
- return errorResult(`connect failed: ${JSON.stringify(res.error)}`);
25
- }
26
- const describe = res.body;
27
- deps.bindings.setDescribe(deps.sessionId, describe);
28
- return okResult({
29
- appName: describe.name,
30
- appVersion: describe.version,
31
- status: 'connected',
32
- });
21
+ for (const desc of TOOL_DESCRIPTORS) {
22
+ registerToolDescriptor(server, deps, desc);
23
+ }
24
+ registerPrompts(server);
25
+ return server;
26
+ }
27
+ function registerToolDescriptor(server, deps, desc) {
28
+ if (desc.kind === 'meta') {
29
+ if (desc.name === 'llui_connect_session') {
30
+ registerConnectSession(server, deps, desc);
31
+ }
32
+ else if (desc.name === 'llui_disconnect_session') {
33
+ registerDisconnectSession(server, deps, desc);
33
34
  }
34
- if (name === 'llui_disconnect_session') {
35
+ return;
36
+ }
37
+ registerForwardedTool(server, deps, desc);
38
+ }
39
+ function registerConnectSession(server, deps, desc) {
40
+ server.registerTool(desc.name, { description: desc.description, inputSchema: desc.schema.shape }, async (args) => {
41
+ const { url, token } = args;
42
+ deps.bindings.set(deps.sessionId, url, token);
43
+ // Validate AND prefetch the bootstrap bundle in one call.
44
+ // /observe returns {state, actions, description, context} —
45
+ // exactly what the LLM needs to start acting. Without this,
46
+ // Claude has to follow up with `observe` to get anything
47
+ // usable, costing round-trips and creating a window where
48
+ // the connect tool's "you are now connected" result is the
49
+ // entire context the LLM has to reason about.
50
+ const res = await forwardLap(url, token, '/observe', {}, { fetch: deps.fetch });
51
+ if (!res.ok) {
35
52
  deps.bindings.clear(deps.sessionId);
36
- return okResult({ status: 'disconnected' });
53
+ return errorResult(`connect failed: ${JSON.stringify(res.error)}`);
37
54
  }
38
- // Forwarded tools
55
+ const observe = res.body;
56
+ deps.bindings.setDescribe(deps.sessionId, observe.description);
57
+ return okResult({
58
+ status: 'connected',
59
+ appName: observe.description.name,
60
+ appVersion: observe.description.version,
61
+ // Full observe payload — same shape the `observe` tool returns —
62
+ // so a `describe_app` / `get_state` / `list_actions` /
63
+ // `describe_context` follow-up is unnecessary on the first turn.
64
+ state: observe.state,
65
+ actions: observe.actions,
66
+ description: observe.description,
67
+ context: observe.context,
68
+ });
69
+ });
70
+ }
71
+ function registerDisconnectSession(server, deps, desc) {
72
+ server.registerTool(desc.name, { description: desc.description, inputSchema: desc.schema.shape }, async () => {
73
+ deps.bindings.clear(deps.sessionId);
74
+ return okResult({ status: 'disconnected' });
75
+ });
76
+ }
77
+ function registerForwardedTool(server, deps, desc) {
78
+ server.registerTool(desc.name, { description: desc.description, inputSchema: desc.schema.shape }, async (args) => {
39
79
  const binding = deps.bindings.get(deps.sessionId);
40
80
  if (!binding) {
41
- return errorResult('not bound — ask the user to run /llui-connect <url> <token> first');
81
+ return errorResult('not bound — ask the user to copy the connect snippet from the LLui app, ' +
82
+ 'or call `llui_connect_session` with the url and token they provide. ' +
83
+ '(In Claude Desktop only, the snippet is also available as the slash command `/llui-connect`.)');
42
84
  }
43
- // describe_app can serve from cache
44
- if (name === 'describe_app' && binding.describe) {
85
+ // describe_app can serve from cache when one is available.
86
+ if (desc.name === 'describe_app' && binding.describe) {
45
87
  return okResult(binding.describe);
46
88
  }
47
- const lapPath = TOOL_TO_LAP_PATH[name];
48
- if (!lapPath)
49
- return errorResult(`unknown tool: ${name}`);
50
- const res = await forwardLap(binding.url, binding.token, lapPath, args, { fetch: deps.fetch });
89
+ const res = await forwardLap(binding.url, binding.token, desc.lapPath, args ?? {}, {
90
+ fetch: deps.fetch,
91
+ });
51
92
  if (!res.ok) {
52
- return errorResult(`LAP ${lapPath} failed: status=${res.status} ${JSON.stringify(res.error)}`);
93
+ return errorResult(`LAP ${desc.lapPath} failed: status=${res.status} ${JSON.stringify(res.error)}`);
53
94
  }
54
- // Cache describe_app responses after the first call too
55
- if (name === 'describe_app') {
95
+ // Cache describe_app responses after the first call too.
96
+ if (desc.name === 'describe_app') {
56
97
  deps.bindings.setDescribe(deps.sessionId, res.body);
57
98
  }
99
+ // observe returns description on every call; cache it so a later
100
+ // describe_app can short-circuit the LAP round-trip.
101
+ if (desc.name === 'observe') {
102
+ const obs = res.body;
103
+ if (obs?.description)
104
+ deps.bindings.setDescribe(deps.sessionId, obs.description);
105
+ }
58
106
  return okResult(res.body);
59
107
  });
60
- registerPrompts(server);
61
- return server;
62
108
  }
63
109
  function okResult(body) {
110
+ // structuredContent is what current Claude clients (Desktop + CC)
111
+ // consume preferentially when present — typed JSON instead of a
112
+ // stringified blob. The `content` array stays as a `text` fallback
113
+ // so older clients still see something sensible.
64
114
  return {
115
+ structuredContent: body,
65
116
  content: [{ type: 'text', text: JSON.stringify(body) }],
66
117
  };
67
118
  }
@@ -1 +1 @@
1
- {"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"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","sourcesContent":["import { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js'\nimport {\n CallToolRequestSchema,\n ListToolsRequestSchema,\n type CallToolResult,\n type ListToolsResult,\n} from '@modelcontextprotocol/sdk/types.js'\nimport { TOOLS, TOOL_TO_LAP_PATH } from './tools.js'\nimport { BindingMap } from './binding.js'\nimport { forwardLap } from './forwarder.js'\nimport type { LapDescribeResponse } from '@llui/agent/protocol'\nimport { registerPrompts } from './prompts.js'\n\nexport type BridgeDeps = {\n /** Injectable for tests. */\n fetch?: typeof fetch\n /** MCP session ID for this client. In stdio mode there's one session; derive from the Server instance. */\n sessionId: string\n /** Shared binding map (one BindingMap per process). */\n bindings: BindingMap\n /** Package version — set from package.json at boot. */\n version: string\n}\n\nexport function createBridgeServer(deps: BridgeDeps): McpServer {\n const server = new McpServer(\n { name: 'llui-agent', version: deps.version },\n { capabilities: { tools: {}, prompts: {} } },\n )\n\n server.setRequestHandler(\n ListToolsRequestSchema,\n async (): Promise<ListToolsResult> => ({\n tools: TOOLS,\n }),\n )\n\n server.setRequestHandler(CallToolRequestSchema, async (req): Promise<CallToolResult> => {\n const { name, arguments: args = {} } = req.params\n\n if (name === 'llui_connect_session') {\n const { url, token } = args as { url?: string; token?: string }\n if (typeof url !== 'string' || typeof token !== 'string') {\n return errorResult('invalid: url and token required')\n }\n deps.bindings.set(deps.sessionId, url, token)\n // Validate immediately by pinging /describe\n const res = await forwardLap(url, token, '/describe', {}, { fetch: deps.fetch })\n if (!res.ok) {\n deps.bindings.clear(deps.sessionId)\n return errorResult(`connect failed: ${JSON.stringify(res.error)}`)\n }\n const describe = res.body as LapDescribeResponse\n deps.bindings.setDescribe(deps.sessionId, describe)\n return okResult({\n appName: describe.name,\n appVersion: describe.version,\n status: 'connected',\n })\n }\n\n if (name === 'llui_disconnect_session') {\n deps.bindings.clear(deps.sessionId)\n return okResult({ status: 'disconnected' })\n }\n\n // Forwarded tools\n const binding = deps.bindings.get(deps.sessionId)\n if (!binding) {\n return errorResult('not bound — ask the user to run /llui-connect <url> <token> first')\n }\n\n // describe_app can serve from cache\n if (name === 'describe_app' && binding.describe) {\n return okResult(binding.describe)\n }\n\n const lapPath = TOOL_TO_LAP_PATH[name]\n if (!lapPath) return errorResult(`unknown tool: ${name}`)\n\n const res = await forwardLap(binding.url, binding.token, lapPath, args, { fetch: deps.fetch })\n if (!res.ok) {\n return errorResult(`LAP ${lapPath} failed: status=${res.status} ${JSON.stringify(res.error)}`)\n }\n\n // Cache describe_app responses after the first call too\n if (name === 'describe_app') {\n deps.bindings.setDescribe(deps.sessionId, res.body as LapDescribeResponse)\n }\n\n return okResult(res.body)\n })\n\n registerPrompts(server)\n\n return server\n}\n\nfunction okResult(body: unknown): CallToolResult {\n return {\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"]}
1
+ {"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.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 { TOOL_DESCRIPTORS, type ToolDescriptor } from './tools.js'\nimport { BindingMap } from './binding.js'\nimport { forwardLap } from './forwarder.js'\nimport type { LapDescribeResponse, LapObserveResponse } from '@llui/agent/protocol'\nimport { registerPrompts } from './prompts.js'\n\nexport type BridgeDeps = {\n /** Injectable for tests. */\n fetch?: typeof fetch\n /** MCP session ID for this client. In stdio mode there's one session; derive from the Server instance. */\n sessionId: string\n /** Shared binding map (one BindingMap per process). */\n bindings: BindingMap\n /** Package version — set from package.json at boot. */\n version: string\n}\n\n/**\n * Builds the bridge's MCP server using the high-level `McpServer`\n * registrars. Each tool's Zod schema (declared once in `tools.ts`)\n * drives both runtime input validation and the JSON Schema published\n * to `tools/list` — eliminating the hand-written-schema-vs-handler\n * drift that the low-level `setRequestHandler` pattern is prone to.\n *\n * Forwarded tools (`kind: 'forward'`) share a generic forwarder that\n * looks up the binding, dispatches to LAP, and caches description\n * payloads where applicable. The two meta tools\n * (`llui_connect_session`, `llui_disconnect_session`) carry custom\n * handlers that mutate the BindingMap directly.\n */\nexport function createBridgeServer(deps: BridgeDeps): McpServer {\n const server = new McpServer(\n { name: 'llui-agent', version: deps.version },\n { capabilities: { tools: {}, prompts: {} } },\n )\n\n for (const desc of TOOL_DESCRIPTORS) {\n registerToolDescriptor(server, deps, desc)\n }\n\n registerPrompts(server)\n\n return server\n}\n\nfunction registerToolDescriptor(server: McpServer, deps: BridgeDeps, desc: ToolDescriptor): void {\n if (desc.kind === 'meta') {\n if (desc.name === 'llui_connect_session') {\n registerConnectSession(server, deps, desc)\n } else if (desc.name === 'llui_disconnect_session') {\n registerDisconnectSession(server, deps, desc)\n }\n return\n }\n registerForwardedTool(server, deps, desc)\n}\n\nfunction registerConnectSession(server: McpServer, deps: BridgeDeps, desc: ToolDescriptor): void {\n server.registerTool(\n desc.name,\n { description: desc.description, inputSchema: desc.schema.shape },\n async (args) => {\n const { url, token } = args as { url: string; token: string }\n deps.bindings.set(deps.sessionId, url, token)\n // Validate AND prefetch the bootstrap bundle in one call.\n // /observe returns {state, actions, description, context} —\n // exactly what the LLM needs to start acting. Without this,\n // Claude has to follow up with `observe` to get anything\n // usable, costing round-trips and creating a window where\n // the connect tool's \"you are now connected\" result is the\n // entire context the LLM has to reason about.\n const res = await forwardLap(url, token, '/observe', {}, { fetch: deps.fetch })\n if (!res.ok) {\n deps.bindings.clear(deps.sessionId)\n return errorResult(`connect failed: ${JSON.stringify(res.error)}`)\n }\n const observe = res.body as LapObserveResponse\n deps.bindings.setDescribe(deps.sessionId, observe.description)\n return okResult({\n status: 'connected',\n appName: observe.description.name,\n appVersion: observe.description.version,\n // Full observe payload — same shape the `observe` tool returns —\n // so a `describe_app` / `get_state` / `list_actions` /\n // `describe_context` follow-up is unnecessary on the first turn.\n state: observe.state,\n actions: observe.actions,\n description: observe.description,\n context: observe.context,\n })\n },\n )\n}\n\nfunction registerDisconnectSession(\n server: McpServer,\n deps: BridgeDeps,\n desc: ToolDescriptor,\n): void {\n server.registerTool(\n desc.name,\n { description: desc.description, inputSchema: desc.schema.shape },\n async () => {\n deps.bindings.clear(deps.sessionId)\n return okResult({ status: 'disconnected' })\n },\n )\n}\n\nfunction registerForwardedTool(\n server: McpServer,\n deps: BridgeDeps,\n desc: Extract<ToolDescriptor, { kind: 'forward' }>,\n): void {\n server.registerTool(\n desc.name,\n { description: desc.description, inputSchema: desc.schema.shape },\n async (args) => {\n const binding = deps.bindings.get(deps.sessionId)\n if (!binding) {\n return errorResult(\n 'not bound — ask the user to copy the connect snippet from the LLui app, ' +\n 'or call `llui_connect_session` with the url and token they provide. ' +\n '(In Claude Desktop only, the snippet is also available as the slash command `/llui-connect`.)',\n )\n }\n\n // describe_app can serve from cache when one is available.\n if (desc.name === 'describe_app' && binding.describe) {\n return okResult(binding.describe)\n }\n\n const res = await forwardLap(binding.url, binding.token, desc.lapPath, args ?? {}, {\n fetch: deps.fetch,\n })\n if (!res.ok) {\n return errorResult(\n `LAP ${desc.lapPath} failed: status=${res.status} ${JSON.stringify(res.error)}`,\n )\n }\n\n // Cache describe_app responses after the first call too.\n if (desc.name === 'describe_app') {\n deps.bindings.setDescribe(deps.sessionId, res.body as LapDescribeResponse)\n }\n\n // observe returns description on every call; cache it so a later\n // describe_app can short-circuit the LAP round-trip.\n if (desc.name === 'observe') {\n const obs = res.body as LapObserveResponse\n if (obs?.description) deps.bindings.setDescribe(deps.sessionId, obs.description)\n }\n\n return okResult(res.body)\n },\n )\n}\n\nfunction okResult(body: unknown): CallToolResult {\n // structuredContent is what current Claude clients (Desktop + CC)\n // consume preferentially when present — typed JSON instead of a\n // stringified blob. The `content` array stays as a `text` fallback\n // so older clients still see something sensible.\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"]}
package/dist/prompts.d.ts CHANGED
@@ -1,3 +1,11 @@
1
- import type { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js';
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ /**
3
+ * Registers the bundled `llui-connect` MCP prompt. Both Claude Desktop
4
+ * and Claude Code surface it as a slash command (Desktop:
5
+ * `/llui-connect <url> <token>`; CC: `/mcp__<server>__llui-connect …`).
6
+ * The prompt body Claude sees is the same natural-language instruction
7
+ * the LLui app shows in its connect snippet — so pasting either form
8
+ * lands the same `llui_connect_session` tool call.
9
+ */
2
10
  export declare function registerPrompts(server: McpServer): void;
3
11
  //# sourceMappingURL=prompts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,2CAA2C,CAAA;AAQpF,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAuCvD"}
1
+ {"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAGxE;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA0BvD"}
package/dist/prompts.js CHANGED
@@ -1,36 +1,31 @@
1
- import { GetPromptRequestSchema, ListPromptsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
1
+ import { z } from 'zod';
2
+ /**
3
+ * Registers the bundled `llui-connect` MCP prompt. Both Claude Desktop
4
+ * and Claude Code surface it as a slash command (Desktop:
5
+ * `/llui-connect <url> <token>`; CC: `/mcp__<server>__llui-connect …`).
6
+ * The prompt body Claude sees is the same natural-language instruction
7
+ * the LLui app shows in its connect snippet — so pasting either form
8
+ * lands the same `llui_connect_session` tool call.
9
+ */
2
10
  export function registerPrompts(server) {
3
- server.setRequestHandler(ListPromptsRequestSchema, async () => ({
4
- prompts: [
11
+ server.registerPrompt('llui-connect', {
12
+ description: 'Bind this Claude conversation to an LLui app. Paste the URL and token the app showed you.',
13
+ argsSchema: {
14
+ url: z.string().describe('LAP base URL'),
15
+ token: z.string().describe('Bearer token'),
16
+ },
17
+ }, ({ url, token }) => ({
18
+ description: `Bind to LLui app at ${url}`,
19
+ messages: [
5
20
  {
6
- name: 'llui-connect',
7
- description: 'Bind this Claude conversation to an LLui app. Paste the URL and token the app showed you.',
8
- arguments: [
9
- { name: 'url', description: 'LAP base URL', required: true },
10
- { name: 'token', description: 'Bearer token', required: true },
11
- ],
21
+ role: 'user',
22
+ content: {
23
+ type: 'text',
24
+ text: `Please connect this conversation to the LLui app at ${url}. ` +
25
+ `Call llui_connect_session with url=${JSON.stringify(url)} and token=${JSON.stringify(token)}.`,
26
+ },
12
27
  },
13
28
  ],
14
29
  }));
15
- server.setRequestHandler(GetPromptRequestSchema, async (req) => {
16
- if (req.params.name !== 'llui-connect') {
17
- throw new Error(`unknown prompt: ${req.params.name}`);
18
- }
19
- const url = req.params.arguments?.['url'] ?? '';
20
- const token = req.params.arguments?.['token'] ?? '';
21
- return {
22
- description: `Bind to LLui app at ${url}`,
23
- messages: [
24
- {
25
- role: 'user',
26
- content: {
27
- type: 'text',
28
- text: `Please connect this conversation to the LLui app at ${url}. ` +
29
- `Call llui_connect_session with url=${JSON.stringify(url)} and token=${JSON.stringify(token)}.`,
30
- },
31
- },
32
- ],
33
- };
34
- });
35
30
  }
36
31
  //# sourceMappingURL=prompts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"prompts.js","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"AACA,OAAO,EACL,sBAAsB,EACtB,wBAAwB,GAGzB,MAAM,oCAAoC,CAAA;AAE3C,MAAM,UAAU,eAAe,CAAC,MAAiB;IAC/C,MAAM,CAAC,iBAAiB,CACtB,wBAAwB,EACxB,KAAK,IAAgC,EAAE,CAAC,CAAC;QACvC,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,cAAc;gBACpB,WAAW,EACT,2FAA2F;gBAC7F,SAAS,EAAE;oBACT,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,QAAQ,EAAE,IAAI,EAAE;oBAC5D,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,QAAQ,EAAE,IAAI,EAAE;iBAC/D;aACF;SACF;KACF,CAAC,CACH,CAAA;IAED,MAAM,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,EAAE,GAAG,EAA4B,EAAE;QACvF,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;YACvC,MAAM,IAAI,KAAK,CAAC,mBAAmB,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAA;QACvD,CAAC;QACD,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAA;QAC/C,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,CAAA;QACnD,OAAO;YACL,WAAW,EAAE,uBAAuB,GAAG,EAAE;YACzC,QAAQ,EAAE;gBACR;oBACE,IAAI,EAAE,MAAM;oBACZ,OAAO,EAAE;wBACP,IAAI,EAAE,MAAM;wBACZ,IAAI,EACF,uDAAuD,GAAG,IAAI;4BAC9D,sCAAsC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,cAAc,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG;qBAClG;iBACF;aACF;SACF,CAAA;IACH,CAAC,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import type { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js'\nimport {\n GetPromptRequestSchema,\n ListPromptsRequestSchema,\n type ListPromptsResult,\n type GetPromptResult,\n} from '@modelcontextprotocol/sdk/types.js'\n\nexport function registerPrompts(server: McpServer): void {\n server.setRequestHandler(\n ListPromptsRequestSchema,\n async (): Promise<ListPromptsResult> => ({\n prompts: [\n {\n name: 'llui-connect',\n description:\n 'Bind this Claude conversation to an LLui app. Paste the URL and token the app showed you.',\n arguments: [\n { name: 'url', description: 'LAP base URL', required: true },\n { name: 'token', description: 'Bearer token', required: true },\n ],\n },\n ],\n }),\n )\n\n server.setRequestHandler(GetPromptRequestSchema, async (req): Promise<GetPromptResult> => {\n if (req.params.name !== 'llui-connect') {\n throw new Error(`unknown prompt: ${req.params.name}`)\n }\n const url = req.params.arguments?.['url'] ?? ''\n const token = req.params.arguments?.['token'] ?? ''\n return {\n description: `Bind to LLui app at ${url}`,\n messages: [\n {\n role: 'user',\n content: {\n type: 'text',\n text:\n `Please connect this conversation to the LLui app at ${url}. ` +\n `Call llui_connect_session with url=${JSON.stringify(url)} and token=${JSON.stringify(token)}.`,\n },\n },\n ],\n }\n })\n}\n"]}
1
+ {"version":3,"file":"prompts.js","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,MAAiB;IAC/C,MAAM,CAAC,cAAc,CACnB,cAAc,EACd;QACE,WAAW,EACT,2FAA2F;QAC7F,UAAU,EAAE;YACV,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,cAAc,CAAC;YACxC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,cAAc,CAAC;SAC3C;KACF,EACD,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;QACnB,WAAW,EAAE,uBAAuB,GAAG,EAAE;QACzC,QAAQ,EAAE;YACR;gBACE,IAAI,EAAE,MAAM;gBACZ,OAAO,EAAE;oBACP,IAAI,EAAE,MAAM;oBACZ,IAAI,EACF,uDAAuD,GAAG,IAAI;wBAC9D,sCAAsC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,cAAc,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG;iBAClG;aACF;SACF;KACF,CAAC,CACH,CAAA;AACH,CAAC","sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { z } from 'zod'\n\n/**\n * Registers the bundled `llui-connect` MCP prompt. Both Claude Desktop\n * and Claude Code surface it as a slash command (Desktop:\n * `/llui-connect <url> <token>`; CC: `/mcp__<server>__llui-connect …`).\n * The prompt body Claude sees is the same natural-language instruction\n * the LLui app shows in its connect snippet — so pasting either form\n * lands the same `llui_connect_session` tool call.\n */\nexport function registerPrompts(server: McpServer): void {\n server.registerPrompt(\n 'llui-connect',\n {\n description:\n 'Bind this Claude conversation to an LLui app. Paste the URL and token the app showed you.',\n argsSchema: {\n url: z.string().describe('LAP base URL'),\n token: z.string().describe('Bearer token'),\n },\n },\n ({ url, token }) => ({\n description: `Bind to LLui app at ${url}`,\n messages: [\n {\n role: 'user',\n content: {\n type: 'text',\n text:\n `Please connect this conversation to the LLui app at ${url}. ` +\n `Call llui_connect_session with url=${JSON.stringify(url)} and token=${JSON.stringify(token)}.`,\n },\n },\n ],\n }),\n )\n}\n"]}
package/dist/tools.d.ts CHANGED
@@ -1,14 +1,21 @@
1
- import type { ListToolsResult } from '@modelcontextprotocol/sdk/types.js';
2
- /**
3
- * The 10 MCP tools Claude sees:
4
- * 2 meta-tools (bind/unbind) + 8 forwarded tools (1:1 with LAP endpoints).
5
- *
6
- * Spec §8.
7
- */
8
- export declare const TOOLS: ListToolsResult['tools'];
9
- /**
10
- * Mapping from tool name → LAP path for the forwarded subset.
11
- * Meta-tools handled separately in bridge.ts.
12
- */
13
- export declare const TOOL_TO_LAP_PATH: Record<string, string>;
1
+ import { z } from 'zod';
2
+ /** Descriptor for a tool that forwards directly to the bound LAP server. */
3
+ export interface ForwardedToolDescriptor {
4
+ kind: 'forward';
5
+ name: string;
6
+ description: string;
7
+ /** Zod schema defining the tool's input shape. */
8
+ schema: z.ZodObject<z.ZodRawShape>;
9
+ /** LAP endpoint path (relative to the binding's base URL). */
10
+ lapPath: string;
11
+ }
12
+ /** Descriptor for a tool whose handler is implemented in the bridge itself. */
13
+ export interface MetaToolDescriptor {
14
+ kind: 'meta';
15
+ name: string;
16
+ description: string;
17
+ schema: z.ZodObject<z.ZodRawShape>;
18
+ }
19
+ export type ToolDescriptor = ForwardedToolDescriptor | MetaToolDescriptor;
20
+ export declare const TOOL_DESCRIPTORS: ToolDescriptor[];
14
21
  //# sourceMappingURL=tools.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAA;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,KAAK,EAAE,eAAe,CAAC,OAAO,CAkH1C,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAUnD,CAAA"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AA6BvB,4EAA4E;AAC5E,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,SAAS,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,kDAAkD;IAClD,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAA;IAClC,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,+EAA+E;AAC/E,MAAM,WAAW,kBAAkB;IACjC,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,cAAc,GAAG,uBAAuB,GAAG,kBAAkB,CAAA;AAEzE,eAAO,MAAM,gBAAgB,EAAE,cAAc,EAoL5C,CAAA"}
package/dist/tools.js CHANGED
@@ -1,129 +1,187 @@
1
+ import { z } from 'zod';
1
2
  /**
2
- * The 10 MCP tools Claude sees:
3
- * 2 meta-tools (bind/unbind) + 8 forwarded tools (1:1 with LAP endpoints).
3
+ * Tool catalogue exposed to Claude through the MCP bridge. Two tiers:
4
+ *
5
+ * - **Efficient path (recommended)**: `observe` + `send_message`.
6
+ * `observe` returns state + actions + description + context in a
7
+ * single LAP call, replacing the old describe_app + get_state +
8
+ * list_actions trio. `send_message` defaults to `waitFor:'drained'`,
9
+ * blocking until the message queue goes idle (http/delay/debounce
10
+ * round-trips complete) and returning the new state + actions +
11
+ * drain meta. Together these cut the "check state → act → check
12
+ * state" loop from 5 round-trips to 2.
13
+ *
14
+ * - **Legacy / specialized**: `describe_app`, `get_state`,
15
+ * `list_actions`, `wait_for_change`. Kept for back-compat and niche
16
+ * uses (e.g. scoped state reads via JSON pointer, external state
17
+ * pushes). New integrations should prefer `observe`.
4
18
  *
5
19
  * Spec §8.
20
+ *
21
+ * The catalogue is the single source of truth — Zod schemas drive both
22
+ * runtime input validation and the JSON Schema published in
23
+ * `tools/list`. Forwarded tools also carry their LAP endpoint path so
24
+ * `bridge.ts` can register one generic forwarder that loops over them.
6
25
  */
7
- export const TOOLS = [
26
+ const empty = z.object({});
27
+ export const TOOL_DESCRIPTORS = [
8
28
  {
29
+ kind: 'meta',
9
30
  name: 'llui_connect_session',
10
- description: 'Bind this Claude conversation to a specific LLui app. Call ONCE per chat when the user pastes /llui-connect <url> <token>. Subsequent LLui tool calls target the bound app.',
11
- inputSchema: {
12
- type: 'object',
13
- properties: {
14
- url: {
15
- type: 'string',
16
- description: 'LAP base URL (e.g. https://app.example/agent/lap/v1)',
17
- },
18
- token: { type: 'string', description: 'Bearer token for LAP calls' },
19
- },
20
- required: ['url', 'token'],
21
- },
31
+ description: 'Bind this Claude conversation to a specific LLui app. Call ONCE per chat when the user pastes a connect snippet from the LLui app — the snippet contains the url and token to forward here. The result includes the full observe bundle ({state, actions, description, context}) so you have everything you need to start acting — no separate describe_app / get_state / list_actions / describe_context follow-up is required on the first turn. Use observe later when you want a refreshed snapshot.',
32
+ schema: z.object({
33
+ url: z.string().describe('LAP base URL (e.g. https://app.example/agent/lap/v1)'),
34
+ token: z.string().describe('Bearer token for LAP calls'),
35
+ }),
22
36
  },
23
37
  {
38
+ kind: 'meta',
24
39
  name: 'llui_disconnect_session',
25
40
  description: 'Clear the binding for this Claude conversation. Subsequent LLui tool calls will fail until rebind.',
26
- inputSchema: { type: 'object', properties: {} },
41
+ schema: empty,
27
42
  },
28
43
  {
44
+ kind: 'forward',
45
+ name: 'observe',
46
+ 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.',
47
+ schema: empty,
48
+ lapPath: '/observe',
49
+ },
50
+ {
51
+ kind: 'forward',
29
52
  name: 'describe_app',
30
- description: "Return the bound app's name, version, state/message schemas, annotations, and static docs.",
31
- inputSchema: { type: 'object', properties: {} },
53
+ description: "Return the bound app's name, version, state/message schemas, annotations, and static docs. Legacy — prefer `observe`, which includes this as `description`.",
54
+ schema: empty,
55
+ lapPath: '/describe',
32
56
  },
33
57
  {
58
+ kind: 'forward',
34
59
  name: 'get_state',
35
- description: 'Return the current app state. Optional `path` (JSON-pointer) to narrow the slice.',
36
- inputSchema: {
37
- type: 'object',
38
- properties: {
39
- path: { type: 'string', description: 'Optional JSON-pointer, e.g. "/user/name"' },
40
- },
41
- },
60
+ 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.',
61
+ schema: z.object({
62
+ path: z.string().optional().describe('Optional JSON-pointer, e.g. "/user/name"'),
63
+ }),
64
+ lapPath: '/state',
65
+ },
66
+ {
67
+ kind: 'forward',
68
+ name: 'query_state',
69
+ 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 `~`).',
70
+ schema: z.object({
71
+ path: z.string().describe('JSON-pointer (RFC 6901) — `/auth/user` or `""` for whole state'),
72
+ }),
73
+ lapPath: '/query-state',
42
74
  },
43
75
  {
76
+ kind: 'forward',
77
+ name: 'describe_recent_actions',
78
+ 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.',
79
+ schema: z.object({
80
+ n: z.number().int().positive().optional().describe('How many entries to return (default 10)'),
81
+ kind: z
82
+ .string()
83
+ .optional()
84
+ .describe('Filter to a specific kind (e.g. "dispatched", "read", "error")'),
85
+ }),
86
+ lapPath: '/recent-actions',
87
+ },
88
+ {
89
+ kind: 'forward',
90
+ name: 'would_dispatch',
91
+ 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.',
92
+ schema: z.object({
93
+ msg: z
94
+ .object({ type: z.string() })
95
+ .passthrough()
96
+ .describe('The candidate message; must have a `type` string'),
97
+ }),
98
+ lapPath: '/would-dispatch',
99
+ },
100
+ {
101
+ kind: 'forward',
44
102
  name: 'list_actions',
45
- description: 'Return the currently-affordable actions: visible UI bindings plus agent-affordable registry entries, filtered by annotation gates.',
46
- inputSchema: { type: 'object', properties: {} },
103
+ 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`.',
104
+ schema: empty,
105
+ lapPath: '/actions',
47
106
  },
48
107
  {
108
+ kind: 'forward',
49
109
  name: 'send_message',
50
- description: 'Dispatch a message to the app. Auto-proposes a user confirmation when the message variant is @requiresConfirm. Returns dispatched / pending-confirmation / rejected.',
51
- inputSchema: {
52
- type: 'object',
53
- properties: {
54
- msg: { type: 'object', description: 'The message to dispatch; must have a `type` string' },
55
- reason: {
56
- type: 'string',
57
- description: 'User-facing rationale (required for confirm-gated variants)',
58
- },
59
- waitFor: { type: 'string', enum: ['idle', 'none'], description: 'default "idle"' },
60
- timeoutMs: { type: 'number' },
61
- },
62
- required: ['msg'],
63
- },
110
+ 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.',
111
+ schema: z.object({
112
+ msg: z
113
+ .object({ type: z.string() })
114
+ .passthrough()
115
+ .describe('The message to dispatch; must have a `type` string'),
116
+ reason: z
117
+ .string()
118
+ .optional()
119
+ .describe('User-facing rationale (required for confirm-gated variants)'),
120
+ waitFor: z
121
+ .enum(['drained', 'idle', 'none'])
122
+ .optional()
123
+ .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.'),
124
+ drainQuietMs: z
125
+ .number()
126
+ .optional()
127
+ .describe('Quiescence window for waitFor:"drained". Drain completes when no commit fires for this many ms. Default 100.'),
128
+ timeoutMs: z
129
+ .number()
130
+ .optional()
131
+ .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.'),
132
+ includeState: z
133
+ .boolean()
134
+ .optional()
135
+ .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).'),
136
+ }),
137
+ lapPath: '/message',
64
138
  },
65
139
  {
140
+ kind: 'forward',
66
141
  name: 'get_confirm_result',
67
142
  description: 'Poll a pending-confirmation by confirmId. Returns confirmed / rejected / still-pending.',
68
- inputSchema: {
69
- type: 'object',
70
- properties: {
71
- confirmId: { type: 'string' },
72
- timeoutMs: { type: 'number' },
73
- },
74
- required: ['confirmId'],
75
- },
143
+ schema: z.object({
144
+ confirmId: z.string(),
145
+ timeoutMs: z.number().optional(),
146
+ }),
147
+ lapPath: '/confirm-result',
76
148
  },
77
149
  {
150
+ kind: 'forward',
78
151
  name: 'wait_for_change',
79
- description: 'Long-poll for a state change. Returns changed / timeout.',
80
- inputSchema: {
81
- type: 'object',
82
- properties: {
83
- path: {
84
- type: 'string',
85
- description: 'Optional JSON-pointer to narrow which state changes trigger resolution',
86
- },
87
- timeoutMs: { type: 'number' },
88
- },
89
- },
152
+ 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.',
153
+ schema: z.object({
154
+ path: z
155
+ .string()
156
+ .optional()
157
+ .describe('Optional JSON-pointer to narrow which state changes trigger resolution'),
158
+ timeoutMs: z.number().optional(),
159
+ }),
160
+ lapPath: '/wait',
90
161
  },
91
162
  {
163
+ kind: 'forward',
92
164
  name: 'query_dom',
93
165
  description: 'Read elements tagged with data-agent="<name>" in the rendered UI.',
94
- inputSchema: {
95
- type: 'object',
96
- properties: {
97
- name: { type: 'string' },
98
- multiple: { type: 'boolean' },
99
- },
100
- required: ['name'],
101
- },
166
+ schema: z.object({
167
+ name: z.string(),
168
+ multiple: z.boolean().optional(),
169
+ }),
170
+ lapPath: '/query-dom',
102
171
  },
103
172
  {
173
+ kind: 'forward',
104
174
  name: 'describe_visible_content',
105
175
  description: 'Return a structured outline of the currently-visible data-agent-tagged subtrees.',
106
- inputSchema: { type: 'object', properties: {} },
176
+ schema: empty,
177
+ lapPath: '/describe-visible',
107
178
  },
108
179
  {
180
+ kind: 'forward',
109
181
  name: 'describe_context',
110
182
  description: 'Return the current per-state narrative docs (agentContext) — what the user is trying to do right now.',
111
- inputSchema: { type: 'object', properties: {} },
183
+ schema: empty,
184
+ lapPath: '/context',
112
185
  },
113
186
  ];
114
- /**
115
- * Mapping from tool name → LAP path for the forwarded subset.
116
- * Meta-tools handled separately in bridge.ts.
117
- */
118
- export const TOOL_TO_LAP_PATH = {
119
- describe_app: '/describe',
120
- get_state: '/state',
121
- list_actions: '/actions',
122
- send_message: '/message',
123
- get_confirm_result: '/confirm-result',
124
- wait_for_change: '/wait',
125
- query_dom: '/query-dom',
126
- describe_visible_content: '/describe-visible',
127
- describe_context: '/context',
128
- };
129
187
  //# sourceMappingURL=tools.js.map
package/dist/tools.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ListToolsResult } from '@modelcontextprotocol/sdk/types.js'\n\n/**\n * The 10 MCP tools Claude sees:\n * 2 meta-tools (bind/unbind) + 8 forwarded tools (1:1 with LAP endpoints).\n *\n * Spec §8.\n */\nexport const TOOLS: ListToolsResult['tools'] = [\n {\n name: 'llui_connect_session',\n description:\n 'Bind this Claude conversation to a specific LLui app. Call ONCE per chat when the user pastes /llui-connect <url> <token>. Subsequent LLui tool calls target the bound app.',\n inputSchema: {\n type: 'object',\n properties: {\n url: {\n type: 'string',\n description: 'LAP base URL (e.g. https://app.example/agent/lap/v1)',\n },\n token: { type: 'string', description: 'Bearer token for LAP calls' },\n },\n required: ['url', 'token'],\n },\n },\n {\n name: 'llui_disconnect_session',\n description:\n 'Clear the binding for this Claude conversation. Subsequent LLui tool calls will fail until rebind.',\n inputSchema: { type: 'object', properties: {} },\n },\n {\n name: 'describe_app',\n description:\n \"Return the bound app's name, version, state/message schemas, annotations, and static docs.\",\n inputSchema: { type: 'object', properties: {} },\n },\n {\n name: 'get_state',\n description:\n 'Return the current app state. Optional `path` (JSON-pointer) to narrow the slice.',\n inputSchema: {\n type: 'object',\n properties: {\n path: { type: 'string', description: 'Optional JSON-pointer, e.g. \"/user/name\"' },\n },\n },\n },\n {\n name: 'list_actions',\n description:\n 'Return the currently-affordable actions: visible UI bindings plus agent-affordable registry entries, filtered by annotation gates.',\n inputSchema: { type: 'object', properties: {} },\n },\n {\n name: 'send_message',\n description:\n 'Dispatch a message to the app. Auto-proposes a user confirmation when the message variant is @requiresConfirm. Returns dispatched / pending-confirmation / rejected.',\n inputSchema: {\n type: 'object',\n properties: {\n msg: { type: 'object', description: 'The message to dispatch; must have a `type` string' },\n reason: {\n type: 'string',\n description: 'User-facing rationale (required for confirm-gated variants)',\n },\n waitFor: { type: 'string', enum: ['idle', 'none'], description: 'default \"idle\"' },\n timeoutMs: { type: 'number' },\n },\n required: ['msg'],\n },\n },\n {\n name: 'get_confirm_result',\n description:\n 'Poll a pending-confirmation by confirmId. Returns confirmed / rejected / still-pending.',\n inputSchema: {\n type: 'object',\n properties: {\n confirmId: { type: 'string' },\n timeoutMs: { type: 'number' },\n },\n required: ['confirmId'],\n },\n },\n {\n name: 'wait_for_change',\n description: 'Long-poll for a state change. Returns changed / timeout.',\n inputSchema: {\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description: 'Optional JSON-pointer to narrow which state changes trigger resolution',\n },\n timeoutMs: { type: 'number' },\n },\n },\n },\n {\n name: 'query_dom',\n description: 'Read elements tagged with data-agent=\"<name>\" in the rendered UI.',\n inputSchema: {\n type: 'object',\n properties: {\n name: { type: 'string' },\n multiple: { type: 'boolean' },\n },\n required: ['name'],\n },\n },\n {\n name: 'describe_visible_content',\n description: 'Return a structured outline of the currently-visible data-agent-tagged subtrees.',\n inputSchema: { type: 'object', properties: {} },\n },\n {\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 inputSchema: { type: 'object', properties: {} },\n },\n]\n\n/**\n * Mapping from tool name → LAP path for the forwarded subset.\n * Meta-tools handled separately in bridge.ts.\n */\nexport const TOOL_TO_LAP_PATH: Record<string, string> = {\n describe_app: '/describe',\n get_state: '/state',\n list_actions: '/actions',\n send_message: '/message',\n get_confirm_result: '/confirm-result',\n wait_for_change: '/wait',\n query_dom: '/query-dom',\n describe_visible_content: '/describe-visible',\n describe_context: '/context',\n}\n"]}
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from 'zod'\n\n/**\n * Tool catalogue exposed to Claude through the MCP bridge. Two tiers:\n *\n * - **Efficient path (recommended)**: `observe` + `send_message`.\n * `observe` returns state + actions + description + context in a\n * single LAP call, replacing the old describe_app + get_state +\n * list_actions trio. `send_message` defaults to `waitFor:'drained'`,\n * blocking until the message queue goes idle (http/delay/debounce\n * round-trips complete) and returning the new state + actions +\n * drain meta. Together these cut the \"check state → act → check\n * state\" loop from 5 round-trips to 2.\n *\n * - **Legacy / specialized**: `describe_app`, `get_state`,\n * `list_actions`, `wait_for_change`. Kept for back-compat and niche\n * uses (e.g. scoped state reads via JSON pointer, external state\n * pushes). New integrations should prefer `observe`.\n *\n * Spec §8.\n *\n * The catalogue is the single source of truth — Zod schemas drive both\n * runtime input validation and the JSON Schema published in\n * `tools/list`. Forwarded tools also carry their LAP endpoint path so\n * `bridge.ts` can register one generic forwarder that loops over them.\n */\n\nconst empty = z.object({})\n\n/** Descriptor for a tool that forwards directly to the bound LAP server. */\nexport interface ForwardedToolDescriptor {\n kind: 'forward'\n name: string\n description: string\n /** Zod schema defining the tool's input shape. */\n schema: z.ZodObject<z.ZodRawShape>\n /** LAP endpoint path (relative to the binding's base URL). */\n lapPath: string\n}\n\n/** Descriptor for a tool whose handler is implemented in the bridge itself. */\nexport interface MetaToolDescriptor {\n kind: 'meta'\n name: string\n description: string\n schema: z.ZodObject<z.ZodRawShape>\n}\n\nexport type ToolDescriptor = ForwardedToolDescriptor | MetaToolDescriptor\n\nexport const TOOL_DESCRIPTORS: ToolDescriptor[] = [\n {\n kind: 'meta',\n name: 'llui_connect_session',\n description:\n 'Bind this Claude conversation to a specific LLui app. Call ONCE per chat when the user pastes a connect snippet from the LLui app — the snippet contains the url and token to forward here. The result includes the full observe bundle ({state, actions, description, context}) so you have everything you need to start acting — no separate describe_app / get_state / list_actions / describe_context follow-up is required on the first turn. Use observe later when you want a refreshed snapshot.',\n schema: z.object({\n url: z.string().describe('LAP base URL (e.g. https://app.example/agent/lap/v1)'),\n token: z.string().describe('Bearer token for LAP calls'),\n }),\n },\n {\n kind: 'meta',\n name: 'llui_disconnect_session',\n description:\n 'Clear the binding for this Claude conversation. Subsequent LLui tool calls will fail until rebind.',\n schema: empty,\n },\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: '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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llui-agent",
3
- "version": "0.0.1",
3
+ "version": "0.0.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "llui-agent": "./dist/cli.js"
@@ -20,7 +20,8 @@
20
20
  ],
21
21
  "dependencies": {
22
22
  "@modelcontextprotocol/sdk": "^1.29.0",
23
- "@llui/agent": "0.0.29"
23
+ "zod": "^4.0.0",
24
+ "@llui/agent": "0.0.34"
24
25
  },
25
26
  "devDependencies": {
26
27
  "@types/node": "^22.0.0"