@worfilo/mcp 0.2.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Worfilo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,266 @@
1
+ # Worfilo MCP
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@worfilo/mcp.svg)](https://www.npmjs.com/package/@worfilo/mcp)
4
+ [![CI](https://github.com/Worfilo/mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/Worfilo/mcp/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+
7
+ Connect coding agents to [Worfilo](https://worfilo.com) so they can design, build, test and ship AI workflows, then integrate them into your codebase.
8
+
9
+ This repository contains:
10
+
11
+ - **`@worfilo/mcp`**: a command-line tool that configures Claude Code, Cursor, Antigravity, VS Code and Windsurf to use the hosted Worfilo MCP server. It also includes a local stdio bridge for clients that cannot authenticate to remote servers.
12
+ - **The `worfilo-workflows` skill**: guidance that teaches agents the Worfilo workflow model, so the workflows they produce validate the first time.
13
+ - **A Claude Code plugin** that bundles the server configuration and the skill.
14
+
15
+ ## Contents
16
+
17
+ - [What agents can do](#what-agents-can-do)
18
+ - [Requirements](#requirements)
19
+ - [Quick start](#quick-start)
20
+ - [Installation options](#installation-options)
21
+ - [How it works](#how-it-works)
22
+ - [Tools](#tools)
23
+ - [Permissions](#permissions)
24
+ - [CLI reference](#cli-reference)
25
+ - [Security](#security)
26
+ - [Troubleshooting](#troubleshooting)
27
+ - [Development](#development)
28
+ - [License](#license)
29
+
30
+ ## What agents can do
31
+
32
+ With Worfilo connected, you can ask your agent for an automation in plain language, for example "when a support email arrives, classify it and open a Linear issue". The agent then:
33
+
34
+ 1. **Discovers** the available node types and the apps, credentials, APIs and MCP servers connected to your account.
35
+ 2. **Plans** a workflow graph from your description and shows you the steps before saving anything.
36
+ 3. **Builds** the workflow as a draft and resolves validation issues.
37
+ 4. **Tests** the draft with realistic input and repairs any failing nodes.
38
+ 5. **Publishes** a version once you approve it.
39
+ 6. **Integrates** it: creates an API key limited to that workflow, stores it in your environment file, and adds the API call to your code.
40
+
41
+ Every workflow created this way is a standard Worfilo workflow. You can open it on the canvas, inspect its runs, and edit it like any other.
42
+
43
+ ## Requirements
44
+
45
+ - A [Worfilo](https://worfilo.com) account.
46
+ - Node.js 20 or later, for the installer and the stdio bridge.
47
+ - One or more supported clients: Claude Code, Cursor, Antigravity, VS Code (agent mode) or Windsurf.
48
+
49
+ ## Quick start
50
+
51
+ Run the installer from the root of your project:
52
+
53
+ ```sh
54
+ npx @worfilo/mcp install
55
+ ```
56
+
57
+ The installer detects your editors, adds the Worfilo server to each one, and installs the `/worfilo-workflows` guide. It writes configuration only, never credentials, so the generated files are safe to commit.
58
+
59
+ The first time your agent calls Worfilo, a browser window opens. Sign in, review the permissions the agent is requesting, and approve. Then ask your agent to build a workflow, or invoke `/worfilo-workflows` directly.
60
+
61
+ The installer runs with any package runner:
62
+
63
+ ```sh
64
+ pnpm dlx @worfilo/mcp install
65
+ yarn dlx @worfilo/mcp install
66
+ bunx @worfilo/mcp install
67
+ ```
68
+
69
+ ## Installation options
70
+
71
+ ### Choose editors and scope
72
+
73
+ ```sh
74
+ npx @worfilo/mcp install --client claude-code,cursor # specific editors
75
+ npx @worfilo/mcp install --scope user # all projects, not only this repository
76
+ npx @worfilo/mcp install --dry-run # preview changes without writing
77
+ npx @worfilo/mcp uninstall # remove the configuration
78
+ ```
79
+
80
+ The installer writes to these locations:
81
+
82
+ | Client | Server configuration | Guide |
83
+ |---|---|---|
84
+ | Claude Code | `.mcp.json`, or `claude mcp add` with `--scope user` | `.claude/skills/worfilo-workflows/SKILL.md` |
85
+ | Cursor | `.cursor/mcp.json` | `.cursor/commands/` and `.cursor/rules/` |
86
+ | Antigravity | `~/.gemini/antigravity/mcp_config.json` | `.agent/rules/` and `.agent/workflows/` |
87
+ | VS Code | `.vscode/mcp.json` | `.github/prompts/worfilo-workflows.prompt.md` |
88
+ | Windsurf | `~/.codeium/windsurf/mcp_config.json` | `.windsurf/rules/` |
89
+
90
+ Existing entries in these files are preserved. If a file contains comments and cannot be parsed as plain JSON, the installer leaves it unchanged and prints the entry to add manually.
91
+
92
+ ### Claude Code plugin
93
+
94
+ In Claude Code, you can install Worfilo as a plugin. The plugin bundles the server configuration and the skill, and updates through the plugin system:
95
+
96
+ ```
97
+ /plugin marketplace add Worfilo/mcp
98
+ /plugin install worfilo@worfilo
99
+ ```
100
+
101
+ ### Manual configuration
102
+
103
+ The server endpoint is `https://api.worfilo.com/mcp`, using the Streamable HTTP transport with OAuth 2.1.
104
+
105
+ Claude Code:
106
+
107
+ ```sh
108
+ claude mcp add --transport http --scope user worfilo https://api.worfilo.com/mcp
109
+ ```
110
+
111
+ Cursor (`.cursor/mcp.json`):
112
+
113
+ ```json
114
+ {
115
+ "mcpServers": {
116
+ "worfilo": { "url": "https://api.worfilo.com/mcp" }
117
+ }
118
+ }
119
+ ```
120
+
121
+ VS Code (`.vscode/mcp.json`):
122
+
123
+ ```json
124
+ {
125
+ "servers": {
126
+ "worfilo": { "type": "http", "url": "https://api.worfilo.com/mcp" }
127
+ }
128
+ }
129
+ ```
130
+
131
+ Clients without remote OAuth support can use the stdio bridge:
132
+
133
+ ```json
134
+ {
135
+ "mcpServers": {
136
+ "worfilo": { "command": "npx", "args": ["-y", "@worfilo/mcp@latest", "serve"] }
137
+ }
138
+ }
139
+ ```
140
+
141
+ ## How it works
142
+
143
+ ```
144
+ Coding agent --- MCP over HTTPS ---------------------> api.worfilo.com/mcp ---> your Worfilo account
145
+ | ^
146
+ +--- stdio ---> worfilo-mcp serve (local bridge) -------+
147
+ ```
148
+
149
+ - **Hosted server.** Worfilo runs the MCP server. Your agent connects over HTTPS, and nothing runs on your machine besides your editor, unless you use the bridge.
150
+ - **Authentication.** The server is an OAuth 2.1 protected resource (RFC 9728).
151
+ - Clients register dynamically (RFC 7591) and sign in with the authorization code flow and PKCE.
152
+ - Access tokens expire after one hour and refresh automatically.
153
+ - Refresh tokens rotate on every use. Reusing an old refresh token revokes the connection.
154
+ - **Stdio bridge.** `worfilo-mcp serve` reads JSON-RPC messages on stdin and relays them to the hosted server. It runs the OAuth flow itself through a loopback redirect and stores tokens locally.
155
+ - **Guide.** The installer puts the `worfilo-workflows` skill into each editor in that editor's native format. The server also exposes it as the `design_workflow` prompt for any MCP client.
156
+
157
+ ## Tools
158
+
159
+ The agent sees only the tools allowed by the permissions you grant. Publishing, activating versions and creating API keys are marked as consequential, so clients ask for confirmation before calling them.
160
+
161
+ | Tool | Permission | Description |
162
+ |---|---|---|
163
+ | `list_node_types` | `workflows:read` | Node types available to workflow graphs |
164
+ | `get_node_type` | `workflows:read` | Ports and configuration schema for one node type |
165
+ | `list_integrations` | `workflows:read` | Connected apps and actions, credential names, custom APIs and MCP servers |
166
+ | `list_workflows` | `workflows:read` | Workflows with publish state and latest run |
167
+ | `get_workflow` | `workflows:read` | Draft graph, published versions and the active version |
168
+ | `validate_workflow` | `workflows:read` | The checks that publishing runs |
169
+ | `get_run` | `workflows:read` | Run status, output, and failing nodes |
170
+ | `get_integration_snippet` | `workflows:read` | TypeScript, JavaScript, Python or curl code for the Workflow API |
171
+ | `plan_workflow` | `workflows:write` | Drafts a graph and a readable plan without saving |
172
+ | `create_workflow` | `workflows:write` | Saves a new draft and returns validation issues |
173
+ | `update_workflow` | `workflows:write` | Replaces a draft graph and returns validation issues |
174
+ | `publish_workflow` | `workflows:publish` | Publishes the draft as a new version, optionally activating it |
175
+ | `activate_version` | `workflows:publish` | Selects the version the API runs |
176
+ | `run_workflow` | `runs:write` | Runs the draft or published version and returns the result |
177
+ | `create_api_key` | `api_keys:write` | Creates an API key limited to specific workflows |
178
+
179
+ ## Permissions
180
+
181
+ You choose the permissions on the consent screen when the agent first connects.
182
+
183
+ | Scope | Allows |
184
+ |---|---|
185
+ | `workflows:read` | Reading workflows, node types, integrations and credential names. Always granted. |
186
+ | `workflows:write` | Planning, creating and editing draft workflows. |
187
+ | `workflows:publish` | Publishing versions and changing the active version. |
188
+ | `runs:write` | Running workflows and reading their results. |
189
+ | `api_keys:write` | Creating API keys restricted to chosen workflows. |
190
+
191
+ To review or revoke connected agents, open **API keys > Connected agents** in Worfilo. Revocation takes effect immediately.
192
+
193
+ ## CLI reference
194
+
195
+ ```
196
+ worfilo-mcp <command> [options]
197
+ ```
198
+
199
+ | Command | Description |
200
+ |---|---|
201
+ | `install` | Configure detected or selected editors (the default when run in a terminal) |
202
+ | `uninstall` | Remove the configuration and guides |
203
+ | `serve` | Run the stdio bridge (the default when started by an editor) |
204
+ | `login` | Sign in for the stdio bridge ahead of time |
205
+ | `logout` | Revoke the bridge's tokens and delete them locally |
206
+ | `status` | Show the bridge's sign-in state |
207
+ | `skill` | Print the workflow guide |
208
+
209
+ | Option | Description |
210
+ |---|---|
211
+ | `--client <ids>` | Comma-separated: `claude-code`, `cursor`, `antigravity`, `vscode`, `windsurf` |
212
+ | `--scope <scope>` | `project` (default) or `user` |
213
+ | `--url <url>` | MCP server URL |
214
+ | `--bridge` | Configure editors to use the stdio bridge instead of HTTP |
215
+ | `--no-skill` | Skip installing the guide |
216
+ | `--dry-run` | Show changes without writing files |
217
+ | `--yes` | Use detected editors without prompting |
218
+
219
+ | Environment variable | Description |
220
+ |---|---|
221
+ | `WORFILO_MCP_URL` | MCP server URL, for example `http://localhost:8000/mcp` |
222
+ | `WORFILO_API_URL` | API base URL; `/mcp` is appended |
223
+ | `WORFILO_CONFIG_DIR` | Directory for the bridge's credentials file |
224
+
225
+ ## Security
226
+
227
+ - The installer writes server URLs and guides only. It never writes tokens or API keys.
228
+ - The bridge stores tokens in `~/.config/worfilo/credentials.json`, or `%APPDATA%\worfilo` on Windows, readable only by your user.
229
+ - On the server, tokens are stored as SHA-256 hashes, and you can revoke any grant from your account.
230
+ - Agents are instructed to keep API keys in untracked environment files, out of source control.
231
+
232
+ To report a vulnerability, see [SECURITY.md](SECURITY.md).
233
+
234
+ ## Troubleshooting
235
+
236
+ **The browser did not open during sign-in.** The bridge prints the authorization URL to stderr. Open it manually.
237
+
238
+ **A configuration file was not updated.** The installer does not modify files that contain comments. Add the printed entry by hand.
239
+
240
+ **The agent reports a missing permission.** Reconnect Worfilo from your editor and grant the scope named in the message. With the bridge, run `npx @worfilo/mcp logout`, then `login`.
241
+
242
+ **Connecting to a local or self-hosted deployment.** Set `WORFILO_MCP_URL`, or pass `--url` to `install`. Server URLs must use https; plain http is accepted only for `localhost`, so tokens are never sent unencrypted.
243
+
244
+ ## Development
245
+
246
+ ```sh
247
+ git clone https://github.com/Worfilo/mcp.git
248
+ cd mcp
249
+ npm ci
250
+ npm test
251
+ npm run build
252
+ WORFILO_MCP_URL=http://localhost:8000/mcp node dist/cli.js install --dry-run
253
+ ```
254
+
255
+ `skills/worfilo-workflows/SKILL.md` is the single source for the guide. The installer, the Claude Code plugin and the Worfilo server's `design_workflow` prompt all derive from it.
256
+
257
+ ### Releasing
258
+
259
+ 1. Update the version in `package.json`, `src/constants.ts`, `.claude-plugin/plugin.json` and `.claude-plugin/marketplace.json`. The test suite fails if they differ.
260
+ 2. Push a matching tag, for example `v0.1.2`.
261
+
262
+ The release workflow publishes to npm through trusted publishing, with provenance.
263
+
264
+ ## License
265
+
266
+ [MIT](LICENSE)
package/dist/cli.js ADDED
@@ -0,0 +1,753 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { spawnSync } from "child_process";
5
+ import { existsSync as existsSync3 } from "fs";
6
+ import { homedir as homedir2 } from "os";
7
+ import { delimiter, join as join3, relative } from "path";
8
+ import { createInterface as createInterface2 } from "readline/promises";
9
+ import { parseArgs } from "util";
10
+
11
+ // src/auth.ts
12
+ import { createHash, randomBytes } from "crypto";
13
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
14
+ import { createServer } from "http";
15
+ import { homedir } from "os";
16
+ import { dirname, join } from "path";
17
+
18
+ // src/constants.ts
19
+ var PACKAGE = "@worfilo/mcp";
20
+ var VERSION = "0.2.0";
21
+ var SERVER_NAME = "worfilo";
22
+ var SKILL_NAME = "worfilo-workflows";
23
+ var DEFAULT_URL = "https://api.worfilo.com/mcp";
24
+ var DOCS_URL = "https://worfilo.com/developers/mcp";
25
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
26
+ var SHELL_UNSAFE = /[\s"'`^|<>&;$\\]/;
27
+ function checkUrl(value, what = "URL", strict = true) {
28
+ let url;
29
+ try {
30
+ url = new URL(value);
31
+ } catch {
32
+ throw new Error(`${what} is not a valid URL: ${value}`);
33
+ }
34
+ const local = url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname);
35
+ if (url.protocol !== "https:" && !local) {
36
+ throw new Error(`${what} must use https (http is allowed only for localhost): ${value}`);
37
+ }
38
+ if (url.username || url.password) throw new Error(`${what} must not contain credentials: ${value}`);
39
+ const checked = strict ? value : value.split(/[?#]/)[0] ?? "";
40
+ if (SHELL_UNSAFE.test(checked)) throw new Error(`${what} contains characters that are not allowed: ${value}`);
41
+ return value;
42
+ }
43
+ function serverUrl(flag, env = process.env) {
44
+ let url = DEFAULT_URL;
45
+ if (flag) url = flag.replace(/\/$/, "");
46
+ else if (env.WORFILO_MCP_URL) url = env.WORFILO_MCP_URL.replace(/\/$/, "");
47
+ else if (env.WORFILO_API_URL) url = `${env.WORFILO_API_URL.replace(/\/$/, "")}/mcp`;
48
+ return checkUrl(url, "The Worfilo server URL");
49
+ }
50
+ function log(message) {
51
+ process.stderr.write(`${message}
52
+ `);
53
+ }
54
+
55
+ // src/auth.ts
56
+ var REFRESH_MARGIN_MS = 6e4;
57
+ function defaultStorePath(env = process.env, platform = process.platform) {
58
+ if (env.WORFILO_CONFIG_DIR) return join(env.WORFILO_CONFIG_DIR, "credentials.json");
59
+ if (platform === "win32" && env.APPDATA) return join(env.APPDATA, "worfilo", "credentials.json");
60
+ return join(env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "worfilo", "credentials.json");
61
+ }
62
+ var TokenStore = class {
63
+ constructor(path) {
64
+ this.path = path;
65
+ }
66
+ readAll() {
67
+ if (!existsSync(this.path)) return {};
68
+ try {
69
+ return JSON.parse(readFileSync(this.path, "utf8"));
70
+ } catch {
71
+ return {};
72
+ }
73
+ }
74
+ get(url) {
75
+ return this.readAll()[url] ?? {};
76
+ }
77
+ set(url, tokens) {
78
+ mkdirSync(dirname(this.path), { recursive: true, mode: 448 });
79
+ writeFileSync(this.path, `${JSON.stringify({ ...this.readAll(), [url]: tokens }, null, 2)}
80
+ `, { mode: 384 });
81
+ chmodSync(this.path, 384);
82
+ }
83
+ delete(url) {
84
+ const all = this.readAll();
85
+ if (!(url in all)) return;
86
+ delete all[url];
87
+ writeFileSync(this.path, `${JSON.stringify(all, null, 2)}
88
+ `, { mode: 384 });
89
+ }
90
+ };
91
+ var b64url = (buffer) => buffer.toString("base64url");
92
+ function pkce() {
93
+ const verifier = b64url(randomBytes(48));
94
+ return { verifier, challenge: b64url(createHash("sha256").update(verifier).digest()) };
95
+ }
96
+ async function firstJson(fetchFn, urls) {
97
+ for (const url of urls) {
98
+ try {
99
+ const response = await fetchFn(url, { headers: { Accept: "application/json" } });
100
+ if (response.ok) return await response.json();
101
+ } catch {
102
+ }
103
+ }
104
+ return void 0;
105
+ }
106
+ async function discover(url, fetchFn) {
107
+ const resource = new URL(url);
108
+ const path = resource.pathname === "/" ? "" : resource.pathname;
109
+ const prm = await firstJson(fetchFn, [
110
+ `${resource.origin}/.well-known/oauth-protected-resource${path}`,
111
+ `${resource.origin}/.well-known/oauth-protected-resource`
112
+ ]);
113
+ const servers = prm?.authorization_servers;
114
+ const issuer = new URL(
115
+ checkUrl(Array.isArray(servers) && typeof servers[0] === "string" ? servers[0] : resource.origin, "The authorization server")
116
+ );
117
+ const issuerPath = issuer.pathname === "/" ? "" : issuer.pathname.replace(/\/$/, "");
118
+ const metadata = await firstJson(fetchFn, [
119
+ `${issuer.origin}/.well-known/oauth-authorization-server${issuerPath}`,
120
+ ...issuerPath ? [`${issuer.origin}${issuerPath}/.well-known/oauth-authorization-server`] : [],
121
+ `${issuer.origin}/.well-known/openid-configuration${issuerPath}`
122
+ ]);
123
+ if (!metadata?.authorization_endpoint || !metadata.token_endpoint) {
124
+ throw new Error(`Could not find the sign-in endpoints for ${url}. Check the URL, or that the server is up.`);
125
+ }
126
+ for (const key of ["authorization_endpoint", "token_endpoint", "registration_endpoint", "revocation_endpoint"]) {
127
+ const value = metadata[key];
128
+ if (value !== void 0) checkUrl(String(value), `The server's ${key}`);
129
+ }
130
+ return metadata;
131
+ }
132
+ async function postForm(fetchFn, url, form) {
133
+ const response = await fetchFn(url, {
134
+ method: "POST",
135
+ headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
136
+ body: new URLSearchParams(form).toString()
137
+ });
138
+ const body = await response.json().catch(() => ({}));
139
+ if (!response.ok) {
140
+ throw new Error(String(body.error_description ?? body.error ?? `sign-in failed with ${response.status}`));
141
+ }
142
+ return body;
143
+ }
144
+ function toTokens(clientId, body) {
145
+ return {
146
+ clientId,
147
+ accessToken: String(body.access_token),
148
+ refreshToken: body.refresh_token ? String(body.refresh_token) : void 0,
149
+ expiresAt: Date.now() + Number(body.expires_in ?? 3600) * 1e3,
150
+ scope: body.scope ? String(body.scope) : void 0
151
+ };
152
+ }
153
+ var DONE_PAGE = (message) => `<!doctype html><meta charset="utf-8"><title>Worfilo</title><body style="font:16px system-ui;padding:3rem;max-width:32rem;margin:auto"><h1 style="font-size:1.25rem">${message}</h1><p>You can close this tab and return to your editor.</p></body>`;
154
+ async function login(url, deps) {
155
+ const metadata = await discover(url, deps.fetch);
156
+ const { verifier, challenge } = pkce();
157
+ const state = b64url(randomBytes(16));
158
+ let settle;
159
+ const received = new Promise((resolve, reject) => settle = { resolve, reject });
160
+ const server = createServer((request, response) => {
161
+ const query = new URL(request.url ?? "/", "http://127.0.0.1").searchParams;
162
+ if (!request.url?.startsWith("/callback")) {
163
+ response.writeHead(404).end();
164
+ return;
165
+ }
166
+ const error = query.get("error");
167
+ const ok = !error && query.get("state") === state && query.get("code");
168
+ response.writeHead(ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8" });
169
+ response.end(DONE_PAGE(ok ? "Worfilo is connected" : "Worfilo was not connected"));
170
+ if (error) settle?.reject(new Error(query.get("error_description") ?? error));
171
+ else if (query.get("state") !== state) settle?.reject(new Error("The sign-in response did not match this request."));
172
+ else settle?.resolve(query.get("code") ?? "");
173
+ });
174
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
175
+ const address = server.address();
176
+ const redirectUri = `http://127.0.0.1:${typeof address === "object" && address ? address.port : 0}/callback`;
177
+ try {
178
+ let clientId = deps.store.get(url).clientId;
179
+ if (!clientId) {
180
+ if (!metadata.registration_endpoint) throw new Error("This server does not support client registration.");
181
+ const response = await deps.fetch(metadata.registration_endpoint, {
182
+ method: "POST",
183
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
184
+ body: JSON.stringify({
185
+ client_name: `Worfilo MCP CLI ${VERSION}`,
186
+ redirect_uris: [redirectUri],
187
+ grant_types: ["authorization_code", "refresh_token"],
188
+ response_types: ["code"],
189
+ token_endpoint_auth_method: "none"
190
+ })
191
+ });
192
+ const body = await response.json();
193
+ if (!response.ok) throw new Error(String(body.error_description ?? "client registration failed"));
194
+ clientId = String(body.client_id);
195
+ }
196
+ const authorize = new URL(metadata.authorization_endpoint);
197
+ for (const [key, value] of Object.entries({
198
+ response_type: "code",
199
+ client_id: clientId,
200
+ redirect_uri: redirectUri,
201
+ code_challenge: challenge,
202
+ code_challenge_method: "S256",
203
+ state,
204
+ resource: url
205
+ })) {
206
+ authorize.searchParams.set(key, value);
207
+ }
208
+ deps.log(`Opening your browser to sign in to Worfilo. If it does not open, visit:
209
+ ${authorize.toString()}`);
210
+ deps.open(authorize.toString());
211
+ const timeout = new Promise(
212
+ (_, reject) => setTimeout(() => reject(new Error("Timed out waiting for sign-in.")), deps.timeoutMs ?? 5 * 6e4).unref()
213
+ );
214
+ const code = await Promise.race([received, timeout]);
215
+ const tokens = toTokens(
216
+ clientId,
217
+ await postForm(deps.fetch, metadata.token_endpoint, {
218
+ grant_type: "authorization_code",
219
+ code,
220
+ code_verifier: verifier,
221
+ client_id: clientId,
222
+ redirect_uri: redirectUri,
223
+ resource: url
224
+ })
225
+ );
226
+ deps.store.set(url, tokens);
227
+ return tokens;
228
+ } finally {
229
+ server.close();
230
+ }
231
+ }
232
+ async function refresh(url, deps) {
233
+ const stored = deps.store.get(url);
234
+ if (!stored.refreshToken || !stored.clientId) return void 0;
235
+ try {
236
+ const metadata = await discover(url, deps.fetch);
237
+ const tokens = toTokens(
238
+ stored.clientId,
239
+ await postForm(deps.fetch, metadata.token_endpoint, {
240
+ grant_type: "refresh_token",
241
+ refresh_token: stored.refreshToken,
242
+ client_id: stored.clientId,
243
+ resource: url
244
+ })
245
+ );
246
+ deps.store.set(url, tokens);
247
+ return tokens;
248
+ } catch (error) {
249
+ deps.log(`Could not refresh the Worfilo session: ${error.message}`);
250
+ deps.store.set(url, { clientId: stored.clientId });
251
+ return void 0;
252
+ }
253
+ }
254
+ async function accessToken(url, deps) {
255
+ const stored = deps.store.get(url);
256
+ if (stored.accessToken && (stored.expiresAt ?? 0) - REFRESH_MARGIN_MS > Date.now()) return stored.accessToken;
257
+ return (await refresh(url, deps))?.accessToken;
258
+ }
259
+ async function logout(url, deps) {
260
+ const stored = deps.store.get(url);
261
+ const token = stored.refreshToken ?? stored.accessToken;
262
+ if (token) {
263
+ try {
264
+ const metadata = await discover(url, deps.fetch);
265
+ if (metadata.revocation_endpoint) await postForm(deps.fetch, metadata.revocation_endpoint, { token });
266
+ } catch {
267
+ }
268
+ }
269
+ deps.store.delete(url);
270
+ return Boolean(token);
271
+ }
272
+
273
+ // src/bridge.ts
274
+ import { createInterface } from "readline";
275
+ var Bridge = class {
276
+ constructor(url, deps, write) {
277
+ this.url = url;
278
+ this.deps = deps;
279
+ this.write = write;
280
+ }
281
+ signingIn;
282
+ protocolVersion;
283
+ async token() {
284
+ const token = await accessToken(this.url, this.deps);
285
+ if (token) return token;
286
+ this.signingIn ??= login(this.url, this.deps).finally(() => this.signingIn = void 0);
287
+ await this.signingIn;
288
+ return await accessToken(this.url, this.deps) ?? "";
289
+ }
290
+ post(message, token) {
291
+ const headers = {
292
+ "Content-Type": "application/json",
293
+ Accept: "application/json, text/event-stream",
294
+ Authorization: `Bearer ${token}`
295
+ };
296
+ if (this.protocolVersion) headers["MCP-Protocol-Version"] = this.protocolVersion;
297
+ return this.deps.fetch(this.url, { method: "POST", headers, body: JSON.stringify(message) });
298
+ }
299
+ async relay(message) {
300
+ const expectsAnswer = message.method !== void 0 && message.id !== void 0 && message.id !== null;
301
+ try {
302
+ let response = await this.post(message, await this.token());
303
+ if (response.status === 401) {
304
+ const renewed = (await refresh(this.url, this.deps))?.accessToken ?? await this.token();
305
+ response = await this.post(message, renewed);
306
+ }
307
+ if (response.status === 202 || !expectsAnswer) return;
308
+ const type = response.headers.get("content-type") ?? "";
309
+ const text = await response.text();
310
+ const answers = type.includes("text/event-stream") ? fromEventStream(text) : [text];
311
+ for (const answer of answers) {
312
+ if (message.method === "initialize") this.rememberVersion(answer);
313
+ this.write(answer);
314
+ }
315
+ if (!answers.length) this.fail(message, `Worfilo answered ${response.status} with no body.`);
316
+ } catch (error) {
317
+ if (expectsAnswer) this.fail(message, error.message);
318
+ }
319
+ }
320
+ rememberVersion(answer) {
321
+ try {
322
+ this.protocolVersion = JSON.parse(answer).result?.protocolVersion;
323
+ } catch {
324
+ }
325
+ }
326
+ fail(message, detail) {
327
+ this.write(JSON.stringify({ jsonrpc: "2.0", id: message.id, error: { code: -32603, message: `Worfilo: ${detail}` } }));
328
+ }
329
+ };
330
+ function fromEventStream(text) {
331
+ return text.split(/\r?\n\r?\n/).map(
332
+ (event) => event.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n")
333
+ ).filter(Boolean);
334
+ }
335
+ async function serve(url, deps) {
336
+ const bridge = new Bridge(url, deps, (line) => process.stdout.write(`${line}
337
+ `));
338
+ const pending = /* @__PURE__ */ new Set();
339
+ const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
340
+ for await (const line of lines) {
341
+ if (!line.trim()) continue;
342
+ let message;
343
+ try {
344
+ message = JSON.parse(line);
345
+ } catch {
346
+ deps.log("Ignored a line that is not JSON.");
347
+ continue;
348
+ }
349
+ const task = bridge.relay(message).finally(() => pending.delete(task));
350
+ pending.add(task);
351
+ }
352
+ await Promise.all(pending);
353
+ }
354
+
355
+ // src/browser.ts
356
+ import { spawn } from "child_process";
357
+ function browserCommand(url, platform = process.platform) {
358
+ checkUrl(url, "The sign-in URL", false);
359
+ if (platform === "darwin") return ["open", [url]];
360
+ if (platform === "win32") return ["rundll32", ["url.dll,FileProtocolHandler", url]];
361
+ return ["xdg-open", [url]];
362
+ }
363
+ function openBrowser(url) {
364
+ try {
365
+ const [command, args] = browserCommand(url);
366
+ spawn(command, args, { stdio: "ignore", detached: true }).on("error", () => void 0).unref();
367
+ } catch {
368
+ }
369
+ }
370
+
371
+ // src/clients.ts
372
+ import { join as join2 } from "path";
373
+
374
+ // src/skill.ts
375
+ import { readFileSync as readFileSync2 } from "fs";
376
+ function parseSkill(raw) {
377
+ const match = /^---\n([\s\S]*?)\n---\n+([\s\S]*)$/.exec(raw);
378
+ if (!match) throw new Error("SKILL.md has no frontmatter");
379
+ const field = (key) => new RegExp(`^${key}:\\s*(.+)$`, "m").exec(match[1] ?? "")?.[1]?.trim() ?? "";
380
+ return { name: field("name"), description: field("description"), body: match[2] ?? "", raw };
381
+ }
382
+ function loadSkill() {
383
+ return parseSkill(readFileSync2(new URL("../skills/worfilo-workflows/SKILL.md", import.meta.url), "utf8"));
384
+ }
385
+ var frontmatter = (fields) => `---
386
+ ${Object.entries(fields).map(([key, value]) => `${key}: ${typeof value === "string" ? JSON.stringify(value) : value}`).join("\n")}
387
+ ---
388
+
389
+ `;
390
+ var variants = {
391
+ claudeSkill: (skill) => skill.raw,
392
+ cursorRule: (skill) => frontmatter({ description: skill.description, alwaysApply: false }) + skill.body,
393
+ plainCommand: (skill) => skill.body,
394
+ modelDecisionRule: (skill) => frontmatter({ trigger: "model_decision", description: skill.description }) + skill.body,
395
+ antigravityWorkflow: (skill) => frontmatter({ description: skill.description }) + skill.body,
396
+ vscodePrompt: (skill) => frontmatter({ description: skill.description, mode: "agent" }) + skill.body
397
+ };
398
+
399
+ // src/clients.ts
400
+ var bridgeEntry = (url) => ({
401
+ command: "npx",
402
+ args: ["-y", `${PACKAGE}@latest`, "serve", "--url", url]
403
+ });
404
+ function vscodeUserDir(place2) {
405
+ if (place2.platform === "win32") return join2(place2.appData ?? join2(place2.home, "AppData", "Roaming"), "Code", "User");
406
+ if (place2.platform === "darwin") return join2(place2.home, "Library", "Application Support", "Code", "User");
407
+ return join2(place2.home, ".config", "Code", "User");
408
+ }
409
+ var userOnly = (name) => ({
410
+ kind: "note",
411
+ text: `${name} reads the guide per project; run install with --scope project inside a repo to add it there.`
412
+ });
413
+ var CLIENTS = [
414
+ {
415
+ id: "claude-code",
416
+ name: "Claude Code",
417
+ detect: (place2, { onPath: onPath2, exists }) => onPath2("claude") || exists(join2(place2.home, ".claude")),
418
+ plan: (place2, options) => {
419
+ const entry = options.bridge ? bridgeEntry(options.url) : { type: "http", url: options.url };
420
+ const steps = options.scope === "project" ? [{ kind: "server", path: join2(place2.cwd, ".mcp.json"), key: "mcpServers", entry }] : [
421
+ {
422
+ kind: "command",
423
+ argv: options.bridge ? ["claude", "mcp", "add", "--scope", "user", SERVER_NAME, "--", "npx", "-y", `${PACKAGE}@latest`, "serve", "--url", options.url] : ["claude", "mcp", "add", "--transport", "http", "--scope", "user", SERVER_NAME, options.url],
424
+ undo: ["claude", "mcp", "remove", "--scope", "user", SERVER_NAME]
425
+ }
426
+ ];
427
+ if (options.withSkill) {
428
+ const root = options.scope === "project" ? place2.cwd : place2.home;
429
+ steps.push({
430
+ kind: "file",
431
+ path: join2(root, ".claude", "skills", SKILL_NAME, "SKILL.md"),
432
+ content: variants.claudeSkill(options.skill),
433
+ removeDir: true
434
+ });
435
+ }
436
+ steps.push({ kind: "note", text: `In Claude Code, run /mcp and choose ${SERVER_NAME} to sign in. Then use /${SKILL_NAME}.` });
437
+ return steps;
438
+ }
439
+ },
440
+ {
441
+ id: "cursor",
442
+ name: "Cursor",
443
+ detect: (place2, { onPath: onPath2, exists }) => onPath2("cursor") || exists(join2(place2.home, ".cursor")),
444
+ plan: (place2, options) => {
445
+ const root = options.scope === "project" ? place2.cwd : place2.home;
446
+ const entry = options.bridge ? bridgeEntry(options.url) : { url: options.url };
447
+ const steps = [{ kind: "server", path: join2(root, ".cursor", "mcp.json"), key: "mcpServers", entry }];
448
+ if (options.withSkill) {
449
+ steps.push({ kind: "file", path: join2(root, ".cursor", "commands", `${SKILL_NAME}.md`), content: variants.plainCommand(options.skill) });
450
+ if (options.scope === "project") {
451
+ steps.push({ kind: "file", path: join2(root, ".cursor", "rules", `${SKILL_NAME}.mdc`), content: variants.cursorRule(options.skill) });
452
+ }
453
+ }
454
+ steps.push({ kind: "note", text: `In Cursor, open Settings > MCP and select Connect on ${SERVER_NAME} to sign in. Then use /${SKILL_NAME}.` });
455
+ return steps;
456
+ }
457
+ },
458
+ {
459
+ id: "antigravity",
460
+ name: "Antigravity",
461
+ detect: (place2, { onPath: onPath2, exists }) => onPath2("antigravity") || exists(join2(place2.home, ".gemini", "antigravity")),
462
+ plan: (place2, options) => {
463
+ const entry = options.bridge ? bridgeEntry(options.url) : { serverUrl: options.url };
464
+ const steps = [
465
+ { kind: "server", path: join2(place2.home, ".gemini", "antigravity", "mcp_config.json"), key: "mcpServers", entry }
466
+ ];
467
+ if (options.withSkill) {
468
+ if (options.scope === "project") {
469
+ steps.push(
470
+ { kind: "file", path: join2(place2.cwd, ".agent", "rules", `${SKILL_NAME}.md`), content: variants.modelDecisionRule(options.skill) },
471
+ { kind: "file", path: join2(place2.cwd, ".agent", "workflows", `${SKILL_NAME}.md`), content: variants.antigravityWorkflow(options.skill) }
472
+ );
473
+ } else {
474
+ steps.push(userOnly("Antigravity"));
475
+ }
476
+ }
477
+ steps.push({ kind: "note", text: `In Antigravity, refresh the MCP servers panel and sign in when the browser opens. Then use /${SKILL_NAME}.` });
478
+ return steps;
479
+ }
480
+ },
481
+ {
482
+ id: "vscode",
483
+ name: "VS Code",
484
+ detect: (place2, { onPath: onPath2, exists }) => onPath2("code") || exists(vscodeUserDir(place2)),
485
+ plan: (place2, options) => {
486
+ const entry = options.bridge ? { type: "stdio", ...bridgeEntry(options.url) } : { type: "http", url: options.url };
487
+ const path = options.scope === "project" ? join2(place2.cwd, ".vscode", "mcp.json") : join2(vscodeUserDir(place2), "mcp.json");
488
+ const steps = [{ kind: "server", path, key: "servers", entry }];
489
+ if (options.withSkill) {
490
+ steps.push(
491
+ options.scope === "project" ? { kind: "file", path: join2(place2.cwd, ".github", "prompts", `${SKILL_NAME}.prompt.md`), content: variants.vscodePrompt(options.skill) } : { kind: "file", path: join2(vscodeUserDir(place2), "prompts", `${SKILL_NAME}.prompt.md`), content: variants.vscodePrompt(options.skill) }
492
+ );
493
+ }
494
+ steps.push({ kind: "note", text: `In VS Code, open mcp.json and select Start on ${SERVER_NAME} to sign in. Then use /${SKILL_NAME} in agent mode.` });
495
+ return steps;
496
+ }
497
+ },
498
+ {
499
+ id: "windsurf",
500
+ name: "Windsurf",
501
+ detect: (place2, { onPath: onPath2, exists }) => onPath2("windsurf") || exists(join2(place2.home, ".codeium", "windsurf")),
502
+ plan: (place2, options) => {
503
+ const entry = options.bridge ? bridgeEntry(options.url) : { serverUrl: options.url };
504
+ const steps = [
505
+ { kind: "server", path: join2(place2.home, ".codeium", "windsurf", "mcp_config.json"), key: "mcpServers", entry }
506
+ ];
507
+ if (options.withSkill) {
508
+ steps.push(
509
+ options.scope === "project" ? { kind: "file", path: join2(place2.cwd, ".windsurf", "rules", `${SKILL_NAME}.md`), content: variants.modelDecisionRule(options.skill) } : userOnly("Windsurf")
510
+ );
511
+ }
512
+ steps.push({ kind: "note", text: `In Windsurf, refresh MCP servers in Cascade and sign in when asked.` });
513
+ return steps;
514
+ }
515
+ }
516
+ ];
517
+ var CLIENT_IDS = CLIENTS.map((client) => client.id);
518
+ function findClient(id) {
519
+ const aliases = { claude: "claude-code", code: "vscode", "vs-code": "vscode", gemini: "antigravity" };
520
+ const wanted = aliases[id] ?? id;
521
+ return CLIENTS.find((client) => client.id === wanted);
522
+ }
523
+
524
+ // src/files.ts
525
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
526
+ import { dirname as dirname2 } from "path";
527
+ var ConfigError = class extends Error {
528
+ };
529
+ function readJson(path) {
530
+ if (!existsSync2(path)) return {};
531
+ const text = readFileSync3(path, "utf8");
532
+ if (!text.trim()) return {};
533
+ try {
534
+ const parsed = JSON.parse(text);
535
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
536
+ } catch {
537
+ }
538
+ throw new ConfigError(`${path} is not plain JSON, so it was left untouched`);
539
+ }
540
+ function withServer(config, key, name, entry) {
541
+ const servers = config[key] && typeof config[key] === "object" ? config[key] : {};
542
+ return { ...config, [key]: { ...servers, [name]: entry } };
543
+ }
544
+ function withoutServer(config, key, name) {
545
+ const servers = config[key] && typeof config[key] === "object" ? { ...config[key] } : {};
546
+ delete servers[name];
547
+ return { ...config, [key]: servers };
548
+ }
549
+ function writeText(path, content) {
550
+ mkdirSync2(dirname2(path), { recursive: true });
551
+ writeFileSync2(path, content);
552
+ }
553
+ function writeJson(path, value) {
554
+ writeText(path, `${JSON.stringify(value, null, 2)}
555
+ `);
556
+ }
557
+ function remove(path) {
558
+ if (!existsSync2(path)) return false;
559
+ rmSync(path, { recursive: true, force: true });
560
+ return true;
561
+ }
562
+
563
+ // src/cli.ts
564
+ var HELP = `${PACKAGE} ${VERSION}: connect coding agents to Worfilo
565
+
566
+ Usage
567
+ npx ${PACKAGE} install [options] Add Worfilo and the /worfilo-workflows guide to your editors
568
+ npx ${PACKAGE} uninstall [options] Remove them again
569
+ npx ${PACKAGE} login Sign in for the local bridge
570
+ npx ${PACKAGE} logout Sign out and revoke the bridge's tokens
571
+ npx ${PACKAGE} status Show the bridge's sign-in state
572
+ npx ${PACKAGE} serve Run the stdio bridge (editors start this for you)
573
+ npx ${PACKAGE} skill Print the workflow guide
574
+
575
+ Options
576
+ --client <ids> ${CLIENT_IDS.join(", ")} (comma separated; default: detected)
577
+ --scope <scope> project (this repo, default) or user (every project)
578
+ --url <url> MCP server URL (default: WORFILO_MCP_URL, WORFILO_API_URL/mcp, or production)
579
+ --bridge Connect through the local stdio bridge instead of over HTTP
580
+ --no-skill Skip the /worfilo-workflows guide
581
+ --dry-run Show what would change without writing anything
582
+ --yes Do not ask; use detected editors
583
+
584
+ Docs: ${DOCS_URL}`;
585
+ function onPath(bin) {
586
+ const extensions = process.platform === "win32" ? ["", ".cmd", ".exe", ".bat"] : [""];
587
+ return (process.env.PATH ?? "").split(delimiter).some((dir) => dir && extensions.some((extension) => existsSync3(join3(dir, bin + extension))));
588
+ }
589
+ function authDeps() {
590
+ return { fetch: globalThis.fetch, store: new TokenStore(defaultStorePath()), open: openBrowser, log };
591
+ }
592
+ var place = () => ({ cwd: process.cwd(), home: homedir2(), platform: process.platform, appData: process.env.APPDATA });
593
+ var shown = (path) => path.startsWith(process.cwd()) ? relative(process.cwd(), path) || "." : path.replace(homedir2(), "~");
594
+ async function chooseClients(ids, yes) {
595
+ if (ids) {
596
+ return ids.split(",").map((id) => {
597
+ const client = findClient(id.trim());
598
+ if (!client) throw new Error(`Unknown client '${id}'. Choose from: ${CLIENT_IDS.join(", ")}.`);
599
+ return client;
600
+ });
601
+ }
602
+ const probe = { onPath, exists: existsSync3 };
603
+ const detected = CLIENTS.filter((client) => client.detect(place(), probe));
604
+ if (yes || !process.stdin.isTTY) {
605
+ if (!detected.length) throw new Error(`No supported editor found. Pass --client with one of: ${CLIENT_IDS.join(", ")}.`);
606
+ return detected;
607
+ }
608
+ log("Which editors should use Worfilo?");
609
+ CLIENTS.forEach((client, index) => log(` ${index + 1}. ${client.name}${detected.includes(client) ? " (detected)" : ""}`));
610
+ const prompt = createInterface2({ input: process.stdin, output: process.stderr });
611
+ const fallback = detected.map((client) => CLIENTS.indexOf(client) + 1).join(",");
612
+ const answer = (await prompt.question(`Numbers, comma separated [${fallback || "1"}]: `)).trim() || fallback || "1";
613
+ prompt.close();
614
+ const chosen = answer.split(",").map((part) => CLIENTS[Number(part.trim()) - 1]).filter((client) => Boolean(client));
615
+ if (!chosen.length) throw new Error("No editor chosen.");
616
+ return chosen;
617
+ }
618
+ function apply(step, dryRun, undo) {
619
+ const prefix = dryRun ? "would " : "";
620
+ switch (step.kind) {
621
+ case "server": {
622
+ try {
623
+ const current = readJson(step.path);
624
+ const next = undo ? withoutServer(current, step.key, SERVER_NAME) : withServer(current, step.key, SERVER_NAME, step.entry);
625
+ if (!dryRun) writeJson(step.path, next);
626
+ return `${prefix}${undo ? "remove" : "add"} ${SERVER_NAME} in ${shown(step.path)}`;
627
+ } catch (error) {
628
+ if (!(error instanceof ConfigError)) throw error;
629
+ return `${error.message}. Add this under "${step.key}" by hand:
630
+ "${SERVER_NAME}": ${JSON.stringify(step.entry)}`;
631
+ }
632
+ }
633
+ case "file": {
634
+ if (undo) {
635
+ const path = step.removeDir ? join3(step.path, "..") : step.path;
636
+ if (!dryRun) remove(path);
637
+ return `${prefix}remove ${shown(path)}`;
638
+ }
639
+ if (!dryRun) writeText(step.path, step.content);
640
+ return `${prefix}write ${shown(step.path)}`;
641
+ }
642
+ case "command": {
643
+ const argv = undo ? step.undo : step.argv;
644
+ const [bin, ...args] = argv;
645
+ if (!bin || !onPath(bin)) return `run this yourself: ${argv.join(" ")}`;
646
+ if (dryRun) return `would run: ${argv.join(" ")}`;
647
+ const result = spawnSync(bin, args, { stdio: "inherit", shell: process.platform === "win32" });
648
+ return result.status === 0 ? `ran: ${argv.join(" ")}` : `this failed, run it yourself: ${argv.join(" ")}`;
649
+ }
650
+ case "note":
651
+ return void 0;
652
+ }
653
+ }
654
+ async function install(values, undo) {
655
+ const scope = values.scope ?? "project";
656
+ if (scope !== "project" && scope !== "user") throw new Error("--scope is project or user.");
657
+ const url = serverUrl(values.url);
658
+ const clients = await chooseClients(values.client, Boolean(values.yes));
659
+ const options = { url, scope, skill: loadSkill(), withSkill: !values["no-skill"], bridge: Boolean(values.bridge) };
660
+ const notes = [];
661
+ for (const client of clients) {
662
+ log(`
663
+ ${client.name}`);
664
+ for (const step of client.plan(place(), options)) {
665
+ if (step.kind === "note") {
666
+ if (!undo) notes.push(`${client.name}: ${step.text}`);
667
+ continue;
668
+ }
669
+ const line = apply(step, Boolean(values["dry-run"]), undo);
670
+ if (line) log(` ${line}`);
671
+ }
672
+ }
673
+ if (undo) {
674
+ log(`
675
+ Removed. To revoke access too, run: npx ${PACKAGE} logout, and disconnect the agent under API keys in Worfilo.`);
676
+ return;
677
+ }
678
+ log(`
679
+ Worfilo MCP server: ${url}`);
680
+ for (const note of notes) log(`- ${note}`);
681
+ if (scope === "project") log("- Commit the new files to share the setup with your team; no secrets were written.");
682
+ }
683
+ async function main(argv) {
684
+ const { values, positionals } = parseArgs({
685
+ args: argv,
686
+ allowPositionals: true,
687
+ options: {
688
+ client: { type: "string" },
689
+ scope: { type: "string" },
690
+ url: { type: "string" },
691
+ bridge: { type: "boolean" },
692
+ "no-skill": { type: "boolean" },
693
+ "dry-run": { type: "boolean" },
694
+ yes: { type: "boolean", short: "y" },
695
+ help: { type: "boolean", short: "h" },
696
+ version: { type: "boolean", short: "v" }
697
+ }
698
+ });
699
+ if (values.version) {
700
+ log(VERSION);
701
+ return 0;
702
+ }
703
+ const command = positionals[0] ?? (values.help ? "help" : process.stdin.isTTY ? "install" : "serve");
704
+ const url = serverUrl(values.url);
705
+ switch (command) {
706
+ case "install":
707
+ case "uninstall":
708
+ await install(values, command === "uninstall");
709
+ return 0;
710
+ case "serve":
711
+ await serve(url, authDeps());
712
+ return 0;
713
+ case "login": {
714
+ const tokens = await login(url, authDeps());
715
+ log(`Signed in to ${url} with: ${tokens.scope ?? "all permissions"}`);
716
+ return 0;
717
+ }
718
+ case "logout":
719
+ log(await logout(url, authDeps()) ? `Signed out of ${url}.` : `Not signed in to ${url}.`);
720
+ return 0;
721
+ case "status": {
722
+ const tokens = new TokenStore(defaultStorePath()).get(url);
723
+ if (!tokens.refreshToken && !tokens.accessToken) log(`Not signed in to ${url}. Run: npx ${PACKAGE} login`);
724
+ else log(`Signed in to ${url}
725
+ Permissions: ${tokens.scope ?? "unknown"}
726
+ Token refreshes: ${tokens.refreshToken ? "yes" : "no"}`);
727
+ return 0;
728
+ }
729
+ case "skill":
730
+ process.stdout.write(loadSkill().raw);
731
+ return 0;
732
+ case "help":
733
+ log(HELP);
734
+ return 0;
735
+ default:
736
+ log(`Unknown command '${command}'.
737
+
738
+ ${HELP}`);
739
+ return 1;
740
+ }
741
+ }
742
+ main(process.argv.slice(2)).then(
743
+ (code) => {
744
+ process.exitCode = code;
745
+ },
746
+ (error) => {
747
+ log(`Error: ${error.message}`);
748
+ process.exitCode = 1;
749
+ }
750
+ );
751
+ export {
752
+ main
753
+ };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@worfilo/mcp",
3
+ "version": "0.2.0",
4
+ "description": "Connect Claude Code, Cursor, Antigravity and other coding agents to Worfilo, so they can plan, build and integrate workflows.",
5
+ "keywords": [
6
+ "worfilo",
7
+ "mcp",
8
+ "model-context-protocol",
9
+ "claude-code",
10
+ "cursor",
11
+ "antigravity",
12
+ "workflows",
13
+ "ai-agents"
14
+ ],
15
+ "homepage": "https://github.com/Worfilo/mcp#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/Worfilo/mcp/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/Worfilo/mcp.git"
22
+ },
23
+ "license": "MIT",
24
+ "type": "module",
25
+ "bin": {
26
+ "worfilo-mcp": "dist/cli.js"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "skills"
31
+ ],
32
+ "engines": {
33
+ "node": ">=20"
34
+ },
35
+ "scripts": {
36
+ "build": "tsup",
37
+ "typecheck": "tsc --noEmit",
38
+ "test": "vitest run",
39
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^22.0.0",
43
+ "tsup": "^8.5.0",
44
+ "typescript": "^5.9.0",
45
+ "vitest": "^5.0.2"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public",
49
+ "provenance": true
50
+ }
51
+ }
@@ -0,0 +1,78 @@
1
+ ---
2
+ name: worfilo-workflows
3
+ description: Plan, build, test, publish and integrate Worfilo AI workflows into this codebase through the Worfilo MCP server. Use when the user wants to automate a task, build an AI agent workflow, or call a Worfilo workflow from their code.
4
+ ---
5
+
6
+ # Building Worfilo workflows
7
+
8
+ Worfilo runs AI agent workflows: a trigger, then nodes (agents, app actions, HTTP calls, logic) joined by edges. You build them through the Worfilo MCP tools, then call them from the user's code through the public API.
9
+
10
+ ## Procedure
11
+
12
+ 1. **Understand the goal.** Ask what should start the workflow, what it must produce, and which apps it touches. Read the codebase to see where it will be called from and what input it can send.
13
+ 2. **Discover.** Call `list_integrations` to see connected apps, credentials, custom APIs and MCP servers. Call `list_node_types`, then `get_node_type` for each type you plan to use. Never invent node types, ports or config fields.
14
+ 3. **Plan.** Call `plan_workflow` with a precise description. It drafts a graph from the account's real integrations. Show the user the `steps` in plain words and agree on changes before saving. For revisions, pass `previous_graph` and an `instruction`.
15
+ 4. **Create.** Call `create_workflow` with the agreed graph. Fix every `error` in `issues` with `update_workflow` (it replaces the whole draft graph). Warnings do not block.
16
+ 5. **Test.** Call `run_workflow` on the draft with a realistic `input`. If it fails, read `failed_nodes`, fix the graph, and run again. Runs can call real apps and spend tokens, so say what a test run will do first.
17
+ 6. **Publish.** Ask the user, then call `publish_workflow`. It activates the new version by default, which is what the API runs.
18
+ 7. **Integrate.**
19
+ - Call `create_api_key` scoped to this workflow only, and write the key to an untracked env file as `WORFILO_API_KEY`. Check `.gitignore` covers it, and never print the key back or commit it.
20
+ - Call `get_integration_snippet` in the codebase's language, then adapt it to the project's HTTP client, types, config loading and error handling.
21
+ - Handle both answers: 200 means finished, and 202 means poll `status_url`.
22
+ 8. **Hand over.** Tell the user the workflow name, the `editor_url`, where the call site lives, and which env var to set in each deployment.
23
+
24
+ ## The graph
25
+
26
+ ```json
27
+ {
28
+ "schema_version": 1,
29
+ "nodes": [
30
+ {"id": "trigger", "type": "manual_trigger", "name": "Trigger", "position": {"x": 0, "y": 0},
31
+ "config": {"test_payload": {"message": "Summarise this"}}},
32
+ {"id": "summarise", "type": "agent", "name": "Summarise", "position": {"x": 260, "y": 0},
33
+ "config": {"credential_id": "<an anthropic credential_id>", "model": "claude-haiku-4-5-20251001",
34
+ "user_prompt": "Summarise: {{ trigger.message }}"}},
35
+ {"id": "result", "type": "output", "name": "Result", "position": {"x": 520, "y": 0},
36
+ "config": {"value": "{{ nodes.summarise.output.text }}"}}
37
+ ],
38
+ "edges": [
39
+ {"id": "e1", "source": "trigger", "target": "summarise"},
40
+ {"id": "e2", "source": "summarise", "target": "result"}
41
+ ],
42
+ "settings": {}
43
+ }
44
+ ```
45
+
46
+ The example shows the shape only. Take each node's exact config fields from `get_node_type`.
47
+
48
+ ## Rules
49
+
50
+ - **Trigger.** Exactly one trigger node. Use `manual_trigger` for workflows called from code through the API, with a realistic `config.test_payload`. `webhook_trigger` and `schedule_trigger` exist for other starts.
51
+ - **Ids and names.** Node ids are unique lowercase slugs matching `^[a-z][a-z0-9_]*$`. Names are short Title Case labels.
52
+ - **Config.** Every config must satisfy its node type's `config_schema`, required fields included.
53
+ - **Edges.** An edge goes from an output port to an input port. Omit ports for the defaults (`out` to `in`).
54
+ - **Branching.**
55
+ - An `if` node fires `true` or `false`; connect each branch with `source_port` set to that name.
56
+ - A `switch` node's `config.cases` each name a port, plus `default`.
57
+ - When branches rejoin, route them through a `merge` node.
58
+ - **Flow.** Data flows forward only: no cycles, and every node is reachable from the trigger. End with an `output` node, whose value is what the API returns.
59
+ - **Templates.**
60
+ - `{{ trigger.<field> }}` reads the trigger payload. `{{ nodes.<id>.output.<field> }}` reads an upstream node, and only upstream nodes may be referenced.
61
+ - `if` and `switch` expressions are bare, without braces, e.g. `nodes.classify.output.structured.urgency == 'high'`.
62
+ - **Agents.**
63
+ - Set `credential_id` to an `anthropic` credential from `list_integrations`.
64
+ - An agent's output has `.text`, and `.structured` when `config.output_schema` is set. Set `output_schema` whenever later nodes branch on the answer.
65
+ - Use `claude-haiku-4-5-20251001` unless the task needs deeper reasoning, then `claude-sonnet-5`.
66
+ - **Apps.**
67
+ - GitHub, Slack, Notion, Linear and Tavily use a `connector_action` node with `connector_id`, `action` and `args`. Set `connection_id` from `list_integrations`, and prefer it over `http_request`.
68
+ - The user's own APIs use `api_request` with `api_id`, `endpoint` and `args`. Their MCP servers use `mcp_tool`.
69
+ - **Tools for agents.** An `http_request`, `connector_action` or `api_request` node can be attached to an agent with an edge of `kind: "tool"` and `target_port: "tools"`. A tool node has no other edges.
70
+ - **Secrets.** Never put keys or tokens in a graph. Reference stored credentials and connections by id. If one is missing, send the user to `connect_more` from `list_integrations`.
71
+ - **Size.** Prefer a few well-chosen nodes over many.
72
+
73
+ ## Guardrails
74
+
75
+ - Confirm the plan before `create_workflow`, and ask before `publish_workflow`, `activate_version` and `create_api_key`.
76
+ - Edit drafts freely. Published versions change only when you publish again.
77
+ - Keep API keys out of source control, logs and chat.
78
+ - If a tool says a permission was not granted, ask the user to reconnect Worfilo and allow it.