@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/README.md +91 -24
- package/dist/adapterRouting.d.ts +16 -0
- package/dist/adapterRouting.js +59 -0
- package/dist/bridge.js +18 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +14 -6
- package/dist/cli.mjs +236 -55
- package/dist/discovery.d.ts +19 -2
- package/dist/discovery.js +55 -7
- package/dist/projectSessionClient.d.ts +5 -0
- package/dist/projectSessionClient.js +12 -2
- package/dist/protocol.d.ts +1 -0
- package/dist/protocol.js +1 -0
- package/dist/server.d.ts +3 -2
- package/dist/server.js +69 -29
- package/dist/sessionRegistry.d.ts +10 -1
- package/dist/sessionRegistry.js +48 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,33 +5,61 @@
|
|
|
5
5
|
|
|
6
6
|
`@nudge-ui/mcp` connects a local Nudge inspector to an MCP-capable coding
|
|
7
7
|
agent. The development server owns the browser bridge. The agent host starts a
|
|
8
|
-
small stdio adapter
|
|
9
|
-
|
|
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
12
|
|
|
13
|
-
|
|
14
|
-
|
|
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:
|
|
15
30
|
|
|
16
31
|
```sh
|
|
17
|
-
|
|
32
|
+
npx nudge-ui agent setup --mcp-package /absolute/path/to/nudge-ui-mcp.tgz
|
|
18
33
|
```
|
|
19
34
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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.
|
|
26
44
|
|
|
27
|
-
|
|
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:
|
|
28
51
|
|
|
29
52
|
```sh
|
|
30
|
-
|
|
53
|
+
npx nudge-ui agent doctor
|
|
31
54
|
```
|
|
32
55
|
|
|
33
|
-
The doctor
|
|
34
|
-
|
|
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`.
|
|
35
63
|
|
|
36
64
|
## Project integration
|
|
37
65
|
|
|
@@ -65,13 +93,51 @@ paths, optional Git common directory and branch metadata, origin, and loopback
|
|
|
65
93
|
endpoint. The descriptor is mode `0600` inside a mode `0700` registry. It also
|
|
66
94
|
contains a private random credential used only between the bridge and adapter.
|
|
67
95
|
|
|
68
|
-
The adapter lists
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
+
```
|
|
111
|
+
|
|
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.
|
|
119
|
+
|
|
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.
|
|
75
141
|
|
|
76
142
|
One adapter owns a selected project session until it closes. Other adapters
|
|
77
143
|
receive a claimed-session error instead of taking browser work. A heartbeat
|
|
@@ -91,8 +157,9 @@ pnpm exec nudge-mcp \
|
|
|
91
157
|
--workspace-root /path/to/my-app
|
|
92
158
|
```
|
|
93
159
|
|
|
94
|
-
This mode remains available for custom
|
|
95
|
-
framework integrations should use the
|
|
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.
|
|
96
163
|
|
|
97
164
|
The agent calls `nudge_listen`, applies the delivered prompt, reports the
|
|
98
165
|
result through `nudge_report_status`, and listens again. Canvas tools remain
|
|
@@ -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.js
CHANGED
|
@@ -310,7 +310,7 @@ export function createLoopbackBridge(options) {
|
|
|
310
310
|
pageUrl: lastPageUrl,
|
|
311
311
|
}, incomingOrigin);
|
|
312
312
|
};
|
|
313
|
-
const
|
|
313
|
+
const readPairRequest = async (request) => {
|
|
314
314
|
if (!loopbackAddress(request.socket.remoteAddress)) {
|
|
315
315
|
throw new BridgeRequestError(403, "loopback_only", "The browser bridge accepts loopback connections only.");
|
|
316
316
|
}
|
|
@@ -332,6 +332,10 @@ export function createLoopbackBridge(options) {
|
|
|
332
332
|
}
|
|
333
333
|
requestedPageUrl = new URL(body.pageUrl, requestedOrigin).href;
|
|
334
334
|
}
|
|
335
|
+
return { body, incomingOrigin, requestedPageUrl };
|
|
336
|
+
};
|
|
337
|
+
const handlePair = async (request, response) => {
|
|
338
|
+
const { body, incomingOrigin, requestedPageUrl } = await readPairRequest(request);
|
|
335
339
|
if (sessionToken) {
|
|
336
340
|
if (body.sessionToken !== sessionToken) {
|
|
337
341
|
throw new BridgeRequestError(409, "already_paired", "A browser is already paired with this project.");
|
|
@@ -367,6 +371,15 @@ export function createLoopbackBridge(options) {
|
|
|
367
371
|
sendJson(response, 200, result, incomingOrigin);
|
|
368
372
|
broadcastStatus();
|
|
369
373
|
};
|
|
374
|
+
const handleTakeover = async (request, response) => {
|
|
375
|
+
const { incomingOrigin, requestedPageUrl } = await readPairRequest(request);
|
|
376
|
+
// Takeover is intentionally separate from normal pairing. The caller has
|
|
377
|
+
// made an explicit UI choice to revoke the existing browser session.
|
|
378
|
+
if (sessionToken)
|
|
379
|
+
disconnectBrowser(sessionToken);
|
|
380
|
+
const pairing = pairBrowser(options.projectId, incomingOrigin, requestedPageUrl);
|
|
381
|
+
sendJson(response, 200, pairing, incomingOrigin);
|
|
382
|
+
};
|
|
370
383
|
const handleEvents = (request, response, url) => {
|
|
371
384
|
parseProjectId(url.searchParams.get("projectId"), options.projectId);
|
|
372
385
|
const incomingOrigin = requireBrowserRequest(request, url);
|
|
@@ -661,6 +674,10 @@ export function createLoopbackBridge(options) {
|
|
|
661
674
|
await handlePair(request, response);
|
|
662
675
|
return;
|
|
663
676
|
}
|
|
677
|
+
if (request.method === "POST" && url.pathname === BRIDGE_ENDPOINTS.takeover) {
|
|
678
|
+
await handleTakeover(request, response);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
664
681
|
if (request.method === "GET" && url.pathname === BRIDGE_ENDPOINTS.events) {
|
|
665
682
|
handleEvents(request, response, url);
|
|
666
683
|
return;
|
package/dist/cli.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export interface CliOptions {
|
|
|
3
3
|
readonly command: "serve" | "doctor";
|
|
4
4
|
readonly projectId: string;
|
|
5
5
|
readonly origin?: string;
|
|
6
|
-
readonly workspaceRoot
|
|
6
|
+
readonly workspaceRoot?: string;
|
|
7
7
|
readonly host: "127.0.0.1" | "::1" | "localhost";
|
|
8
8
|
readonly port: number;
|
|
9
9
|
readonly commandTimeoutMs: number;
|
|
@@ -32,6 +32,6 @@ export interface PageLaunchDependencies {
|
|
|
32
32
|
export declare function openPairedPage(pageUrl: string, dependencies?: PageLaunchDependencies): Promise<boolean>;
|
|
33
33
|
/** Parses only project-scoped, host-neutral CLI options. */
|
|
34
34
|
export declare function parseCliArguments(argv?: readonly string[], environment?: CliEnvironment): CliOptions;
|
|
35
|
-
export declare const CLI_USAGE = "Usage: nudge-mcp [doctor] [options]\n\nStarts a standard MCP stdio adapter. It discovers the bridge owned by the\nrunning project unless --origin selects the legacy coupled mode.\n\nOptions:\n --project-id <id> Stable project pairing identity\n --origin <origin> Exact app origin required for browser pairing\n --workspace-root <path>
|
|
35
|
+
export declare const CLI_USAGE = "Usage: nudge-mcp [doctor] [options]\n\nStarts a standard MCP stdio adapter. It discovers the bridge owned by the\nrunning project unless --origin selects the legacy coupled mode.\n\nOptions:\n --project-id <id> Stable project pairing identity\n --origin <origin> Exact app origin required for browser pairing\n --workspace-root <path> Optional legacy project restriction; omitted for reusable adapters\n --host <loopback> 127.0.0.1, ::1, or localhost\n --port <port> Bridge port; default is deterministic per project, 0 is ephemeral\n --command-timeout-ms <ms> Canvas acknowledgement timeout\n --help Show this message\n";
|
|
36
36
|
/** Runs the CLI until the stdio MCP connection closes or the process stops. */
|
|
37
37
|
export declare function runCli(argv?: readonly string[], environment?: CliEnvironment): Promise<void>;
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { basename, resolve } from "node:path";
|
|
4
4
|
import { defaultBridgePort } from "./internal/agent-protocol.js";
|
|
5
|
-
import { discoverProjectSessions } from "./discovery.js";
|
|
5
|
+
import { diagnoseProjectSessions, discoverProjectSessions } from "./discovery.js";
|
|
6
6
|
import { createAgentCompanion, createDiscoveredAgentAdapter, } from "./server.js";
|
|
7
7
|
const DEFAULT_PORT = 0;
|
|
8
8
|
const DEFAULT_COMMAND_TIMEOUT_MS = 5_000;
|
|
@@ -73,7 +73,7 @@ export function parseCliArguments(argv = process.argv.slice(2), environment = pr
|
|
|
73
73
|
let commandTimeoutMs = environment.NUDGE_UI_COMMAND_TIMEOUT_MS === undefined
|
|
74
74
|
? DEFAULT_COMMAND_TIMEOUT_MS
|
|
75
75
|
: parseInteger(environment.NUDGE_UI_COMMAND_TIMEOUT_MS, "NUDGE_UI_COMMAND_TIMEOUT_MS", 60_000);
|
|
76
|
-
let workspaceRoot = root;
|
|
76
|
+
let workspaceRoot = environment.NUDGE_UI_WORKSPACE_ROOT === undefined ? undefined : root;
|
|
77
77
|
let help = false;
|
|
78
78
|
let explicitPort = environment.NUDGE_UI_BRIDGE_PORT !== undefined;
|
|
79
79
|
for (let index = 0; index < argumentsToParse.length; index += 1) {
|
|
@@ -133,7 +133,7 @@ running project unless --origin selects the legacy coupled mode.
|
|
|
133
133
|
Options:
|
|
134
134
|
--project-id <id> Stable project pairing identity
|
|
135
135
|
--origin <origin> Exact app origin required for browser pairing
|
|
136
|
-
--workspace-root <path>
|
|
136
|
+
--workspace-root <path> Optional legacy project restriction; omitted for reusable adapters
|
|
137
137
|
--host <loopback> 127.0.0.1, ::1, or localhost
|
|
138
138
|
--port <port> Bridge port; default is deterministic per project, 0 is ephemeral
|
|
139
139
|
--command-timeout-ms <ms> Canvas acknowledgement timeout
|
|
@@ -148,13 +148,19 @@ export async function runCli(argv = process.argv.slice(2), environment = process
|
|
|
148
148
|
}
|
|
149
149
|
if (options.command === "doctor") {
|
|
150
150
|
const sessions = await discoverProjectSessions(options.workspaceRoot);
|
|
151
|
-
const matches = sessions.filter((session) => session.matchesApplication);
|
|
151
|
+
const matches = sessions.filter((session) => options.workspaceRoot === undefined || session.matchesApplication);
|
|
152
|
+
const diagnostics = await diagnoseProjectSessions(options.workspaceRoot);
|
|
152
153
|
process.stdout.write([
|
|
153
|
-
`[nudge-ui]
|
|
154
|
+
`[nudge-ui] registry: ${diagnostics.registryRoot}`,
|
|
155
|
+
`[nudge-ui] workspace: ${diagnostics.workspaceRoot ?? "all projects"}`,
|
|
156
|
+
`[nudge-ui] application: ${diagnostics.applicationRoot ?? "all applications"}`,
|
|
157
|
+
`[nudge-ui] descriptors: ${diagnostics.descriptorCount}; invalid: ${diagnostics.invalidCount}; incompatible: ${diagnostics.incompatibleCount}`,
|
|
158
|
+
`[nudge-ui] matching unreachable descriptors: ${diagnostics.matchingUnreachableCount}`,
|
|
159
|
+
...diagnostics.guidance.map((message) => `[nudge-ui] ${message}`),
|
|
154
160
|
`[nudge-ui] live sessions: ${sessions.length}`,
|
|
155
161
|
`[nudge-ui] matching sessions: ${matches.length}`,
|
|
156
162
|
...(matches.length === 0
|
|
157
|
-
? ["[nudge-ui] no
|
|
163
|
+
? ["[nudge-ui] no reachable compatible bridge matches; inspect the diagnostics above or start the development server"]
|
|
158
164
|
: matches.map((session) => {
|
|
159
165
|
const state = session.status.connection === "working"
|
|
160
166
|
? "working"
|
|
@@ -195,8 +201,10 @@ export async function runCli(argv = process.argv.slice(2), environment = process
|
|
|
195
201
|
}
|
|
196
202
|
return;
|
|
197
203
|
}
|
|
204
|
+
process.stderr.write("[nudge-ui] --origin uses deprecated coupled mode. Rerun agent setup to use project-owned bridges and avoid port collisions.\n");
|
|
198
205
|
const companion = createAgentCompanion({
|
|
199
206
|
...options,
|
|
207
|
+
workspaceRoot: options.workspaceRoot ?? resolve(environment.INIT_CWD ?? process.cwd()),
|
|
200
208
|
onControllerUnavailable: (pageUrl) => openPairedPage(pageUrl).then(() => undefined),
|
|
201
209
|
});
|
|
202
210
|
let stopping = false;
|