@llui/mcp 0.0.16 → 0.0.18
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/dist/cli.js +21 -9
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +32 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +65 -7
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -65,14 +65,17 @@ async function main() {
|
|
|
65
65
|
res.end(JSON.stringify({ error: String(err) }));
|
|
66
66
|
});
|
|
67
67
|
});
|
|
68
|
-
// Single bridge
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
// Single bridge host: owns the WS relay, tool registry, and marker
|
|
69
|
+
// file. All MCP sessions route tool calls through its relay via
|
|
70
|
+
// `createSessionMcp()` — ensures the browser-connected state is
|
|
71
|
+
// shared instead of each session creating its own dead relay.
|
|
72
|
+
const bridgeHost = new LluiMcpServer({ bridgePort: httpPort, attachTo: httpServer });
|
|
73
|
+
bridgeHost.startBridge();
|
|
71
74
|
httpServer.listen(httpPort, '127.0.0.1', () => {
|
|
72
75
|
process.stderr.write(`[llui-mcp] HTTP transport on http://127.0.0.1:${httpPort}/mcp; bridge ws://127.0.0.1:${httpPort}/bridge\n`);
|
|
73
76
|
});
|
|
74
77
|
const shutdown = async () => {
|
|
75
|
-
|
|
78
|
+
bridgeHost.stopBridge();
|
|
76
79
|
for (const t of mcpTransports.values())
|
|
77
80
|
await t.close();
|
|
78
81
|
mcpTransports.clear();
|
|
@@ -100,8 +103,10 @@ async function main() {
|
|
|
100
103
|
const sessionId = typeof sessionHeader === 'string' ? sessionHeader : undefined;
|
|
101
104
|
let transport = sessionId ? mcpTransports.get(sessionId) : undefined;
|
|
102
105
|
if (!transport) {
|
|
103
|
-
// New session.
|
|
104
|
-
//
|
|
106
|
+
// New session. SDK requires one `McpServer` per transport, but
|
|
107
|
+
// all sessions must share the single browser bridge — route
|
|
108
|
+
// through `createSessionMcp()` so the session's tool dispatch
|
|
109
|
+
// lands on bridgeHost's registry + relay.
|
|
105
110
|
transport = new StreamableHTTPServerTransport({
|
|
106
111
|
sessionIdGenerator: () => randomUUID(),
|
|
107
112
|
onsessioninitialized: (id) => {
|
|
@@ -113,8 +118,8 @@ async function main() {
|
|
|
113
118
|
if (id)
|
|
114
119
|
mcpTransports.delete(id);
|
|
115
120
|
};
|
|
116
|
-
const
|
|
117
|
-
await
|
|
121
|
+
const sessionMcp = bridgeHost.createSessionMcp();
|
|
122
|
+
await sessionMcp.connect(transport);
|
|
118
123
|
}
|
|
119
124
|
await transport.handleRequest(req, res);
|
|
120
125
|
}
|
|
@@ -123,6 +128,13 @@ async function doctor(port) {
|
|
|
123
128
|
// Offline checks only — doctor doesn't require the server to be
|
|
124
129
|
// running. Walks the same states the RelayUnavailableError diagnostic
|
|
125
130
|
// surfaces at runtime, plus a port-liveness probe.
|
|
131
|
+
//
|
|
132
|
+
// Glyphs: emoji ✓/✗ by default, fall back to `OK`/`FAIL` when the
|
|
133
|
+
// environment requests plain output. Honors `--plain` and the
|
|
134
|
+
// standard `NO_COLOR` env var (https://no-color.org).
|
|
135
|
+
const plain = args.includes('--plain') || process.env.NO_COLOR !== undefined;
|
|
136
|
+
const ok = plain ? 'OK ' : '✓';
|
|
137
|
+
const fail = plain ? 'FAIL' : '✗';
|
|
126
138
|
const markerPath = mcpActiveFilePath();
|
|
127
139
|
const checks = [];
|
|
128
140
|
checks.push({
|
|
@@ -171,7 +183,7 @@ async function doctor(port) {
|
|
|
171
183
|
process.stdout.write('—\n');
|
|
172
184
|
for (const c of checks) {
|
|
173
185
|
allOk = allOk && c.ok;
|
|
174
|
-
process.stdout.write(`${c.ok ?
|
|
186
|
+
process.stdout.write(`${c.ok ? ok : fail} ${c.name.padEnd(32)} ${c.detail}\n`);
|
|
175
187
|
}
|
|
176
188
|
process.stdout.write('—\n');
|
|
177
189
|
process.stdout.write(allOk ? 'All checks passed.\n' : 'Some checks failed — see above.\n');
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport { createServer } from 'node:http'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport { randomUUID } from 'node:crypto'\nimport { existsSync, readFileSync } from 'node:fs'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'\nimport { LluiMcpServer, mcpActiveFilePath } from './index.js'\n\n/**\n * Parse `--http [port]` from argv. Returns:\n * - null → stdio mode (default)\n * - number → HTTP mode on that port\n */\nfunction parseHttpFlag(argv: string[]): number | null {\n const idx = argv.indexOf('--http')\n if (idx < 0) return null\n const next = argv[idx + 1]\n if (next && !next.startsWith('-') && /^\\d+$/.test(next)) {\n return Number(next)\n }\n return Number(process.env.LLUI_MCP_PORT ?? 5200)\n}\n\nconst bridgePort = Number(process.env.LLUI_MCP_PORT ?? 5200)\nconst args = process.argv.slice(2)\nconst httpPort = parseHttpFlag(args)\n\nif (args[0] === 'doctor') {\n doctor(bridgePort).then(\n (ok) => process.exit(ok ? 0 : 1),\n (err) => {\n process.stderr.write(`[llui-mcp doctor] fatal: ${String(err)}\\n`)\n process.exit(2)\n },\n )\n} else {\n main().catch((err) => {\n process.stderr.write(`[llui-mcp] fatal: ${String(err)}\\n`)\n process.exit(1)\n })\n}\n\nasync function main(): Promise<void> {\n if (httpPort === null) {\n // Stdio mode — Claude's `.mcp.json` spawns llui-mcp and talks over\n // stdin/stdout. The bridge runs on its own WebSocket server on\n // `bridgePort`.\n const server = new LluiMcpServer(bridgePort)\n server.startBridge()\n const transport = new StdioServerTransport()\n await server.connect(transport)\n process.stderr.write(`[llui-mcp] listening on stdio; bridge ws://127.0.0.1:${bridgePort}\\n`)\n\n const shutdown = (): void => {\n server.stopBridge()\n process.exit(0)\n }\n process.on('SIGINT', shutdown)\n process.on('SIGTERM', shutdown)\n return\n }\n\n // HTTP mode — plugin-spawned. One `http.Server` serves both the MCP\n // Streamable HTTP transport (`/mcp`) and the browser bridge WebSocket\n // (upgrade on `/bridge`). `.mcp.json` uses type: \"http\" with url\n // `http://127.0.0.1:<port>/mcp`.\n const mcpTransports = new Map<string, StreamableHTTPServerTransport>()\n const httpServer = createServer((req, res) => {\n handleHttp(req, res).catch((err) => {\n res.statusCode = 500\n res.setHeader('content-type', 'application/json')\n res.end(JSON.stringify({ error: String(err) }))\n })\n })\n\n // Single bridge attached to the HTTP server; all MCP sessions share it.\n const bridgeServer = new LluiMcpServer({ bridgePort: httpPort, attachTo: httpServer })\n bridgeServer.startBridge()\n\n httpServer.listen(httpPort, '127.0.0.1', () => {\n process.stderr.write(\n `[llui-mcp] HTTP transport on http://127.0.0.1:${httpPort}/mcp; bridge ws://127.0.0.1:${httpPort}/bridge\\n`,\n )\n })\n\n const shutdown = async (): Promise<void> => {\n bridgeServer.stopBridge()\n for (const t of mcpTransports.values()) await t.close()\n mcpTransports.clear()\n httpServer.close()\n process.exit(0)\n }\n process.on('SIGINT', () => {\n shutdown().catch(() => process.exit(1))\n })\n process.on('SIGTERM', () => {\n shutdown().catch(() => process.exit(1))\n })\n\n async function handleHttp(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const url = req.url ?? '/'\n if (!url.startsWith('/mcp')) {\n res.statusCode = 404\n res.end('not found')\n return\n }\n\n // Session routing: the SDK's StreamableHTTPServerTransport is\n // stateful. The first request (initialize) creates a session id\n // returned in the `mcp-session-id` response header; subsequent\n // requests carry it as the `mcp-session-id` header.\n const sessionHeader = req.headers['mcp-session-id']\n const sessionId = typeof sessionHeader === 'string' ? sessionHeader : undefined\n let transport = sessionId ? mcpTransports.get(sessionId) : undefined\n\n if (!transport) {\n // New session. Each MCP session gets its own server instance\n // (SDK requirement), but all share the one browser bridge.\n transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n onsessioninitialized: (id: string) => {\n mcpTransports.set(id, transport!)\n },\n })\n transport.onclose = () => {\n const id = transport!.sessionId\n if (id) mcpTransports.delete(id)\n }\n const sessionServer = new LluiMcpServer({ bridgePort: httpPort!, attachTo: httpServer })\n await sessionServer.connect(transport)\n }\n\n await transport.handleRequest(req, res)\n }\n}\n\nasync function doctor(port: number): Promise<boolean> {\n // Offline checks only — doctor doesn't require the server to be\n // running. Walks the same states the RelayUnavailableError diagnostic\n // surfaces at runtime, plus a port-liveness probe.\n const markerPath = mcpActiveFilePath()\n const checks: Array<{ name: string; ok: boolean; detail: string }> = []\n\n checks.push({\n name: 'marker file',\n ok: existsSync(markerPath),\n detail: markerPath,\n })\n\n let markerPayload: { port?: number; pid?: number; devUrl?: string } | null = null\n if (existsSync(markerPath)) {\n try {\n markerPayload = JSON.parse(readFileSync(markerPath, 'utf8')) as {\n port?: number\n pid?: number\n devUrl?: string\n }\n } catch {\n markerPayload = null\n }\n checks.push({\n name: 'marker valid JSON',\n ok: markerPayload !== null,\n detail: markerPayload !== null ? 'OK' : 'malformed — delete and restart MCP',\n })\n checks.push({\n name: 'plugin devUrl stamped',\n ok: typeof markerPayload?.devUrl === 'string',\n detail:\n typeof markerPayload?.devUrl === 'string'\n ? markerPayload.devUrl\n : 'vite-plugin has not stamped its dev URL',\n })\n }\n\n const targetPort = markerPayload?.port ?? port\n const reachable = await probePort(targetPort)\n checks.push({\n name: `bridge port ${targetPort} listening`,\n ok: reachable,\n detail: reachable ? '127.0.0.1 connectable' : 'no process bound; MCP server not running',\n })\n\n if (typeof markerPayload?.pid === 'number') {\n const alive = isPidAlive(markerPayload.pid)\n checks.push({\n name: `marker pid ${markerPayload.pid}`,\n ok: alive,\n detail: alive ? 'process alive' : 'stale — delete the marker',\n })\n }\n\n let allOk = true\n process.stdout.write('llui-mcp doctor\\n')\n process.stdout.write('—\\n')\n for (const c of checks) {\n allOk = allOk && c.ok\n process.stdout.write(`${c.ok ? '✓' : '✗'} ${c.name.padEnd(32)} ${c.detail}\\n`)\n }\n process.stdout.write('—\\n')\n process.stdout.write(allOk ? 'All checks passed.\\n' : 'Some checks failed — see above.\\n')\n return allOk\n}\n\nasync function probePort(port: number): Promise<boolean> {\n const { Socket } = await import('node:net')\n return new Promise<boolean>((resolve) => {\n const sock = new Socket()\n const done = (ok: boolean): void => {\n sock.destroy()\n resolve(ok)\n }\n sock.setTimeout(500)\n sock.on('connect', () => done(true))\n sock.on('error', () => done(false))\n sock.on('timeout', () => done(false))\n sock.connect(port, '127.0.0.1')\n })\n}\n\nfunction isPidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0)\n return true\n } catch {\n return false\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport { createServer } from 'node:http'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport { randomUUID } from 'node:crypto'\nimport { existsSync, readFileSync } from 'node:fs'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'\nimport { LluiMcpServer, mcpActiveFilePath } from './index.js'\n\n/**\n * Parse `--http [port]` from argv. Returns:\n * - null → stdio mode (default)\n * - number → HTTP mode on that port\n */\nfunction parseHttpFlag(argv: string[]): number | null {\n const idx = argv.indexOf('--http')\n if (idx < 0) return null\n const next = argv[idx + 1]\n if (next && !next.startsWith('-') && /^\\d+$/.test(next)) {\n return Number(next)\n }\n return Number(process.env.LLUI_MCP_PORT ?? 5200)\n}\n\nconst bridgePort = Number(process.env.LLUI_MCP_PORT ?? 5200)\nconst args = process.argv.slice(2)\nconst httpPort = parseHttpFlag(args)\n\nif (args[0] === 'doctor') {\n doctor(bridgePort).then(\n (ok) => process.exit(ok ? 0 : 1),\n (err) => {\n process.stderr.write(`[llui-mcp doctor] fatal: ${String(err)}\\n`)\n process.exit(2)\n },\n )\n} else {\n main().catch((err) => {\n process.stderr.write(`[llui-mcp] fatal: ${String(err)}\\n`)\n process.exit(1)\n })\n}\n\nasync function main(): Promise<void> {\n if (httpPort === null) {\n // Stdio mode — Claude's `.mcp.json` spawns llui-mcp and talks over\n // stdin/stdout. The bridge runs on its own WebSocket server on\n // `bridgePort`.\n const server = new LluiMcpServer(bridgePort)\n server.startBridge()\n const transport = new StdioServerTransport()\n await server.connect(transport)\n process.stderr.write(`[llui-mcp] listening on stdio; bridge ws://127.0.0.1:${bridgePort}\\n`)\n\n const shutdown = (): void => {\n server.stopBridge()\n process.exit(0)\n }\n process.on('SIGINT', shutdown)\n process.on('SIGTERM', shutdown)\n return\n }\n\n // HTTP mode — plugin-spawned. One `http.Server` serves both the MCP\n // Streamable HTTP transport (`/mcp`) and the browser bridge WebSocket\n // (upgrade on `/bridge`). `.mcp.json` uses type: \"http\" with url\n // `http://127.0.0.1:<port>/mcp`.\n const mcpTransports = new Map<string, StreamableHTTPServerTransport>()\n const httpServer = createServer((req, res) => {\n handleHttp(req, res).catch((err) => {\n res.statusCode = 500\n res.setHeader('content-type', 'application/json')\n res.end(JSON.stringify({ error: String(err) }))\n })\n })\n\n // Single bridge host: owns the WS relay, tool registry, and marker\n // file. All MCP sessions route tool calls through its relay via\n // `createSessionMcp()` — ensures the browser-connected state is\n // shared instead of each session creating its own dead relay.\n const bridgeHost = new LluiMcpServer({ bridgePort: httpPort, attachTo: httpServer })\n bridgeHost.startBridge()\n\n httpServer.listen(httpPort, '127.0.0.1', () => {\n process.stderr.write(\n `[llui-mcp] HTTP transport on http://127.0.0.1:${httpPort}/mcp; bridge ws://127.0.0.1:${httpPort}/bridge\\n`,\n )\n })\n\n const shutdown = async (): Promise<void> => {\n bridgeHost.stopBridge()\n for (const t of mcpTransports.values()) await t.close()\n mcpTransports.clear()\n httpServer.close()\n process.exit(0)\n }\n process.on('SIGINT', () => {\n shutdown().catch(() => process.exit(1))\n })\n process.on('SIGTERM', () => {\n shutdown().catch(() => process.exit(1))\n })\n\n async function handleHttp(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const url = req.url ?? '/'\n if (!url.startsWith('/mcp')) {\n res.statusCode = 404\n res.end('not found')\n return\n }\n\n // Session routing: the SDK's StreamableHTTPServerTransport is\n // stateful. The first request (initialize) creates a session id\n // returned in the `mcp-session-id` response header; subsequent\n // requests carry it as the `mcp-session-id` header.\n const sessionHeader = req.headers['mcp-session-id']\n const sessionId = typeof sessionHeader === 'string' ? sessionHeader : undefined\n let transport = sessionId ? mcpTransports.get(sessionId) : undefined\n\n if (!transport) {\n // New session. SDK requires one `McpServer` per transport, but\n // all sessions must share the single browser bridge — route\n // through `createSessionMcp()` so the session's tool dispatch\n // lands on bridgeHost's registry + relay.\n transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n onsessioninitialized: (id: string) => {\n mcpTransports.set(id, transport!)\n },\n })\n transport.onclose = () => {\n const id = transport!.sessionId\n if (id) mcpTransports.delete(id)\n }\n const sessionMcp = bridgeHost.createSessionMcp()\n await sessionMcp.connect(transport)\n }\n\n await transport.handleRequest(req, res)\n }\n}\n\nasync function doctor(port: number): Promise<boolean> {\n // Offline checks only — doctor doesn't require the server to be\n // running. Walks the same states the RelayUnavailableError diagnostic\n // surfaces at runtime, plus a port-liveness probe.\n //\n // Glyphs: emoji ✓/✗ by default, fall back to `OK`/`FAIL` when the\n // environment requests plain output. Honors `--plain` and the\n // standard `NO_COLOR` env var (https://no-color.org).\n const plain = args.includes('--plain') || process.env.NO_COLOR !== undefined\n const ok = plain ? 'OK ' : '✓'\n const fail = plain ? 'FAIL' : '✗'\n const markerPath = mcpActiveFilePath()\n const checks: Array<{ name: string; ok: boolean; detail: string }> = []\n\n checks.push({\n name: 'marker file',\n ok: existsSync(markerPath),\n detail: markerPath,\n })\n\n let markerPayload: { port?: number; pid?: number; devUrl?: string } | null = null\n if (existsSync(markerPath)) {\n try {\n markerPayload = JSON.parse(readFileSync(markerPath, 'utf8')) as {\n port?: number\n pid?: number\n devUrl?: string\n }\n } catch {\n markerPayload = null\n }\n checks.push({\n name: 'marker valid JSON',\n ok: markerPayload !== null,\n detail: markerPayload !== null ? 'OK' : 'malformed — delete and restart MCP',\n })\n checks.push({\n name: 'plugin devUrl stamped',\n ok: typeof markerPayload?.devUrl === 'string',\n detail:\n typeof markerPayload?.devUrl === 'string'\n ? markerPayload.devUrl\n : 'vite-plugin has not stamped its dev URL',\n })\n }\n\n const targetPort = markerPayload?.port ?? port\n const reachable = await probePort(targetPort)\n checks.push({\n name: `bridge port ${targetPort} listening`,\n ok: reachable,\n detail: reachable ? '127.0.0.1 connectable' : 'no process bound; MCP server not running',\n })\n\n if (typeof markerPayload?.pid === 'number') {\n const alive = isPidAlive(markerPayload.pid)\n checks.push({\n name: `marker pid ${markerPayload.pid}`,\n ok: alive,\n detail: alive ? 'process alive' : 'stale — delete the marker',\n })\n }\n\n let allOk = true\n process.stdout.write('llui-mcp doctor\\n')\n process.stdout.write('—\\n')\n for (const c of checks) {\n allOk = allOk && c.ok\n process.stdout.write(`${c.ok ? ok : fail} ${c.name.padEnd(32)} ${c.detail}\\n`)\n }\n process.stdout.write('—\\n')\n process.stdout.write(allOk ? 'All checks passed.\\n' : 'Some checks failed — see above.\\n')\n return allOk\n}\n\nasync function probePort(port: number): Promise<boolean> {\n const { Socket } = await import('node:net')\n return new Promise<boolean>((resolve) => {\n const sock = new Socket()\n const done = (ok: boolean): void => {\n sock.destroy()\n resolve(ok)\n }\n sock.setTimeout(500)\n sock.on('connect', () => done(true))\n sock.on('error', () => done(false))\n sock.on('timeout', () => done(false))\n sock.connect(port, '127.0.0.1')\n })\n}\n\nfunction isPidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0)\n return true\n } catch {\n return false\n }\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { LluiDebugAPI } from '@llui/dom';
|
|
2
2
|
import type { Server as HttpServer } from 'node:http';
|
|
3
|
+
import { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
4
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
4
5
|
import { type ToolDefinition } from './tool-registry.js';
|
|
5
6
|
/**
|
|
@@ -48,8 +49,38 @@ export declare class LluiMcpServer {
|
|
|
48
49
|
private readonly bridgePort;
|
|
49
50
|
private readonly mcp;
|
|
50
51
|
private devUrl;
|
|
52
|
+
/**
|
|
53
|
+
* @param optsOrPort options object (preferred) or bridge port (legacy).
|
|
54
|
+
* The numeric-port form is kept for one release cycle of back-compat;
|
|
55
|
+
* new code should always pass an options object. The options form
|
|
56
|
+
* supports `attachTo` for HTTP-transport deployments that share a
|
|
57
|
+
* single port between MCP and the browser bridge — the numeric form
|
|
58
|
+
* can't express that.
|
|
59
|
+
* @deprecated numeric `optsOrPort` — pass `{ bridgePort }` instead.
|
|
60
|
+
* This overload will be removed in a future breaking release.
|
|
61
|
+
*/
|
|
51
62
|
constructor(optsOrPort?: LluiMcpServerOptions | number);
|
|
52
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Build a fresh SDK `McpServer` wired to THIS instance's tool
|
|
65
|
+
* registry and browser relay. The primary `this.mcp` uses one.
|
|
66
|
+
* `createSessionMcp()` returns additional ones for HTTP-transport
|
|
67
|
+
* deployments where every session needs its own SDK Server — each
|
|
68
|
+
* routes tool calls through the shared relay, so the single
|
|
69
|
+
* bridgeHost owns all the browser-facing state.
|
|
70
|
+
*/
|
|
71
|
+
private buildMcpServer;
|
|
72
|
+
/**
|
|
73
|
+
* Build a new SDK MCP server sharing this instance's registry + relay,
|
|
74
|
+
* for HTTP-transport deployments where each session needs its own
|
|
75
|
+
* `Server` (SDK requirement). Call-site pattern:
|
|
76
|
+
*
|
|
77
|
+
* const bridgeHost = new LluiMcpServer({ bridgePort, attachTo: httpServer })
|
|
78
|
+
* bridgeHost.startBridge()
|
|
79
|
+
* // Per session:
|
|
80
|
+
* const sessionMcp = bridgeHost.createSessionMcp()
|
|
81
|
+
* await sessionMcp.connect(transport)
|
|
82
|
+
*/
|
|
83
|
+
createSessionMcp(): McpServer;
|
|
53
84
|
/**
|
|
54
85
|
* Connect the SDK MCP server to a transport (stdio, HTTP, etc).
|
|
55
86
|
* The CLI builds the transport based on command-line flags and
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAI7C,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,WAAW,CAAA;AACrD,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,2CAA2C,CAAA;AAC/E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAA;AAE9E,OAAO,EAAkC,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAA;AA0BxF;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,GAAE,MAAsB,GAAG,MAAM,CAgBvE;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAsB,GAAG,MAAM,CAErE;AAID,MAAM,WAAW,oBAAoB;IACnC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,UAAU,CAAA;CACtB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAc;IACvC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyB;IAC/C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAW;IAC/B,OAAO,CAAC,MAAM,CAAsB;IAEpC;;;;;;;;;OASG;gBACS,UAAU,GAAE,oBAAoB,GAAG,MAAa;IAqB5D;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc;IAwCtB;;;;;;;;;;OAUG;IACH,gBAAgB,IAAI,SAAS;IAI7B;;;;OAIG;IACG,OAAO,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,uEAAuE;IACvE,aAAa,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI;IAItC;;;;;OAKG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAK5B;;;;OAIG;IACH,WAAW,IAAI,IAAI;IAQnB,UAAU,IAAI,IAAI;IAKlB,OAAO,CAAC,eAAe;IAevB,OAAO,CAAC,gBAAgB;IASxB,6CAA6C;IAC7C,QAAQ,IAAI,cAAc,EAAE;IAI5B,8BAA8B;IACxB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;CAIpF;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,EAAE,cAAc,EAI3C,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,31 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync, unlinkSync, existsSync } from 'node:fs';
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync, unlinkSync, existsSync } from 'node:fs';
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
3
4
|
import { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js';
|
|
4
5
|
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
6
|
import { ToolRegistry } from './tool-registry.js';
|
|
6
7
|
import { registerDebugApiTools } from './tools/index.js';
|
|
7
8
|
import { WebSocketRelayTransport, RelayUnavailableError } from './transports/index.js';
|
|
9
|
+
/**
|
|
10
|
+
* Version advertised in the MCP `initialize` handshake. Read once from
|
|
11
|
+
* our own `package.json` so it stays in sync with the publish bump,
|
|
12
|
+
* instead of a hardcoded literal that silently drifts each release.
|
|
13
|
+
*
|
|
14
|
+
* Falls back to `'unknown'` on read failure — SDK initialization still
|
|
15
|
+
* succeeds; only the cosmetic serverInfo.version is affected.
|
|
16
|
+
*/
|
|
17
|
+
const PACKAGE_VERSION = (() => {
|
|
18
|
+
try {
|
|
19
|
+
// dist layout: `dist/index.js` → `package.json` is two levels up
|
|
20
|
+
// from the module file at runtime.
|
|
21
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const pkg = JSON.parse(readFileSync(resolve(here, '../package.json'), 'utf8'));
|
|
23
|
+
return typeof pkg.version === 'string' ? pkg.version : 'unknown';
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return 'unknown';
|
|
27
|
+
}
|
|
28
|
+
})();
|
|
8
29
|
/**
|
|
9
30
|
* Walk up from `start` until we find a workspace root marker. Used by
|
|
10
31
|
* both the MCP server (writing the active marker) and the Vite plugin
|
|
@@ -55,30 +76,52 @@ export class LluiMcpServer {
|
|
|
55
76
|
bridgePort;
|
|
56
77
|
mcp;
|
|
57
78
|
devUrl = null;
|
|
79
|
+
/**
|
|
80
|
+
* @param optsOrPort options object (preferred) or bridge port (legacy).
|
|
81
|
+
* The numeric-port form is kept for one release cycle of back-compat;
|
|
82
|
+
* new code should always pass an options object. The options form
|
|
83
|
+
* supports `attachTo` for HTTP-transport deployments that share a
|
|
84
|
+
* single port between MCP and the browser bridge — the numeric form
|
|
85
|
+
* can't express that.
|
|
86
|
+
* @deprecated numeric `optsOrPort` — pass `{ bridgePort }` instead.
|
|
87
|
+
* This overload will be removed in a future breaking release.
|
|
88
|
+
*/
|
|
58
89
|
constructor(optsOrPort = 5200) {
|
|
59
90
|
const opts = typeof optsOrPort === 'number' ? { bridgePort: optsOrPort } : optsOrPort;
|
|
60
91
|
this.bridgePort = opts.bridgePort ?? 5200;
|
|
61
92
|
this.registry = new ToolRegistry();
|
|
93
|
+
// Pass bridgePort even in attachTo mode — the relay's diagnose()
|
|
94
|
+
// needs it for the port field of BridgeDiagnostic. The `start()`
|
|
95
|
+
// path is gated on `attachTo` first so a standalone listener
|
|
96
|
+
// never gets created twice.
|
|
62
97
|
this.relay = new WebSocketRelayTransport({
|
|
63
|
-
port:
|
|
98
|
+
port: this.bridgePort,
|
|
64
99
|
attachTo: opts.attachTo,
|
|
65
100
|
markerPath: mcpActiveFilePath(),
|
|
66
101
|
});
|
|
67
102
|
registerDebugApiTools(this.registry);
|
|
68
103
|
// SDK-managed MCP server — owns the JSON-RPC protocol, handshake,
|
|
69
104
|
// session lifecycle. Transport is plugged in later via `connect()`.
|
|
70
|
-
this.mcp =
|
|
71
|
-
this.registerMcpHandlers();
|
|
105
|
+
this.mcp = this.buildMcpServer();
|
|
72
106
|
}
|
|
73
|
-
|
|
74
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Build a fresh SDK `McpServer` wired to THIS instance's tool
|
|
109
|
+
* registry and browser relay. The primary `this.mcp` uses one.
|
|
110
|
+
* `createSessionMcp()` returns additional ones for HTTP-transport
|
|
111
|
+
* deployments where every session needs its own SDK Server — each
|
|
112
|
+
* routes tool calls through the shared relay, so the single
|
|
113
|
+
* bridgeHost owns all the browser-facing state.
|
|
114
|
+
*/
|
|
115
|
+
buildMcpServer() {
|
|
116
|
+
const mcp = new McpServer({ name: '@llui/mcp', version: PACKAGE_VERSION }, { capabilities: { tools: {} } });
|
|
117
|
+
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
75
118
|
tools: this.getTools().map((t) => ({
|
|
76
119
|
name: t.name,
|
|
77
120
|
description: t.description,
|
|
78
121
|
inputSchema: t.inputSchema,
|
|
79
122
|
})),
|
|
80
123
|
}));
|
|
81
|
-
|
|
124
|
+
mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
82
125
|
const { name, arguments: args } = request.params;
|
|
83
126
|
try {
|
|
84
127
|
const result = await this.handleToolCall(name, args ?? {});
|
|
@@ -104,6 +147,21 @@ export class LluiMcpServer {
|
|
|
104
147
|
throw err;
|
|
105
148
|
}
|
|
106
149
|
});
|
|
150
|
+
return mcp;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Build a new SDK MCP server sharing this instance's registry + relay,
|
|
154
|
+
* for HTTP-transport deployments where each session needs its own
|
|
155
|
+
* `Server` (SDK requirement). Call-site pattern:
|
|
156
|
+
*
|
|
157
|
+
* const bridgeHost = new LluiMcpServer({ bridgePort, attachTo: httpServer })
|
|
158
|
+
* bridgeHost.startBridge()
|
|
159
|
+
* // Per session:
|
|
160
|
+
* const sessionMcp = bridgeHost.createSessionMcp()
|
|
161
|
+
* await sessionMcp.connect(transport)
|
|
162
|
+
*/
|
|
163
|
+
createSessionMcp() {
|
|
164
|
+
return this.buildMcpServer();
|
|
107
165
|
}
|
|
108
166
|
/**
|
|
109
167
|
* Connect the SDK MCP server to a transport (stdio, HTTP, etc).
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { LluiDebugAPI } from '@llui/dom'\nimport { mkdirSync, writeFileSync, unlinkSync, existsSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\nimport type { Server as HttpServer } from 'node:http'\nimport { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js'\nimport type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'\nimport { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'\nimport { ToolRegistry, type ToolContext, type ToolDefinition } from './tool-registry.js'\nimport { registerDebugApiTools } from './tools/index.js'\nimport { WebSocketRelayTransport, RelayUnavailableError } from './transports/index.js'\n\n/**\n * Walk up from `start` until we find a workspace root marker. Used by\n * both the MCP server (writing the active marker) and the Vite plugin\n * (watching it) so they agree on a single shared location regardless of\n * which subdirectory each process happens to be running in.\n *\n * Strong markers (workspace root): pnpm-workspace.yaml, .git directory.\n * If neither is found anywhere up the chain, falls back to the highest\n * package.json above `start`. For pnpm monorepos this finds the workspace\n * root from any subpackage; for single-package projects it finds the\n * package root.\n */\nexport function findWorkspaceRoot(start: string = process.cwd()): string {\n let dir = resolve(start)\n let lastPackageJson: string | null = null\n while (true) {\n // Strong markers — return immediately\n if (existsSync(resolve(dir, 'pnpm-workspace.yaml'))) return dir\n if (existsSync(resolve(dir, '.git'))) return dir\n // Track the highest package.json as a fallback\n if (existsSync(resolve(dir, 'package.json'))) lastPackageJson = dir\n const parent = dirname(dir)\n if (parent === dir) {\n // Reached filesystem root — return the highest package.json we saw\n return lastPackageJson ?? start\n }\n dir = parent\n }\n}\n\n/**\n * Path where the MCP server writes its active port marker. Vite plugins\n * watch this file to auto-trigger browser-side `__lluiConnect()` whenever\n * the MCP server starts, regardless of whether Vite or MCP started first.\n *\n * Resolved relative to the workspace root (not the immediate cwd) so the\n * MCP server and the Vite plugin always agree on a single location even\n * when one runs from the repo root and the other from a subpackage.\n */\nexport function mcpActiveFilePath(cwd: string = process.cwd()): string {\n return resolve(findWorkspaceRoot(cwd), 'node_modules/.cache/llui-mcp/active.json')\n}\n\n// ── MCP Server ──────────────────────────────────────────────────\n\nexport interface LluiMcpServerOptions {\n /**\n * Port for the browser-relay WebSocket bridge. When the MCP transport\n * is stdio (the CLI default), the relay stands up its own server on\n * this port. When the MCP transport is HTTP, the relay attaches to\n * that HTTP server and the MCP protocol + bridge share a single port.\n */\n bridgePort?: number\n /**\n * Optional pre-existing `http.Server` to share with the bridge. When\n * provided, the bridge attaches to it via upgrade routing on\n * `/bridge`; `bridgePort` is ignored for server-creation purposes\n * (but still written into the marker file so consumers know where to\n * connect).\n */\n attachTo?: HttpServer\n}\n\nexport class LluiMcpServer {\n private readonly registry: ToolRegistry\n private readonly relay: WebSocketRelayTransport\n private readonly bridgePort: number\n private readonly mcp: McpServer\n private devUrl: string | null = null\n\n constructor(optsOrPort: LluiMcpServerOptions | number = 5200) {\n const opts: LluiMcpServerOptions =\n typeof optsOrPort === 'number' ? { bridgePort: optsOrPort } : optsOrPort\n this.bridgePort = opts.bridgePort ?? 5200\n this.registry = new ToolRegistry()\n this.relay = new WebSocketRelayTransport({\n port: opts.attachTo ? undefined : this.bridgePort,\n attachTo: opts.attachTo,\n markerPath: mcpActiveFilePath(),\n })\n registerDebugApiTools(this.registry)\n\n // SDK-managed MCP server — owns the JSON-RPC protocol, handshake,\n // session lifecycle. Transport is plugged in later via `connect()`.\n this.mcp = new McpServer(\n { name: '@llui/mcp', version: '0.0.15' },\n { capabilities: { tools: {} } },\n )\n this.registerMcpHandlers()\n }\n\n private registerMcpHandlers(): void {\n this.mcp.setRequestHandler(ListToolsRequestSchema, async () => ({\n tools: this.getTools().map((t) => ({\n name: t.name,\n description: t.description,\n inputSchema: t.inputSchema,\n })),\n }))\n\n this.mcp.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params\n try {\n const result = await this.handleToolCall(name, args ?? {})\n return {\n content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],\n }\n } catch (err) {\n // Bridge-unavailable errors carry a structured diagnostic — surface\n // it as an isError tool result so the caller (typically Claude) sees\n // WHY the browser isn't reachable, not just that it failed.\n if (err instanceof RelayUnavailableError) {\n return {\n isError: true,\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ error: 'bridge-unavailable', ...err.diagnostic }, null, 2),\n },\n ],\n }\n }\n throw err\n }\n })\n }\n\n /**\n * Connect the SDK MCP server to a transport (stdio, HTTP, etc).\n * The CLI builds the transport based on command-line flags and\n * hands it in here.\n */\n async connect(transport: Transport): Promise<void> {\n await this.mcp.connect(transport)\n }\n\n /** Connect to a debug API instance directly (for in-process usage). */\n connectDirect(api: LluiDebugAPI): void {\n this.relay.connectDirect(api)\n }\n\n /**\n * Set the dev-server URL that Phase 2's CDP fallback can navigate a\n * Playwright browser to. Persisted into the active marker file so the\n * Vite plugin (or other consumers) can rebroadcast it. If the bridge is\n * already running, rewrites the marker so consumers see the update.\n */\n setDevUrl(url: string): void {\n this.devUrl = url\n if (this.relay.isServerRunning()) this.writeActiveFile()\n }\n\n /**\n * Start a WebSocket server on the configured bridge port. The browser-side\n * relay (injected by the Vite plugin in dev mode) connects here and forwards\n * debug-API calls.\n */\n startBridge(): void {\n this.relay.start()\n\n // Write the active marker file so Vite plugins watching it can\n // dispatch an HMR custom event to auto-trigger browser connects.\n this.writeActiveFile()\n }\n\n stopBridge(): void {\n this.relay.stop()\n this.removeActiveFile()\n }\n\n private writeActiveFile(): void {\n try {\n const path = mcpActiveFilePath()\n mkdirSync(dirname(path), { recursive: true })\n const payload: { port: number; pid: number; devUrl?: string } = {\n port: this.bridgePort,\n pid: process.pid,\n }\n if (this.devUrl !== null) payload.devUrl = this.devUrl\n writeFileSync(path, JSON.stringify(payload))\n } catch {\n // Best-effort — failure to write the marker should not crash the server\n }\n }\n\n private removeActiveFile(): void {\n try {\n const path = mcpActiveFilePath()\n if (existsSync(path)) unlinkSync(path)\n } catch {\n // Ignore — file may already be gone\n }\n }\n\n /** Get tool definitions for MCP handshake */\n getTools(): ToolDefinition[] {\n return this.registry.listDefinitions()\n }\n\n /** Handle an MCP tool call */\n async handleToolCall(name: string, args: Record<string, unknown>): Promise<unknown> {\n const ctx: ToolContext = { relay: this.relay, cdp: null }\n return this.registry.dispatch(name, args, ctx)\n }\n}\n\n/**\n * Snapshot of all registered tool definitions. Kept as a named export for\n * backward compatibility with downstream consumers that used to import the\n * `TOOLS` array re-export under this alias.\n */\nexport const mcpToolDefinitions: ToolDefinition[] = (() => {\n const registry = new ToolRegistry()\n registerDebugApiTools(registry)\n return registry.listDefinitions()\n})()\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACxF,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,2CAA2C,CAAA;AAE/E,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAA;AAClG,OAAO,EAAE,YAAY,EAAyC,MAAM,oBAAoB,CAAA;AACxF,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AACxD,OAAO,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAEtF;;;;;;;GAOG;AACH,MAAM,eAAe,GAAW,CAAC,GAAG,EAAE;IACpC,IAAI,CAAC;QACH,iEAAiE;QACjE,mCAAmC;QACnC,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;QACpD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,EAAE,iBAAiB,CAAC,EAAE,MAAM,CAAC,CAE5E,CAAA;QACD,OAAO,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAA;IAClE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC,CAAC,EAAE,CAAA;AAEJ;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAgB,OAAO,CAAC,GAAG,EAAE;IAC7D,IAAI,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,CAAA;IACxB,IAAI,eAAe,GAAkB,IAAI,CAAA;IACzC,OAAO,IAAI,EAAE,CAAC;QACZ,sCAAsC;QACtC,IAAI,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC;YAAE,OAAO,GAAG,CAAA;QAC/D,IAAI,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YAAE,OAAO,GAAG,CAAA;QAChD,+CAA+C;QAC/C,IAAI,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;YAAE,eAAe,GAAG,GAAG,CAAA;QACnE,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;QAC3B,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;YACnB,mEAAmE;YACnE,OAAO,eAAe,IAAI,KAAK,CAAA;QACjC,CAAC;QACD,GAAG,GAAG,MAAM,CAAA;IACd,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAc,OAAO,CAAC,GAAG,EAAE;IAC3D,OAAO,OAAO,CAAC,iBAAiB,CAAC,GAAG,CAAC,EAAE,0CAA0C,CAAC,CAAA;AACpF,CAAC;AAsBD,MAAM,OAAO,aAAa;IACP,QAAQ,CAAc;IACtB,KAAK,CAAyB;IAC9B,UAAU,CAAQ;IAClB,GAAG,CAAW;IACvB,MAAM,GAAkB,IAAI,CAAA;IAEpC;;;;;;;;;OASG;IACH,YAAY,aAA4C,IAAI;QAC1D,MAAM,IAAI,GACR,OAAO,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,UAAU,CAAA;QAC1E,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,IAAI,CAAA;QACzC,IAAI,CAAC,QAAQ,GAAG,IAAI,YAAY,EAAE,CAAA;QAClC,iEAAiE;QACjE,iEAAiE;QACjE,6DAA6D;QAC7D,4BAA4B;QAC5B,IAAI,CAAC,KAAK,GAAG,IAAI,uBAAuB,CAAC;YACvC,IAAI,EAAE,IAAI,CAAC,UAAU;YACrB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,UAAU,EAAE,iBAAiB,EAAE;SAChC,CAAC,CAAA;QACF,qBAAqB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QAEpC,kEAAkE;QAClE,oEAAoE;QACpE,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,cAAc,EAAE,CAAA;IAClC,CAAC;IAED;;;;;;;OAOG;IACK,cAAc;QACpB,MAAM,GAAG,GAAG,IAAI,SAAS,CACvB,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,eAAe,EAAE,EAC/C,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAChC,CAAA;QACD,GAAG,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;YACzD,KAAK,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBACjC,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,WAAW,EAAE,CAAC,CAAC,WAAW;gBAC1B,WAAW,EAAE,CAAC,CAAC,WAAW;aAC3B,CAAC,CAAC;SACJ,CAAC,CAAC,CAAA;QACH,GAAG,CAAC,iBAAiB,CAAC,qBAAqB,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;YAC7D,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,MAAM,CAAA;YAChD,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,CAAA;gBAC1D,OAAO;oBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;iBAC5E,CAAA;YACH,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,oEAAoE;gBACpE,qEAAqE;gBACrE,4DAA4D;gBAC5D,IAAI,GAAG,YAAY,qBAAqB,EAAE,CAAC;oBACzC,OAAO;wBACL,OAAO,EAAE,IAAI;wBACb,OAAO,EAAE;4BACP;gCACE,IAAI,EAAE,MAAe;gCACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,oBAAoB,EAAE,GAAG,GAAG,CAAC,UAAU,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;6BAClF;yBACF;qBACF,CAAA;gBACH,CAAC;gBACD,MAAM,GAAG,CAAA;YACX,CAAC;QACH,CAAC,CAAC,CAAA;QACF,OAAO,GAAG,CAAA;IACZ,CAAC;IAED;;;;;;;;;;OAUG;IACH,gBAAgB;QACd,OAAO,IAAI,CAAC,cAAc,EAAE,CAAA;IAC9B,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,OAAO,CAAC,SAAoB;QAChC,MAAM,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;IACnC,CAAC;IAED,uEAAuE;IACvE,aAAa,CAAC,GAAiB;QAC7B,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,CAAA;IAC/B,CAAC;IAED;;;;;OAKG;IACH,SAAS,CAAC,GAAW;QACnB,IAAI,CAAC,MAAM,GAAG,GAAG,CAAA;QACjB,IAAI,IAAI,CAAC,KAAK,CAAC,eAAe,EAAE;YAAE,IAAI,CAAC,eAAe,EAAE,CAAA;IAC1D,CAAC;IAED;;;;OAIG;IACH,WAAW;QACT,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;QAElB,+DAA+D;QAC/D,iEAAiE;QACjE,IAAI,CAAC,eAAe,EAAE,CAAA;IACxB,CAAC;IAED,UAAU;QACR,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAA;QACjB,IAAI,CAAC,gBAAgB,EAAE,CAAA;IACzB,CAAC;IAEO,eAAe;QACrB,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,iBAAiB,EAAE,CAAA;YAChC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;YAC7C,MAAM,OAAO,GAAmD;gBAC9D,IAAI,EAAE,IAAI,CAAC,UAAU;gBACrB,GAAG,EAAE,OAAO,CAAC,GAAG;aACjB,CAAA;YACD,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI;gBAAE,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAA;YACtD,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAA;QAC9C,CAAC;QAAC,MAAM,CAAC;YACP,wEAAwE;QAC1E,CAAC;IACH,CAAC;IAEO,gBAAgB;QACtB,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,iBAAiB,EAAE,CAAA;YAChC,IAAI,UAAU,CAAC,IAAI,CAAC;gBAAE,UAAU,CAAC,IAAI,CAAC,CAAA;QACxC,CAAC;QAAC,MAAM,CAAC;YACP,oCAAoC;QACtC,CAAC;IACH,CAAC;IAED,6CAA6C;IAC7C,QAAQ;QACN,OAAO,IAAI,CAAC,QAAQ,CAAC,eAAe,EAAE,CAAA;IACxC,CAAC;IAED,8BAA8B;IAC9B,KAAK,CAAC,cAAc,CAAC,IAAY,EAAE,IAA6B;QAC9D,MAAM,GAAG,GAAgB,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,CAAA;QACzD,OAAO,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAA;IAChD,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAqB,CAAC,GAAG,EAAE;IACxD,MAAM,QAAQ,GAAG,IAAI,YAAY,EAAE,CAAA;IACnC,qBAAqB,CAAC,QAAQ,CAAC,CAAA;IAC/B,OAAO,QAAQ,CAAC,eAAe,EAAE,CAAA;AACnC,CAAC,CAAC,EAAE,CAAA","sourcesContent":["import type { LluiDebugAPI } from '@llui/dom'\nimport { mkdirSync, readFileSync, writeFileSync, unlinkSync, existsSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport type { Server as HttpServer } from 'node:http'\nimport { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js'\nimport type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'\nimport { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'\nimport { ToolRegistry, type ToolContext, type ToolDefinition } from './tool-registry.js'\nimport { registerDebugApiTools } from './tools/index.js'\nimport { WebSocketRelayTransport, RelayUnavailableError } from './transports/index.js'\n\n/**\n * Version advertised in the MCP `initialize` handshake. Read once from\n * our own `package.json` so it stays in sync with the publish bump,\n * instead of a hardcoded literal that silently drifts each release.\n *\n * Falls back to `'unknown'` on read failure — SDK initialization still\n * succeeds; only the cosmetic serverInfo.version is affected.\n */\nconst PACKAGE_VERSION: string = (() => {\n try {\n // dist layout: `dist/index.js` → `package.json` is two levels up\n // from the module file at runtime.\n const here = dirname(fileURLToPath(import.meta.url))\n const pkg = JSON.parse(readFileSync(resolve(here, '../package.json'), 'utf8')) as {\n version?: string\n }\n return typeof pkg.version === 'string' ? pkg.version : 'unknown'\n } catch {\n return 'unknown'\n }\n})()\n\n/**\n * Walk up from `start` until we find a workspace root marker. Used by\n * both the MCP server (writing the active marker) and the Vite plugin\n * (watching it) so they agree on a single shared location regardless of\n * which subdirectory each process happens to be running in.\n *\n * Strong markers (workspace root): pnpm-workspace.yaml, .git directory.\n * If neither is found anywhere up the chain, falls back to the highest\n * package.json above `start`. For pnpm monorepos this finds the workspace\n * root from any subpackage; for single-package projects it finds the\n * package root.\n */\nexport function findWorkspaceRoot(start: string = process.cwd()): string {\n let dir = resolve(start)\n let lastPackageJson: string | null = null\n while (true) {\n // Strong markers — return immediately\n if (existsSync(resolve(dir, 'pnpm-workspace.yaml'))) return dir\n if (existsSync(resolve(dir, '.git'))) return dir\n // Track the highest package.json as a fallback\n if (existsSync(resolve(dir, 'package.json'))) lastPackageJson = dir\n const parent = dirname(dir)\n if (parent === dir) {\n // Reached filesystem root — return the highest package.json we saw\n return lastPackageJson ?? start\n }\n dir = parent\n }\n}\n\n/**\n * Path where the MCP server writes its active port marker. Vite plugins\n * watch this file to auto-trigger browser-side `__lluiConnect()` whenever\n * the MCP server starts, regardless of whether Vite or MCP started first.\n *\n * Resolved relative to the workspace root (not the immediate cwd) so the\n * MCP server and the Vite plugin always agree on a single location even\n * when one runs from the repo root and the other from a subpackage.\n */\nexport function mcpActiveFilePath(cwd: string = process.cwd()): string {\n return resolve(findWorkspaceRoot(cwd), 'node_modules/.cache/llui-mcp/active.json')\n}\n\n// ── MCP Server ──────────────────────────────────────────────────\n\nexport interface LluiMcpServerOptions {\n /**\n * Port for the browser-relay WebSocket bridge. When the MCP transport\n * is stdio (the CLI default), the relay stands up its own server on\n * this port. When the MCP transport is HTTP, the relay attaches to\n * that HTTP server and the MCP protocol + bridge share a single port.\n */\n bridgePort?: number\n /**\n * Optional pre-existing `http.Server` to share with the bridge. When\n * provided, the bridge attaches to it via upgrade routing on\n * `/bridge`; `bridgePort` is ignored for server-creation purposes\n * (but still written into the marker file so consumers know where to\n * connect).\n */\n attachTo?: HttpServer\n}\n\nexport class LluiMcpServer {\n private readonly registry: ToolRegistry\n private readonly relay: WebSocketRelayTransport\n private readonly bridgePort: number\n private readonly mcp: McpServer\n private devUrl: string | null = null\n\n /**\n * @param optsOrPort options object (preferred) or bridge port (legacy).\n * The numeric-port form is kept for one release cycle of back-compat;\n * new code should always pass an options object. The options form\n * supports `attachTo` for HTTP-transport deployments that share a\n * single port between MCP and the browser bridge — the numeric form\n * can't express that.\n * @deprecated numeric `optsOrPort` — pass `{ bridgePort }` instead.\n * This overload will be removed in a future breaking release.\n */\n constructor(optsOrPort: LluiMcpServerOptions | number = 5200) {\n const opts: LluiMcpServerOptions =\n typeof optsOrPort === 'number' ? { bridgePort: optsOrPort } : optsOrPort\n this.bridgePort = opts.bridgePort ?? 5200\n this.registry = new ToolRegistry()\n // Pass bridgePort even in attachTo mode — the relay's diagnose()\n // needs it for the port field of BridgeDiagnostic. The `start()`\n // path is gated on `attachTo` first so a standalone listener\n // never gets created twice.\n this.relay = new WebSocketRelayTransport({\n port: this.bridgePort,\n attachTo: opts.attachTo,\n markerPath: mcpActiveFilePath(),\n })\n registerDebugApiTools(this.registry)\n\n // SDK-managed MCP server — owns the JSON-RPC protocol, handshake,\n // session lifecycle. Transport is plugged in later via `connect()`.\n this.mcp = this.buildMcpServer()\n }\n\n /**\n * Build a fresh SDK `McpServer` wired to THIS instance's tool\n * registry and browser relay. The primary `this.mcp` uses one.\n * `createSessionMcp()` returns additional ones for HTTP-transport\n * deployments where every session needs its own SDK Server — each\n * routes tool calls through the shared relay, so the single\n * bridgeHost owns all the browser-facing state.\n */\n private buildMcpServer(): McpServer {\n const mcp = new McpServer(\n { name: '@llui/mcp', version: PACKAGE_VERSION },\n { capabilities: { tools: {} } },\n )\n mcp.setRequestHandler(ListToolsRequestSchema, async () => ({\n tools: this.getTools().map((t) => ({\n name: t.name,\n description: t.description,\n inputSchema: t.inputSchema,\n })),\n }))\n mcp.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params\n try {\n const result = await this.handleToolCall(name, args ?? {})\n return {\n content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],\n }\n } catch (err) {\n // Bridge-unavailable errors carry a structured diagnostic — surface\n // it as an isError tool result so the caller (typically Claude) sees\n // WHY the browser isn't reachable, not just that it failed.\n if (err instanceof RelayUnavailableError) {\n return {\n isError: true,\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ error: 'bridge-unavailable', ...err.diagnostic }, null, 2),\n },\n ],\n }\n }\n throw err\n }\n })\n return mcp\n }\n\n /**\n * Build a new SDK MCP server sharing this instance's registry + relay,\n * for HTTP-transport deployments where each session needs its own\n * `Server` (SDK requirement). Call-site pattern:\n *\n * const bridgeHost = new LluiMcpServer({ bridgePort, attachTo: httpServer })\n * bridgeHost.startBridge()\n * // Per session:\n * const sessionMcp = bridgeHost.createSessionMcp()\n * await sessionMcp.connect(transport)\n */\n createSessionMcp(): McpServer {\n return this.buildMcpServer()\n }\n\n /**\n * Connect the SDK MCP server to a transport (stdio, HTTP, etc).\n * The CLI builds the transport based on command-line flags and\n * hands it in here.\n */\n async connect(transport: Transport): Promise<void> {\n await this.mcp.connect(transport)\n }\n\n /** Connect to a debug API instance directly (for in-process usage). */\n connectDirect(api: LluiDebugAPI): void {\n this.relay.connectDirect(api)\n }\n\n /**\n * Set the dev-server URL that Phase 2's CDP fallback can navigate a\n * Playwright browser to. Persisted into the active marker file so the\n * Vite plugin (or other consumers) can rebroadcast it. If the bridge is\n * already running, rewrites the marker so consumers see the update.\n */\n setDevUrl(url: string): void {\n this.devUrl = url\n if (this.relay.isServerRunning()) this.writeActiveFile()\n }\n\n /**\n * Start a WebSocket server on the configured bridge port. The browser-side\n * relay (injected by the Vite plugin in dev mode) connects here and forwards\n * debug-API calls.\n */\n startBridge(): void {\n this.relay.start()\n\n // Write the active marker file so Vite plugins watching it can\n // dispatch an HMR custom event to auto-trigger browser connects.\n this.writeActiveFile()\n }\n\n stopBridge(): void {\n this.relay.stop()\n this.removeActiveFile()\n }\n\n private writeActiveFile(): void {\n try {\n const path = mcpActiveFilePath()\n mkdirSync(dirname(path), { recursive: true })\n const payload: { port: number; pid: number; devUrl?: string } = {\n port: this.bridgePort,\n pid: process.pid,\n }\n if (this.devUrl !== null) payload.devUrl = this.devUrl\n writeFileSync(path, JSON.stringify(payload))\n } catch {\n // Best-effort — failure to write the marker should not crash the server\n }\n }\n\n private removeActiveFile(): void {\n try {\n const path = mcpActiveFilePath()\n if (existsSync(path)) unlinkSync(path)\n } catch {\n // Ignore — file may already be gone\n }\n }\n\n /** Get tool definitions for MCP handshake */\n getTools(): ToolDefinition[] {\n return this.registry.listDefinitions()\n }\n\n /** Handle an MCP tool call */\n async handleToolCall(name: string, args: Record<string, unknown>): Promise<unknown> {\n const ctx: ToolContext = { relay: this.relay, cdp: null }\n return this.registry.dispatch(name, args, ctx)\n }\n}\n\n/**\n * Snapshot of all registered tool definitions. Kept as a named export for\n * backward compatibility with downstream consumers that used to import the\n * `TOOLS` array re-export under this alias.\n */\nexport const mcpToolDefinitions: ToolDefinition[] = (() => {\n const registry = new ToolRegistry()\n registerDebugApiTools(registry)\n return registry.listDefinitions()\n})()\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llui/mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
20
20
|
"ws": "^8.18.0",
|
|
21
|
-
"@llui/dom": "0.0.
|
|
21
|
+
"@llui/dom": "0.0.24",
|
|
22
22
|
"@llui/lint-idiomatic": "0.0.12"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|