cyberdyne-mcp 0.5.3 → 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 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` works without a token; every other
24
- tool returns a clear error until `CYBERDYNE_IDENTITY_TOKEN` is set.
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). Mint your `cyb_…`
120
- agent key in the app's Agent Console, then:
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? Skip step 1 and pass it inline instead:
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
- // 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() {
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?.identity_token === "string" ? parsed.identity_token.trim() : undefined;
32
+ return parsed && typeof parsed === "object" ? parsed : {};
35
33
  }
36
34
  catch {
37
- return undefined;
35
+ return {};
38
36
  }
39
37
  }
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.
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({ identity_token: token.trim() }, null, 2);
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(/\/+$/, "");
@@ -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 = process.env.CYBERDYNE_EVM_PRIVATE_KEY;
30
+ const pk = resolvePrivateKey();
21
31
  if (!pk)
22
- throw new Error("CYBERDYNE_EVM_PRIVATE_KEY not set");
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() {
@@ -45,21 +55,28 @@ const ERC20_TRANSFER_ABI = [
45
55
  */
46
56
  export async function payDeployFee(params) {
47
57
  const wallet = createWalletClient({ account: account(), chain: chain(), transport: http(process.env.CYBERDYNE_RPC_URL) });
48
- return wallet.writeContract({
58
+ const hash = await wallet.writeContract({
49
59
  address: params.token,
50
60
  abi: ERC20_TRANSFER_ABI,
51
61
  functionName: "transfer",
52
62
  args: [params.recipient, parseUnits(params.amountUsd.toFixed(6), 6)],
53
63
  chain: chain(),
54
64
  });
65
+ // Wait until the fee tx is ≥1 block deep BEFORE returning, so the platform's
66
+ // verifyFeePayment (which requires 1 confirmation, anti-reorg) accepts it on the
67
+ // authorize call that immediately follows. Without this the agent pays the fee
68
+ // then gets a 402 fee_unverified because the tx isn't mined/deep enough yet.
69
+ const pub = createPublicClient({ chain: chain(), transport: http(process.env.CYBERDYNE_RPC_URL) });
70
+ await pub.waitForTransactionReceipt({ hash, confirmations: 2 });
71
+ return hash;
55
72
  }
56
73
  export function hasEvmKey() {
57
- return !!process.env.CYBERDYNE_EVM_PRIVATE_KEY;
74
+ return !!resolvePrivateKey();
58
75
  }
59
76
  function scheme() {
60
- const pk = process.env.CYBERDYNE_EVM_PRIVATE_KEY;
77
+ const pk = resolvePrivateKey();
61
78
  if (!pk)
62
- throw new Error("CYBERDYNE_EVM_PRIVATE_KEY not set — cannot sign the escrow authorization");
79
+ throw new Error("no signing key (set CYBERDYNE_EVM_PRIVATE_KEY or run `cyberdyne-mcp onboard`) — cannot sign the escrow authorization");
63
80
  const chainId = Number(process.env.CYBERDYNE_CHAIN_ID ?? 8453);
64
81
  const chain = chainId === 8453 ? base : baseSepolia;
65
82
  const account = privateKeyToAccount((pk.startsWith("0x") ? pk : `0x${pk}`));
@@ -69,7 +86,9 @@ function scheme() {
69
86
  }
70
87
  /** The agent's signing wallet address (so the platform can verify payer == this). */
71
88
  export function evmAddress() {
72
- const pk = process.env.CYBERDYNE_EVM_PRIVATE_KEY;
89
+ const pk = resolvePrivateKey();
90
+ if (!pk)
91
+ throw new Error("no signing key (set CYBERDYNE_EVM_PRIVATE_KEY or run `cyberdyne-mcp onboard`)");
73
92
  return privateKeyToAccount((pk.startsWith("0x") ? pk : `0x${pk}`)).address;
74
93
  }
75
94
  /**
@@ -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.5.3" });
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_…` or set CYBERDYNE_IDENTITY_TOKEN; networked tools error until then)") +
281
- ". Tools (13): 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.");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyberdyne-mcp",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
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
- // 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 {
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 { identity_token?: unknown };
43
- return typeof parsed?.identity_token === "string" ? parsed.identity_token.trim() : undefined;
46
+ const parsed = JSON.parse(readFileSync(configPath(), "utf8")) as SavedConfig;
47
+ return parsed && typeof parsed === "object" ? parsed : {};
44
48
  } catch {
45
- return undefined;
49
+ return {};
46
50
  }
47
51
  }
48
52
 
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
+ // 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({ identity_token: token.trim() }, null, 2);
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 = process.env.CYBERDYNE_EVM_PRIVATE_KEY;
22
- if (!pk) throw new Error("CYBERDYNE_EVM_PRIVATE_KEY not set");
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() {
@@ -51,22 +62,29 @@ export async function payDeployFee(params: {
51
62
  token: string;
52
63
  }): Promise<string> {
53
64
  const wallet = createWalletClient({ account: account(), chain: chain(), transport: http(process.env.CYBERDYNE_RPC_URL) });
54
- return wallet.writeContract({
65
+ const hash = await wallet.writeContract({
55
66
  address: params.token as `0x${string}`,
56
67
  abi: ERC20_TRANSFER_ABI,
57
68
  functionName: "transfer",
58
69
  args: [params.recipient as `0x${string}`, parseUnits(params.amountUsd.toFixed(6), 6)],
59
70
  chain: chain(),
60
71
  });
72
+ // Wait until the fee tx is ≥1 block deep BEFORE returning, so the platform's
73
+ // verifyFeePayment (which requires 1 confirmation, anti-reorg) accepts it on the
74
+ // authorize call that immediately follows. Without this the agent pays the fee
75
+ // then gets a 402 fee_unverified because the tx isn't mined/deep enough yet.
76
+ const pub = createPublicClient({ chain: chain(), transport: http(process.env.CYBERDYNE_RPC_URL) });
77
+ await pub.waitForTransactionReceipt({ hash, confirmations: 2 });
78
+ return hash;
61
79
  }
62
80
 
63
81
  export function hasEvmKey(): boolean {
64
- return !!process.env.CYBERDYNE_EVM_PRIVATE_KEY;
82
+ return !!resolvePrivateKey();
65
83
  }
66
84
 
67
85
  function scheme() {
68
- const pk = process.env.CYBERDYNE_EVM_PRIVATE_KEY;
69
- if (!pk) throw new Error("CYBERDYNE_EVM_PRIVATE_KEY not set — cannot sign the escrow authorization");
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");
70
88
  const chainId = Number(process.env.CYBERDYNE_CHAIN_ID ?? 8453);
71
89
  const chain = chainId === 8453 ? base : baseSepolia;
72
90
  const account = privateKeyToAccount((pk.startsWith("0x") ? pk : `0x${pk}`) as `0x${string}`);
@@ -77,7 +95,8 @@ function scheme() {
77
95
 
78
96
  /** The agent's signing wallet address (so the platform can verify payer == this). */
79
97
  export function evmAddress(): string {
80
- const pk = process.env.CYBERDYNE_EVM_PRIVATE_KEY!;
98
+ const pk = resolvePrivateKey();
99
+ if (!pk) throw new Error("no signing key (set CYBERDYNE_EVM_PRIVATE_KEY or run `cyberdyne-mcp onboard`)");
81
100
  return privateKeyToAccount((pk.startsWith("0x") ? pk : `0x${pk}`) as `0x${string}`).address;
82
101
  }
83
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.5.3" });
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_…` or set CYBERDYNE_IDENTITY_TOKEN; networked tools error until then)") +
394
- ". Tools (13): 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.",
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
  );