@nudge-ui/mcp 0.2.1 → 0.2.2

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.mjs CHANGED
@@ -7479,7 +7479,7 @@ function isCanvasCommand(value) {
7479
7479
  // src/discovery.ts
7480
7480
  import { randomUUID as randomUUID3 } from "node:crypto";
7481
7481
  import { realpath as realpath3 } from "node:fs/promises";
7482
- import { resolve as resolve3 } from "node:path";
7482
+ import { isAbsolute as isAbsolute2, resolve as resolve3 } from "node:path";
7483
7483
 
7484
7484
  // src/projectSessionClient.ts
7485
7485
  import { randomUUID } from "node:crypto";
@@ -7488,6 +7488,7 @@ import { randomUUID } from "node:crypto";
7488
7488
  var BRIDGE_ENDPOINTS = {
7489
7489
  health: "/health",
7490
7490
  pair: "/pair",
7491
+ takeover: "/takeover",
7491
7492
  events: "/events",
7492
7493
  prompt: "/prompt",
7493
7494
  status: "/status",
@@ -7508,6 +7509,14 @@ var AGENT_CONTROL_ENDPOINTS = {
7508
7509
  // src/projectSessionClient.ts
7509
7510
  var CONTROL_TIMEOUT_MS = 5e3;
7510
7511
  var CANVAS_TIMEOUT_MS = 65e3;
7512
+ var ProjectSessionResponseError = class extends Error {
7513
+ status;
7514
+ constructor(status, message) {
7515
+ super(message);
7516
+ this.name = "ProjectSessionResponseError";
7517
+ this.status = status;
7518
+ }
7519
+ };
7511
7520
  async function requestJson(session, path, init = {}, { signal, timeoutMs = CONTROL_TIMEOUT_MS } = {}) {
7512
7521
  const response = await fetch(`${session.endpoint}${path}`, {
7513
7522
  ...init,
@@ -7520,7 +7529,12 @@ async function requestJson(session, path, init = {}, { signal, timeoutMs = CONTR
7520
7529
  }
7521
7530
  });
7522
7531
  const body = await response.json().catch(() => void 0);
7523
- if (!response.ok) throw new Error(body?.error?.message ?? `Nudge project bridge returned ${response.status}.`);
7532
+ if (!response.ok) {
7533
+ throw new ProjectSessionResponseError(
7534
+ response.status,
7535
+ body?.error?.message ?? `Nudge project bridge returned ${response.status}.`
7536
+ );
7537
+ }
7524
7538
  return body;
7525
7539
  }
7526
7540
  async function readSessionHealth(session, timeoutMs = CONTROL_TIMEOUT_MS) {
@@ -7595,13 +7609,21 @@ import { lstat, readFile, readdir, realpath } from "node:fs/promises";
7595
7609
  import { resolve } from "node:path";
7596
7610
  var PROBE_TIMEOUT_MS = 750;
7597
7611
  var MAX_DESCRIPTOR_BYTES = 32 * 1024;
7612
+ async function inspectProjectSessions(registryRoot) {
7613
+ const names = (await readRegistryDirectory(registryRoot)).filter((name) => name.endsWith(".json"));
7614
+ const records = await Promise.all(names.map((name) => readDescriptor(resolve(registryRoot, name))));
7615
+ const descriptors = records.filter((record2) => typeof record2 === "object");
7616
+ const probes = await Promise.all(descriptors.map(probeSession));
7617
+ return {
7618
+ descriptorCount: names.length,
7619
+ invalidCount: records.filter((record2) => record2 === void 0).length + probes.filter((probe) => probe.kind === "invalid").length,
7620
+ incompatibleCount: records.filter((record2) => record2 === "incompatible").length,
7621
+ unreachable: descriptors.filter((_, index) => probes[index]?.kind === "unreachable").map(({ workspaceRoot, appRoot, sessionId }) => ({ workspaceRoot, appRoot, sessionId })),
7622
+ live: probes.flatMap((probe) => probe.kind === "live" ? [probe.session] : [])
7623
+ };
7624
+ }
7598
7625
  async function readLiveProjectSessions(registryRoot) {
7599
- const names = await readRegistryDirectory(registryRoot);
7600
- const descriptors = await Promise.all(
7601
- names.filter((name) => name.endsWith(".json")).map((name) => readDescriptor(resolve(registryRoot, name)))
7602
- );
7603
- const live = await Promise.all(descriptors.filter(isPresent).map(probeSession));
7604
- return live.filter(isPresent);
7626
+ return (await inspectProjectSessions(registryRoot)).live;
7605
7627
  }
7606
7628
  async function readRegistryDirectory(registryRoot) {
7607
7629
  try {
@@ -7619,8 +7641,9 @@ async function readDescriptor(path) {
7619
7641
  if ((metadata.mode & 63) !== 0) return void 0;
7620
7642
  const parsed = JSON.parse(await readFile(path, "utf8"));
7621
7643
  if (!isStoredSession(parsed)) return void 0;
7644
+ if (parsed.protocolVersion !== AGENT_PROTOCOL_VERSION) return "incompatible";
7622
7645
  const [workspaceRoot, appRoot] = await Promise.all([realpath(parsed.workspaceRoot), realpath(parsed.appRoot)]);
7623
- return { ...parsed, workspaceRoot, appRoot };
7646
+ return { ...parsed, protocolVersion: AGENT_PROTOCOL_VERSION, workspaceRoot, appRoot };
7624
7647
  } catch {
7625
7648
  return void 0;
7626
7649
  }
@@ -7628,14 +7651,21 @@ async function readDescriptor(path) {
7628
7651
  async function probeSession(descriptor) {
7629
7652
  try {
7630
7653
  const health = await readSessionHealth(descriptor, PROBE_TIMEOUT_MS);
7631
- if (health.projectId !== descriptor.projectId || health.available !== true) return void 0;
7632
- return { descriptor, health };
7633
- } catch {
7634
- return void 0;
7654
+ if (!isSessionHealth(health) || health.projectId !== descriptor.projectId) return { kind: "invalid" };
7655
+ return { kind: "live", session: { descriptor, health } };
7656
+ } catch (error2) {
7657
+ return { kind: error2 instanceof ProjectSessionResponseError ? "invalid" : "unreachable" };
7635
7658
  }
7636
7659
  }
7637
- function isPresent(value) {
7638
- return value !== void 0;
7660
+ function isSessionHealth(value) {
7661
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
7662
+ const health = value;
7663
+ return typeof health.projectId === "string" && health.available === true && typeof health.claimed === "boolean" && isAgentStatusSnapshot(health.status);
7664
+ }
7665
+ function isAgentStatusSnapshot(value) {
7666
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
7667
+ const status = value;
7668
+ return status.protocolVersion === AGENT_PROTOCOL_VERSION && typeof status.projectId === "string" && (status.connection === "offline" || status.connection === "listening" || status.connection === "paired" || status.connection === "working") && typeof status.listenerActive === "boolean" && typeof status.paired === "boolean" && (status.request === null || typeof status.request === "object" && !Array.isArray(status.request));
7639
7669
  }
7640
7670
  function isLoopbackEndpoint(value) {
7641
7671
  if (typeof value !== "string") return false;
@@ -7649,7 +7679,7 @@ function isLoopbackEndpoint(value) {
7649
7679
  function isStoredSession(value) {
7650
7680
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
7651
7681
  const session = value;
7652
- return session.schemaVersion === 1 && session.protocolVersion === AGENT_PROTOCOL_VERSION && typeof session.sessionId === "string" && session.sessionId.length > 0 && typeof session.projectId === "string" && session.projectId.length > 0 && typeof session.workspaceRoot === "string" && session.workspaceRoot.length > 0 && typeof session.appRoot === "string" && session.appRoot.length > 0 && (session.gitCommonDir === void 0 || typeof session.gitCommonDir === "string") && (session.branch === void 0 || typeof session.branch === "string") && typeof session.origin === "string" && isLoopbackEndpoint(session.endpoint) && typeof session.startedAt === "string" && typeof session.pid === "number" && Number.isSafeInteger(session.pid) && typeof session.controlToken === "string" && session.controlToken.length >= 32 && session.controlToken.length <= 256;
7682
+ return session.schemaVersion === 1 && typeof session.protocolVersion === "number" && Number.isSafeInteger(session.protocolVersion) && typeof session.sessionId === "string" && session.sessionId.length > 0 && typeof session.projectId === "string" && session.projectId.length > 0 && typeof session.workspaceRoot === "string" && session.workspaceRoot.length > 0 && typeof session.appRoot === "string" && session.appRoot.length > 0 && (session.gitCommonDir === void 0 || typeof session.gitCommonDir === "string") && (session.branch === void 0 || typeof session.branch === "string") && typeof session.origin === "string" && isLoopbackEndpoint(session.endpoint) && typeof session.startedAt === "string" && typeof session.pid === "number" && Number.isSafeInteger(session.pid) && typeof session.controlToken === "string" && session.controlToken.length >= 32 && session.controlToken.length <= 256;
7653
7683
  }
7654
7684
 
7655
7685
  // src/sessionSelection.ts
@@ -7985,7 +8015,7 @@ function createLoopbackBridge(options) {
7985
8015
  pageUrl: lastPageUrl
7986
8016
  }, incomingOrigin);
7987
8017
  };
7988
- const handlePair = async (request, response) => {
8018
+ const readPairRequest = async (request) => {
7989
8019
  if (!loopbackAddress(request.socket.remoteAddress)) {
7990
8020
  throw new BridgeRequestError(403, "loopback_only", "The browser bridge accepts loopback connections only.");
7991
8021
  }
@@ -8006,6 +8036,10 @@ function createLoopbackBridge(options) {
8006
8036
  }
8007
8037
  requestedPageUrl = new URL(body.pageUrl, requestedOrigin).href;
8008
8038
  }
8039
+ return { body, incomingOrigin, requestedPageUrl };
8040
+ };
8041
+ const handlePair = async (request, response) => {
8042
+ const { body, incomingOrigin, requestedPageUrl } = await readPairRequest(request);
8009
8043
  if (sessionToken) {
8010
8044
  if (body.sessionToken !== sessionToken) {
8011
8045
  throw new BridgeRequestError(409, "already_paired", "A browser is already paired with this project.");
@@ -8041,6 +8075,12 @@ function createLoopbackBridge(options) {
8041
8075
  sendJson(response, 200, result, incomingOrigin);
8042
8076
  broadcastStatus();
8043
8077
  };
8078
+ const handleTakeover = async (request, response) => {
8079
+ const { incomingOrigin, requestedPageUrl } = await readPairRequest(request);
8080
+ if (sessionToken) disconnectBrowser(sessionToken);
8081
+ const pairing = pairBrowser(options.projectId, incomingOrigin, requestedPageUrl);
8082
+ sendJson(response, 200, pairing, incomingOrigin);
8083
+ };
8044
8084
  const handleEvents = (request, response, url) => {
8045
8085
  parseProjectId(url.searchParams.get("projectId"), options.projectId);
8046
8086
  const incomingOrigin = requireBrowserRequest(request, url);
@@ -8322,6 +8362,10 @@ function createLoopbackBridge(options) {
8322
8362
  await handlePair(request, response);
8323
8363
  return;
8324
8364
  }
8365
+ if (request.method === "POST" && url.pathname === BRIDGE_ENDPOINTS.takeover) {
8366
+ await handleTakeover(request, response);
8367
+ return;
8368
+ }
8325
8369
  if (request.method === "GET" && url.pathname === BRIDGE_ENDPOINTS.events) {
8326
8370
  handleEvents(request, response, url);
8327
8371
  return;
@@ -8620,15 +8664,15 @@ function publicSession(session) {
8620
8664
  return visible;
8621
8665
  }
8622
8666
  async function discoverProjectSessions(applicationRoot, registryRoot = defaultSessionRegistryRoot()) {
8623
- const canonicalApplication = await realpath3(resolve3(applicationRoot));
8624
- const canonicalWorkspace = await resolveWorkspaceRoot(canonicalApplication);
8667
+ const canonicalApplication = applicationRoot === void 0 ? void 0 : await realpath3(requireAbsoluteWorkspaceRoot(applicationRoot));
8668
+ const canonicalWorkspace = canonicalApplication === void 0 ? void 0 : await resolveWorkspaceRoot(canonicalApplication);
8625
8669
  const live = await readLiveProjectSessions(resolve3(registryRoot));
8626
8670
  return live.map(({ descriptor, health }) => {
8627
8671
  const matchesWorkspace = descriptor.workspaceRoot === canonicalWorkspace;
8628
8672
  return {
8629
8673
  ...publicSession(descriptor),
8630
8674
  matchesWorkspace,
8631
- matchesApplication: matchesApplication(descriptor, {
8675
+ matchesApplication: canonicalWorkspace !== void 0 && canonicalApplication !== void 0 && matchesApplication(descriptor, {
8632
8676
  workspaceRoot: canonicalWorkspace,
8633
8677
  applicationRoot: canonicalApplication
8634
8678
  }),
@@ -8648,14 +8692,23 @@ var DiscoveredProjectRouter = class _DiscoveredProjectRouter {
8648
8692
  agentId;
8649
8693
  client = null;
8650
8694
  releaseGeneration = 0;
8695
+ connectionQueue = Promise.resolve();
8651
8696
  constructor(workspaceRoot, applicationRoot, registryRoot, agentId) {
8652
8697
  this.workspaceRoot = workspaceRoot;
8653
8698
  this.applicationRoot = applicationRoot;
8654
8699
  this.registryRoot = registryRoot;
8655
8700
  this.agentId = agentId;
8656
8701
  }
8702
+ get selectedSessionId() {
8703
+ return this.client?.descriptor.sessionId;
8704
+ }
8705
+ static forSession(session, registryRoot) {
8706
+ const router = new _DiscoveredProjectRouter(session.workspaceRoot, session.appRoot, registryRoot, randomUUID3());
8707
+ router.client = new ProjectSessionClient(session, router.agentId);
8708
+ return router;
8709
+ }
8657
8710
  static async create(workspaceRoot, registryRoot = defaultSessionRegistryRoot()) {
8658
- const applicationRoot = await realpath3(resolve3(workspaceRoot));
8711
+ const applicationRoot = await realpath3(requireAbsoluteWorkspaceRoot(workspaceRoot));
8659
8712
  return new _DiscoveredProjectRouter(
8660
8713
  await resolveWorkspaceRoot(applicationRoot),
8661
8714
  applicationRoot,
@@ -8666,7 +8719,12 @@ var DiscoveredProjectRouter = class _DiscoveredProjectRouter {
8666
8719
  async listSessions() {
8667
8720
  return await discoverProjectSessions(this.applicationRoot, this.registryRoot);
8668
8721
  }
8669
- async connectToSession(sessionId) {
8722
+ connectToSession(sessionId) {
8723
+ const connection = this.connectionQueue.then(() => this.selectSession(sessionId));
8724
+ this.connectionQueue = connection.then(() => void 0, () => void 0);
8725
+ return connection;
8726
+ }
8727
+ async selectSession(sessionId) {
8670
8728
  const live = await readLiveProjectSessions(this.registryRoot);
8671
8729
  const selected = selectProjectSession(live.map(({ descriptor }) => descriptor), {
8672
8730
  workspaceRoot: this.workspaceRoot,
@@ -8712,6 +8770,7 @@ var DiscoveredProjectRouter = class _DiscoveredProjectRouter {
8712
8770
  }
8713
8771
  async release() {
8714
8772
  this.releaseGeneration += 1;
8773
+ await this.connectionQueue;
8715
8774
  const client = this.client;
8716
8775
  this.client = null;
8717
8776
  await client?.close();
@@ -8720,6 +8779,33 @@ var DiscoveredProjectRouter = class _DiscoveredProjectRouter {
8720
8779
  await this.release();
8721
8780
  }
8722
8781
  };
8782
+ async function diagnoseProjectSessions(applicationRoot, registryRoot = defaultSessionRegistryRoot()) {
8783
+ const resolvedRegistryRoot = resolve3(registryRoot);
8784
+ const app = applicationRoot === void 0 ? void 0 : await realpath3(requireAbsoluteWorkspaceRoot(applicationRoot));
8785
+ const workspace = app === void 0 ? void 0 : await resolveWorkspaceRoot(app);
8786
+ const inspection = await inspectProjectSessions(resolvedRegistryRoot);
8787
+ const matching = (session) => app === void 0 || session.workspaceRoot === workspace && (app === workspace || session.appRoot === app);
8788
+ return {
8789
+ registryRoot: resolvedRegistryRoot,
8790
+ workspaceRoot: workspace,
8791
+ applicationRoot: app,
8792
+ descriptorCount: inspection.descriptorCount,
8793
+ invalidCount: inspection.invalidCount,
8794
+ incompatibleCount: inspection.incompatibleCount,
8795
+ reachableCount: inspection.live.length,
8796
+ matchingReachableCount: inspection.live.filter(({ descriptor }) => matching(descriptor)).length,
8797
+ matchingUnreachableCount: inspection.unreachable.filter(matching).length,
8798
+ guidance: [
8799
+ ...inspection.invalidCount > 0 ? ["Some session descriptors or authenticated health responses are invalid. Check the adapter and project bridge versions, descriptor permissions, and bridge logs."] : [],
8800
+ ...inspection.unreachable.some(matching) ? ["Matching descriptors exist but their loopback endpoints are unreachable. The server may have stopped or local networking may be blocked. Retry outside the agent command sandbox; do not treat this as proof that no bridge registered."] : [],
8801
+ ...inspection.incompatibleCount > 0 ? ["Some registrations use an incompatible protocol. Upgrade the reusable adapter and the project's @nudge-ui/mcp dependency, then restart the development server and agent host."] : []
8802
+ ]
8803
+ };
8804
+ }
8805
+ function requireAbsoluteWorkspaceRoot(workspaceRoot) {
8806
+ if (!isAbsolute2(workspaceRoot)) throw new TypeError("workspaceRoot must be an absolute path.");
8807
+ return workspaceRoot;
8808
+ }
8723
8809
 
8724
8810
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
8725
8811
  var external_exports = {};
@@ -22955,7 +23041,64 @@ var StdioServerTransport = class {
22955
23041
  };
22956
23042
 
22957
23043
  // src/server.ts
23044
+ import { realpath as realpath5 } from "node:fs/promises";
22958
23045
  import { createRequire } from "node:module";
23046
+ import { isAbsolute as isAbsolute4 } from "node:path";
23047
+
23048
+ // src/adapterRouting.ts
23049
+ import { realpath as realpath4 } from "node:fs/promises";
23050
+ import { isAbsolute as isAbsolute3 } from "node:path";
23051
+ var AdapterRouting = class {
23052
+ constructor(registryRoot = defaultSessionRegistryRoot()) {
23053
+ this.registryRoot = registryRoot;
23054
+ }
23055
+ closing = false;
23056
+ selections = /* @__PURE__ */ new Map();
23057
+ applications = /* @__PURE__ */ new Map();
23058
+ async target({ workspaceRoot, sessionId }) {
23059
+ if (!workspaceRoot || !isAbsolute3(workspaceRoot)) {
23060
+ throw new Error("Supply workspaceRoot as the absolute path of the checkout or application you are editing.");
23061
+ }
23062
+ const applicationRoot = await realpath4(workspaceRoot);
23063
+ if (this.closing) throw new Error("The MCP adapter is closing.");
23064
+ const key = JSON.stringify([applicationRoot, sessionId]);
23065
+ let selection = this.selections.get(key);
23066
+ if (!selection) {
23067
+ selection = this.select(applicationRoot, sessionId);
23068
+ this.selections.set(key, selection);
23069
+ void selection.catch(() => {
23070
+ if (this.selections.get(key) === selection) this.selections.delete(key);
23071
+ });
23072
+ }
23073
+ return await selection;
23074
+ }
23075
+ async select(applicationRoot, sessionId) {
23076
+ const workspaceRoot = await resolveWorkspaceRoot(applicationRoot);
23077
+ const live = await readLiveProjectSessions(this.registryRoot);
23078
+ if (this.closing) throw new Error("The MCP adapter is closing.");
23079
+ const selected = selectProjectSession(live.map(({ descriptor }) => descriptor), {
23080
+ workspaceRoot,
23081
+ applicationRoot,
23082
+ requestedSessionId: sessionId
23083
+ });
23084
+ const key = selected.sessionId;
23085
+ let router = this.applications.get(key) ?? [...this.applications.values()].find((candidate) => candidate.selectedSessionId === selected.sessionId);
23086
+ if (!router) {
23087
+ router = DiscoveredProjectRouter.forSession(selected, this.registryRoot);
23088
+ this.applications.set(key, router);
23089
+ }
23090
+ return router;
23091
+ }
23092
+ async close() {
23093
+ this.closing = true;
23094
+ await Promise.allSettled(this.selections.values());
23095
+ await Promise.all([...this.applications.values()].map((router) => router.close()));
23096
+ this.selections.clear();
23097
+ this.applications.clear();
23098
+ }
23099
+ };
23100
+
23101
+ // src/server.ts
22959
23102
  var MCP_SERVER_NAME = "nudge-ui";
22960
23103
  var MCP_SERVER_VERSION = readPackageVersion();
22961
23104
  function readPackageVersion() {
@@ -22967,13 +23110,14 @@ function readPackageVersion() {
22967
23110
  }
22968
23111
  var MCP_SERVER_INSTRUCTIONS = [
22969
23112
  "Nudge UI is a local browser companion for one exact project workspace or Git worktree.",
22970
- "Before listening, compare each session's workspaceRoot and appRoot with the checkout and application you will edit; if they differ, rerun Nudge agent setup in the intended checkout.",
23113
+ "Supply workspaceRoot with the absolute checkout or application path you are editing on every tool call. Listing and selecting sessions requires this explicit scope; repeat the same workspaceRoot and sessionId for status, Canvas, and release calls. Never use the MCP process working directory to infer the task workspace.",
22971
23114
  "When the user asks to listen to Nudge, call nudge_list_sessions when that tool is available; if several apps are running, select only a session marked matchesApplication.",
22972
23115
  "Call nudge_listen (or nudge_connect) and keep the call open while waiting for a browser request; the bridge may be running before a listener exists.",
22973
23116
  "After a prompt is delivered, edit the project source using the host's normal approval flow.",
22974
23117
  "Report completed, failed, or interrupted status with nudge_report_status before listening again.",
22975
23118
  "When the user asks to stop listening, call nudge_release when that tool is available so another agent can claim the project session.",
22976
23119
  "Canvas tools operate only on same-origin routes from the paired project.",
23120
+ "Automated browsers such as Playwright and headless Chrome get the plain app without the Nudge editor; add ?nudge-ui=on to a URL only when testing Nudge itself.",
22977
23121
  "No request, prompt, or browser state is persisted by this companion."
22978
23122
  ].join(" ");
22979
23123
  function textResult(value) {
@@ -23019,17 +23163,29 @@ function boundedWorkspaceRoot(value) {
23019
23163
  if (value.length === 0 || value.length > 4096) throw new TypeError("workspaceRoot must be a bounded non-empty string");
23020
23164
  return value;
23021
23165
  }
23022
- function installTools(mcpServer, bridge) {
23166
+ function installTools(mcpServer, bridge, routing) {
23167
+ const target = async (args) => {
23168
+ if (routing) return await routing.target(args);
23169
+ if (bridge instanceof DiscoveredProjectRouter && args.workspaceRoot !== void 0 && await realpath5(args.workspaceRoot) !== bridge.applicationRoot) {
23170
+ throw new Error("This legacy MCP configuration is restricted to another application. Rerun agent setup to migrate it to the reusable adapter.");
23171
+ }
23172
+ return bridge;
23173
+ };
23174
+ const workspaceInput = external_exports.string().min(1).max(4096).refine(isAbsolute4, "workspaceRoot must be an absolute path.").describe("Absolute path of the checkout or application being edited.");
23175
+ const scopeInput = {
23176
+ workspaceRoot: routing ? workspaceInput : workspaceInput.optional(),
23177
+ sessionId: external_exports.string().uuid().optional()
23178
+ };
23023
23179
  const listen = async (args, extra) => {
23024
23180
  try {
23025
- return promptResult(await bridge.waitForPrompt(extra.signal, args.sessionId));
23181
+ return promptResult(await (await target(args)).waitForPrompt(extra.signal, args.sessionId));
23026
23182
  } catch (error2) {
23027
23183
  return errorResult(error2);
23028
23184
  }
23029
23185
  };
23030
23186
  const listenDescription = "When the user asks to listen to Nudge, wait for one browser prompt. Keep this standard MCP call open, then call nudge_report_status and listen again after the request ends.";
23031
- const emptyInput = {};
23032
- const listenInput = bridge.listSessions ? { sessionId: external_exports.string().uuid().optional() } : emptyInput;
23187
+ const emptyInput = scopeInput;
23188
+ const listenInput = scopeInput;
23033
23189
  mcpServer.registerTool("nudge_listen", {
23034
23190
  title: "Listen for Nudge requests",
23035
23191
  description: listenDescription,
@@ -23047,24 +23203,38 @@ function installTools(mcpServer, bridge) {
23047
23203
  description: "Read the in-memory browser pairing and request status for this project.",
23048
23204
  inputSchema: emptyInput,
23049
23205
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
23050
- }, async () => textResult(await bridge.getStatus()));
23051
- if (bridge.listSessions) {
23206
+ }, async (args) => {
23207
+ try {
23208
+ return textResult(await (await target(args)).getStatus());
23209
+ } catch (error2) {
23210
+ return errorResult(error2);
23211
+ }
23212
+ });
23213
+ if (routing || bridge?.listSessions) {
23052
23214
  mcpServer.registerTool("nudge_list_sessions", {
23053
23215
  title: "List Nudge project sessions",
23054
- description: "List live local Nudge sessions. Only sessions marked matchesApplication can be selected by an application-scoped adapter.",
23216
+ description: "List live local Nudge sessions for the supplied checkout or application path. The workspaceRoot value is required so the adapter can select only sessions in that explicit scope.",
23055
23217
  inputSchema: emptyInput,
23056
23218
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
23057
- }, async () => textResult(await bridge.listSessions()));
23219
+ }, async (args) => textResult(routing ? await discoverProjectSessions(args.workspaceRoot, routing.registryRoot) : await bridge.listSessions()));
23058
23220
  }
23059
- if (bridge.release) {
23221
+ if (routing) {
23222
+ mcpServer.registerTool("nudge_diagnose", {
23223
+ title: "Diagnose Nudge discovery",
23224
+ description: "Diagnose Nudge discovery for the supplied checkout or application path. The result includes the resolved registry, workspace, and application scope plus counts for missing registrations, invalid descriptors or health responses, unreachable loopback endpoints, and incompatible bridge protocols; credentials are never exposed.",
23225
+ inputSchema: scopeInput,
23226
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
23227
+ }, async (args) => textResult(await diagnoseProjectSessions(args.workspaceRoot, routing.registryRoot)));
23228
+ }
23229
+ if (routing || bridge?.release) {
23060
23230
  mcpServer.registerTool("nudge_release", {
23061
23231
  title: "Release Nudge session",
23062
23232
  description: "Stop listening and release this adapter's project-session claim so another agent can connect.",
23063
23233
  inputSchema: emptyInput,
23064
23234
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
23065
- }, async () => {
23235
+ }, async (args) => {
23066
23236
  try {
23067
- await bridge.release();
23237
+ await (await target(args)).release();
23068
23238
  return textResult({ released: true });
23069
23239
  } catch (error2) {
23070
23240
  return errorResult(error2);
@@ -23073,8 +23243,9 @@ function installTools(mcpServer, bridge) {
23073
23243
  }
23074
23244
  mcpServer.registerTool("nudge_report_status", {
23075
23245
  title: "Report Nudge request status",
23076
- description: "Report working, completed, failed, or interrupted status for the prompt currently being handled.",
23246
+ description: "Report working, completed, failed, or interrupted status for the prompt currently being handled. Report completed only after implementing the request; the browser removes its submitted sketches if their revisions are unchanged.",
23077
23247
  inputSchema: {
23248
+ ...scopeInput,
23078
23249
  requestId: external_exports.string().min(1).max(256),
23079
23250
  status: external_exports.enum(["working", "completed", "failed", "interrupted"]),
23080
23251
  summary: external_exports.string().max(32e3).optional(),
@@ -23089,8 +23260,8 @@ function installTools(mcpServer, bridge) {
23089
23260
  ...args.error === void 0 ? {} : { error: args.error }
23090
23261
  };
23091
23262
  try {
23092
- const updated = await bridge.updateRequestStatus(update);
23093
- return textResult(updated ?? await bridge.getStatus());
23263
+ const updated = await (await target(args)).updateRequestStatus(update);
23264
+ return textResult(updated ?? await (await target(args)).getStatus());
23094
23265
  } catch (error2) {
23095
23266
  return errorResult(error2);
23096
23267
  }
@@ -23100,9 +23271,9 @@ function installTools(mcpServer, bridge) {
23100
23271
  description: "Read the paired browser's current Canvas groups and focus.",
23101
23272
  inputSchema: emptyInput,
23102
23273
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
23103
- }, async () => {
23274
+ }, async (args) => {
23104
23275
  try {
23105
- return commandResult(await bridge.dispatchCanvasCommand({ type: "read-state" }));
23276
+ return commandResult(await (await target(args)).dispatchCanvasCommand({ type: "read-state" }));
23106
23277
  } catch (error2) {
23107
23278
  return errorResult(error2);
23108
23279
  }
@@ -23111,6 +23282,7 @@ function installTools(mcpServer, bridge) {
23111
23282
  title: "Present routes in Canvas",
23112
23283
  description: "Append or focus an agent-owned, labeled comparison group of same-origin project routes in Canvas.",
23113
23284
  inputSchema: {
23285
+ ...scopeInput,
23114
23286
  label: external_exports.string().min(1).max(160),
23115
23287
  groupId: external_exports.string().min(1).max(256).regex(/^agent-[A-Za-z0-9._:-]+$/).optional(),
23116
23288
  routes: external_exports.array(external_exports.object({
@@ -23122,7 +23294,7 @@ function installTools(mcpServer, bridge) {
23122
23294
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
23123
23295
  }, async (args) => {
23124
23296
  try {
23125
- return commandResult(await bridge.dispatchCanvasCommand({
23297
+ return commandResult(await (await target(args)).dispatchCanvasCommand({
23126
23298
  type: "present-routes",
23127
23299
  groupId: args.groupId ?? `agent-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`,
23128
23300
  label: args.label,
@@ -23135,11 +23307,11 @@ function installTools(mcpServer, bridge) {
23135
23307
  mcpServer.registerTool("nudge_focus_canvas_group", {
23136
23308
  title: "Focus Canvas group",
23137
23309
  description: "Focus an existing Canvas comparison group by its stable group ID.",
23138
- inputSchema: { groupId: external_exports.string().min(1).max(256) },
23310
+ inputSchema: { ...scopeInput, groupId: external_exports.string().min(1).max(256) },
23139
23311
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
23140
23312
  }, async (args) => {
23141
23313
  try {
23142
- return commandResult(await bridge.dispatchCanvasCommand({ type: "focus-group", groupId: args.groupId }));
23314
+ return commandResult(await (await target(args)).dispatchCanvasCommand({ type: "focus-group", groupId: args.groupId }));
23143
23315
  } catch (error2) {
23144
23316
  return errorResult(error2);
23145
23317
  }
@@ -23149,9 +23321,9 @@ function installTools(mcpServer, bridge) {
23149
23321
  description: "Fit all current Canvas cards into the browser board.",
23150
23322
  inputSchema: emptyInput,
23151
23323
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
23152
- }, async () => {
23324
+ }, async (args) => {
23153
23325
  try {
23154
- return commandResult(await bridge.dispatchCanvasCommand({ type: "fit-all" }));
23326
+ return commandResult(await (await target(args)).dispatchCanvasCommand({ type: "fit-all" }));
23155
23327
  } catch (error2) {
23156
23328
  return errorResult(error2);
23157
23329
  }
@@ -23159,11 +23331,11 @@ function installTools(mcpServer, bridge) {
23159
23331
  mcpServer.registerTool("nudge_remove_canvas_group", {
23160
23332
  title: "Remove agent Canvas group",
23161
23333
  description: "Remove an agent-owned Canvas comparison group. The browser refuses removal of user-owned groups.",
23162
- inputSchema: { groupId: external_exports.string().min(1).max(256).regex(/^agent-[A-Za-z0-9._:-]+$/) },
23334
+ inputSchema: { ...scopeInput, groupId: external_exports.string().min(1).max(256).regex(/^agent-[A-Za-z0-9._:-]+$/) },
23163
23335
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
23164
23336
  }, async (args) => {
23165
23337
  try {
23166
- return commandResult(await bridge.dispatchCanvasCommand({ type: "remove-group", groupId: args.groupId }));
23338
+ return commandResult(await (await target(args)).dispatchCanvasCommand({ type: "remove-group", groupId: args.groupId }));
23167
23339
  } catch (error2) {
23168
23340
  return errorResult(error2);
23169
23341
  }
@@ -23221,7 +23393,8 @@ function createAgentCompanion(options) {
23221
23393
  };
23222
23394
  }
23223
23395
  async function createDiscoveredAgentAdapter(options) {
23224
- const router = await DiscoveredProjectRouter.create(options.workspaceRoot, options.registryRoot);
23396
+ const routing = options.workspaceRoot === void 0 ? new AdapterRouting(options.registryRoot) : void 0;
23397
+ const router = options.workspaceRoot === void 0 ? void 0 : await DiscoveredProjectRouter.create(options.workspaceRoot, options.registryRoot);
23225
23398
  const mcpServer = new McpServer(
23226
23399
  {
23227
23400
  name: options.serverName ?? MCP_SERVER_NAME,
@@ -23229,13 +23402,13 @@ async function createDiscoveredAgentAdapter(options) {
23229
23402
  },
23230
23403
  { instructions: MCP_SERVER_INSTRUCTIONS }
23231
23404
  );
23232
- installTools(mcpServer, router);
23405
+ installTools(mcpServer, router, routing);
23233
23406
  let transport = null;
23234
23407
  let started = false;
23235
23408
  let closePromise = null;
23236
23409
  return {
23237
23410
  mcpServer,
23238
- router,
23411
+ router: router ?? routing,
23239
23412
  async start(providedTransport) {
23240
23413
  if (started) return;
23241
23414
  if (closePromise) throw new Error("The Nudge MCP adapter is closing.");
@@ -23246,7 +23419,7 @@ async function createDiscoveredAgentAdapter(options) {
23246
23419
  close() {
23247
23420
  if (closePromise) return closePromise;
23248
23421
  closePromise = (async () => {
23249
- await router.close();
23422
+ await (router ?? routing).close();
23250
23423
  if (transport) await mcpServer.close().catch(() => void 0);
23251
23424
  })();
23252
23425
  return closePromise;
@@ -23309,7 +23482,7 @@ function parseCliArguments(argv = process.argv.slice(2), environment = process.e
23309
23482
  let host = environment.NUDGE_UI_BRIDGE_HOST ?? "127.0.0.1";
23310
23483
  let port = environment.NUDGE_UI_BRIDGE_PORT === void 0 ? DEFAULT_PORT : parseInteger(environment.NUDGE_UI_BRIDGE_PORT, "NUDGE_UI_BRIDGE_PORT", 65535);
23311
23484
  let commandTimeoutMs = environment.NUDGE_UI_COMMAND_TIMEOUT_MS === void 0 ? DEFAULT_COMMAND_TIMEOUT_MS2 : parseInteger(environment.NUDGE_UI_COMMAND_TIMEOUT_MS, "NUDGE_UI_COMMAND_TIMEOUT_MS", 6e4);
23312
- let workspaceRoot = root;
23485
+ let workspaceRoot = environment.NUDGE_UI_WORKSPACE_ROOT === void 0 ? void 0 : root;
23313
23486
  let help = false;
23314
23487
  let explicitPort = environment.NUDGE_UI_BRIDGE_PORT !== void 0;
23315
23488
  for (let index = 0; index < argumentsToParse.length; index += 1) {
@@ -23366,7 +23539,7 @@ running project unless --origin selects the legacy coupled mode.
23366
23539
  Options:
23367
23540
  --project-id <id> Stable project pairing identity
23368
23541
  --origin <origin> Exact app origin required for browser pairing
23369
- --workspace-root <path> Project root supplied to the agent
23542
+ --workspace-root <path> Optional legacy project restriction; omitted for reusable adapters
23370
23543
  --host <loopback> 127.0.0.1, ::1, or localhost
23371
23544
  --port <port> Bridge port; default is deterministic per project, 0 is ephemeral
23372
23545
  --command-timeout-ms <ms> Canvas acknowledgement timeout
@@ -23380,12 +23553,18 @@ async function runCli(argv = process.argv.slice(2), environment = process.env) {
23380
23553
  }
23381
23554
  if (options.command === "doctor") {
23382
23555
  const sessions = await discoverProjectSessions(options.workspaceRoot);
23383
- const matches = sessions.filter((session) => session.matchesApplication);
23556
+ const matches = sessions.filter((session) => options.workspaceRoot === void 0 || session.matchesApplication);
23557
+ const diagnostics = await diagnoseProjectSessions(options.workspaceRoot);
23384
23558
  process.stdout.write([
23385
- `[nudge-ui] workspace: ${options.workspaceRoot}`,
23559
+ `[nudge-ui] registry: ${diagnostics.registryRoot}`,
23560
+ `[nudge-ui] workspace: ${diagnostics.workspaceRoot ?? "all projects"}`,
23561
+ `[nudge-ui] application: ${diagnostics.applicationRoot ?? "all applications"}`,
23562
+ `[nudge-ui] descriptors: ${diagnostics.descriptorCount}; invalid: ${diagnostics.invalidCount}; incompatible: ${diagnostics.incompatibleCount}`,
23563
+ `[nudge-ui] matching unreachable descriptors: ${diagnostics.matchingUnreachableCount}`,
23564
+ ...diagnostics.guidance.map((message) => `[nudge-ui] ${message}`),
23386
23565
  `[nudge-ui] live sessions: ${sessions.length}`,
23387
23566
  `[nudge-ui] matching sessions: ${matches.length}`,
23388
- ...matches.length === 0 ? ["[nudge-ui] no matching project bridge is running; start the development server to make Nudge available"] : matches.map((session) => {
23567
+ ...matches.length === 0 ? ["[nudge-ui] no reachable compatible bridge matches; inspect the diagnostics above or start the development server"] : matches.map((session) => {
23389
23568
  const state = session.status.connection === "working" ? "working" : session.status.listenerActive ? "listening" : "available";
23390
23569
  return `[nudge-ui] ${state}: ${session.projectId} (${session.origin})${session.claimed ? " [claimed]" : ""}`;
23391
23570
  })
@@ -23419,8 +23598,10 @@ async function runCli(argv = process.argv.slice(2), environment = process.env) {
23419
23598
  }
23420
23599
  return;
23421
23600
  }
23601
+ process.stderr.write("[nudge-ui] --origin uses deprecated coupled mode. Rerun agent setup to use project-owned bridges and avoid port collisions.\n");
23422
23602
  const companion = createAgentCompanion({
23423
23603
  ...options,
23604
+ workspaceRoot: options.workspaceRoot ?? resolve4(environment.INIT_CWD ?? process.cwd()),
23424
23605
  onControllerUnavailable: (pageUrl) => openPairedPage(pageUrl).then(() => void 0)
23425
23606
  });
23426
23607
  let stopping = false;
@@ -1,13 +1,13 @@
1
1
  import type { AgentPromptRequest, AgentStatusSnapshot, AgentStatusUpdate, CanvasCommandResult } from "./internal/agent-protocol.js";
2
2
  import type { CanvasCommandInput } from "./bridge.js";
3
- import { type ProjectSession } from "./project.js";
3
+ import { type ProjectSession, type StoredProjectSession } from "./project.js";
4
4
  export interface DiscoveredProjectSession extends ProjectSession {
5
5
  readonly matchesWorkspace: boolean;
6
6
  readonly matchesApplication: boolean;
7
7
  readonly claimed: boolean;
8
8
  readonly status: Pick<AgentStatusSnapshot, "connection" | "listenerActive" | "paired">;
9
9
  }
10
- export declare function discoverProjectSessions(applicationRoot: string, registryRoot?: string): Promise<DiscoveredProjectSession[]>;
10
+ export declare function discoverProjectSessions(applicationRoot: string | undefined, registryRoot?: string): Promise<DiscoveredProjectSession[]>;
11
11
  /** Routes MCP tools to a live project in the adapter's exact worktree. */
12
12
  export declare class DiscoveredProjectRouter {
13
13
  readonly workspaceRoot: string;
@@ -16,10 +16,14 @@ export declare class DiscoveredProjectRouter {
16
16
  readonly agentId: string;
17
17
  private client;
18
18
  private releaseGeneration;
19
+ private connectionQueue;
19
20
  private constructor();
21
+ get selectedSessionId(): string | undefined;
22
+ static forSession(session: StoredProjectSession, registryRoot: string): DiscoveredProjectRouter;
20
23
  static create(workspaceRoot: string, registryRoot?: string): Promise<DiscoveredProjectRouter>;
21
24
  listSessions(): Promise<DiscoveredProjectSession[]>;
22
25
  private connectToSession;
26
+ private selectSession;
23
27
  waitForPrompt(signal?: AbortSignal, sessionId?: string): Promise<AgentPromptRequest>;
24
28
  getStatus(): Promise<AgentStatusSnapshot>;
25
29
  updateRequestStatus(update: AgentStatusUpdate): Promise<AgentStatusSnapshot>;
@@ -27,3 +31,16 @@ export declare class DiscoveredProjectRouter {
27
31
  release(): Promise<void>;
28
32
  close(): Promise<void>;
29
33
  }
34
+ /** Returns diagnostic counts, scope, and public paths without exposing registry credentials. */
35
+ export declare function diagnoseProjectSessions(applicationRoot?: string, registryRoot?: string): Promise<{
36
+ registryRoot: string;
37
+ workspaceRoot: string | undefined;
38
+ applicationRoot: string | undefined;
39
+ descriptorCount: number;
40
+ invalidCount: number;
41
+ incompatibleCount: number;
42
+ reachableCount: number;
43
+ matchingReachableCount: number;
44
+ matchingUnreachableCount: number;
45
+ guidance: string[];
46
+ }>;