webflow-mcp 0.3.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.
Files changed (94) hide show
  1. package/.webflow/flows/_template/index.ts +14 -0
  2. package/.webflow/flows/_template/schema.json +11 -0
  3. package/.webflow/skills/README.md +3 -0
  4. package/README.md +215 -0
  5. package/dist/src/anti-block/provider-gate.d.ts +19 -0
  6. package/dist/src/anti-block/provider-gate.js +116 -0
  7. package/dist/src/cli/commands/approvals.d.ts +5 -0
  8. package/dist/src/cli/commands/approvals.js +69 -0
  9. package/dist/src/cli/commands/config.d.ts +4 -0
  10. package/dist/src/cli/commands/config.js +56 -0
  11. package/dist/src/cli/commands/create-flow.d.ts +4 -0
  12. package/dist/src/cli/commands/create-flow.js +27 -0
  13. package/dist/src/cli/commands/doctor.d.ts +19 -0
  14. package/dist/src/cli/commands/doctor.js +241 -0
  15. package/dist/src/cli/commands/fork.d.ts +4 -0
  16. package/dist/src/cli/commands/fork.js +19 -0
  17. package/dist/src/cli/commands/init.d.ts +4 -0
  18. package/dist/src/cli/commands/init.js +22 -0
  19. package/dist/src/cli/commands/login.d.ts +26 -0
  20. package/dist/src/cli/commands/login.js +145 -0
  21. package/dist/src/cli/commands/profiles.d.ts +40 -0
  22. package/dist/src/cli/commands/profiles.js +142 -0
  23. package/dist/src/cli/commands/run.d.ts +10 -0
  24. package/dist/src/cli/commands/run.js +13 -0
  25. package/dist/src/cli/commands/setup.d.ts +11 -0
  26. package/dist/src/cli/commands/setup.js +46 -0
  27. package/dist/src/cli/commands/worker.d.ts +29 -0
  28. package/dist/src/cli/commands/worker.js +30 -0
  29. package/dist/src/cli/index.d.ts +2 -0
  30. package/dist/src/cli/index.js +336 -0
  31. package/dist/src/config/webflow-config.d.ts +35 -0
  32. package/dist/src/config/webflow-config.js +119 -0
  33. package/dist/src/errors/self-healing.d.ts +62 -0
  34. package/dist/src/errors/self-healing.js +77 -0
  35. package/dist/src/mcp/cli.d.ts +2 -0
  36. package/dist/src/mcp/cli.js +36 -0
  37. package/dist/src/mcp/relay-gateway.d.ts +24 -0
  38. package/dist/src/mcp/relay-gateway.js +208 -0
  39. package/dist/src/mcp/server.d.ts +8 -0
  40. package/dist/src/mcp/server.js +62 -0
  41. package/dist/src/mcp/tool-registry.d.ts +10 -0
  42. package/dist/src/mcp/tool-registry.js +366 -0
  43. package/dist/src/profiles/profile-manager.d.ts +33 -0
  44. package/dist/src/profiles/profile-manager.js +37 -0
  45. package/dist/src/profiles/profile-state.d.ts +31 -0
  46. package/dist/src/profiles/profile-state.js +122 -0
  47. package/dist/src/repertoire/local-repertoire.d.ts +3 -0
  48. package/dist/src/repertoire/local-repertoire.js +59 -0
  49. package/dist/src/runner/browser-session.d.ts +23 -0
  50. package/dist/src/runner/browser-session.js +58 -0
  51. package/dist/src/runner/cdp-session.d.ts +25 -0
  52. package/dist/src/runner/cdp-session.js +156 -0
  53. package/dist/src/runner/chrome.d.ts +18 -0
  54. package/dist/src/runner/chrome.js +56 -0
  55. package/dist/src/runner/flow-loader.d.ts +11 -0
  56. package/dist/src/runner/flow-loader.js +115 -0
  57. package/dist/src/runner/flow-runner.d.ts +70 -0
  58. package/dist/src/runner/flow-runner.js +378 -0
  59. package/dist/src/runner/types.d.ts +112 -0
  60. package/dist/src/runner/types.js +1 -0
  61. package/dist/src/schema/translator.d.ts +5 -0
  62. package/dist/src/schema/translator.js +104 -0
  63. package/dist/src/schema/types.d.ts +32 -0
  64. package/dist/src/schema/types.js +1 -0
  65. package/dist/src/security/flow-approvals.d.ts +34 -0
  66. package/dist/src/security/flow-approvals.js +324 -0
  67. package/dist/src/security/flow-release.d.ts +82 -0
  68. package/dist/src/security/flow-release.js +392 -0
  69. package/dist/src/security/official-flow-trust.d.ts +2 -0
  70. package/dist/src/security/official-flow-trust.js +16 -0
  71. package/dist/src/security/trusted-flow-keys.d.ts +8 -0
  72. package/dist/src/security/trusted-flow-keys.js +132 -0
  73. package/dist/src/shared/credentials.d.ts +21 -0
  74. package/dist/src/shared/credentials.js +99 -0
  75. package/dist/src/shared/errors.d.ts +6 -0
  76. package/dist/src/shared/errors.js +16 -0
  77. package/dist/src/shared/fs.d.ts +5 -0
  78. package/dist/src/shared/fs.js +23 -0
  79. package/dist/src/shared/paths.d.ts +112 -0
  80. package/dist/src/shared/paths.js +257 -0
  81. package/dist/src/shared/semaphore.d.ts +8 -0
  82. package/dist/src/shared/semaphore.js +23 -0
  83. package/dist/src/shared/taxonomy.d.ts +21 -0
  84. package/dist/src/shared/taxonomy.js +20 -0
  85. package/dist/src/worker/connection.d.ts +72 -0
  86. package/dist/src/worker/connection.js +427 -0
  87. package/dist/src/worker/lifecycle.d.ts +50 -0
  88. package/dist/src/worker/lifecycle.js +154 -0
  89. package/dist/src/worker/protocol.d.ts +188 -0
  90. package/dist/src/worker/protocol.js +156 -0
  91. package/dist/src/worker/worker-service.d.ts +112 -0
  92. package/dist/src/worker/worker-service.js +258 -0
  93. package/package.json +50 -0
  94. package/taxonomy.json +99 -0
@@ -0,0 +1,62 @@
1
+ export interface RawDomSnapshot {
2
+ html?: string;
3
+ text?: string;
4
+ }
5
+ export interface SelfHealingPayload {
6
+ code: string;
7
+ message: string;
8
+ flow: string;
9
+ profile?: string;
10
+ profileStatus?: string;
11
+ loginUrl?: string;
12
+ setupCommand?: string;
13
+ approvalCommand?: string;
14
+ flowDigest?: string;
15
+ releaseId?: string;
16
+ releaseVersion?: string;
17
+ releaseSequence?: number;
18
+ signerKeyId?: string;
19
+ trustDomain?: string;
20
+ step?: string;
21
+ url?: string;
22
+ retryable: boolean;
23
+ suggestedAction?: string;
24
+ domSnapshot?: {
25
+ html?: string;
26
+ text?: string;
27
+ truncated: boolean;
28
+ originalBytes: number;
29
+ capturedBytes: number;
30
+ };
31
+ validationErrors?: Array<{
32
+ path: string;
33
+ message: string;
34
+ }>;
35
+ }
36
+ export interface SelfHealingContext {
37
+ flow: string;
38
+ profile?: string;
39
+ profileStatus?: string;
40
+ loginUrl?: string;
41
+ setupCommand?: string;
42
+ approvalCommand?: string;
43
+ flowDigest?: string;
44
+ releaseId?: string;
45
+ releaseVersion?: string;
46
+ releaseSequence?: number;
47
+ signerKeyId?: string;
48
+ trustDomain?: string;
49
+ step?: string;
50
+ url?: string;
51
+ }
52
+ export declare function buildDomSnapshot(snapshot?: RawDomSnapshot): SelfHealingPayload['domSnapshot'];
53
+ export declare function buildSelfHealingPayload(error: unknown, context: SelfHealingContext & {
54
+ code?: string;
55
+ retryable?: boolean;
56
+ suggestedAction?: string;
57
+ domSnapshot?: RawDomSnapshot;
58
+ validationErrors?: Array<{
59
+ path: string;
60
+ message: string;
61
+ }>;
62
+ }): SelfHealingPayload;
@@ -0,0 +1,77 @@
1
+ const HTML_CAP = 48 * 1024;
2
+ const TEXT_CAP = 16 * 1024;
3
+ const TOTAL_CAP = 64 * 1024;
4
+ function byteLength(value) {
5
+ return value ? Buffer.byteLength(value, 'utf8') : 0;
6
+ }
7
+ function stripDangerousTags(html) {
8
+ return html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '').replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '');
9
+ }
10
+ function truncate(value, cap) {
11
+ if (!value) {
12
+ return { value: undefined, truncated: false };
13
+ }
14
+ if (Buffer.byteLength(value, 'utf8') <= cap) {
15
+ return { value, truncated: false };
16
+ }
17
+ let current = value;
18
+ while (Buffer.byteLength(current, 'utf8') > cap && current.length > 0) {
19
+ current = current.slice(0, Math.max(1, Math.floor(current.length * 0.9)));
20
+ }
21
+ return { value: `${current}…`, truncated: true };
22
+ }
23
+ export function buildDomSnapshot(snapshot) {
24
+ if (!snapshot?.html && !snapshot?.text) {
25
+ return undefined;
26
+ }
27
+ const sanitizedHtml = snapshot.html ? stripDangerousTags(snapshot.html) : undefined;
28
+ const html = truncate(sanitizedHtml, HTML_CAP);
29
+ const text = truncate(snapshot.text, TEXT_CAP);
30
+ const originalBytes = byteLength(sanitizedHtml) + byteLength(snapshot.text);
31
+ let capturedHtml = html.value;
32
+ let capturedText = text.value;
33
+ while (byteLength(capturedHtml) + byteLength(capturedText) > TOTAL_CAP) {
34
+ if ((capturedHtml?.length ?? 0) >= (capturedText?.length ?? 0) && capturedHtml) {
35
+ capturedHtml = capturedHtml.slice(0, Math.max(1, Math.floor(capturedHtml.length * 0.9)));
36
+ }
37
+ else if (capturedText) {
38
+ capturedText = capturedText.slice(0, Math.max(1, Math.floor(capturedText.length * 0.9)));
39
+ }
40
+ else {
41
+ break;
42
+ }
43
+ }
44
+ const capturedBytes = byteLength(capturedHtml) + byteLength(capturedText);
45
+ return {
46
+ html: capturedHtml,
47
+ text: capturedText,
48
+ truncated: html.truncated || text.truncated || capturedBytes < originalBytes,
49
+ originalBytes,
50
+ capturedBytes
51
+ };
52
+ }
53
+ export function buildSelfHealingPayload(error, context) {
54
+ const message = error instanceof Error ? error.message : typeof error === 'string' ? error : 'Flow execution failed';
55
+ return {
56
+ code: context.code ?? 'FLOW_STEP_FAILED',
57
+ message,
58
+ flow: context.flow,
59
+ profile: context.profile,
60
+ profileStatus: context.profileStatus,
61
+ loginUrl: context.loginUrl,
62
+ setupCommand: context.setupCommand,
63
+ approvalCommand: context.approvalCommand,
64
+ flowDigest: context.flowDigest,
65
+ releaseId: context.releaseId,
66
+ releaseVersion: context.releaseVersion,
67
+ releaseSequence: context.releaseSequence,
68
+ signerKeyId: context.signerKeyId,
69
+ trustDomain: context.trustDomain,
70
+ step: context.step,
71
+ url: context.url,
72
+ retryable: context.retryable ?? true,
73
+ suggestedAction: context.suggestedAction,
74
+ domSnapshot: buildDomSnapshot(context.domSnapshot),
75
+ validationErrors: context.validationErrors
76
+ };
77
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from 'node:path';
3
+ import { startStdioServer } from './server.js';
4
+ import { toErrorMessage } from '../shared/errors.js';
5
+ function resolveProjectRoot(argv) {
6
+ const diagnostics = [];
7
+ const projectRootIndex = argv.findIndex((arg) => arg === '--projectRoot' || arg === '--project-root');
8
+ const explicitArg = projectRootIndex >= 0 ? argv[projectRootIndex + 1] : argv.find((arg) => !arg.startsWith('-'));
9
+ if (explicitArg) {
10
+ return { projectRoot: resolve(explicitArg), diagnostics };
11
+ }
12
+ if (process.env.CLAUDE_PROJECT_DIR) {
13
+ const projectRoot = resolve(process.env.CLAUDE_PROJECT_DIR);
14
+ diagnostics.push(`No projectRoot argument provided; using CLAUDE_PROJECT_DIR fallback: ${projectRoot}`);
15
+ return { projectRoot, diagnostics };
16
+ }
17
+ const projectRoot = resolve(process.cwd());
18
+ diagnostics.push(`No projectRoot argument provided; using current working directory fallback: ${projectRoot}`);
19
+ return { projectRoot, diagnostics };
20
+ }
21
+ async function main() {
22
+ // Redirect console.log to console.error to keep stdout clean for MCP JSON-RPC stdio transport
23
+ console.log = (...args) => {
24
+ console.error(...args);
25
+ };
26
+ const { projectRoot, diagnostics } = resolveProjectRoot(process.argv.slice(2));
27
+ diagnostics.forEach((diagnostic) => console.error(`[diagnostic] ${diagnostic}`));
28
+ await startStdioServer(projectRoot);
29
+ if (process.env.WEBFLOW_MCP_SMOKE_EXIT_AFTER_CONNECT === '1') {
30
+ process.exit(0);
31
+ }
32
+ }
33
+ main().catch((error) => {
34
+ console.error(toErrorMessage(error));
35
+ process.exitCode = 1;
36
+ });
@@ -0,0 +1,24 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
3
+ export { RELAY_MCP_TOKEN_ENV, RELAY_MCP_URL_ENV } from '../shared/paths.js';
4
+ export interface RelayGatewayConfig {
5
+ url: URL;
6
+ token: string;
7
+ }
8
+ export declare function resolveRelayGatewayConfig(environment?: NodeJS.ProcessEnv): RelayGatewayConfig | undefined;
9
+ export declare class RelayGateway {
10
+ private readonly config;
11
+ private client?;
12
+ private transport?;
13
+ private connecting?;
14
+ private closed;
15
+ private activeCalls;
16
+ private resetWhenIdle;
17
+ constructor(config: RelayGatewayConfig);
18
+ get endpoint(): string;
19
+ private connect;
20
+ private reset;
21
+ callTool(name: string, args: Record<string, unknown>, signal: AbortSignal): Promise<CallToolResult>;
22
+ close(): Promise<void>;
23
+ }
24
+ export declare function registerRelayGatewayTools(server: McpServer, gateway: RelayGateway): void;
@@ -0,0 +1,208 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js';
3
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
4
+ import { z } from 'zod';
5
+ import { WebFlowError } from '../shared/errors.js';
6
+ import { RELAY_MCP_TOKEN_ENV, RELAY_MCP_URL_ENV } from '../shared/paths.js';
7
+ export { RELAY_MCP_TOKEN_ENV, RELAY_MCP_URL_ENV } from '../shared/paths.js';
8
+ const RELAY_CALL_TIMEOUT_MS = 21 * 60_000;
9
+ const MAX_INPUT_FIELDS = 100;
10
+ function errorResult(code, message) {
11
+ const error = { code, message };
12
+ return {
13
+ isError: true,
14
+ content: [{ type: 'text', text: JSON.stringify(error, null, 2) }],
15
+ structuredContent: error
16
+ };
17
+ }
18
+ function boundedInputRecord() {
19
+ return z.record(z.string(), z.unknown()).refine((value) => Object.keys(value).length <= MAX_INPUT_FIELDS, {
20
+ message: `Too many top-level input fields (max ${MAX_INPUT_FIELDS}).`
21
+ });
22
+ }
23
+ export function resolveRelayGatewayConfig(environment = process.env) {
24
+ const rawUrl = environment[RELAY_MCP_URL_ENV]?.trim();
25
+ const token = environment[RELAY_MCP_TOKEN_ENV]?.trim();
26
+ if (!rawUrl && !token)
27
+ return undefined;
28
+ if (!rawUrl || !token) {
29
+ throw new WebFlowError('INCOMPLETE_RELAY_MCP_CONFIG', `${RELAY_MCP_URL_ENV} and ${RELAY_MCP_TOKEN_ENV} must be configured together.`);
30
+ }
31
+ if (!token.startsWith('tok_') || token.length <= 4) {
32
+ throw new WebFlowError('INVALID_RELAY_MCP_TOKEN', `${RELAY_MCP_TOKEN_ENV} must contain an MCP token beginning with "tok_".`);
33
+ }
34
+ let url;
35
+ try {
36
+ url = new URL(rawUrl);
37
+ }
38
+ catch {
39
+ throw new WebFlowError('INVALID_RELAY_MCP_URL', `${RELAY_MCP_URL_ENV} is not a valid URL.`);
40
+ }
41
+ const loopback = url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]';
42
+ if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) {
43
+ throw new WebFlowError('INSECURE_RELAY_MCP_URL', 'The relay MCP URL must use https. Plain http is allowed only for loopback development.');
44
+ }
45
+ if (url.username || url.password || url.hash) {
46
+ throw new WebFlowError('INVALID_RELAY_MCP_URL', 'The relay MCP URL must not contain credentials or a fragment.');
47
+ }
48
+ if (url.search) {
49
+ throw new WebFlowError('INVALID_RELAY_MCP_URL', 'The relay MCP URL must not contain query parameters.');
50
+ }
51
+ return { url, token };
52
+ }
53
+ export class RelayGateway {
54
+ config;
55
+ client;
56
+ transport;
57
+ connecting;
58
+ closed = false;
59
+ activeCalls = 0;
60
+ resetWhenIdle = false;
61
+ constructor(config) {
62
+ this.config = config;
63
+ }
64
+ get endpoint() {
65
+ return this.config.url.toString();
66
+ }
67
+ async connect() {
68
+ if (this.closed)
69
+ throw new WebFlowError('RELAY_MCP_UNAVAILABLE', 'The local gateway is shutting down.');
70
+ if (this.client)
71
+ return this.client;
72
+ if (this.connecting)
73
+ return this.connecting;
74
+ this.connecting = (async () => {
75
+ const client = new Client({ name: 'webflow-local-gateway', version: '0.3.0' });
76
+ const transport = new StreamableHTTPClientTransport(this.config.url, {
77
+ requestInit: {
78
+ headers: { Authorization: `Bearer ${this.config.token}` },
79
+ redirect: 'error'
80
+ }
81
+ });
82
+ try {
83
+ await client.connect(transport);
84
+ this.client = client;
85
+ this.transport = transport;
86
+ return client;
87
+ }
88
+ catch (error) {
89
+ await client.close().catch(() => undefined);
90
+ if (error instanceof UnauthorizedError) {
91
+ throw new WebFlowError('RELAY_MCP_AUTH_FAILED', 'The relay rejected the configured MCP token.');
92
+ }
93
+ throw new WebFlowError('RELAY_MCP_UNAVAILABLE', 'Could not connect to the configured relay MCP endpoint.');
94
+ }
95
+ })();
96
+ try {
97
+ return await this.connecting;
98
+ }
99
+ finally {
100
+ this.connecting = undefined;
101
+ }
102
+ }
103
+ async reset() {
104
+ const client = this.client;
105
+ this.client = undefined;
106
+ this.transport = undefined;
107
+ if (client)
108
+ await client.close().catch(() => undefined);
109
+ }
110
+ async callTool(name, args, signal) {
111
+ let connected = false;
112
+ try {
113
+ const client = await this.connect();
114
+ this.activeCalls += 1;
115
+ connected = true;
116
+ return await client.callTool({ name, arguments: args }, undefined, { signal, timeout: RELAY_CALL_TIMEOUT_MS, maxTotalTimeout: RELAY_CALL_TIMEOUT_MS });
117
+ }
118
+ catch (error) {
119
+ if (signal.aborted)
120
+ return errorResult('RELAY_MCP_CANCELLED', 'The relay MCP request was cancelled.');
121
+ this.resetWhenIdle = true;
122
+ if (!connected) {
123
+ this.resetWhenIdle = false;
124
+ await this.reset();
125
+ }
126
+ if (error instanceof WebFlowError)
127
+ return errorResult(error.code, error.message);
128
+ if (error instanceof UnauthorizedError)
129
+ return errorResult('RELAY_MCP_AUTH_FAILED', 'The relay rejected the configured MCP token.');
130
+ return errorResult('RELAY_MCP_UNAVAILABLE', 'The relay MCP request failed. Retry only if the previous call could not have started a flow.');
131
+ }
132
+ finally {
133
+ if (connected) {
134
+ this.activeCalls -= 1;
135
+ if (this.activeCalls === 0 && this.resetWhenIdle) {
136
+ this.resetWhenIdle = false;
137
+ await this.reset();
138
+ }
139
+ }
140
+ }
141
+ }
142
+ async close() {
143
+ this.closed = true;
144
+ await this.connecting?.catch(() => undefined);
145
+ const transport = this.transport;
146
+ const client = this.client;
147
+ this.client = undefined;
148
+ this.transport = undefined;
149
+ if (transport?.sessionId)
150
+ await transport.terminateSession().catch(() => undefined);
151
+ if (client)
152
+ await client.close().catch(() => undefined);
153
+ }
154
+ }
155
+ export function registerRelayGatewayTools(server, gateway) {
156
+ const proxy = (name) => async (args, extra) => gateway.callTool(name, args, extra.signal);
157
+ server.registerTool('browse_catalog', {
158
+ title: 'browse_catalog',
159
+ description: 'Navigate the remote catalog top-down: sector, service, then flow. Never assume a flow name that was not returned by browse_catalog or search_flows.',
160
+ inputSchema: z.object({ category: z.string().max(200).optional(), service: z.string().max(200).optional() })
161
+ }, proxy('browse_catalog'));
162
+ server.registerTool('search_flows', {
163
+ title: 'search_flows',
164
+ description: 'Search the remote flow catalog by name or description keyword.',
165
+ inputSchema: z.object({ query: z.string().min(1).max(200) })
166
+ }, proxy('search_flows'));
167
+ server.registerTool('describe_flow', {
168
+ title: 'describe_flow',
169
+ description: 'Read remote flow metadata and its real signed input schema before execution.',
170
+ inputSchema: z.object({ name: z.string().min(1).max(200) })
171
+ }, proxy('describe_flow'));
172
+ server.registerTool('add_to_repertoire', {
173
+ title: 'add_to_repertoire',
174
+ description: 'Save a discovered flow in this worker\'s local repertoire. This is a standing preference and requires the user\'s prior agreement. In gateway mode, use run_flow_with_options to execute saved flows.',
175
+ inputSchema: z.object({ name: z.string().min(1).max(200) })
176
+ }, proxy('add_to_repertoire'));
177
+ server.registerTool('remove_from_repertoire', {
178
+ title: 'remove_from_repertoire',
179
+ description: 'Remove a flow from this worker\'s local repertoire. Only call after the user explicitly asks or agrees.',
180
+ inputSchema: z.object({ name: z.string().min(1).max(200) })
181
+ }, proxy('remove_from_repertoire'));
182
+ server.registerTool('run_flow_once', {
183
+ title: 'run_flow_once',
184
+ description: 'Run a discovered flow outside the saved repertoire once. Call without confirmed first, ask the user, then retry with confirmed:true. Use run_flow_with_options for saved flows.',
185
+ inputSchema: z.object({ name: z.string().min(1).max(200), input: boundedInputRecord().optional(), confirmed: z.boolean().optional() })
186
+ }, proxy('run_flow_once'));
187
+ server.registerTool('run_flow_with_options', {
188
+ title: 'run_flow_with_options',
189
+ description: 'Run any visible remote flow. Saved flows need no confirmation unless one-off browser options are supplied; unsaved flows require confirmed:true after asking the user.',
190
+ inputSchema: z.object({
191
+ name: z.string().min(1).max(200),
192
+ input: boundedInputRecord().optional(),
193
+ browserMode: z.enum(['managed', 'cdp']).optional(),
194
+ browserVisibility: z.enum(['visible', 'hidden']).optional(),
195
+ confirmed: z.boolean().optional()
196
+ })
197
+ }, proxy('run_flow_with_options'));
198
+ server.registerTool('check_profile_status', {
199
+ title: 'check_profile_status',
200
+ description: 'Check the worker-local login status using the remote flow\'s authoritative profile metadata.',
201
+ inputSchema: z.object({ flowName: z.string().min(1).max(200) })
202
+ }, proxy('check_profile_status'));
203
+ server.registerTool('start_profile_login', {
204
+ title: 'start_profile_login',
205
+ description: 'Open a real visible Chrome login window on this machine. Call without confirmed first, ask the user, then retry with confirmed:true.',
206
+ inputSchema: z.object({ flowName: z.string().min(1).max(200), confirmed: z.boolean().optional() })
207
+ }, proxy('start_profile_login'));
208
+ }
@@ -0,0 +1,8 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function createMcpServer(projectRoot: string, nodeVersion?: string): Promise<{
3
+ server: McpServer;
4
+ diagnostics: string[];
5
+ flowNames: string[];
6
+ mode: 'local' | 'gateway';
7
+ }>;
8
+ export declare function startStdioServer(projectRoot: string): Promise<void>;
@@ -0,0 +1,62 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import { registerFlowTools } from './tool-registry.js';
6
+ import { RelayGateway, resolveRelayGatewayConfig } from './relay-gateway.js';
7
+ import { assertSupportedNode, assertWorkspaceReady, getPackageRoot, getProfilesRoot, getWorkspacePaths, resolveFlowsProjectRoot } from '../shared/paths.js';
8
+ async function readVersion(projectRoot) {
9
+ try {
10
+ const raw = await readFile(resolve(projectRoot, 'package.json'), 'utf8');
11
+ return JSON.parse(raw).version ?? '0.0.0';
12
+ }
13
+ catch {
14
+ const raw = await readFile(resolve(getPackageRoot(import.meta.url), 'package.json'), 'utf8');
15
+ return JSON.parse(raw).version ?? '0.0.0';
16
+ }
17
+ }
18
+ export async function createMcpServer(projectRoot, nodeVersion = process.versions.node) {
19
+ assertSupportedNode(nodeVersion);
20
+ const gatewayConfig = resolveRelayGatewayConfig();
21
+ if (!gatewayConfig)
22
+ await assertWorkspaceReady(projectRoot);
23
+ const gateway = gatewayConfig ? new RelayGateway(gatewayConfig) : undefined;
24
+ const server = new McpServer({
25
+ name: 'webflow-mcp',
26
+ version: await readVersion(projectRoot)
27
+ }, {
28
+ instructions: gateway
29
+ ? 'This is the single local WebFlow gateway. Worker lifecycle, browser preferences, profile administration ' +
30
+ 'and signed-flow approvals operate only on this machine; catalog discovery and flow execution are proxied ' +
31
+ 'to the configured relay. Use start_worker before remote operations. Navigate browse_catalog top-down or ' +
32
+ 'use search_flows, call describe_flow before execution, and use run_flow_with_options for saved flows. ' +
33
+ 'When FLOW_UPDATE_APPROVAL_REQUIRED occurs, call list_flow_approvals, show the exact digest/signer/trust ' +
34
+ 'domain to the user, then call approve_flow_update only after explicit agreement. Never invent confirmed:true. ' +
35
+ 'Initial worker pairing still requires the user to run webflow login and approve it in their browser.'
36
+ : 'Expose local .webflow flows as validated MCP tools. For a flow that requires login, call ' +
37
+ 'check_profile_status first — if it is not probably valid, ask the user before calling start_profile_login ' +
38
+ '(with confirmed:true), which opens a real Chrome window for them to log in manually. Worker lifecycle and ' +
39
+ 'signed-flow approval tools operate only on this machine.'
40
+ });
41
+ const { diagnostics, flows } = await registerFlowTools(server, projectRoot, { gateway });
42
+ if (gateway) {
43
+ diagnostics.push(`Mode: unified local gateway`);
44
+ diagnostics.push(`Relay MCP endpoint: ${gateway.endpoint}`);
45
+ server.server.onclose = () => {
46
+ void gateway.close();
47
+ };
48
+ }
49
+ else {
50
+ const workspacePaths = getWorkspacePaths(projectRoot);
51
+ diagnostics.push(`Flows loaded from: ${getWorkspacePaths(resolveFlowsProjectRoot(projectRoot)).flowsDir}`);
52
+ diagnostics.push(`Downloads (this repo): ${workspacePaths.downloadsDir}`);
53
+ }
54
+ diagnostics.push(`Login sessions (global, shared across repos): ${getProfilesRoot()}`);
55
+ return { server, diagnostics, flowNames: flows.map((flow) => flow.name), mode: gateway ? 'gateway' : 'local' };
56
+ }
57
+ export async function startStdioServer(projectRoot) {
58
+ const { server, diagnostics } = await createMcpServer(projectRoot);
59
+ diagnostics.forEach((diagnostic) => console.error(`[diagnostic] ${diagnostic}`));
60
+ const transport = new StdioServerTransport();
61
+ await server.connect(transport);
62
+ }
@@ -0,0 +1,10 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { FlowDefinition } from '../runner/types.js';
3
+ import { type RelayGateway } from './relay-gateway.js';
4
+ export interface ToolRegistryResult {
5
+ diagnostics: string[];
6
+ flows: FlowDefinition[];
7
+ }
8
+ export declare function registerFlowTools(server: McpServer, projectRoot: string, options?: {
9
+ gateway?: RelayGateway;
10
+ }): Promise<ToolRegistryResult>;