@nudge-ui/mcp 0.2.0 → 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/README.md CHANGED
@@ -1,62 +1,154 @@
1
1
  # `@nudge-ui/mcp`
2
2
 
3
- > **Early alpha:** This integration is still under heavy testing. Expect
4
- > breaking changes, incomplete host compatibility, and other rough edges. Do
5
- > not rely on it for production workflows yet.
3
+ > **Early alpha:** This integration can make breaking changes while host
4
+ > support and the setup workflow stabilize.
6
5
 
7
- `@nudge-ui/mcp` connects a local Nudge UI inspector to an MCP-capable coding
8
- agent. It combines a standard MCP stdio server with a project-scoped loopback
9
- browser bridge. The browser does not speak MCP directly.
6
+ `@nudge-ui/mcp` connects a local Nudge inspector to an MCP-capable coding
7
+ agent. The development server owns the browser bridge. The agent host starts a
8
+ small stdio adapter installed independently of any checkout. Each tool call supplies
9
+ the checkout or application being edited.
10
10
 
11
- ## Install
11
+ ## Configure an agent
12
+
13
+ Run `npx nudge-ui agent setup` from an application, or
14
+ `npm create nudge-ui@latest -- --agent-only`. Setup installs:
15
+
16
+ - A project-local `@nudge-ui/mcp` dependency for the development server's bridge.
17
+ - A versioned adapter under `~/.nudge-ui/adapters/<initializer-version>/` using npm.
18
+
19
+ Setup registers the adapter globally with an absolute Node executable and adapter
20
+ entry path. The entry contains no application path, origin, or port. npm must be
21
+ available even when the application uses pnpm, Yarn, or Bun. The adapter version
22
+ comes from the initializer; the bridge version follows the application package.
23
+
24
+ Before changing agent settings, setup starts a fresh MCP client, completes
25
+ initialization, and checks that the tools support connection-time selection.
26
+ This verifies the executable, not whether an already running agent host has
27
+ reloaded it. Fully quit and restart the host after migration.
28
+
29
+ When testing a local `@nudge-ui/mcp` tarball, pass the same artifact to setup:
12
30
 
13
31
  ```sh
14
- pnpm add -D @nudge-ui/mcp@0.2.0
32
+ npx nudge-ui agent setup --mcp-package /absolute/path/to/nudge-ui-mcp.tgz
15
33
  ```
16
34
 
17
- ### Configure automatically
35
+ This installs the local build for both the application's project bridge and the
36
+ reusable adapter. Without the override, the adapter is intentionally resolved
37
+ from the published registry and may not contain local changes.
38
+
39
+ Setup backs up existing `nudge_ui` settings, replaces the global entry, and removes
40
+ the current project's overriding entry. Other agent settings are preserved.
41
+ Run setup once in other projects that still have legacy overrides; setup does
42
+ not scan unrelated repositories. New checkouts need the bridge dependency and
43
+ framework integration, but no project-specific MCP registration.
18
44
 
19
- From the application project root, use [`add-mcp`](https://github.com/neon-solutions/add-mcp) to detect supported coding agents and write each host's native configuration format:
45
+ To configure a host manually, use the Node executable and managed adapter entry
46
+ printed by setup, with no `--workspace-root` argument. A bare `nudge-mcp` command
47
+ also works if that executable is installed independently and available on the
48
+ host's PATH.
49
+
50
+ Run diagnostics from an application:
20
51
 
21
52
  ```sh
22
- npx add-mcp \
23
- 'npx -y @nudge-ui/mcp@0.2.0 --project-id my-app --origin http://localhost:5173 --workspace-root .' \
24
- --name nudge_ui
53
+ npx nudge-ui agent doctor
25
54
  ```
26
55
 
27
- The command prompts for detected project agents. Use `-a codex` (or another
28
- supported agent name) to target one host, or `-y` to skip the prompt and use
29
- the detected project agents. Keep this project-scoped; do not use `-g` unless
30
- the configuration intentionally targets one fixed project.
56
+ The doctor distinguishes descriptor, reachable, matching-unreachable, invalid,
57
+ and incompatible-protocol counts. Failed loopback probes do not prove that a
58
+ bridge never registered: a sandbox can block local networking. Retry diagnostics
59
+ outside the command sandbox when appropriate. Credentials are never printed.
60
+ The output includes the resolved registry path and the workspace/application
61
+ scope, even when the registry has no sessions.
62
+ The reusable MCP adapter exposes the same diagnostics through `nudge_diagnose`.
63
+
64
+ ## Project integration
65
+
66
+ Framework adapters start and stop the project bridge with the development
67
+ server:
31
68
 
32
- `--origin` must exactly match the development application's browser origin.
33
- The `--project-id` value must match the ID used by the Nudge host integration.
34
- For standard Vite and Astro projects, `my-app` normally matches the project
35
- directory name. Next.js and standalone HTML projects use host-specific IDs, so
36
- retain the explicit ID from their integration configuration.
69
+ ```ts
70
+ import { startProjectBridge } from "@nudge-ui/mcp/project";
37
71
 
38
- The generated command pins the published `@nudge-ui/mcp@0.2.0` package through
39
- `npx` for reproducible tool versions. Update the version deliberately when
40
- upgrading the MCP integration. After configuration, restart or reload the
41
- agent host so it refreshes its MCP tool catalog.
72
+ const runtime = await startProjectBridge({
73
+ workspaceRoot: process.cwd(),
74
+ appRoot: process.cwd(),
75
+ projectId: "my-app",
76
+ origin: "http://localhost:5173",
77
+ allowedOrigins: ["http://127.0.0.1:5173"],
78
+ });
42
79
 
43
- The package has no install-time project mutation. The manual configuration
44
- below remains useful when an agent host is not supported by `add-mcp` or when
45
- you need an explicit custom command.
80
+ // Give browser code only these non-secret values.
81
+ console.log(runtime.browser.bridgeUrl, runtime.browser.projectId);
46
82
 
47
- After adding or changing the host configuration, restart or reload the MCP
48
- server, or start a fresh agent task. Open **Connect MCP** from the inspector's
49
- prompt menu for setup instructions, connection status, and explicit pairing.
50
- You can pair a page before the agent starts listening. Ask the agent to call
51
- `nudge_listen` and keep it active to enable sending. **Copy prompt** remains
52
- available while the agent is idle; prompts are not queued.
83
+ await runtime.close();
84
+ ```
85
+
86
+ `origin` must be the actual canonical development origin after the framework
87
+ chooses its port. `workspaceRoot` defaults to the Git worktree root when one is
88
+ available and otherwise defaults to `appRoot`. `appRoot` distinguishes several
89
+ applications running from one monorepo.
90
+
91
+ Each live bridge registers a random session ID, canonical workspace and app
92
+ paths, optional Git common directory and branch metadata, origin, and loopback
93
+ endpoint. The descriptor is mode `0600` inside a mode `0700` registry. It also
94
+ contains a private random credential used only between the bridge and adapter.
95
+
96
+ The adapter lists live local sessions through `nudge_list_sessions`. Supply
97
+ `workspaceRoot` on every tool call, including status, Canvas, and release. Use
98
+ the absolute checkout root or application directory being edited. The adapter
99
+ does not infer a task's project from its process working directory.
100
+
101
+ For example, the first discovery call is:
102
+
103
+ ```json
104
+ {
105
+ "name": "nudge_list_sessions",
106
+ "arguments": {
107
+ "workspaceRoot": "/path/to/nudge-examples"
108
+ }
109
+ }
110
+ ```
53
111
 
54
- For Codex desktop, confirm that `nudge_ui` appears in the current task after
55
- the MCP restart. For Codex CLI, `codex mcp list` verifies the project entry.
56
- Restart a long-running OpenCode service after adding or changing its MCP
57
- configuration so it refreshes its tool catalog.
112
+ Selection is automatic only when the supplied scope contains one live session.
113
+ If several applications match, also supply the chosen `sessionId` and repeat
114
+ both arguments on subsequent calls. A workspace mismatch never falls back to
115
+ the only session elsewhere. Independent selections can run concurrently within
116
+ one MCP connection. Each selection stays bound to its application through
117
+ restarts; old requests are not replayed. An explicit stale session ID requires
118
+ rediscovery before listening again.
58
119
 
59
- ## Run manually
120
+ Existing configurations with `--workspace-root` remain restricted to that scope
121
+ until migrated. Branch names and shared Git metadata never authorize selecting
122
+ another checkout.
123
+
124
+ ## OpenCode configuration
125
+
126
+ The setup integration delegates OpenCode file handling to `add-mcp`. It
127
+ preserves the documented V2 shape, `mcp.servers.<name>`, and also preserves the
128
+ legacy `mcp.<name>` shape when that is what an existing file uses. Setup tests
129
+ both layouts and migrates only the Nudge entry.
130
+
131
+ OpenCode's CLI does not currently provide a Nudge-specific removal command. To
132
+ remove only Nudge's registration, use `add-mcp` and select the scope explicitly:
133
+
134
+ ```sh
135
+ npx add-mcp remove nudge_ui --global --agent opencode --yes
136
+ npx add-mcp remove nudge_ui --agent opencode --yes
137
+ ```
138
+
139
+ Run the first command for the global registration and the second for a project
140
+ override. These commands leave other MCP servers in place.
141
+
142
+ One adapter owns a selected project session until it closes. Other adapters
143
+ receive a claimed-session error instead of taking browser work. A heartbeat
144
+ allows the bridge to recover a claim after an adapter process is killed. Call
145
+ `nudge_release` when the user stops listening so another adapter can claim the
146
+ session without restarting the MCP host.
147
+
148
+ ## Legacy coupled mode
149
+
150
+ Passing `--origin` retains the previous behavior in which the MCP process also
151
+ owns its browser bridge:
60
152
 
61
153
  ```sh
62
154
  pnpm exec nudge-mcp \
@@ -65,17 +157,10 @@ pnpm exec nudge-mcp \
65
157
  --workspace-root /path/to/my-app
66
158
  ```
67
159
 
68
- Pass the exact application origin with `--origin` (or
69
- `NUDGE_UI_ORIGIN`). The bridge rejects browser origins that are not explicitly
70
- configured; it never trusts the first origin that connects. The bridge chooses
71
- a deterministic loopback port from the project ID so the inspector can find
72
- it without a port file. Use `--port 0` for an ephemeral test port.
73
-
74
- For integrations that establish the browser origin out of band, trusted host
75
- code can use the `pairBrowser` API as an explicit manual approval path. An
76
- unconfigured bridge does not accept HTTP pairing requests.
160
+ This deprecated mode prints a warning and remains available for custom
161
+ integrations during migration. New framework integrations should use the
162
+ project-owned bridge.
77
163
 
78
- The agent should call `nudge_listen` immediately, apply the delivered prompt
79
- through its normal source-editing workflow, call `nudge_report_status`, and
80
- listen again. Canvas tools are limited to same-origin routes and agent-owned
81
- groups.
164
+ The agent calls `nudge_listen`, applies the delivered prompt, reports the
165
+ result through `nudge_report_status`, and listens again. Canvas tools remain
166
+ limited to routes from the paired application origin and agent-owned groups.
@@ -0,0 +1,16 @@
1
+ import { DiscoveredProjectRouter } from "./discovery.js";
2
+ export interface ProjectTarget {
3
+ readonly workspaceRoot?: string;
4
+ readonly sessionId?: string;
5
+ }
6
+ /** Keeps each explicit project selection independent for the lifetime of an MCP connection. */
7
+ export declare class AdapterRouting {
8
+ readonly registryRoot: string;
9
+ private closing;
10
+ private readonly selections;
11
+ private readonly applications;
12
+ constructor(registryRoot?: string);
13
+ target({ workspaceRoot, sessionId }: ProjectTarget): Promise<DiscoveredProjectRouter>;
14
+ private select;
15
+ close(): Promise<void>;
16
+ }
@@ -0,0 +1,59 @@
1
+ import { realpath } from "node:fs/promises";
2
+ import { isAbsolute } from "node:path";
3
+ import { DiscoveredProjectRouter } from "./discovery.js";
4
+ import { defaultSessionRegistryRoot, resolveWorkspaceRoot } from "./project.js";
5
+ import { readLiveProjectSessions } from "./sessionRegistry.js";
6
+ import { selectProjectSession } from "./sessionSelection.js";
7
+ /** Keeps each explicit project selection independent for the lifetime of an MCP connection. */
8
+ export class AdapterRouting {
9
+ registryRoot;
10
+ closing = false;
11
+ selections = new Map();
12
+ applications = new Map();
13
+ constructor(registryRoot = defaultSessionRegistryRoot()) {
14
+ this.registryRoot = registryRoot;
15
+ }
16
+ async target({ workspaceRoot, sessionId }) {
17
+ if (!workspaceRoot || !isAbsolute(workspaceRoot)) {
18
+ throw new Error("Supply workspaceRoot as the absolute path of the checkout or application you are editing.");
19
+ }
20
+ const applicationRoot = await realpath(workspaceRoot);
21
+ if (this.closing)
22
+ throw new Error("The MCP adapter is closing.");
23
+ const key = JSON.stringify([applicationRoot, sessionId]);
24
+ let selection = this.selections.get(key);
25
+ if (!selection) {
26
+ selection = this.select(applicationRoot, sessionId);
27
+ this.selections.set(key, selection);
28
+ void selection.catch(() => {
29
+ if (this.selections.get(key) === selection)
30
+ this.selections.delete(key);
31
+ });
32
+ }
33
+ return await selection;
34
+ }
35
+ async select(applicationRoot, sessionId) {
36
+ const workspaceRoot = await resolveWorkspaceRoot(applicationRoot);
37
+ const live = await readLiveProjectSessions(this.registryRoot);
38
+ if (this.closing)
39
+ throw new Error("The MCP adapter is closing.");
40
+ const selected = selectProjectSession(live.map(({ descriptor }) => descriptor), {
41
+ workspaceRoot, applicationRoot, requestedSessionId: sessionId,
42
+ });
43
+ const key = selected.sessionId;
44
+ let router = this.applications.get(key)
45
+ ?? [...this.applications.values()].find((candidate) => candidate.selectedSessionId === selected.sessionId);
46
+ if (!router) {
47
+ router = DiscoveredProjectRouter.forSession(selected, this.registryRoot);
48
+ this.applications.set(key, router);
49
+ }
50
+ return router;
51
+ }
52
+ async close() {
53
+ this.closing = true;
54
+ await Promise.allSettled(this.selections.values());
55
+ await Promise.all([...this.applications.values()].map((router) => router.close()));
56
+ this.selections.clear();
57
+ this.applications.clear();
58
+ }
59
+ }
package/dist/bridge.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Server } from "node:http";
2
- import type { AgentPromptRequest, AgentRequestOutcome, AgentStatusSnapshot, AgentStatusUpdate, CanvasCommandResult, PairingResponse } from "./internal/agent-protocol.js";
2
+ import type { AgentDeliveredPrompt, AgentRequestOutcome, AgentStatusSnapshot, AgentStatusUpdate, CanvasCommandResult, PairingResponse } from "./internal/agent-protocol.js";
3
3
  /** A clock port keeps command timeout tests deterministic. */
4
4
  export interface BridgeClock {
5
5
  now(): number;
@@ -22,6 +22,8 @@ export interface BrowserBridgeOptions {
22
22
  readonly tokenFactory?: () => string;
23
23
  /** Test seam; production defaults to random UUIDs. */
24
24
  readonly idFactory?: () => string;
25
+ /** Private credential for a separately running MCP adapter. */
26
+ readonly agentControlToken?: string;
25
27
  /**
26
28
  * Best-effort hook used by the CLI to reopen the last paired page when an
27
29
  * agent rearms listening but no browser controller stream is attached.
@@ -49,7 +51,7 @@ export interface BrowserBridge {
49
51
  start(): Promise<BridgeAddress>;
50
52
  close(): Promise<void>;
51
53
  /** Blocks until a paired browser dispatches a prompt. */
52
- waitForPrompt(signal?: AbortSignal): Promise<AgentPromptRequest>;
54
+ waitForPrompt(signal?: AbortSignal): Promise<AgentDeliveredPrompt>;
53
55
  /** Marks the agent-side listener as stopped without ending the project. */
54
56
  cancelListener(reason?: string): void;
55
57
  /** Reports the terminal result for the currently running browser request. */