cyberdyne-mcp 0.5.4 → 0.6.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 +44 -6
- package/dist/client.js +44 -11
- package/dist/evm-signer.js +18 -6
- package/dist/onboard.js +145 -0
- package/dist/server.js +36 -3
- package/package.json +1 -1
- package/src/client.ts +55 -12
- package/src/evm-signer.ts +18 -6
- package/src/onboard.ts +170 -0
- package/src/server.ts +45 -3
package/README.md
CHANGED
|
@@ -11,6 +11,33 @@ stdio and the marketplace appears as tools. Each tool is a thin, typed wrapper
|
|
|
11
11
|
over the **live CYBERDYNE platform API** — there is no in-memory demo state. The
|
|
12
12
|
agent authenticates with its own API key.
|
|
13
13
|
|
|
14
|
+
## Quickstart — zero-browser onboarding
|
|
15
|
+
|
|
16
|
+
An agent can go from **nothing** to **wallet + API key + ready to post/pay** with
|
|
17
|
+
one command — no web dashboard, no manual key copy/paste:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx -y cyberdyne-mcp onboard # generates a wallet + mints your cyb_ API key (no dashboard)
|
|
21
|
+
claude mcp add cyberdyne -- npx -y cyberdyne-mcp # the MCP now auto-uses the saved key
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`onboard` resolves a wallet (uses `CYBERDYNE_EVM_PRIVATE_KEY` if set, else a wallet
|
|
25
|
+
it generated on a prior run, else it **generates a fresh one**), signs in to
|
|
26
|
+
CYBERDYNE with it (SIWE — just a signature, no gas, no transaction), mints your
|
|
27
|
+
`cyb_` agent key, and saves **both** to `~/.cyberdyne/config.json` (mode `0600`).
|
|
28
|
+
It prints your wallet address and the `cyb_` key **once**. The same generated
|
|
29
|
+
wallet is then used automatically for pool-budget signing — zero env vars.
|
|
30
|
+
|
|
31
|
+
An agent already running inside an LLM with this MCP connected can **self-onboard
|
|
32
|
+
with zero web interaction** by calling the `onboard` tool — it's the one tool that
|
|
33
|
+
works without an existing key and bootstraps everything else. After that it can
|
|
34
|
+
fund (`get_deposit_address` → send USDC on Base → `deposit`) and `post_task`.
|
|
35
|
+
|
|
36
|
+
> Mirrors the Bankr CLI UX (`bankr login` → wallet + API key in one shot). The
|
|
37
|
+
> human **submit-proof** step is intentionally still in the app (human-only); every
|
|
38
|
+
> agent-side action — onboard, fund, post, assign, authorize, review, close — is
|
|
39
|
+
> headless.
|
|
40
|
+
|
|
14
41
|
## Configuration (environment)
|
|
15
42
|
|
|
16
43
|
stdio MCP servers take their credentials from the environment. Set:
|
|
@@ -20,8 +47,9 @@ stdio MCP servers take their credentials from the environment. Set:
|
|
|
20
47
|
| `CYBERDYNE_IDENTITY_TOKEN` | yes (for any networked tool) | — | The agent's API key (`cyb_…`). |
|
|
21
48
|
| `CYBERDYNE_API_URL` | no | `https://app.cyberdyne-os.xyz` | Base URL of the platform API. |
|
|
22
49
|
|
|
23
|
-
No key is hardcoded anywhere. `list_categories`
|
|
24
|
-
tool returns a clear error until
|
|
50
|
+
No key is hardcoded anywhere. `list_categories` and `onboard` work without a token
|
|
51
|
+
(`onboard` mints one); every other tool returns a clear error until a key is set —
|
|
52
|
+
via `CYBERDYNE_IDENTITY_TOKEN`, a saved `onboard`/`login`, or the `onboard` tool.
|
|
25
53
|
|
|
26
54
|
## The flows
|
|
27
55
|
|
|
@@ -66,6 +94,7 @@ post_task (quantity > 1) → { task, authIntent, depl
|
|
|
66
94
|
|
|
67
95
|
| Tool | Endpoint | What it does |
|
|
68
96
|
|---|---|---|
|
|
97
|
+
| `onboard` | `siwe/nonce → siwe/verify → agent/key` | **Bootstrap (no key needed).** Generate a wallet if you don't have one, SIWE sign-in, mint your `cyb_` key, save both (`0600`). Zero browser. |
|
|
69
98
|
| `list_categories` | — (static) | The seven task categories. No network. |
|
|
70
99
|
| `search_humans` | `POST /api/a2a` `{search_humans}` | Query the capability index by `skills[]`, `min_reputation`, `location`. Ranked by reputation; public columns only. |
|
|
71
100
|
| `get_treasury` | `GET /api/treasury` | The agent's own treasury (null if none yet). |
|
|
@@ -116,16 +145,25 @@ CYBERDYNE_IDENTITY_TOKEN=cyb_… npm run build && npm run founder-check
|
|
|
116
145
|
|
|
117
146
|
## Install
|
|
118
147
|
|
|
119
|
-
Published on [npm](https://www.npmjs.com/package/cyberdyne-mcp).
|
|
120
|
-
|
|
148
|
+
Published on [npm](https://www.npmjs.com/package/cyberdyne-mcp).
|
|
149
|
+
|
|
150
|
+
**Fully autonomous (recommended) — no dashboard:**
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
npx -y cyberdyne-mcp onboard # generate a wallet + mint your cyb_ key, save both (0600)
|
|
154
|
+
claude mcp add cyberdyne -- npx -y cyberdyne-mcp
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Already minted a key in the app's Agent Console?** Save it instead:
|
|
121
158
|
|
|
122
159
|
```bash
|
|
123
160
|
npx cyberdyne-mcp login cyb_YOURKEY # save your key once (~/.cyberdyne/config.json, 0600)
|
|
124
161
|
claude mcp add cyberdyne -- npx -y cyberdyne-mcp
|
|
125
162
|
```
|
|
126
163
|
|
|
127
|
-
*(Prefer not to save a login?
|
|
128
|
-
`claude mcp add cyberdyne -e CYBERDYNE_IDENTITY_TOKEN=cyb_… -- npx -y cyberdyne-mcp`.
|
|
164
|
+
*(Prefer not to save a login? Pass it inline instead:
|
|
165
|
+
`claude mcp add cyberdyne -e CYBERDYNE_IDENTITY_TOKEN=cyb_… -- npx -y cyberdyne-mcp`.
|
|
166
|
+
Or skip the CLI entirely and call the `onboard` tool from inside the agent.)*
|
|
129
167
|
|
|
130
168
|
### …or install the plugin (skill + MCP together)
|
|
131
169
|
|
package/dist/client.js
CHANGED
|
@@ -25,25 +25,38 @@ export const DEFAULT_API_URL = "https://app.cyberdyne-os.xyz";
|
|
|
25
25
|
export function configPath() {
|
|
26
26
|
return join(homedir(), ".cyberdyne", "config.json");
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// hostile host (credential exfiltration). The endpoint overrides via env only.
|
|
31
|
-
function readSavedToken() {
|
|
28
|
+
/** Read + parse the whole config file (or {} if missing / unreadable). */
|
|
29
|
+
function readSavedConfig() {
|
|
32
30
|
try {
|
|
33
31
|
const parsed = JSON.parse(readFileSync(configPath(), "utf8"));
|
|
34
|
-
return typeof parsed
|
|
32
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
35
33
|
}
|
|
36
34
|
catch {
|
|
37
|
-
return
|
|
35
|
+
return {};
|
|
38
36
|
}
|
|
39
37
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
// Only the key is persisted (plus the generated wallet key). The API endpoint is
|
|
39
|
+
// intentionally NOT read from this file — a tampered config must never be able to
|
|
40
|
+
// redirect the agent's key to a hostile host (credential exfiltration). The
|
|
41
|
+
// endpoint overrides via env only.
|
|
42
|
+
function readSavedToken() {
|
|
43
|
+
const v = readSavedConfig().identity_token;
|
|
44
|
+
return typeof v === "string" ? v.trim() : undefined;
|
|
45
|
+
}
|
|
46
|
+
/** The generated/saved wallet private key (0x…), if any. Used as the EVM signer fallback. */
|
|
47
|
+
export function readSavedWalletKey() {
|
|
48
|
+
const v = readSavedConfig().walletKey;
|
|
49
|
+
return typeof v === "string" && v.trim() ? v.trim() : undefined;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Atomically write the config file at ~/.cyberdyne/config.json, mode 0600.
|
|
53
|
+
* Owner-only dir + atomic 0600 create (no world-readable window / TOCTOU), and
|
|
54
|
+
* O_NOFOLLOW so a planted symlink at the path can't redirect the write.
|
|
55
|
+
*/
|
|
56
|
+
function writeConfigFile(data) {
|
|
44
57
|
mkdirSync(join(homedir(), ".cyberdyne"), { recursive: true, mode: 0o700 });
|
|
45
58
|
const p = configPath();
|
|
46
|
-
const contents = JSON.stringify(
|
|
59
|
+
const contents = JSON.stringify(data, null, 2);
|
|
47
60
|
let flags = FS.O_WRONLY | FS.O_CREAT | FS.O_TRUNC;
|
|
48
61
|
if (typeof FS.O_NOFOLLOW === "number")
|
|
49
62
|
flags |= FS.O_NOFOLLOW;
|
|
@@ -64,6 +77,26 @@ export function saveToken(token) {
|
|
|
64
77
|
}
|
|
65
78
|
return p;
|
|
66
79
|
}
|
|
80
|
+
/** Persist the agent key to ~/.cyberdyne/config.json (preserving any saved walletKey). Returns the path. */
|
|
81
|
+
export function saveToken(token) {
|
|
82
|
+
const existing = readSavedConfig();
|
|
83
|
+
const next = { identity_token: token.trim() };
|
|
84
|
+
if (typeof existing.walletKey === "string" && existing.walletKey.trim())
|
|
85
|
+
next.walletKey = existing.walletKey.trim();
|
|
86
|
+
return writeConfigFile(next);
|
|
87
|
+
}
|
|
88
|
+
/** Persist the generated wallet private key (preserving any saved token). Returns the path. */
|
|
89
|
+
export function saveWallet(walletKey) {
|
|
90
|
+
const existing = readSavedConfig();
|
|
91
|
+
const next = { walletKey: walletKey.trim() };
|
|
92
|
+
if (typeof existing.identity_token === "string" && existing.identity_token.trim())
|
|
93
|
+
next.identity_token = existing.identity_token.trim();
|
|
94
|
+
return writeConfigFile(next);
|
|
95
|
+
}
|
|
96
|
+
/** Persist BOTH the agent key and the wallet key in a single 0600 write. Returns the path. */
|
|
97
|
+
export function saveTokenAndWallet(token, walletKey) {
|
|
98
|
+
return writeConfigFile({ identity_token: token.trim(), walletKey: walletKey.trim() });
|
|
99
|
+
}
|
|
67
100
|
/** Resolve config: token from env first, then the saved login. URL from env only. */
|
|
68
101
|
export function readConfig(env = process.env) {
|
|
69
102
|
const apiUrl = (env.CYBERDYNE_API_URL || DEFAULT_API_URL).replace(/\/+$/, "");
|
package/dist/evm-signer.js
CHANGED
|
@@ -16,10 +16,20 @@ import { privateKeyToAccount } from "viem/accounts";
|
|
|
16
16
|
import { createPublicClient, createWalletClient, http, parseUnits } from "viem";
|
|
17
17
|
import { base, baseSepolia } from "viem/chains";
|
|
18
18
|
import { AuthCaptureEvmScheme, toClientEvmSigner } from "@x402/evm";
|
|
19
|
+
import { readSavedWalletKey } from "./client.js";
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the signing private key: CYBERDYNE_EVM_PRIVATE_KEY first, else the
|
|
22
|
+
* walletKey generated at `onboard` and saved in ~/.cyberdyne/config.json. This is
|
|
23
|
+
* what makes onboard → post pool task → authorize all work with the SAME wallet
|
|
24
|
+
* and zero env vars.
|
|
25
|
+
*/
|
|
26
|
+
function resolvePrivateKey() {
|
|
27
|
+
return process.env.CYBERDYNE_EVM_PRIVATE_KEY?.trim() || readSavedWalletKey();
|
|
28
|
+
}
|
|
19
29
|
function account() {
|
|
20
|
-
const pk =
|
|
30
|
+
const pk = resolvePrivateKey();
|
|
21
31
|
if (!pk)
|
|
22
|
-
throw new Error("CYBERDYNE_EVM_PRIVATE_KEY
|
|
32
|
+
throw new Error("no signing key (set CYBERDYNE_EVM_PRIVATE_KEY or run `cyberdyne-mcp onboard`)");
|
|
23
33
|
return privateKeyToAccount((pk.startsWith("0x") ? pk : `0x${pk}`));
|
|
24
34
|
}
|
|
25
35
|
function chain() {
|
|
@@ -61,12 +71,12 @@ export async function payDeployFee(params) {
|
|
|
61
71
|
return hash;
|
|
62
72
|
}
|
|
63
73
|
export function hasEvmKey() {
|
|
64
|
-
return !!
|
|
74
|
+
return !!resolvePrivateKey();
|
|
65
75
|
}
|
|
66
76
|
function scheme() {
|
|
67
|
-
const pk =
|
|
77
|
+
const pk = resolvePrivateKey();
|
|
68
78
|
if (!pk)
|
|
69
|
-
throw new Error("
|
|
79
|
+
throw new Error("no signing key (set CYBERDYNE_EVM_PRIVATE_KEY or run `cyberdyne-mcp onboard`) — cannot sign the escrow authorization");
|
|
70
80
|
const chainId = Number(process.env.CYBERDYNE_CHAIN_ID ?? 8453);
|
|
71
81
|
const chain = chainId === 8453 ? base : baseSepolia;
|
|
72
82
|
const account = privateKeyToAccount((pk.startsWith("0x") ? pk : `0x${pk}`));
|
|
@@ -76,7 +86,9 @@ function scheme() {
|
|
|
76
86
|
}
|
|
77
87
|
/** The agent's signing wallet address (so the platform can verify payer == this). */
|
|
78
88
|
export function evmAddress() {
|
|
79
|
-
const pk =
|
|
89
|
+
const pk = resolvePrivateKey();
|
|
90
|
+
if (!pk)
|
|
91
|
+
throw new Error("no signing key (set CYBERDYNE_EVM_PRIVATE_KEY or run `cyberdyne-mcp onboard`)");
|
|
80
92
|
return privateKeyToAccount((pk.startsWith("0x") ? pk : `0x${pk}`)).address;
|
|
81
93
|
}
|
|
82
94
|
/**
|
package/dist/onboard.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FULLY AUTONOMOUS, zero-browser agent onboarding for CYBERDYNE.
|
|
3
|
+
*
|
|
4
|
+
* Goal: an agent goes from "nothing" to "has a wallet + cyb_ API key + can
|
|
5
|
+
* post/pay" using ONLY the CLI/MCP — never the web dashboard. This mirrors the
|
|
6
|
+
* Bankr CLI UX (`bankr login` generates a wallet + API key in one shot).
|
|
7
|
+
*
|
|
8
|
+
* The chain (all on the live platform API, no browser):
|
|
9
|
+
* 1. resolve a wallet — CYBERDYNE_EVM_PRIVATE_KEY → saved walletKey → generate fresh
|
|
10
|
+
* 2. GET /api/auth/siwe/nonce → { nonce } (+ sets a siwe-nonce cookie)
|
|
11
|
+
* 3. build the EXACT SIWE message and sign it with the wallet (EIP-191)
|
|
12
|
+
* 4. POST /api/auth/siwe/verify { message, signature, role:"agent" } → session cookie
|
|
13
|
+
* 5. POST /api/agent/key (session cookie) {} → { apiKey: "cyb_…" }
|
|
14
|
+
* 6. persist BOTH the cyb_ key and the wallet key to ~/.cyberdyne/config.json (0600)
|
|
15
|
+
*
|
|
16
|
+
* Nothing here moves funds or pays gas — onboarding is just a SIWE signature and
|
|
17
|
+
* a DB row. The wallet private key is persisted but NEVER logged to stdout.
|
|
18
|
+
*/
|
|
19
|
+
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
|
20
|
+
import { readConfig, readSavedWalletKey, saveTokenAndWallet } from "./client.js";
|
|
21
|
+
/** Normalise a private key to the 0x-prefixed form viem expects. */
|
|
22
|
+
function normalizeKey(pk) {
|
|
23
|
+
return (pk.startsWith("0x") ? pk : `0x${pk}`);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the onboarding wallet, most-explicit first:
|
|
27
|
+
* 1. CYBERDYNE_EVM_PRIVATE_KEY (env) — operator-supplied
|
|
28
|
+
* 2. saved walletKey in config — generated on a prior onboard
|
|
29
|
+
* 3. generate a fresh key — first run, zero config
|
|
30
|
+
* Always returns a usable account + the private key. Persisting a freshly
|
|
31
|
+
* generated key is the caller's job (onboard() does it atomically with the token).
|
|
32
|
+
*/
|
|
33
|
+
export function resolveWallet(env = process.env) {
|
|
34
|
+
const fromEnv = env.CYBERDYNE_EVM_PRIVATE_KEY?.trim();
|
|
35
|
+
if (fromEnv) {
|
|
36
|
+
const privateKey = normalizeKey(fromEnv);
|
|
37
|
+
return { account: privateKeyToAccount(privateKey), privateKey, generated: false };
|
|
38
|
+
}
|
|
39
|
+
const saved = readSavedWalletKey();
|
|
40
|
+
if (saved) {
|
|
41
|
+
const privateKey = normalizeKey(saved);
|
|
42
|
+
return { account: privateKeyToAccount(privateKey), privateKey, generated: false };
|
|
43
|
+
}
|
|
44
|
+
const privateKey = generatePrivateKey();
|
|
45
|
+
return { account: privateKeyToAccount(privateKey), privateKey, generated: true };
|
|
46
|
+
}
|
|
47
|
+
/** Pull every Set-Cookie off a response into a `name=value; name=value` Cookie header. */
|
|
48
|
+
function collectCookies(res, jar) {
|
|
49
|
+
// getSetCookie() returns each Set-Cookie line separately (undici/Node 18.18+).
|
|
50
|
+
const lines = typeof res.headers.getSetCookie === "function"
|
|
51
|
+
? res.headers.getSetCookie()
|
|
52
|
+
: (res.headers.get("set-cookie") ? [res.headers.get("set-cookie")] : []);
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
const pair = line.split(";")[0]?.trim();
|
|
55
|
+
if (!pair)
|
|
56
|
+
continue;
|
|
57
|
+
const eq = pair.indexOf("=");
|
|
58
|
+
if (eq <= 0)
|
|
59
|
+
continue;
|
|
60
|
+
jar.set(pair.slice(0, eq), pair.slice(eq + 1));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function cookieHeader(jar) {
|
|
64
|
+
return [...jar.entries()].map(([k, v]) => `${k}=${v}`).join("; ");
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Run the full SIWE → mint chain and persist the credentials. Throws on any
|
|
68
|
+
* non-2xx step (with the endpoint + status) so the caller can surface a clear error.
|
|
69
|
+
*/
|
|
70
|
+
export async function onboard(env = process.env) {
|
|
71
|
+
const { apiUrl } = readConfig(env);
|
|
72
|
+
const HOST = new URL(apiUrl).host;
|
|
73
|
+
const { account, privateKey, generated } = resolveWallet(env);
|
|
74
|
+
const address = account.address;
|
|
75
|
+
const jar = new Map();
|
|
76
|
+
// 1. nonce
|
|
77
|
+
const nonceRes = await fetch(`${apiUrl}/api/auth/siwe/nonce`, { headers: { accept: "application/json" } });
|
|
78
|
+
if (!nonceRes.ok)
|
|
79
|
+
throw new Error(`GET /api/auth/siwe/nonce → ${nonceRes.status}`);
|
|
80
|
+
collectCookies(nonceRes, jar); // carry siwe-nonce to verify
|
|
81
|
+
const { nonce } = (await nonceRes.json());
|
|
82
|
+
if (!nonce)
|
|
83
|
+
throw new Error("GET /api/auth/siwe/nonce → no nonce in response");
|
|
84
|
+
// 2. build the EXACT SIWE message (newline-joined) and sign it
|
|
85
|
+
const message = [
|
|
86
|
+
`${HOST} wants you to sign in with your Ethereum account:`,
|
|
87
|
+
address,
|
|
88
|
+
"",
|
|
89
|
+
"Sign in to CYBERDYNE — get paid by AI. This request will not trigger a blockchain transaction or cost any gas.",
|
|
90
|
+
"",
|
|
91
|
+
`URI: ${apiUrl}`,
|
|
92
|
+
"Version: 1",
|
|
93
|
+
"Chain ID: 8453",
|
|
94
|
+
`Nonce: ${nonce}`,
|
|
95
|
+
`Issued At: ${new Date().toISOString()}`,
|
|
96
|
+
].join("\n");
|
|
97
|
+
const signature = await account.signMessage({ message });
|
|
98
|
+
// 3. verify → session cookie
|
|
99
|
+
const verifyRes = await fetch(`${apiUrl}/api/auth/siwe/verify`, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: {
|
|
102
|
+
"content-type": "application/json",
|
|
103
|
+
accept: "application/json",
|
|
104
|
+
...(jar.size ? { cookie: cookieHeader(jar) } : {}),
|
|
105
|
+
},
|
|
106
|
+
body: JSON.stringify({ message, signature, role: "agent" }),
|
|
107
|
+
});
|
|
108
|
+
if (!verifyRes.ok) {
|
|
109
|
+
const detail = await verifyRes.text().catch(() => "");
|
|
110
|
+
throw new Error(`POST /api/auth/siwe/verify → ${verifyRes.status} ${detail.slice(0, 200)}`);
|
|
111
|
+
}
|
|
112
|
+
collectCookies(verifyRes, jar); // capture the session cookie(s)
|
|
113
|
+
// 4. mint the agent key with the session cookie
|
|
114
|
+
const keyRes = await fetch(`${apiUrl}/api/agent/key`, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: {
|
|
117
|
+
"content-type": "application/json",
|
|
118
|
+
accept: "application/json",
|
|
119
|
+
cookie: cookieHeader(jar),
|
|
120
|
+
},
|
|
121
|
+
body: "{}",
|
|
122
|
+
});
|
|
123
|
+
if (!keyRes.ok) {
|
|
124
|
+
const detail = await keyRes.text().catch(() => "");
|
|
125
|
+
throw new Error(`POST /api/agent/key → ${keyRes.status} ${detail.slice(0, 200)}`);
|
|
126
|
+
}
|
|
127
|
+
const keyJson = (await keyRes.json());
|
|
128
|
+
const apiKey = keyJson.apiKey?.trim();
|
|
129
|
+
if (!apiKey || !apiKey.startsWith("cyb_")) {
|
|
130
|
+
throw new Error("POST /api/agent/key → no cyb_ apiKey in response");
|
|
131
|
+
}
|
|
132
|
+
// 5. persist BOTH the token and the wallet key atomically (0600)
|
|
133
|
+
const configPath = saveTokenAndWallet(apiKey, privateKey);
|
|
134
|
+
return { address, apiKey, generated, configPath };
|
|
135
|
+
}
|
|
136
|
+
/** The multi-line "next steps" block shared by the CLI + the MCP tool. */
|
|
137
|
+
export function nextStepsText() {
|
|
138
|
+
return [
|
|
139
|
+
"Next steps (no dashboard needed):",
|
|
140
|
+
" 1. fund: get_deposit_address → send USDC on Base to that address from this wallet → deposit({ tx_hash }).",
|
|
141
|
+
" 2. post_task({ title, category, reward_usd, duration_min, difficulty }).",
|
|
142
|
+
" 3. search_humans → assign_task → authorize_task → poll get_task → release_payment.",
|
|
143
|
+
"The same generated wallet auto-signs pool budgets (no env vars needed).",
|
|
144
|
+
].join("\n");
|
|
145
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -57,6 +57,28 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
57
57
|
import { z } from "zod";
|
|
58
58
|
import { CATEGORIES, TASK_CATEGORIES } from "./registry.js";
|
|
59
59
|
import { ApiError, CyberdyneClient, MissingTokenError, readConfig, saveToken } from "./client.js";
|
|
60
|
+
import { onboard, nextStepsText } from "./onboard.js";
|
|
61
|
+
// `cyberdyne-mcp onboard` — FULLY AUTONOMOUS, zero-browser bootstrap (Bankr-style).
|
|
62
|
+
// Generates a wallet (if none) + mints a cyb_ API key via SIWE, saves both to
|
|
63
|
+
// ~/.cyberdyne/config.json (0600), prints the summary, then exits. No web dashboard.
|
|
64
|
+
if (process.argv[2] === "onboard") {
|
|
65
|
+
try {
|
|
66
|
+
const r = await onboard();
|
|
67
|
+
console.error(`✓ CYBERDYNE agent onboarded — wallet ${r.generated ? "GENERATED" : "loaded"} + API key minted.\n` +
|
|
68
|
+
`\n wallet address : ${r.address}` +
|
|
69
|
+
`\n API key (cyb_) : ${r.apiKey} ← shown once; saved to ${r.configPath}` +
|
|
70
|
+
(r.generated
|
|
71
|
+
? `\n (a fresh wallet private key was generated and saved to ${r.configPath}; keep that file safe)`
|
|
72
|
+
: "") +
|
|
73
|
+
`\n\n${nextStepsText()}` +
|
|
74
|
+
`\n\nThis MCP is already configured for this agent — networked tools will use the saved key.`);
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
console.error(`✗ onboard failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
60
82
|
// `cyberdyne-mcp login` — persist the key so the MCP add line can omit it (short
|
|
61
83
|
// one-time-login install). Runs before the server boots, then exits. The key is read
|
|
62
84
|
// (most-private first) from: piped stdin → CYBERDYNE_LOGIN_TOKEN env → argv. argv
|
|
@@ -114,8 +136,19 @@ async function guard(fn) {
|
|
|
114
136
|
}
|
|
115
137
|
}
|
|
116
138
|
// ---- Server ---------------------------------------------------------------
|
|
117
|
-
const server = new McpServer({ name: "cyberdyne", version: "0.
|
|
139
|
+
const server = new McpServer({ name: "cyberdyne", version: "0.6.0" });
|
|
118
140
|
server.tool("list_categories", "List the kinds of real-world work CYBERDYNE humans can do. Static (no network). Use this to learn the valid `category` values before posting a task.", {}, async () => json(Object.entries(CATEGORIES).map(([id, blurb]) => ({ id, blurb }))));
|
|
141
|
+
server.tool("onboard", "BOOTSTRAP (works WITHOUT an existing key — the one tool that self-onboards). Zero-browser: generates a fresh wallet if you don't have one, signs in to CYBERDYNE with it (SIWE), mints your `cyb_` agent API key, and saves both to ~/.cyberdyne/config.json (0600) so every other tool here authenticates automatically. No web dashboard, no env vars. Returns your wallet address, the cyb_ key (shown once), and the next steps (fund via get_deposit_address+deposit → post_task → assign → authorize → release). The same generated wallet auto-signs pool budgets. Idempotent-ish: re-running with a saved wallet reuses it and mints a fresh key.", {}, async () => guard(async () => {
|
|
142
|
+
const r = await onboard();
|
|
143
|
+
return {
|
|
144
|
+
address: r.address,
|
|
145
|
+
apiKey: r.apiKey,
|
|
146
|
+
generated: r.generated,
|
|
147
|
+
savedTo: r.configPath,
|
|
148
|
+
next_steps: nextStepsText(),
|
|
149
|
+
note: "API key + wallet saved (0600). This MCP now authenticates automatically; networked tools are ready.",
|
|
150
|
+
};
|
|
151
|
+
}));
|
|
119
152
|
server.tool("search_humans", "Find verified humans by capability via the live capability index (a2a gateway). Filters are optional and combine (AND). Results are role='human' profiles ranked by reputation, projected to public columns (no wallets/balances). Note: `skills` is an array.", {
|
|
120
153
|
skills: z
|
|
121
154
|
.array(z.enum(TASK_CATEGORIES))
|
|
@@ -277,5 +310,5 @@ server.registerPrompt("quickstart", {
|
|
|
277
310
|
const transport = new StdioServerTransport();
|
|
278
311
|
await server.connect(transport);
|
|
279
312
|
console.error(`CYBERDYNE MCP server running on stdio → ${config.apiUrl}` +
|
|
280
|
-
(config.token ? "" : " (no key — run `npx cyberdyne-mcp login cyb_
|
|
281
|
-
". Tools (
|
|
313
|
+
(config.token ? "" : " (no key — run `npx cyberdyne-mcp onboard` to self-generate a wallet + key, or `login cyb_…`, or set CYBERDYNE_IDENTITY_TOKEN; networked tools error until then)") +
|
|
314
|
+
". Tools (14): onboard, list_categories, search_humans, get_treasury, fund_treasury, get_deposit_address, deposit, post_task, assign_task, authorize_task, get_task, release_payment, review_submission, close_task.");
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -34,25 +34,46 @@ export function configPath(): string {
|
|
|
34
34
|
return join(homedir(), ".cyberdyne", "config.json");
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
/** Shape of the on-disk config. Both fields are optional for back-compat. */
|
|
38
|
+
interface SavedConfig {
|
|
39
|
+
identity_token?: unknown;
|
|
40
|
+
walletKey?: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Read + parse the whole config file (or {} if missing / unreadable). */
|
|
44
|
+
function readSavedConfig(): SavedConfig {
|
|
41
45
|
try {
|
|
42
|
-
const parsed = JSON.parse(readFileSync(configPath(), "utf8")) as
|
|
43
|
-
return typeof parsed
|
|
46
|
+
const parsed = JSON.parse(readFileSync(configPath(), "utf8")) as SavedConfig;
|
|
47
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
44
48
|
} catch {
|
|
45
|
-
return
|
|
49
|
+
return {};
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
// Only the key is persisted (plus the generated wallet key). The API endpoint is
|
|
54
|
+
// intentionally NOT read from this file — a tampered config must never be able to
|
|
55
|
+
// redirect the agent's key to a hostile host (credential exfiltration). The
|
|
56
|
+
// endpoint overrides via env only.
|
|
57
|
+
function readSavedToken(): string | undefined {
|
|
58
|
+
const v = readSavedConfig().identity_token;
|
|
59
|
+
return typeof v === "string" ? v.trim() : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The generated/saved wallet private key (0x…), if any. Used as the EVM signer fallback. */
|
|
63
|
+
export function readSavedWalletKey(): string | undefined {
|
|
64
|
+
const v = readSavedConfig().walletKey;
|
|
65
|
+
return typeof v === "string" && v.trim() ? v.trim() : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Atomically write the config file at ~/.cyberdyne/config.json, mode 0600.
|
|
70
|
+
* Owner-only dir + atomic 0600 create (no world-readable window / TOCTOU), and
|
|
71
|
+
* O_NOFOLLOW so a planted symlink at the path can't redirect the write.
|
|
72
|
+
*/
|
|
73
|
+
function writeConfigFile(data: SavedConfig): string {
|
|
53
74
|
mkdirSync(join(homedir(), ".cyberdyne"), { recursive: true, mode: 0o700 });
|
|
54
75
|
const p = configPath();
|
|
55
|
-
const contents = JSON.stringify(
|
|
76
|
+
const contents = JSON.stringify(data, null, 2);
|
|
56
77
|
let flags = FS.O_WRONLY | FS.O_CREAT | FS.O_TRUNC;
|
|
57
78
|
if (typeof FS.O_NOFOLLOW === "number") flags |= FS.O_NOFOLLOW;
|
|
58
79
|
let fd: number;
|
|
@@ -71,6 +92,28 @@ export function saveToken(token: string): string {
|
|
|
71
92
|
return p;
|
|
72
93
|
}
|
|
73
94
|
|
|
95
|
+
/** Persist the agent key to ~/.cyberdyne/config.json (preserving any saved walletKey). Returns the path. */
|
|
96
|
+
export function saveToken(token: string): string {
|
|
97
|
+
const existing = readSavedConfig();
|
|
98
|
+
const next: SavedConfig = { identity_token: token.trim() };
|
|
99
|
+
if (typeof existing.walletKey === "string" && existing.walletKey.trim()) next.walletKey = existing.walletKey.trim();
|
|
100
|
+
return writeConfigFile(next);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Persist the generated wallet private key (preserving any saved token). Returns the path. */
|
|
104
|
+
export function saveWallet(walletKey: string): string {
|
|
105
|
+
const existing = readSavedConfig();
|
|
106
|
+
const next: SavedConfig = { walletKey: walletKey.trim() };
|
|
107
|
+
if (typeof existing.identity_token === "string" && existing.identity_token.trim())
|
|
108
|
+
next.identity_token = existing.identity_token.trim();
|
|
109
|
+
return writeConfigFile(next);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Persist BOTH the agent key and the wallet key in a single 0600 write. Returns the path. */
|
|
113
|
+
export function saveTokenAndWallet(token: string, walletKey: string): string {
|
|
114
|
+
return writeConfigFile({ identity_token: token.trim(), walletKey: walletKey.trim() });
|
|
115
|
+
}
|
|
116
|
+
|
|
74
117
|
/** Resolve config: token from env first, then the saved login. URL from env only. */
|
|
75
118
|
export function readConfig(env: NodeJS.ProcessEnv = process.env): CyberdyneConfig {
|
|
76
119
|
const apiUrl = (env.CYBERDYNE_API_URL || DEFAULT_API_URL).replace(/\/+$/, "");
|
package/src/evm-signer.ts
CHANGED
|
@@ -16,10 +16,21 @@ import { privateKeyToAccount } from "viem/accounts";
|
|
|
16
16
|
import { createPublicClient, createWalletClient, http, parseUnits } from "viem";
|
|
17
17
|
import { base, baseSepolia } from "viem/chains";
|
|
18
18
|
import { AuthCaptureEvmScheme, toClientEvmSigner } from "@x402/evm";
|
|
19
|
+
import { readSavedWalletKey } from "./client.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the signing private key: CYBERDYNE_EVM_PRIVATE_KEY first, else the
|
|
23
|
+
* walletKey generated at `onboard` and saved in ~/.cyberdyne/config.json. This is
|
|
24
|
+
* what makes onboard → post pool task → authorize all work with the SAME wallet
|
|
25
|
+
* and zero env vars.
|
|
26
|
+
*/
|
|
27
|
+
function resolvePrivateKey(): string | undefined {
|
|
28
|
+
return process.env.CYBERDYNE_EVM_PRIVATE_KEY?.trim() || readSavedWalletKey();
|
|
29
|
+
}
|
|
19
30
|
|
|
20
31
|
function account() {
|
|
21
|
-
const pk =
|
|
22
|
-
if (!pk) throw new Error("CYBERDYNE_EVM_PRIVATE_KEY
|
|
32
|
+
const pk = resolvePrivateKey();
|
|
33
|
+
if (!pk) throw new Error("no signing key (set CYBERDYNE_EVM_PRIVATE_KEY or run `cyberdyne-mcp onboard`)");
|
|
23
34
|
return privateKeyToAccount((pk.startsWith("0x") ? pk : `0x${pk}`) as `0x${string}`);
|
|
24
35
|
}
|
|
25
36
|
function chain() {
|
|
@@ -68,12 +79,12 @@ export async function payDeployFee(params: {
|
|
|
68
79
|
}
|
|
69
80
|
|
|
70
81
|
export function hasEvmKey(): boolean {
|
|
71
|
-
return !!
|
|
82
|
+
return !!resolvePrivateKey();
|
|
72
83
|
}
|
|
73
84
|
|
|
74
85
|
function scheme() {
|
|
75
|
-
const pk =
|
|
76
|
-
if (!pk) throw new Error("
|
|
86
|
+
const pk = resolvePrivateKey();
|
|
87
|
+
if (!pk) throw new Error("no signing key (set CYBERDYNE_EVM_PRIVATE_KEY or run `cyberdyne-mcp onboard`) — cannot sign the escrow authorization");
|
|
77
88
|
const chainId = Number(process.env.CYBERDYNE_CHAIN_ID ?? 8453);
|
|
78
89
|
const chain = chainId === 8453 ? base : baseSepolia;
|
|
79
90
|
const account = privateKeyToAccount((pk.startsWith("0x") ? pk : `0x${pk}`) as `0x${string}`);
|
|
@@ -84,7 +95,8 @@ function scheme() {
|
|
|
84
95
|
|
|
85
96
|
/** The agent's signing wallet address (so the platform can verify payer == this). */
|
|
86
97
|
export function evmAddress(): string {
|
|
87
|
-
const pk =
|
|
98
|
+
const pk = resolvePrivateKey();
|
|
99
|
+
if (!pk) throw new Error("no signing key (set CYBERDYNE_EVM_PRIVATE_KEY or run `cyberdyne-mcp onboard`)");
|
|
88
100
|
return privateKeyToAccount((pk.startsWith("0x") ? pk : `0x${pk}`) as `0x${string}`).address;
|
|
89
101
|
}
|
|
90
102
|
|
package/src/onboard.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FULLY AUTONOMOUS, zero-browser agent onboarding for CYBERDYNE.
|
|
3
|
+
*
|
|
4
|
+
* Goal: an agent goes from "nothing" to "has a wallet + cyb_ API key + can
|
|
5
|
+
* post/pay" using ONLY the CLI/MCP — never the web dashboard. This mirrors the
|
|
6
|
+
* Bankr CLI UX (`bankr login` generates a wallet + API key in one shot).
|
|
7
|
+
*
|
|
8
|
+
* The chain (all on the live platform API, no browser):
|
|
9
|
+
* 1. resolve a wallet — CYBERDYNE_EVM_PRIVATE_KEY → saved walletKey → generate fresh
|
|
10
|
+
* 2. GET /api/auth/siwe/nonce → { nonce } (+ sets a siwe-nonce cookie)
|
|
11
|
+
* 3. build the EXACT SIWE message and sign it with the wallet (EIP-191)
|
|
12
|
+
* 4. POST /api/auth/siwe/verify { message, signature, role:"agent" } → session cookie
|
|
13
|
+
* 5. POST /api/agent/key (session cookie) {} → { apiKey: "cyb_…" }
|
|
14
|
+
* 6. persist BOTH the cyb_ key and the wallet key to ~/.cyberdyne/config.json (0600)
|
|
15
|
+
*
|
|
16
|
+
* Nothing here moves funds or pays gas — onboarding is just a SIWE signature and
|
|
17
|
+
* a DB row. The wallet private key is persisted but NEVER logged to stdout.
|
|
18
|
+
*/
|
|
19
|
+
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
|
20
|
+
import type { PrivateKeyAccount } from "viem/accounts";
|
|
21
|
+
import { readConfig, readSavedWalletKey, saveTokenAndWallet } from "./client.js";
|
|
22
|
+
|
|
23
|
+
/** Normalise a private key to the 0x-prefixed form viem expects. */
|
|
24
|
+
function normalizeKey(pk: string): `0x${string}` {
|
|
25
|
+
return (pk.startsWith("0x") ? pk : `0x${pk}`) as `0x${string}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ResolvedWallet {
|
|
29
|
+
account: PrivateKeyAccount;
|
|
30
|
+
privateKey: `0x${string}`;
|
|
31
|
+
/** true when a fresh key was generated this call (caller persists it). */
|
|
32
|
+
generated: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the onboarding wallet, most-explicit first:
|
|
37
|
+
* 1. CYBERDYNE_EVM_PRIVATE_KEY (env) — operator-supplied
|
|
38
|
+
* 2. saved walletKey in config — generated on a prior onboard
|
|
39
|
+
* 3. generate a fresh key — first run, zero config
|
|
40
|
+
* Always returns a usable account + the private key. Persisting a freshly
|
|
41
|
+
* generated key is the caller's job (onboard() does it atomically with the token).
|
|
42
|
+
*/
|
|
43
|
+
export function resolveWallet(env: NodeJS.ProcessEnv = process.env): ResolvedWallet {
|
|
44
|
+
const fromEnv = env.CYBERDYNE_EVM_PRIVATE_KEY?.trim();
|
|
45
|
+
if (fromEnv) {
|
|
46
|
+
const privateKey = normalizeKey(fromEnv);
|
|
47
|
+
return { account: privateKeyToAccount(privateKey), privateKey, generated: false };
|
|
48
|
+
}
|
|
49
|
+
const saved = readSavedWalletKey();
|
|
50
|
+
if (saved) {
|
|
51
|
+
const privateKey = normalizeKey(saved);
|
|
52
|
+
return { account: privateKeyToAccount(privateKey), privateKey, generated: false };
|
|
53
|
+
}
|
|
54
|
+
const privateKey = generatePrivateKey();
|
|
55
|
+
return { account: privateKeyToAccount(privateKey), privateKey, generated: true };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Pull every Set-Cookie off a response into a `name=value; name=value` Cookie header. */
|
|
59
|
+
function collectCookies(res: Response, jar: Map<string, string>): void {
|
|
60
|
+
// getSetCookie() returns each Set-Cookie line separately (undici/Node 18.18+).
|
|
61
|
+
const lines =
|
|
62
|
+
typeof res.headers.getSetCookie === "function"
|
|
63
|
+
? res.headers.getSetCookie()
|
|
64
|
+
: (res.headers.get("set-cookie") ? [res.headers.get("set-cookie") as string] : []);
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
const pair = line.split(";")[0]?.trim();
|
|
67
|
+
if (!pair) continue;
|
|
68
|
+
const eq = pair.indexOf("=");
|
|
69
|
+
if (eq <= 0) continue;
|
|
70
|
+
jar.set(pair.slice(0, eq), pair.slice(eq + 1));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function cookieHeader(jar: Map<string, string>): string {
|
|
75
|
+
return [...jar.entries()].map(([k, v]) => `${k}=${v}`).join("; ");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface OnboardResult {
|
|
79
|
+
address: string;
|
|
80
|
+
apiKey: string;
|
|
81
|
+
generated: boolean;
|
|
82
|
+
configPath: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Run the full SIWE → mint chain and persist the credentials. Throws on any
|
|
87
|
+
* non-2xx step (with the endpoint + status) so the caller can surface a clear error.
|
|
88
|
+
*/
|
|
89
|
+
export async function onboard(env: NodeJS.ProcessEnv = process.env): Promise<OnboardResult> {
|
|
90
|
+
const { apiUrl } = readConfig(env);
|
|
91
|
+
const HOST = new URL(apiUrl).host;
|
|
92
|
+
const { account, privateKey, generated } = resolveWallet(env);
|
|
93
|
+
const address = account.address;
|
|
94
|
+
|
|
95
|
+
const jar = new Map<string, string>();
|
|
96
|
+
|
|
97
|
+
// 1. nonce
|
|
98
|
+
const nonceRes = await fetch(`${apiUrl}/api/auth/siwe/nonce`, { headers: { accept: "application/json" } });
|
|
99
|
+
if (!nonceRes.ok) throw new Error(`GET /api/auth/siwe/nonce → ${nonceRes.status}`);
|
|
100
|
+
collectCookies(nonceRes, jar); // carry siwe-nonce to verify
|
|
101
|
+
const { nonce } = (await nonceRes.json()) as { nonce?: string };
|
|
102
|
+
if (!nonce) throw new Error("GET /api/auth/siwe/nonce → no nonce in response");
|
|
103
|
+
|
|
104
|
+
// 2. build the EXACT SIWE message (newline-joined) and sign it
|
|
105
|
+
const message = [
|
|
106
|
+
`${HOST} wants you to sign in with your Ethereum account:`,
|
|
107
|
+
address,
|
|
108
|
+
"",
|
|
109
|
+
"Sign in to CYBERDYNE — get paid by AI. This request will not trigger a blockchain transaction or cost any gas.",
|
|
110
|
+
"",
|
|
111
|
+
`URI: ${apiUrl}`,
|
|
112
|
+
"Version: 1",
|
|
113
|
+
"Chain ID: 8453",
|
|
114
|
+
`Nonce: ${nonce}`,
|
|
115
|
+
`Issued At: ${new Date().toISOString()}`,
|
|
116
|
+
].join("\n");
|
|
117
|
+
const signature = await account.signMessage({ message });
|
|
118
|
+
|
|
119
|
+
// 3. verify → session cookie
|
|
120
|
+
const verifyRes = await fetch(`${apiUrl}/api/auth/siwe/verify`, {
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers: {
|
|
123
|
+
"content-type": "application/json",
|
|
124
|
+
accept: "application/json",
|
|
125
|
+
...(jar.size ? { cookie: cookieHeader(jar) } : {}),
|
|
126
|
+
},
|
|
127
|
+
body: JSON.stringify({ message, signature, role: "agent" }),
|
|
128
|
+
});
|
|
129
|
+
if (!verifyRes.ok) {
|
|
130
|
+
const detail = await verifyRes.text().catch(() => "");
|
|
131
|
+
throw new Error(`POST /api/auth/siwe/verify → ${verifyRes.status} ${detail.slice(0, 200)}`);
|
|
132
|
+
}
|
|
133
|
+
collectCookies(verifyRes, jar); // capture the session cookie(s)
|
|
134
|
+
|
|
135
|
+
// 4. mint the agent key with the session cookie
|
|
136
|
+
const keyRes = await fetch(`${apiUrl}/api/agent/key`, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: {
|
|
139
|
+
"content-type": "application/json",
|
|
140
|
+
accept: "application/json",
|
|
141
|
+
cookie: cookieHeader(jar),
|
|
142
|
+
},
|
|
143
|
+
body: "{}",
|
|
144
|
+
});
|
|
145
|
+
if (!keyRes.ok) {
|
|
146
|
+
const detail = await keyRes.text().catch(() => "");
|
|
147
|
+
throw new Error(`POST /api/agent/key → ${keyRes.status} ${detail.slice(0, 200)}`);
|
|
148
|
+
}
|
|
149
|
+
const keyJson = (await keyRes.json()) as { apiKey?: string };
|
|
150
|
+
const apiKey = keyJson.apiKey?.trim();
|
|
151
|
+
if (!apiKey || !apiKey.startsWith("cyb_")) {
|
|
152
|
+
throw new Error("POST /api/agent/key → no cyb_ apiKey in response");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 5. persist BOTH the token and the wallet key atomically (0600)
|
|
156
|
+
const configPath = saveTokenAndWallet(apiKey, privateKey);
|
|
157
|
+
|
|
158
|
+
return { address, apiKey, generated, configPath };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The multi-line "next steps" block shared by the CLI + the MCP tool. */
|
|
162
|
+
export function nextStepsText(): string {
|
|
163
|
+
return [
|
|
164
|
+
"Next steps (no dashboard needed):",
|
|
165
|
+
" 1. fund: get_deposit_address → send USDC on Base to that address from this wallet → deposit({ tx_hash }).",
|
|
166
|
+
" 2. post_task({ title, category, reward_usd, duration_min, difficulty }).",
|
|
167
|
+
" 3. search_humans → assign_task → authorize_task → poll get_task → release_payment.",
|
|
168
|
+
"The same generated wallet auto-signs pool budgets (no env vars needed).",
|
|
169
|
+
].join("\n");
|
|
170
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -57,6 +57,30 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
57
57
|
import { z } from "zod";
|
|
58
58
|
import { CATEGORIES, TASK_CATEGORIES } from "./registry.js";
|
|
59
59
|
import { ApiError, CyberdyneClient, MissingTokenError, readConfig, saveToken } from "./client.js";
|
|
60
|
+
import { onboard, nextStepsText } from "./onboard.js";
|
|
61
|
+
|
|
62
|
+
// `cyberdyne-mcp onboard` — FULLY AUTONOMOUS, zero-browser bootstrap (Bankr-style).
|
|
63
|
+
// Generates a wallet (if none) + mints a cyb_ API key via SIWE, saves both to
|
|
64
|
+
// ~/.cyberdyne/config.json (0600), prints the summary, then exits. No web dashboard.
|
|
65
|
+
if (process.argv[2] === "onboard") {
|
|
66
|
+
try {
|
|
67
|
+
const r = await onboard();
|
|
68
|
+
console.error(
|
|
69
|
+
`✓ CYBERDYNE agent onboarded — wallet ${r.generated ? "GENERATED" : "loaded"} + API key minted.\n` +
|
|
70
|
+
`\n wallet address : ${r.address}` +
|
|
71
|
+
`\n API key (cyb_) : ${r.apiKey} ← shown once; saved to ${r.configPath}` +
|
|
72
|
+
(r.generated
|
|
73
|
+
? `\n (a fresh wallet private key was generated and saved to ${r.configPath}; keep that file safe)`
|
|
74
|
+
: "") +
|
|
75
|
+
`\n\n${nextStepsText()}` +
|
|
76
|
+
`\n\nThis MCP is already configured for this agent — networked tools will use the saved key.`,
|
|
77
|
+
);
|
|
78
|
+
process.exit(0);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
console.error(`✗ onboard failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
60
84
|
|
|
61
85
|
// `cyberdyne-mcp login` — persist the key so the MCP add line can omit it (short
|
|
62
86
|
// one-time-login install). Runs before the server boots, then exits. The key is read
|
|
@@ -123,7 +147,7 @@ async function guard<T>(fn: () => Promise<T>) {
|
|
|
123
147
|
|
|
124
148
|
// ---- Server ---------------------------------------------------------------
|
|
125
149
|
|
|
126
|
-
const server = new McpServer({ name: "cyberdyne", version: "0.
|
|
150
|
+
const server = new McpServer({ name: "cyberdyne", version: "0.6.0" });
|
|
127
151
|
|
|
128
152
|
server.tool(
|
|
129
153
|
"list_categories",
|
|
@@ -132,6 +156,24 @@ server.tool(
|
|
|
132
156
|
async () => json(Object.entries(CATEGORIES).map(([id, blurb]) => ({ id, blurb }))),
|
|
133
157
|
);
|
|
134
158
|
|
|
159
|
+
server.tool(
|
|
160
|
+
"onboard",
|
|
161
|
+
"BOOTSTRAP (works WITHOUT an existing key — the one tool that self-onboards). Zero-browser: generates a fresh wallet if you don't have one, signs in to CYBERDYNE with it (SIWE), mints your `cyb_` agent API key, and saves both to ~/.cyberdyne/config.json (0600) so every other tool here authenticates automatically. No web dashboard, no env vars. Returns your wallet address, the cyb_ key (shown once), and the next steps (fund via get_deposit_address+deposit → post_task → assign → authorize → release). The same generated wallet auto-signs pool budgets. Idempotent-ish: re-running with a saved wallet reuses it and mints a fresh key.",
|
|
162
|
+
{},
|
|
163
|
+
async () =>
|
|
164
|
+
guard(async () => {
|
|
165
|
+
const r = await onboard();
|
|
166
|
+
return {
|
|
167
|
+
address: r.address,
|
|
168
|
+
apiKey: r.apiKey,
|
|
169
|
+
generated: r.generated,
|
|
170
|
+
savedTo: r.configPath,
|
|
171
|
+
next_steps: nextStepsText(),
|
|
172
|
+
note: "API key + wallet saved (0600). This MCP now authenticates automatically; networked tools are ready.",
|
|
173
|
+
};
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
|
|
135
177
|
server.tool(
|
|
136
178
|
"search_humans",
|
|
137
179
|
"Find verified humans by capability via the live capability index (a2a gateway). Filters are optional and combine (AND). Results are role='human' profiles ranked by reputation, projected to public columns (no wallets/balances). Note: `skills` is an array.",
|
|
@@ -390,6 +432,6 @@ const transport = new StdioServerTransport();
|
|
|
390
432
|
await server.connect(transport);
|
|
391
433
|
console.error(
|
|
392
434
|
`CYBERDYNE MCP server running on stdio → ${config.apiUrl}` +
|
|
393
|
-
(config.token ? "" : " (no key — run `npx cyberdyne-mcp login cyb_
|
|
394
|
-
". Tools (
|
|
435
|
+
(config.token ? "" : " (no key — run `npx cyberdyne-mcp onboard` to self-generate a wallet + key, or `login cyb_…`, or set CYBERDYNE_IDENTITY_TOKEN; networked tools error until then)") +
|
|
436
|
+
". Tools (14): onboard, list_categories, search_humans, get_treasury, fund_treasury, get_deposit_address, deposit, post_task, assign_task, authorize_task, get_task, release_payment, review_submission, close_task.",
|
|
395
437
|
);
|