@nudge-ui/mcp 0.1.0

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.
@@ -0,0 +1,4 @@
1
+ export { createAgentCompanion, type AgentCompanion, type AgentCompanionOptions } from "./server.js";
2
+ export { createLoopbackBridge, type BrowserBridge, type BrowserBridgeOptions, type BridgeAddress, type BridgeClock, type CanvasCommandInput, } from "./bridge.js";
3
+ export { openPairedPage, parseCliArguments, runCli, type CliOptions, type PageLaunchDependencies, } from "./cli.js";
4
+ export * from "./protocol.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { createAgentCompanion } from "./server.js";
2
+ export { createLoopbackBridge, } from "./bridge.js";
3
+ export { openPairedPage, parseCliArguments, runCli, } from "./cli.js";
4
+ export * from "./protocol.js";
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Companion-facing aliases and transport envelopes.
3
+ *
4
+ * The domain contracts live in `@nudge-ui/agent-protocol`; this module keeps
5
+ * transport names discoverable for Node consumers while preserving a single
6
+ * protocol vocabulary for browser and agent adapters.
7
+ */
8
+ export { AGENT_PROTOCOL_LIMITS, AGENT_PROTOCOL_VERSION, CANVAS_COMMAND_TYPES, canonicalOrigin, defaultBridgePort, isAgentPromptRequest, isAgentStatusUpdate, isAllowedOrigin, isCanvasCommand, isCanvasGroup, isCanvasState, isCanonicalOrigin, isProjectIdentity, isSameOriginRoute, protocolError, validateRoutes, } from "@nudge-ui/agent-protocol";
9
+ export type { AgentConnectionState, AgentProjectIdentity, AgentPromptRequest, AgentRequestOutcome, AgentRequestStatus, AgentRoute, AgentStatusSnapshot, AgentStatusUpdate, BridgeCanvasCommandEvent, BridgeConnectedEvent, BridgeDisconnectedEvent, BridgeEnvelope, BridgeEvent, BridgeEventType, BridgeStatusEvent, CanvasCommand, CanvasCommandAcknowledgement, CanvasCommandError, CanvasCommandResult, CanvasGroup, CanvasGroupOwner, CanvasState, FitCanvasCommand, FocusCanvasGroupCommand, PairingRequest, PairingResponse, PresentRoutesCommand, PromptDispatchRequest, PromptDispatchResponse, ReadCanvasStateCommand, RemoveCanvasGroupCommand, StatusEventRequest, } from "@nudge-ui/agent-protocol";
10
+ /** Stable endpoint names used by the loopback browser bridge. */
11
+ export declare const BRIDGE_ENDPOINTS: {
12
+ readonly health: "/health";
13
+ readonly pair: "/pair";
14
+ readonly events: "/events";
15
+ readonly prompt: "/prompt";
16
+ readonly status: "/status";
17
+ readonly canvasAcknowledgement: "/canvas/ack";
18
+ readonly disconnect: "/disconnect";
19
+ };
20
+ export type BridgeEndpoint = (typeof BRIDGE_ENDPOINTS)[keyof typeof BRIDGE_ENDPOINTS];
21
+ /** Error body returned by every rejected bridge request. */
22
+ export interface BridgeHttpError {
23
+ readonly error: {
24
+ readonly code: string;
25
+ readonly message: string;
26
+ };
27
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Companion-facing aliases and transport envelopes.
3
+ *
4
+ * The domain contracts live in `@nudge-ui/agent-protocol`; this module keeps
5
+ * transport names discoverable for Node consumers while preserving a single
6
+ * protocol vocabulary for browser and agent adapters.
7
+ */
8
+ export { AGENT_PROTOCOL_LIMITS, AGENT_PROTOCOL_VERSION, CANVAS_COMMAND_TYPES, canonicalOrigin, defaultBridgePort, isAgentPromptRequest, isAgentStatusUpdate, isAllowedOrigin, isCanvasCommand, isCanvasGroup, isCanvasState, isCanonicalOrigin, isProjectIdentity, isSameOriginRoute, protocolError, validateRoutes, } from "@nudge-ui/agent-protocol";
9
+ /** Stable endpoint names used by the loopback browser bridge. */
10
+ export const BRIDGE_ENDPOINTS = {
11
+ health: "/health",
12
+ pair: "/pair",
13
+ events: "/events",
14
+ prompt: "/prompt",
15
+ status: "/status",
16
+ canvasAcknowledgement: "/canvas/ack",
17
+ disconnect: "/disconnect",
18
+ };
@@ -0,0 +1,28 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
3
+ import type { AgentProjectIdentity } from "@nudge-ui/agent-protocol";
4
+ import { type BrowserBridge, type BrowserBridgeOptions, type BridgeAddress } from "./bridge.js";
5
+ export declare const MCP_SERVER_NAME = "nudge-ui";
6
+ export declare const MCP_SERVER_VERSION = "0.1.0";
7
+ /** Instructions are sent through MCP initialization for every host. */
8
+ export declare const MCP_SERVER_INSTRUCTIONS: string;
9
+ export interface AgentCompanionOptions extends BrowserBridgeOptions {
10
+ readonly workspaceRoot?: string;
11
+ readonly serverName?: string;
12
+ readonly serverVersion?: string;
13
+ }
14
+ export interface AgentCompanionStartResult {
15
+ readonly address: BridgeAddress;
16
+ }
17
+ export interface AgentCompanion {
18
+ readonly bridge: BrowserBridge;
19
+ readonly mcpServer: McpServer;
20
+ readonly project: AgentProjectIdentity;
21
+ start(transport?: Transport): Promise<AgentCompanionStartResult>;
22
+ close(): Promise<void>;
23
+ }
24
+ /**
25
+ * Creates one project-scoped MCP companion and its loopback browser bridge.
26
+ * The returned companion is inert until `start` is called.
27
+ */
28
+ export declare function createAgentCompanion(options: AgentCompanionOptions): AgentCompanion;
package/dist/server.js ADDED
@@ -0,0 +1,243 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+ import { validateRoutes } from "@nudge-ui/agent-protocol";
5
+ import { createLoopbackBridge, } from "./bridge.js";
6
+ export const MCP_SERVER_NAME = "nudge-ui";
7
+ export const MCP_SERVER_VERSION = "0.1.0";
8
+ /** Instructions are sent through MCP initialization for every host. */
9
+ export const MCP_SERVER_INSTRUCTIONS = [
10
+ "Nudge UI is a local browser companion for one project workspace.",
11
+ "Browser pairing requires the app origin to be configured explicitly with --origin or NUDGE_UI_ORIGIN; an unconfigured bridge accepts no HTTP pairing.",
12
+ "Call nudge_listen (or nudge_connect) immediately at task start and keep the call open while waiting for a browser request; the bridge may be running before a listener exists.",
13
+ "After a prompt is delivered, edit the project source using the host's normal approval flow.",
14
+ "Report completed, failed, or interrupted status with nudge_report_status before listening again.",
15
+ "Canvas tools operate only on same-origin routes from the paired project.",
16
+ "No request, prompt, or browser state is persisted by this companion.",
17
+ ].join(" ");
18
+ function textResult(value) {
19
+ return {
20
+ content: [{ type: "text", text: JSON.stringify(value) }],
21
+ };
22
+ }
23
+ function errorResult(error) {
24
+ const message = error instanceof Error ? error.message : "The Nudge companion could not complete the operation.";
25
+ return {
26
+ isError: true,
27
+ content: [{ type: "text", text: JSON.stringify({ error: message }) }],
28
+ };
29
+ }
30
+ function promptResult(request) {
31
+ return textResult({
32
+ requestId: request.requestId,
33
+ projectId: request.projectId,
34
+ prompt: request.prompt,
35
+ ...(request.changeRevision === undefined ? {} : { changeRevision: request.changeRevision }),
36
+ });
37
+ }
38
+ function commandResult(result) {
39
+ return {
40
+ ...(result.ok ? {} : { isError: true }),
41
+ ...textResult(result),
42
+ };
43
+ }
44
+ function boundedWorkspaceRoot(value) {
45
+ if (value === undefined)
46
+ return undefined;
47
+ if (value.length === 0 || value.length > 4096)
48
+ throw new TypeError("workspaceRoot must be a bounded non-empty string");
49
+ return value;
50
+ }
51
+ function installTools(mcpServer, bridge) {
52
+ const listen = async (_args, extra) => {
53
+ try {
54
+ return promptResult(await bridge.waitForPrompt(extra.signal));
55
+ }
56
+ catch (error) {
57
+ return errorResult(error);
58
+ }
59
+ };
60
+ const listenDescription = "Call this immediately at task start. Wait for one browser prompt. Keep this standard MCP call open, then call nudge_report_status and listen again after the request ends.";
61
+ const emptyInput = {};
62
+ mcpServer.registerTool("nudge_listen", {
63
+ title: "Listen for Nudge requests",
64
+ description: listenDescription,
65
+ inputSchema: emptyInput,
66
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
67
+ }, listen);
68
+ mcpServer.registerTool("nudge_connect", {
69
+ title: "Connect to Nudge",
70
+ description: "Open the long-lived Nudge browser connection and wait for one prompt.",
71
+ inputSchema: emptyInput,
72
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
73
+ }, listen);
74
+ mcpServer.registerTool("nudge_get_status", {
75
+ title: "Get Nudge status",
76
+ description: "Read the in-memory browser pairing and request status for this project.",
77
+ inputSchema: emptyInput,
78
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
79
+ }, async () => textResult(bridge.getStatus()));
80
+ mcpServer.registerTool("nudge_report_status", {
81
+ title: "Report Nudge request status",
82
+ description: "Report working, completed, failed, or interrupted status for the prompt currently being handled.",
83
+ inputSchema: {
84
+ requestId: z.string().min(1).max(256),
85
+ status: z.enum(["working", "completed", "failed", "interrupted"]),
86
+ summary: z.string().max(32_000).optional(),
87
+ error: z.string().max(32_000).optional(),
88
+ },
89
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
90
+ }, async (args) => {
91
+ const update = {
92
+ requestId: args.requestId,
93
+ status: args.status,
94
+ ...(args.summary === undefined ? {} : { summary: args.summary }),
95
+ ...(args.error === undefined ? {} : { error: args.error }),
96
+ };
97
+ try {
98
+ bridge.updateRequestStatus(update);
99
+ return textResult(bridge.getStatus());
100
+ }
101
+ catch (error) {
102
+ return errorResult(error);
103
+ }
104
+ });
105
+ mcpServer.registerTool("nudge_read_canvas", {
106
+ title: "Read Canvas state",
107
+ description: "Read the paired browser's current Canvas groups and focus.",
108
+ inputSchema: emptyInput,
109
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
110
+ }, async () => {
111
+ try {
112
+ return commandResult(await bridge.dispatchCanvasCommand({ type: "read-state" }));
113
+ }
114
+ catch (error) {
115
+ return errorResult(error);
116
+ }
117
+ });
118
+ mcpServer.registerTool("nudge_present_routes", {
119
+ title: "Present routes in Canvas",
120
+ description: "Append or focus an agent-owned, labeled comparison group of same-origin project routes in Canvas.",
121
+ inputSchema: {
122
+ label: z.string().min(1).max(160),
123
+ groupId: z.string().min(1).max(256).regex(/^agent-[A-Za-z0-9._:-]+$/).optional(),
124
+ routes: z.array(z.object({
125
+ url: z.string().min(1).max(2048),
126
+ title: z.string().max(512).optional(),
127
+ label: z.string().max(160).optional(),
128
+ }).strict()).min(1).max(64),
129
+ },
130
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
131
+ }, async (args) => {
132
+ try {
133
+ if (!bridge.effectiveOrigin)
134
+ throw new Error("Canvas routes require a paired or configured browser origin.");
135
+ const routes = validateRoutes(args.routes, bridge.effectiveOrigin);
136
+ return commandResult(await bridge.dispatchCanvasCommand({
137
+ type: "present-routes",
138
+ groupId: args.groupId ?? `agent-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`,
139
+ label: args.label,
140
+ routes,
141
+ }));
142
+ }
143
+ catch (error) {
144
+ return errorResult(error);
145
+ }
146
+ });
147
+ mcpServer.registerTool("nudge_focus_canvas_group", {
148
+ title: "Focus Canvas group",
149
+ description: "Focus an existing Canvas comparison group by its stable group ID.",
150
+ inputSchema: { groupId: z.string().min(1).max(256) },
151
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
152
+ }, async (args) => {
153
+ try {
154
+ return commandResult(await bridge.dispatchCanvasCommand({ type: "focus-group", groupId: args.groupId }));
155
+ }
156
+ catch (error) {
157
+ return errorResult(error);
158
+ }
159
+ });
160
+ mcpServer.registerTool("nudge_fit_canvas", {
161
+ title: "Fit Canvas",
162
+ description: "Fit all current Canvas cards into the browser board.",
163
+ inputSchema: emptyInput,
164
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
165
+ }, async () => {
166
+ try {
167
+ return commandResult(await bridge.dispatchCanvasCommand({ type: "fit-all" }));
168
+ }
169
+ catch (error) {
170
+ return errorResult(error);
171
+ }
172
+ });
173
+ mcpServer.registerTool("nudge_remove_canvas_group", {
174
+ title: "Remove agent Canvas group",
175
+ description: "Remove an agent-owned Canvas comparison group. The browser refuses removal of user-owned groups.",
176
+ inputSchema: { groupId: z.string().min(1).max(256).regex(/^agent-[A-Za-z0-9._:-]+$/) },
177
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
178
+ }, async (args) => {
179
+ try {
180
+ return commandResult(await bridge.dispatchCanvasCommand({ type: "remove-group", groupId: args.groupId }));
181
+ }
182
+ catch (error) {
183
+ return errorResult(error);
184
+ }
185
+ });
186
+ }
187
+ /**
188
+ * Creates one project-scoped MCP companion and its loopback browser bridge.
189
+ * The returned companion is inert until `start` is called.
190
+ */
191
+ export function createAgentCompanion(options) {
192
+ const workspaceRoot = boundedWorkspaceRoot(options.workspaceRoot);
193
+ const bridge = createLoopbackBridge(options);
194
+ const mcpServer = new McpServer({
195
+ name: options.serverName ?? MCP_SERVER_NAME,
196
+ version: options.serverVersion ?? MCP_SERVER_VERSION,
197
+ }, { instructions: MCP_SERVER_INSTRUCTIONS });
198
+ installTools(mcpServer, bridge);
199
+ const project = {
200
+ projectId: options.projectId,
201
+ ...(bridge.origin === null ? {} : { origin: bridge.origin }),
202
+ ...(workspaceRoot === undefined ? {} : { workspaceRoot }),
203
+ };
204
+ let startPromise = null;
205
+ let closePromise = null;
206
+ let transport = null;
207
+ const start = (providedTransport) => {
208
+ if (closePromise)
209
+ return Promise.reject(new Error("The Nudge companion is closing."));
210
+ if (startPromise)
211
+ return startPromise;
212
+ startPromise = (async () => {
213
+ const address = await bridge.start();
214
+ transport = providedTransport ?? new StdioServerTransport();
215
+ await mcpServer.connect(transport);
216
+ return { address };
217
+ })();
218
+ return startPromise;
219
+ };
220
+ const close = () => {
221
+ if (closePromise)
222
+ return closePromise;
223
+ closePromise = (async () => {
224
+ if (transport) {
225
+ try {
226
+ await mcpServer.close();
227
+ }
228
+ catch {
229
+ // The SDK may already close a transport after a client disconnect.
230
+ }
231
+ }
232
+ await bridge.close();
233
+ })();
234
+ return closePromise;
235
+ };
236
+ return {
237
+ bridge,
238
+ mcpServer,
239
+ project,
240
+ start,
241
+ close,
242
+ };
243
+ }
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@nudge-ui/mcp",
3
+ "version": "0.1.0",
4
+ "description": "Optional local MCP companion for pairing Nudge UI with coding agents.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/charlessmart/nudge-ui.git",
9
+ "directory": "packages/mcp"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/charlessmart/nudge-ui/issues"
13
+ },
14
+ "homepage": "https://github.com/charlessmart/nudge-ui#readme",
15
+ "type": "module",
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "main": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "bin": {
23
+ "nudge-mcp": "./dist/cli.mjs"
24
+ },
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js",
29
+ "default": "./dist/index.js"
30
+ },
31
+ "./protocol": {
32
+ "types": "./dist/protocol.d.ts",
33
+ "import": "./dist/protocol.js",
34
+ "default": "./dist/protocol.js"
35
+ },
36
+ "./bridge": {
37
+ "types": "./dist/bridge.d.ts",
38
+ "import": "./dist/bridge.js",
39
+ "default": "./dist/bridge.js"
40
+ },
41
+ "./server": {
42
+ "types": "./dist/server.d.ts",
43
+ "import": "./dist/server.js",
44
+ "default": "./dist/server.js"
45
+ }
46
+ },
47
+ "scripts": {
48
+ "build": "node ../../scripts/build-package.mjs && node scripts/build.mjs",
49
+ "prepack": "pnpm run build",
50
+ "typecheck": "tsc --noEmit -p tsconfig.json",
51
+ "test:unit": "vitest run",
52
+ "test:unit:watch": "vitest"
53
+ },
54
+ "dependencies": {
55
+ "@modelcontextprotocol/sdk": "^1.30.0",
56
+ "@nudge-ui/agent-protocol": "workspace:*",
57
+ "zod": "^3.25.76"
58
+ },
59
+ "devDependencies": {
60
+ "@types/node": "^22.20.1",
61
+ "esbuild": "^0.21.5",
62
+ "typescript": "^5.9.3",
63
+ "vitest": "^2.1.9"
64
+ },
65
+ "engines": {
66
+ "node": ">=20"
67
+ },
68
+ "publishConfig": {
69
+ "access": "public"
70
+ }
71
+ }