cyberdyne-mcp 0.4.0 → 0.5.1
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 +15 -24
- package/dist/client.js +53 -5
- package/dist/server.js +36 -2
- package/package.json +1 -1
- package/src/client.ts +53 -5
- package/src/server.ts +42 -2
package/README.md
CHANGED
|
@@ -90,37 +90,28 @@ then releases payment on verify. The pattern behind x402-native traders like
|
|
|
90
90
|
CYBERDYNE_IDENTITY_TOKEN=cyb_… npm run build && npm run founder-check
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
## Install
|
|
93
|
+
## Install
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
**Claude Code:**
|
|
95
|
+
Published on [npm](https://www.npmjs.com/package/cyberdyne-mcp). Mint your `cyb_…`
|
|
96
|
+
agent key in the app's Agent Console, then:
|
|
99
97
|
|
|
100
98
|
```bash
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
-- npx -y github:Cyberdyne-OS/cyberdyne-mcp
|
|
99
|
+
npx cyberdyne-mcp login cyb_YOURKEY # save your key once (~/.cyberdyne/config.json, 0600)
|
|
100
|
+
claude mcp add cyberdyne -- npx -y cyberdyne-mcp
|
|
104
101
|
```
|
|
105
102
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
"env": {
|
|
115
|
-
"CYBERDYNE_IDENTITY_TOKEN": "cyb_…",
|
|
116
|
-
"CYBERDYNE_API_URL": "https://app.cyberdyne-os.xyz"
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
103
|
+
*(Prefer not to save a login? Skip step 1 and pass it inline instead:
|
|
104
|
+
`claude mcp add cyberdyne -e CYBERDYNE_IDENTITY_TOKEN=cyb_… -- npx -y cyberdyne-mcp`.)*
|
|
105
|
+
|
|
106
|
+
### …or install the plugin (skill + MCP together)
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
/plugin marketplace add Cyberdyne-OS/cyberdyne-mcp
|
|
110
|
+
/plugin install cyberdyne@cyberdyne-os
|
|
121
111
|
```
|
|
122
112
|
|
|
123
|
-
|
|
113
|
+
Bundles the MCP gateway **and** the usage skill. Once connected, run
|
|
114
|
+
**`/mcp__cyberdyne__quickstart`** for the full fund → post → pay walkthrough.
|
|
124
115
|
|
|
125
116
|
Then ask the agent, e.g.:
|
|
126
117
|
|
package/dist/client.js
CHANGED
|
@@ -9,17 +9,65 @@
|
|
|
9
9
|
* body (`identity_token`). Used for `search_humans` (the REST
|
|
10
10
|
* GET /api/humans is session-only and rejects Bearer keys).
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* The agent key (`cyb_…`) is resolved, in order, from:
|
|
13
|
+
* 1. env CYBERDYNE_IDENTITY_TOKEN (e.g. `claude mcp add … -e CYBERDYNE_IDENTITY_TOKEN=…`)
|
|
14
|
+
* 2. a saved login at ~/.cyberdyne/config.json (written by `cyberdyne-mcp login cyb_…`)
|
|
15
|
+
* so the install line can be the short `claude mcp add cyberdyne -- npx -y cyberdyne-mcp`.
|
|
16
|
+
* CYBERDYNE_API_URL overrides the default "https://app.cyberdyne-os.xyz".
|
|
15
17
|
*
|
|
16
18
|
* No secrets are hardcoded; nothing is logged that could leak the key.
|
|
17
19
|
*/
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { readFileSync, mkdirSync, openSync, writeSync, closeSync, fchmodSync, constants as FS } from "node:fs";
|
|
18
23
|
export const DEFAULT_API_URL = "https://app.cyberdyne-os.xyz";
|
|
19
|
-
/**
|
|
24
|
+
/** Path to the persisted login (mode 600). */
|
|
25
|
+
export function configPath() {
|
|
26
|
+
return join(homedir(), ".cyberdyne", "config.json");
|
|
27
|
+
}
|
|
28
|
+
// Only the key is persisted. The API endpoint is intentionally NOT read from this
|
|
29
|
+
// file — a tampered config must never be able to redirect the agent's key to a
|
|
30
|
+
// hostile host (credential exfiltration). The endpoint overrides via env only.
|
|
31
|
+
function readSavedToken() {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(readFileSync(configPath(), "utf8"));
|
|
34
|
+
return typeof parsed?.identity_token === "string" ? parsed.identity_token.trim() : undefined;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Persist the agent key to ~/.cyberdyne/config.json. Returns the path. */
|
|
41
|
+
export function saveToken(token) {
|
|
42
|
+
// Owner-only dir + atomic 0600 create (no world-readable window / TOCTOU), and
|
|
43
|
+
// O_NOFOLLOW so a planted symlink at the path can't redirect the write.
|
|
44
|
+
mkdirSync(join(homedir(), ".cyberdyne"), { recursive: true, mode: 0o700 });
|
|
45
|
+
const p = configPath();
|
|
46
|
+
const contents = JSON.stringify({ identity_token: token.trim() }, null, 2);
|
|
47
|
+
let flags = FS.O_WRONLY | FS.O_CREAT | FS.O_TRUNC;
|
|
48
|
+
if (typeof FS.O_NOFOLLOW === "number")
|
|
49
|
+
flags |= FS.O_NOFOLLOW;
|
|
50
|
+
let fd;
|
|
51
|
+
try {
|
|
52
|
+
fd = openSync(p, flags, 0o600);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Platforms without O_NOFOLLOW semantics — fall back without it.
|
|
56
|
+
fd = openSync(p, FS.O_WRONLY | FS.O_CREAT | FS.O_TRUNC, 0o600);
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
fchmodSync(fd, 0o600); // tighten perms on the open fd (covers a pre-existing file), race-free
|
|
60
|
+
writeSync(fd, contents);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
closeSync(fd);
|
|
64
|
+
}
|
|
65
|
+
return p;
|
|
66
|
+
}
|
|
67
|
+
/** Resolve config: token from env first, then the saved login. URL from env only. */
|
|
20
68
|
export function readConfig(env = process.env) {
|
|
21
69
|
const apiUrl = (env.CYBERDYNE_API_URL || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
22
|
-
const token = env.CYBERDYNE_IDENTITY_TOKEN?.trim() || undefined;
|
|
70
|
+
const token = env.CYBERDYNE_IDENTITY_TOKEN?.trim() || readSavedToken() || undefined;
|
|
23
71
|
return { apiUrl, token };
|
|
24
72
|
}
|
|
25
73
|
/** An API error surfaced to the caller — carries the HTTP status + the API's error code. */
|
package/dist/server.js
CHANGED
|
@@ -36,11 +36,45 @@
|
|
|
36
36
|
* CYBERDYNE_API_URL default "https://app.cyberdyne-os.xyz"
|
|
37
37
|
* CYBERDYNE_IDENTITY_TOKEN the agent's cyb_ key (required for networked tools)
|
|
38
38
|
*/
|
|
39
|
+
import { readFileSync } from "node:fs";
|
|
39
40
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
40
41
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
41
42
|
import { z } from "zod";
|
|
42
43
|
import { CATEGORIES, TASK_CATEGORIES } from "./registry.js";
|
|
43
|
-
import { ApiError, CyberdyneClient, MissingTokenError, readConfig } from "./client.js";
|
|
44
|
+
import { ApiError, CyberdyneClient, MissingTokenError, readConfig, saveToken } from "./client.js";
|
|
45
|
+
// `cyberdyne-mcp login` — persist the key so the MCP add line can omit it (short
|
|
46
|
+
// DFM-style install). Runs before the server boots, then exits. The key is read
|
|
47
|
+
// (most-private first) from: piped stdin → CYBERDYNE_LOGIN_TOKEN env → argv. argv
|
|
48
|
+
// works but lands the secret in shell history / `ps`, so we steer to the others.
|
|
49
|
+
if (process.argv[2] === "login") {
|
|
50
|
+
const fromArg = process.argv[3]?.trim();
|
|
51
|
+
let token = "";
|
|
52
|
+
if (!process.stdin.isTTY) {
|
|
53
|
+
try {
|
|
54
|
+
token = readFileSync(0, "utf8").trim(); // piped: echo cyb_… | npx cyberdyne-mcp login
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
/* nothing piped */
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
token = token || process.env.CYBERDYNE_LOGIN_TOKEN?.trim() || fromArg || "";
|
|
61
|
+
if (!token.startsWith("cyb_")) {
|
|
62
|
+
console.error("Save your CYBERDYNE key (most private first):\n" +
|
|
63
|
+
" echo cyb_<key> | npx cyberdyne-mcp login\n" +
|
|
64
|
+
" CYBERDYNE_LOGIN_TOKEN=cyb_<key> npx cyberdyne-mcp login\n" +
|
|
65
|
+
" npx cyberdyne-mcp login cyb_<key> (key is left in shell history / process list)\n" +
|
|
66
|
+
"Get your key at https://app.cyberdyne-os.xyz → Agent Console → Generate API key.");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
if (fromArg && token === fromArg) {
|
|
70
|
+
console.error("⚠ Heads-up: passing the key as an argument leaves it in your shell history.\n" +
|
|
71
|
+
" Next time, pipe it instead: echo cyb_<key> | npx cyberdyne-mcp login");
|
|
72
|
+
}
|
|
73
|
+
const path = saveToken(token);
|
|
74
|
+
console.error(`✓ Saved your CYBERDYNE key to ${path}.\n` +
|
|
75
|
+
"Now run: claude mcp add cyberdyne -- npx -y cyberdyne-mcp");
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
44
78
|
const config = readConfig();
|
|
45
79
|
const client = new CyberdyneClient(config);
|
|
46
80
|
// ---- Result helpers -------------------------------------------------------
|
|
@@ -185,5 +219,5 @@ server.registerPrompt("quickstart", {
|
|
|
185
219
|
const transport = new StdioServerTransport();
|
|
186
220
|
await server.connect(transport);
|
|
187
221
|
console.error(`CYBERDYNE MCP server running on stdio → ${config.apiUrl}` +
|
|
188
|
-
(config.token ? "" : " (no
|
|
222
|
+
(config.token ? "" : " (no key — run `npx cyberdyne-mcp login cyb_…` or set CYBERDYNE_IDENTITY_TOKEN; networked tools error until then)") +
|
|
189
223
|
". Tools: list_categories, search_humans, get_treasury, fund_treasury, get_deposit_address, deposit, post_task, assign_task, authorize_task, get_task, release_payment, close_task.");
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -9,13 +9,19 @@
|
|
|
9
9
|
* body (`identity_token`). Used for `search_humans` (the REST
|
|
10
10
|
* GET /api/humans is session-only and rejects Bearer keys).
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* The agent key (`cyb_…`) is resolved, in order, from:
|
|
13
|
+
* 1. env CYBERDYNE_IDENTITY_TOKEN (e.g. `claude mcp add … -e CYBERDYNE_IDENTITY_TOKEN=…`)
|
|
14
|
+
* 2. a saved login at ~/.cyberdyne/config.json (written by `cyberdyne-mcp login cyb_…`)
|
|
15
|
+
* so the install line can be the short `claude mcp add cyberdyne -- npx -y cyberdyne-mcp`.
|
|
16
|
+
* CYBERDYNE_API_URL overrides the default "https://app.cyberdyne-os.xyz".
|
|
15
17
|
*
|
|
16
18
|
* No secrets are hardcoded; nothing is logged that could leak the key.
|
|
17
19
|
*/
|
|
18
20
|
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { readFileSync, mkdirSync, openSync, writeSync, closeSync, fchmodSync, constants as FS } from "node:fs";
|
|
24
|
+
|
|
19
25
|
export const DEFAULT_API_URL = "https://app.cyberdyne-os.xyz";
|
|
20
26
|
|
|
21
27
|
export interface CyberdyneConfig {
|
|
@@ -23,10 +29,52 @@ export interface CyberdyneConfig {
|
|
|
23
29
|
token: string | undefined;
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
/**
|
|
32
|
+
/** Path to the persisted login (mode 600). */
|
|
33
|
+
export function configPath(): string {
|
|
34
|
+
return join(homedir(), ".cyberdyne", "config.json");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Only the key is persisted. The API endpoint is intentionally NOT read from this
|
|
38
|
+
// file — a tampered config must never be able to redirect the agent's key to a
|
|
39
|
+
// hostile host (credential exfiltration). The endpoint overrides via env only.
|
|
40
|
+
function readSavedToken(): string | undefined {
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(readFileSync(configPath(), "utf8")) as { identity_token?: unknown };
|
|
43
|
+
return typeof parsed?.identity_token === "string" ? parsed.identity_token.trim() : undefined;
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Persist the agent key to ~/.cyberdyne/config.json. Returns the path. */
|
|
50
|
+
export function saveToken(token: string): string {
|
|
51
|
+
// Owner-only dir + atomic 0600 create (no world-readable window / TOCTOU), and
|
|
52
|
+
// O_NOFOLLOW so a planted symlink at the path can't redirect the write.
|
|
53
|
+
mkdirSync(join(homedir(), ".cyberdyne"), { recursive: true, mode: 0o700 });
|
|
54
|
+
const p = configPath();
|
|
55
|
+
const contents = JSON.stringify({ identity_token: token.trim() }, null, 2);
|
|
56
|
+
let flags = FS.O_WRONLY | FS.O_CREAT | FS.O_TRUNC;
|
|
57
|
+
if (typeof FS.O_NOFOLLOW === "number") flags |= FS.O_NOFOLLOW;
|
|
58
|
+
let fd: number;
|
|
59
|
+
try {
|
|
60
|
+
fd = openSync(p, flags, 0o600);
|
|
61
|
+
} catch {
|
|
62
|
+
// Platforms without O_NOFOLLOW semantics — fall back without it.
|
|
63
|
+
fd = openSync(p, FS.O_WRONLY | FS.O_CREAT | FS.O_TRUNC, 0o600);
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
fchmodSync(fd, 0o600); // tighten perms on the open fd (covers a pre-existing file), race-free
|
|
67
|
+
writeSync(fd, contents);
|
|
68
|
+
} finally {
|
|
69
|
+
closeSync(fd);
|
|
70
|
+
}
|
|
71
|
+
return p;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Resolve config: token from env first, then the saved login. URL from env only. */
|
|
27
75
|
export function readConfig(env: NodeJS.ProcessEnv = process.env): CyberdyneConfig {
|
|
28
76
|
const apiUrl = (env.CYBERDYNE_API_URL || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
29
|
-
const token = env.CYBERDYNE_IDENTITY_TOKEN?.trim() || undefined;
|
|
77
|
+
const token = env.CYBERDYNE_IDENTITY_TOKEN?.trim() || readSavedToken() || undefined;
|
|
30
78
|
return { apiUrl, token };
|
|
31
79
|
}
|
|
32
80
|
|
package/src/server.ts
CHANGED
|
@@ -36,11 +36,51 @@
|
|
|
36
36
|
* CYBERDYNE_API_URL default "https://app.cyberdyne-os.xyz"
|
|
37
37
|
* CYBERDYNE_IDENTITY_TOKEN the agent's cyb_ key (required for networked tools)
|
|
38
38
|
*/
|
|
39
|
+
import { readFileSync } from "node:fs";
|
|
39
40
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
40
41
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
41
42
|
import { z } from "zod";
|
|
42
43
|
import { CATEGORIES, TASK_CATEGORIES } from "./registry.js";
|
|
43
|
-
import { ApiError, CyberdyneClient, MissingTokenError, readConfig } from "./client.js";
|
|
44
|
+
import { ApiError, CyberdyneClient, MissingTokenError, readConfig, saveToken } from "./client.js";
|
|
45
|
+
|
|
46
|
+
// `cyberdyne-mcp login` — persist the key so the MCP add line can omit it (short
|
|
47
|
+
// DFM-style install). Runs before the server boots, then exits. The key is read
|
|
48
|
+
// (most-private first) from: piped stdin → CYBERDYNE_LOGIN_TOKEN env → argv. argv
|
|
49
|
+
// works but lands the secret in shell history / `ps`, so we steer to the others.
|
|
50
|
+
if (process.argv[2] === "login") {
|
|
51
|
+
const fromArg = process.argv[3]?.trim();
|
|
52
|
+
let token = "";
|
|
53
|
+
if (!process.stdin.isTTY) {
|
|
54
|
+
try {
|
|
55
|
+
token = readFileSync(0, "utf8").trim(); // piped: echo cyb_… | npx cyberdyne-mcp login
|
|
56
|
+
} catch {
|
|
57
|
+
/* nothing piped */
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
token = token || process.env.CYBERDYNE_LOGIN_TOKEN?.trim() || fromArg || "";
|
|
61
|
+
if (!token.startsWith("cyb_")) {
|
|
62
|
+
console.error(
|
|
63
|
+
"Save your CYBERDYNE key (most private first):\n" +
|
|
64
|
+
" echo cyb_<key> | npx cyberdyne-mcp login\n" +
|
|
65
|
+
" CYBERDYNE_LOGIN_TOKEN=cyb_<key> npx cyberdyne-mcp login\n" +
|
|
66
|
+
" npx cyberdyne-mcp login cyb_<key> (key is left in shell history / process list)\n" +
|
|
67
|
+
"Get your key at https://app.cyberdyne-os.xyz → Agent Console → Generate API key.",
|
|
68
|
+
);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
if (fromArg && token === fromArg) {
|
|
72
|
+
console.error(
|
|
73
|
+
"⚠ Heads-up: passing the key as an argument leaves it in your shell history.\n" +
|
|
74
|
+
" Next time, pipe it instead: echo cyb_<key> | npx cyberdyne-mcp login",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
const path = saveToken(token);
|
|
78
|
+
console.error(
|
|
79
|
+
`✓ Saved your CYBERDYNE key to ${path}.\n` +
|
|
80
|
+
"Now run: claude mcp add cyberdyne -- npx -y cyberdyne-mcp",
|
|
81
|
+
);
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
44
84
|
|
|
45
85
|
const config = readConfig();
|
|
46
86
|
const client = new CyberdyneClient(config);
|
|
@@ -285,6 +325,6 @@ const transport = new StdioServerTransport();
|
|
|
285
325
|
await server.connect(transport);
|
|
286
326
|
console.error(
|
|
287
327
|
`CYBERDYNE MCP server running on stdio → ${config.apiUrl}` +
|
|
288
|
-
(config.token ? "" : " (no
|
|
328
|
+
(config.token ? "" : " (no key — run `npx cyberdyne-mcp login cyb_…` or set CYBERDYNE_IDENTITY_TOKEN; networked tools error until then)") +
|
|
289
329
|
". Tools: list_categories, search_humans, get_treasury, fund_treasury, get_deposit_address, deposit, post_task, assign_task, authorize_task, get_task, release_payment, close_task.",
|
|
290
330
|
);
|