@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 +138 -53
- package/dist/adapterRouting.d.ts +16 -0
- package/dist/adapterRouting.js +59 -0
- package/dist/bridge.d.ts +4 -2
- package/dist/bridge.js +255 -16
- package/dist/cli.d.ts +3 -2
- package/dist/cli.js +75 -14
- package/dist/cli.mjs +15386 -14423
- package/dist/discovery.d.ts +46 -0
- package/dist/discovery.js +160 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/internal/agent-protocol.d.ts +65 -1
- package/dist/internal/agent-protocol.js +183 -3
- package/dist/project.d.ts +54 -0
- package/dist/project.js +151 -0
- package/dist/projectSessionClient.d.ts +33 -0
- package/dist/projectSessionClient.js +103 -0
- package/dist/protocol.d.ts +14 -2
- package/dist/protocol.js +13 -1
- package/dist/server.d.ts +15 -0
- package/dist/server.js +142 -34
- package/dist/sessionRegistry.d.ts +17 -0
- package/dist/sessionRegistry.js +121 -0
- package/dist/sessionSelection.d.ts +15 -0
- package/dist/sessionSelection.js +41 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -1,62 +1,154 @@
|
|
|
1
1
|
# `@nudge-ui/mcp`
|
|
2
2
|
|
|
3
|
-
> **Early alpha:** This integration
|
|
4
|
-
>
|
|
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
|
|
8
|
-
agent.
|
|
9
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
32
|
+
npx nudge-ui agent setup --mcp-package /absolute/path/to/nudge-ui-mcp.tgz
|
|
15
33
|
```
|
|
16
34
|
|
|
17
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
the
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
`
|
|
52
|
-
available
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
79
|
-
through
|
|
80
|
-
|
|
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 {
|
|
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<
|
|
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. */
|