@llui/mcp 0.0.16 → 0.0.17

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 CHANGED
@@ -65,14 +65,17 @@ async function main() {
65
65
  res.end(JSON.stringify({ error: String(err) }));
66
66
  });
67
67
  });
68
- // Single bridge attached to the HTTP server; all MCP sessions share it.
69
- const bridgeServer = new LluiMcpServer({ bridgePort: httpPort, attachTo: httpServer });
70
- bridgeServer.startBridge();
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
- bridgeServer.stopBridge();
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. Each MCP session gets its own server instance
104
- // (SDK requirement), but all share the one browser bridge.
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 sessionServer = new LluiMcpServer({ bridgePort: httpPort, attachTo: httpServer });
117
- await sessionServer.connect(transport);
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 ? '✓' : '✗'} ${c.name.padEnd(32)} ${c.detail}\n`);
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
- private registerMcpHandlers;
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
@@ -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;AAG7C,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,WAAW,CAAA;AAErD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAA;AAE9E,OAAO,EAAkC,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAIxF;;;;;;;;;;;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;gBAExB,UAAU,GAAE,oBAAoB,GAAG,MAAa;IAqB5D,OAAO,CAAC,mBAAmB;IAoC3B;;;;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"}
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: opts.attachTo ? undefined : this.bridgePort,
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 = new McpServer({ name: '@llui/mcp', version: '0.0.15' }, { capabilities: { tools: {} } });
71
- this.registerMcpHandlers();
105
+ this.mcp = this.buildMcpServer();
72
106
  }
73
- registerMcpHandlers() {
74
- this.mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
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
- this.mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
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":"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","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.16",
3
+ "version": "0.0.17",
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.22",
21
+ "@llui/dom": "0.0.23",
22
22
  "@llui/lint-idiomatic": "0.0.12"
23
23
  },
24
24
  "devDependencies": {