stoaken 0.4.0 → 0.5.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/README.md +18 -5
- package/dist/index.js +145 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,15 +32,28 @@ npx stoaken init --client cursor
|
|
|
32
32
|
| `copilot` | `.vscode/mcp.json` (`servers.stoaken`, VS Code agent mode) |
|
|
33
33
|
| `claude-desktop` | Nothing: prints the steps to add a custom connector (Customize > Connectors) |
|
|
34
34
|
| `v0` | Nothing: prints the steps to add a custom MCP server in v0 (+ > MCPs) |
|
|
35
|
+
| `windsurf` | `.devin/mcp_config.json` (`mcpServers.stoaken`, URL only). Windsurf is now Devin Desktop; this is for its Devin Local agent and Devin CLI |
|
|
36
|
+
| `zed` | `.zed/settings.json` (`context_servers.stoaken`, URL only; OAuth). With an API key: nothing, prints how to add it to your user settings |
|
|
37
|
+
| `gemini-cli` | `.gemini/settings.json` (`mcpServers.stoaken.httpUrl`; loads in trusted folders only) |
|
|
38
|
+
| `jetbrains` | `.junie/mcp/mcp.json` (`mcpServers.stoaken`, URL only; Junie). With an API key: nothing, prints how to add it to `~/.junie/mcp/mcp.json` |
|
|
39
|
+
| `cline` | Nothing: prints the steps for Cline's Remote Servers tab |
|
|
40
|
+
| `lovable` | Nothing: prints the steps to add a custom MCP server (Connectors > Add custom) |
|
|
41
|
+
| `bolt` | Nothing: prints the steps to add a custom connector (+ > Connectors) |
|
|
42
|
+
| `replit` | Nothing: prints a one-click Replit install link and the manual steps |
|
|
35
43
|
|
|
36
44
|
init merges: other servers in the file are kept and only the `stoaken` entry is replaced. If the
|
|
37
|
-
file isn't plain JSON (for example `.vscode/mcp.json` with comments) it is
|
|
38
|
-
No secret goes into these files, so they are safe to commit:
|
|
45
|
+
file isn't plain JSON (for example `.vscode/mcp.json` or `.zed/settings.json` with comments) it is
|
|
46
|
+
left alone and you're told. No secret goes into these files, so they are safe to commit:
|
|
39
47
|
|
|
40
48
|
- Signed in with the browser: the entry has just the URL, and the tool signs you in itself with
|
|
41
|
-
OAuth (Claude Code `/mcp`, Cursor Connect, `codex mcp login stoaken`, VS Code Start
|
|
49
|
+
OAuth (Claude Code `/mcp`, Cursor Connect, `codex mcp login stoaken`, VS Code Start,
|
|
50
|
+
`/mcp auth stoaken` in Gemini CLI, Authenticate in Devin Local, Zed's sign-in prompt, Junie's
|
|
51
|
+
`/mcp` Authorize).
|
|
42
52
|
- Pasted an API key: the entry reads the `STOAKEN_API_KEY` environment variable (VS Code instead
|
|
43
|
-
asks for the key once and stores it securely).
|
|
53
|
+
asks for the key once and stores it securely). Devin Local, Zed and Junie document no variable
|
|
54
|
+
expansion in headers, so init never writes the key for them: Devin gets a gitignored
|
|
55
|
+
`.devin/mcp_config.local.json` override (`devin mcp add -s local ... -H`), and Zed and Junie are
|
|
56
|
+
told to use a user-level file outside the repo.
|
|
44
57
|
|
|
45
58
|
## Authentication
|
|
46
59
|
|
|
@@ -101,7 +114,7 @@ Bitbucket Pipelines, Azure Pipelines and Vercel builds: see /docs/webhooks#ci an
|
|
|
101
114
|
|
|
102
115
|
## What this connects to
|
|
103
116
|
|
|
104
|
-
Once set up, any MCP-capable client (Claude Code, Claude Desktop, Cursor, Codex, Copilot, v0) or a direct HTTP client can reach Stoaken's MCP server at `/mcp`. Its main tools (full list at /docs/mcp):
|
|
117
|
+
Once set up, any MCP-capable client (Claude Code, Claude Desktop, Cursor, Codex, Copilot, v0, Devin Desktop, Zed, Gemini CLI, Junie, Cline, Lovable, Bolt, Replit) or a direct HTTP client can reach Stoaken's MCP server at `/mcp`. Its main tools (full list at /docs/mcp):
|
|
105
118
|
|
|
106
119
|
- `list_brands` -- every Brand you hold at least Viewer on.
|
|
107
120
|
- `get_theme_tokens(themeId)` -- a Theme's cascade-merged token tree.
|
package/dist/index.js
CHANGED
|
@@ -214,7 +214,7 @@ async function listBrands(serverUrl, credential) {
|
|
|
214
214
|
const transport = new StreamableHTTPClientTransport(new URL("/mcp", serverUrl), {
|
|
215
215
|
requestInit: { headers: { Authorization: `Bearer ${credential}` } }
|
|
216
216
|
});
|
|
217
|
-
const client = new Client({ name: "stoaken-cli", version: "0.
|
|
217
|
+
const client = new Client({ name: "stoaken-cli", version: "0.5.0" });
|
|
218
218
|
await client.connect(transport);
|
|
219
219
|
try {
|
|
220
220
|
const result = await client.callTool({ name: "list_brands", arguments: {} });
|
|
@@ -229,7 +229,22 @@ async function listBrands(serverUrl, credential) {
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
// src/client-config.ts
|
|
232
|
-
var CLIENTS = [
|
|
232
|
+
var CLIENTS = [
|
|
233
|
+
"claude-code",
|
|
234
|
+
"claude-desktop",
|
|
235
|
+
"cursor",
|
|
236
|
+
"codex",
|
|
237
|
+
"copilot",
|
|
238
|
+
"v0",
|
|
239
|
+
"windsurf",
|
|
240
|
+
"zed",
|
|
241
|
+
"gemini-cli",
|
|
242
|
+
"jetbrains",
|
|
243
|
+
"cline",
|
|
244
|
+
"lovable",
|
|
245
|
+
"bolt",
|
|
246
|
+
"replit"
|
|
247
|
+
];
|
|
233
248
|
var SERVER_NAME = "stoaken";
|
|
234
249
|
var API_KEY_ENV = "STOAKEN_API_KEY";
|
|
235
250
|
var CLIENT_LABELS = {
|
|
@@ -238,17 +253,43 @@ var CLIENT_LABELS = {
|
|
|
238
253
|
cursor: "Cursor",
|
|
239
254
|
codex: "OpenAI Codex",
|
|
240
255
|
copilot: "GitHub Copilot (VS Code)",
|
|
241
|
-
v0: "v0 (Vercel)"
|
|
256
|
+
v0: "v0 (Vercel)",
|
|
257
|
+
windsurf: "Windsurf / Devin Desktop",
|
|
258
|
+
zed: "Zed",
|
|
259
|
+
"gemini-cli": "Gemini CLI",
|
|
260
|
+
jetbrains: "JetBrains (Junie)",
|
|
261
|
+
cline: "Cline",
|
|
262
|
+
lovable: "Lovable",
|
|
263
|
+
bolt: "Bolt (bolt.new)",
|
|
264
|
+
replit: "Replit Agent"
|
|
265
|
+
};
|
|
266
|
+
var JSON_CLIENTS = ["claude-code", "cursor", "copilot", "windsurf", "zed", "gemini-cli", "jetbrains"];
|
|
267
|
+
var SERVERS_KEY = {
|
|
268
|
+
"claude-code": "mcpServers",
|
|
269
|
+
cursor: "mcpServers",
|
|
270
|
+
copilot: "servers",
|
|
271
|
+
windsurf: "mcpServers",
|
|
272
|
+
zed: "context_servers",
|
|
273
|
+
"gemini-cli": "mcpServers",
|
|
274
|
+
jetbrains: "mcpServers"
|
|
242
275
|
};
|
|
243
276
|
function isClient(value) {
|
|
244
277
|
return CLIENTS.includes(value);
|
|
245
278
|
}
|
|
279
|
+
function isJsonClient(client) {
|
|
280
|
+
return JSON_CLIENTS.includes(client);
|
|
281
|
+
}
|
|
246
282
|
function mcpUrl(serverUrl) {
|
|
247
283
|
return `${serverUrl.replace(/\/+$/, "")}/mcp`;
|
|
248
284
|
}
|
|
285
|
+
function replitInstallLink(serverUrl) {
|
|
286
|
+
const json = JSON.stringify({ displayName: "Stoaken", baseUrl: mcpUrl(serverUrl) });
|
|
287
|
+
return `https://replit.com/integrations?mcp=${Buffer.from(json, "utf8").toString("base64")}`;
|
|
288
|
+
}
|
|
249
289
|
function planFor(client, auth, serverUrl) {
|
|
250
290
|
const url = mcpUrl(serverUrl);
|
|
251
291
|
const keyStep = `Set ${API_KEY_ENV} in your shell to an API key from Settings > API keys (never commit it).`;
|
|
292
|
+
const oauthInstead = "Or run init again, sign in with the browser, and use OAuth instead.";
|
|
252
293
|
switch (client) {
|
|
253
294
|
case "claude-code":
|
|
254
295
|
return {
|
|
@@ -268,7 +309,8 @@ function planFor(client, auth, serverUrl) {
|
|
|
268
309
|
case "copilot":
|
|
269
310
|
return {
|
|
270
311
|
file: ".vscode/mcp.json",
|
|
271
|
-
nextSteps: auth === "oauth" ? ["In VS Code open .vscode/mcp.json, click Start above stoaken and sign in in the browser.", "Use Copilot Chat in Agent mode."] : ["In VS Code open .vscode/mcp.json, click Start above stoaken and paste an API key when asked (VS Code stores it securely).", "Use Copilot Chat in Agent mode."]
|
|
312
|
+
nextSteps: auth === "oauth" ? ["In VS Code open .vscode/mcp.json, click Start above stoaken and sign in in the browser.", "Use Copilot Chat in Agent mode."] : ["In VS Code open .vscode/mcp.json, click Start above stoaken and paste an API key when asked (VS Code stores it securely).", "Use Copilot Chat in Agent mode."],
|
|
313
|
+
keyNote: "VS Code asks for the key once and stores it securely"
|
|
272
314
|
};
|
|
273
315
|
case "claude-desktop":
|
|
274
316
|
return {
|
|
@@ -290,10 +332,104 @@ function planFor(client, auth, serverUrl) {
|
|
|
290
332
|
"v0 uses the tools while it generates; the app it builds cannot call them."
|
|
291
333
|
]
|
|
292
334
|
};
|
|
335
|
+
case "windsurf":
|
|
336
|
+
return {
|
|
337
|
+
file: ".devin/mcp_config.json",
|
|
338
|
+
nextSteps: auth === "oauth" ? [
|
|
339
|
+
"Open this project in Devin Desktop (formerly Windsurf) with the Devin Local agent. Devin CLI reads the same file.",
|
|
340
|
+
"When stoaken shows Needs auth in the MCP list, click Authenticate (or run: devin mcp login stoaken)."
|
|
341
|
+
] : [
|
|
342
|
+
keyStep,
|
|
343
|
+
`Devin documents no variable expansion in MCP headers, so add the key to the gitignored local override: devin mcp add -s local stoaken ${url} -H "Authorization: Bearer $${API_KEY_ENV}"`,
|
|
344
|
+
"That override replaces the project entry for you only. The legacy Cascade agent ignores project files (see the guide)."
|
|
345
|
+
],
|
|
346
|
+
keyNote: "the key goes in the gitignored `.devin/mcp_config.local.json` override"
|
|
347
|
+
};
|
|
348
|
+
case "zed":
|
|
349
|
+
return auth === "oauth" ? {
|
|
350
|
+
file: ".zed/settings.json",
|
|
351
|
+
nextSteps: [
|
|
352
|
+
"Trust this project in Zed: in Restricted Mode project settings and their MCP servers don't load.",
|
|
353
|
+
"Zed asks you to sign in to stoaken (MCP OAuth). Settings > AI > MCP Servers shows a green dot when it's active."
|
|
354
|
+
]
|
|
355
|
+
} : {
|
|
356
|
+
file: null,
|
|
357
|
+
nextSteps: [
|
|
358
|
+
"Zed documents no variable expansion in headers, so the key can't go in a project file.",
|
|
359
|
+
`Run "zed: open settings file" and add under "context_servers": "stoaken": { "url": "${url}", "headers": { "Authorization": "Bearer <API key>" } }`,
|
|
360
|
+
oauthInstead
|
|
361
|
+
]
|
|
362
|
+
};
|
|
363
|
+
case "gemini-cli":
|
|
364
|
+
return {
|
|
365
|
+
file: ".gemini/settings.json",
|
|
366
|
+
nextSteps: auth === "oauth" ? ["Trust this folder (gemini trust): untrusted folders ignore .gemini/settings.json.", "In Gemini CLI run /mcp auth stoaken to sign in in the browser."] : [keyStep, "Trust this folder (gemini trust): untrusted folders ignore .gemini/settings.json."]
|
|
367
|
+
};
|
|
368
|
+
case "jetbrains":
|
|
369
|
+
return auth === "oauth" ? {
|
|
370
|
+
file: ".junie/mcp/mcp.json",
|
|
371
|
+
nextSteps: [
|
|
372
|
+
"Junie in the IDE: Settings > Tools > Junie > MCP Settings lists stoaken and its status.",
|
|
373
|
+
"Junie CLI: run /mcp, select stoaken, choose Authorize and sign in in the browser."
|
|
374
|
+
]
|
|
375
|
+
} : {
|
|
376
|
+
file: null,
|
|
377
|
+
nextSteps: [
|
|
378
|
+
"Junie has no secret storage or variable expansion for MCP configs, so the key can't go in a project file.",
|
|
379
|
+
`In your user file ~/.junie/mcp/mcp.json, under "mcpServers", add "stoaken": { "url": "${url}", "headers": { "Authorization": "Bearer <API key>" } }`,
|
|
380
|
+
oauthInstead
|
|
381
|
+
]
|
|
382
|
+
};
|
|
383
|
+
case "cline":
|
|
384
|
+
return {
|
|
385
|
+
file: null,
|
|
386
|
+
nextSteps: [
|
|
387
|
+
"Cline keeps MCP servers in its own settings, not a project file.",
|
|
388
|
+
...auth === "oauth" ? [
|
|
389
|
+
`Cline panel > MCP Servers > Remote Servers: name stoaken, URL ${url}, transport Streamable HTTP, Add Server.`,
|
|
390
|
+
"If it asks you to authenticate, sign in in the browser (Cline CLI: cline mcp > Authorize OAuth)."
|
|
391
|
+
] : [
|
|
392
|
+
`Cline panel > MCP Servers > Configure > Configure MCP Servers, and add under "mcpServers": "stoaken": { "type": "streamableHttp", "url": "${url}", "headers": { "Authorization": "Bearer <API key>" } }`
|
|
393
|
+
]
|
|
394
|
+
]
|
|
395
|
+
};
|
|
396
|
+
case "lovable":
|
|
397
|
+
return {
|
|
398
|
+
file: null,
|
|
399
|
+
nextSteps: [
|
|
400
|
+
"Lovable adds MCP servers in its own UI, not through a project file.",
|
|
401
|
+
"Connectors > Add custom > MCP server. Server name: Stoaken.",
|
|
402
|
+
`Server URL: ${url}. Authentication: OAuth (Add & authorize), or Bearer token with an API key.`,
|
|
403
|
+
"The connection is yours alone and only for Lovable's chat; the app it publishes can't call it."
|
|
404
|
+
]
|
|
405
|
+
};
|
|
406
|
+
case "bolt":
|
|
407
|
+
return {
|
|
408
|
+
file: null,
|
|
409
|
+
nextSteps: [
|
|
410
|
+
"Bolt adds MCP servers in its own UI, not through a project file.",
|
|
411
|
+
"In the chatbox: + > Connectors > Manage connectors > Custom MCP server.",
|
|
412
|
+
`Name: Stoaken. URL: ${url}. Transport type: HTTP. Authentication: MCP OAuth.`,
|
|
413
|
+
"Then turn the connector on for the project (+ > Connectors)."
|
|
414
|
+
]
|
|
415
|
+
};
|
|
416
|
+
case "replit":
|
|
417
|
+
return {
|
|
418
|
+
file: null,
|
|
419
|
+
nextSteps: [
|
|
420
|
+
"Replit adds MCP servers in its own UI, not through a project file.",
|
|
421
|
+
`Open ${replitInstallLink(serverUrl)} (or Integrations > MCP Servers > + Add MCP server, URL ${url}).`,
|
|
422
|
+
auth === "oauth" ? "Click Test & save and sign in when Replit runs the OAuth flow." : "Under Advanced settings add the header Authorization: Bearer <API key>, then Test & save."
|
|
423
|
+
]
|
|
424
|
+
};
|
|
293
425
|
}
|
|
294
426
|
}
|
|
295
427
|
function jsonEntry(client, auth, serverUrl) {
|
|
296
428
|
const url = mcpUrl(serverUrl);
|
|
429
|
+
if (client === "windsurf" || client === "zed" || client === "jetbrains") return { url };
|
|
430
|
+
if (client === "gemini-cli") {
|
|
431
|
+
return auth === "oauth" ? { httpUrl: url } : { httpUrl: url, headers: { Authorization: `Bearer \${${API_KEY_ENV}}` } };
|
|
432
|
+
}
|
|
297
433
|
if (client === "claude-code") {
|
|
298
434
|
return auth === "oauth" ? { type: "http", url } : { type: "http", url, headers: { Authorization: `Bearer \${${API_KEY_ENV}}` } };
|
|
299
435
|
}
|
|
@@ -325,8 +461,9 @@ var COPILOT_INPUT = {
|
|
|
325
461
|
};
|
|
326
462
|
function mergeJsonConfig(client, existing, auth, serverUrl) {
|
|
327
463
|
const file = planFor(client, auth, serverUrl).file;
|
|
464
|
+
if (file === null) throw new ConfigMergeError(`${CLIENT_LABELS[client]} gets no project file with an API key; follow the steps instead.`);
|
|
328
465
|
const doc = parseObject(existing, file);
|
|
329
|
-
const key = client
|
|
466
|
+
const key = SERVERS_KEY[client];
|
|
330
467
|
const current = doc[key];
|
|
331
468
|
if (current !== void 0 && (typeof current !== "object" || current === null || Array.isArray(current))) {
|
|
332
469
|
throw new ConfigMergeError(`${file} has a "${key}" that isn't an object, so it was left alone.`);
|
|
@@ -377,7 +514,7 @@ file -- it's already in \`.gitignore\`).
|
|
|
377
514
|
}
|
|
378
515
|
const { client, auth } = target;
|
|
379
516
|
const plan = planFor(client, auth, serverUrl);
|
|
380
|
-
const how = auth === "oauth" ? "the client signs in with OAuth" : `it reads the \`${API_KEY_ENV}\` environment variable`;
|
|
517
|
+
const how = auth === "oauth" ? "the client signs in with OAuth" : plan.keyNote ?? `it reads the \`${API_KEY_ENV}\` environment variable`;
|
|
381
518
|
const where = plan.file ? `It is configured for ${CLIENT_LABELS[client]} in \`${plan.file}\` (no secret in that file: ${how}).` : `${CLIENT_LABELS[client]} connects through its own settings, not a project file.`;
|
|
382
519
|
return `${where}
|
|
383
520
|
|
|
@@ -557,6 +694,7 @@ async function runInit(options) {
|
|
|
557
694
|
const path = join2(cwd, plan.file);
|
|
558
695
|
const existingText = existsSync2(path) ? await readFile2(path, "utf8") : null;
|
|
559
696
|
try {
|
|
697
|
+
if (client !== "codex" && !isJsonClient(client)) throw new Error(`No file writer for ${client}.`);
|
|
560
698
|
const next = client === "codex" ? mergeCodexToml(existingText, auth, serverUrl) : mergeJsonConfig(client, existingText, auth, serverUrl);
|
|
561
699
|
await mkdir2(dirname(path), { recursive: true });
|
|
562
700
|
await writeFile2(path, next, "utf8");
|
|
@@ -590,7 +728,7 @@ async function call(serverUrl, token, path) {
|
|
|
590
728
|
let response;
|
|
591
729
|
try {
|
|
592
730
|
response = await fetch(new URL(path, serverUrl), {
|
|
593
|
-
headers: { Authorization: `Bearer ${token}`, "User-Agent": "stoaken-cli/0.
|
|
731
|
+
headers: { Authorization: `Bearer ${token}`, "User-Agent": "stoaken-cli/0.5.0" }
|
|
594
732
|
});
|
|
595
733
|
} catch (error) {
|
|
596
734
|
throw new Error(`Couldn't reach ${serverUrl}: ${error instanceof Error ? error.message : String(error)}`);
|