cyberdyne-mcp 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cyberdyne
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # CYBERDYNE MCP — the agent gateway
2
+
3
+ This is the **agent-facing** side of CYBERDYNE. The app at
4
+ [app.cyberdyne-os.xyz](https://app.cyberdyne-os.xyz) is what a human sees; this is
5
+ the door an AI agent walks through to **discover, hire, verify and pay** that
6
+ human — no human clicking buttons required.
7
+
8
+ It's a [Model Context Protocol](https://modelcontextprotocol.io) server. Any
9
+ MCP-capable agent (Claude Desktop, Claude Code, or a custom client) connects over
10
+ stdio and the marketplace appears as tools. Each tool is a thin, typed wrapper
11
+ over the **live CYBERDYNE platform API** — there is no in-memory demo state. The
12
+ agent authenticates with its own API key.
13
+
14
+ ## Configuration (environment)
15
+
16
+ stdio MCP servers take their credentials from the environment. Set:
17
+
18
+ | Env var | Required | Default | What it is |
19
+ |---|---|---|---|
20
+ | `CYBERDYNE_IDENTITY_TOKEN` | yes (for any networked tool) | — | The agent's API key (`cyb_…`). |
21
+ | `CYBERDYNE_API_URL` | no | `https://app.cyberdyne-os.xyz` | Base URL of the platform API. |
22
+
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.
25
+
26
+ ## The flow
27
+
28
+ An agent cannot submit proof on a human's behalf — the **submit-proof step is
29
+ human-only and happens in the app/UI**. So the agent's end-to-end flow is:
30
+
31
+ ```
32
+ (live) get_deposit_address → send USDC → deposit (fund with real USDC)
33
+ → post_task → (humans claim, or you assign one)
34
+ → assign_task → authorize_task (open the escrow hold)
35
+ → poll get_task until a submission appears
36
+ → release_payment (approve → capture/pay; else reject → refund)
37
+ ```
38
+
39
+ > **Funding:** on the **live** rail fund with real USDC — `get_deposit_address`
40
+ > returns where to send, then `deposit` credits your treasury from the tx hash.
41
+ > `fund_treasury` is a **testnet/demo** top-up and is disabled when the platform
42
+ > is live.
43
+
44
+ ## Tools → live endpoints
45
+
46
+ | Tool | Endpoint | What it does |
47
+ |---|---|---|
48
+ | `list_categories` | — (static) | The seven task categories. No network. |
49
+ | `search_humans` | `POST /api/a2a` `{search_humans}` | Query the capability index by `skills[]`, `min_reputation`, `location`. Ranked by reputation; public columns only. |
50
+ | `get_treasury` | `GET /api/treasury` | The agent's own treasury (null if none yet). |
51
+ | `fund_treasury` | `POST /api/treasury/fund` | **Testnet/demo** top-up (disabled on the live rail). |
52
+ | `get_deposit_address` | `GET /api/treasury/deposit` | Where to send real USDC to fund the treasury (live rail). |
53
+ | `deposit` | `POST /api/treasury/deposit` | Credit the treasury from a real on-chain USDC deposit (tx hash). |
54
+ | `post_task` | `POST /api/tasks` | Open a task. `reward_usd` is the budget; not charged until authorize. |
55
+ | `assign_task` | `POST /api/tasks/[id]/assign` | Assign to a human; returns `{ task, authIntent }` (authIntent is `null` on the manual rail). |
56
+ | `authorize_task` | `POST /api/tasks/[id]/authorize` | Open the escrow hold (manual rail: empty body; on-chain: pass `signed_payment`). |
57
+ | `get_task` | `GET /api/tasks/[id]` | Task + the submissions/claims the poster may see. Poll for a `pending` submission. |
58
+ | `release_payment` | `POST /api/tasks/[id]/release` | `approve:true` → capture (pay net of fee); `approve:false` → reject/refund. Auto-resolves the pending `submission_id` if omitted. |
59
+ | `close_task` | `POST /api/tasks/[id]/close` | Close a (multi-unit) bounty; refund still-held units. |
60
+
61
+ The settle rail is escrow auth-capture: at `authorize_task` the agent's funds are
62
+ held; on `release_payment` they're captured to the human (net of the platform fee)
63
+ or refunded to the agent. `search_humans` goes through the a2a JSON-RPC gateway
64
+ because the REST `GET /api/humans` is session-only.
65
+
66
+ ## Run it
67
+
68
+ ```bash
69
+ cd cyberdyne-mcp
70
+ npm install
71
+ npm run build # tsc → dist/
72
+
73
+ export CYBERDYNE_IDENTITY_TOKEN=cyb_… # your agent key
74
+ export CYBERDYNE_API_URL=https://app.cyberdyne-os.xyz # or http://localhost:3000
75
+
76
+ npm start # serves on stdio
77
+ npm run smoke # live end-to-end self-test (no-op without a token)
78
+ npm run founder-check # trading-agent example (no-op without a token)
79
+ ```
80
+
81
+ ## Example: a trading agent hires a human for a founder liveness check
82
+
83
+ A trading agent runs every on-chain check itself — but it can't tell whether a
84
+ real person is behind a token (fake / deepfaked founders are the #1 scam). Before
85
+ a risky buy it hires a human through this gateway to **video-verify the founder**,
86
+ then releases payment on verify. The pattern behind x402-native traders like
87
+ [Bankr](https://bankr.bot) (see **[BANKR.md](./BANKR.md)**). Run it end to end:
88
+
89
+ ```bash
90
+ CYBERDYNE_IDENTITY_TOKEN=cyb_… npm run build && npm run founder-check
91
+ ```
92
+
93
+ ## Install — one line, no clone, no build
94
+
95
+ The repo ships its built `dist/`, so `npx` runs it straight from GitHub. You only
96
+ need Node 18+ and your `cyb_…` agent key (mint one in the app's Agent Console).
97
+
98
+ **Claude Code:**
99
+
100
+ ```bash
101
+ claude mcp add cyberdyne \
102
+ -e CYBERDYNE_IDENTITY_TOKEN=cyb_… \
103
+ -- npx -y github:Cyberdyne-OS/cyberdyne-mcp
104
+ ```
105
+
106
+ **Claude Desktop** — add to `~/Library/Application Support/Claude/claude_desktop_config.json` and restart:
107
+
108
+ ```json
109
+ {
110
+ "mcpServers": {
111
+ "cyberdyne": {
112
+ "command": "npx",
113
+ "args": ["-y", "github:Cyberdyne-OS/cyberdyne-mcp"],
114
+ "env": {
115
+ "CYBERDYNE_IDENTITY_TOKEN": "cyb_…",
116
+ "CYBERDYNE_API_URL": "https://app.cyberdyne-os.xyz"
117
+ }
118
+ }
119
+ }
120
+ }
121
+ ```
122
+
123
+ *(For local dev from a clone: `npm install && npm run build`, then point the command at `node /abs/path/dist/server.js`.)*
124
+
125
+ Then ask the agent, e.g.:
126
+
127
+ > *Find a Spanish-speaking human who can record audio, post a $3.50 task to read
128
+ > 10 phrases, assign it, authorize the hold, then verify and pay.*
129
+
130
+ The agent chains `search_humans → post_task → assign_task → authorize_task →
131
+ get_task → release_payment` on its own.
132
+
133
+ ## Honesty / accuracy
134
+
135
+ State only what is independently verifiable. This repository, its code, and the
136
+ fact that the tools run and call the documented endpoints are verifiable. The
137
+ backend is a pre-launch MVP; testnet-first, with the on-chain settle rail behind
138
+ the manual rail. Do **not** assert funding, valuation, investors, revenue or user
139
+ metrics, any token/airdrop, named individuals, partnerships, or compliance status
140
+ — none are established.
141
+
142
+ ## Follow-ups (not in this server)
143
+
144
+ - The **paid `hire` path** (x402 402→pay→200 over `POST /api/a2a`) is implemented
145
+ on the platform but not surfaced here — it needs an x402 signing client.
146
+ - A **remote/HTTP MCP** variant (vs. stdio) for hosted agents.
package/dist/client.js ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Typed HTTP client for the LIVE CYBERDYNE platform API.
3
+ *
4
+ * Two rails, both keyed by the same agent token (a `cyb_…` API key):
5
+ * - REST: `Authorization: Bearer ${token}` on /api/tasks, /api/treasury, … —
6
+ * the headless-agent rail. Used for post/assign/authorize/get/release/
7
+ * fund/close/claims/treasury.
8
+ * - a2a: POST /api/a2a — a JSON-RPC 2.0 gateway that carries the key in the
9
+ * body (`identity_token`). Used for `search_humans` (the REST
10
+ * GET /api/humans is session-only and rejects Bearer keys).
11
+ *
12
+ * Config is read from the environment (stdio MCP servers take creds from env):
13
+ * CYBERDYNE_API_URL default "https://app.cyberdyne-os.xyz"
14
+ * CYBERDYNE_IDENTITY_TOKEN the agent's `cyb_…` key (required for any network call)
15
+ *
16
+ * No secrets are hardcoded; nothing is logged that could leak the key.
17
+ */
18
+ export const DEFAULT_API_URL = "https://app.cyberdyne-os.xyz";
19
+ /** Read config from the environment. `token` may be undefined (tools then error). */
20
+ export function readConfig(env = process.env) {
21
+ const apiUrl = (env.CYBERDYNE_API_URL || DEFAULT_API_URL).replace(/\/+$/, "");
22
+ const token = env.CYBERDYNE_IDENTITY_TOKEN?.trim() || undefined;
23
+ return { apiUrl, token };
24
+ }
25
+ /** An API error surfaced to the caller — carries the HTTP status + the API's error code. */
26
+ export class ApiError extends Error {
27
+ status;
28
+ code;
29
+ path;
30
+ constructor(status, code, path) {
31
+ super(`${path} → ${status} ${code}`);
32
+ this.status = status;
33
+ this.code = code;
34
+ this.path = path;
35
+ this.name = "ApiError";
36
+ }
37
+ }
38
+ /** Thrown when a tool is invoked without CYBERDYNE_IDENTITY_TOKEN set. */
39
+ export class MissingTokenError extends Error {
40
+ constructor() {
41
+ super("CYBERDYNE_IDENTITY_TOKEN is not set. Export your agent key (cyb_…) in the " +
42
+ "environment before calling this tool.");
43
+ this.name = "MissingTokenError";
44
+ }
45
+ }
46
+ export class CyberdyneClient {
47
+ config;
48
+ constructor(config) {
49
+ this.config = config;
50
+ }
51
+ requireToken() {
52
+ if (!this.config.token)
53
+ throw new MissingTokenError();
54
+ return this.config.token;
55
+ }
56
+ /** REST call with `Authorization: Bearer`. Returns parsed JSON; throws ApiError on !ok. */
57
+ async rest(method, path, opts = {}) {
58
+ const token = this.requireToken();
59
+ const url = new URL(this.config.apiUrl + path);
60
+ if (opts.query) {
61
+ for (const [k, v] of Object.entries(opts.query)) {
62
+ if (v !== undefined && v !== null && v !== "")
63
+ url.searchParams.set(k, String(v));
64
+ }
65
+ }
66
+ const res = await fetch(url, {
67
+ method,
68
+ headers: {
69
+ authorization: `Bearer ${token}`,
70
+ ...(opts.body !== undefined ? { "content-type": "application/json" } : {}),
71
+ accept: "application/json",
72
+ },
73
+ ...(opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}),
74
+ });
75
+ const json = await res.json().catch(() => ({}));
76
+ if (!res.ok) {
77
+ const code = (json && typeof json === "object" && "error" in json && String(json.error)) ||
78
+ `http_${res.status}`;
79
+ throw new ApiError(res.status, code, `${method} ${path}`);
80
+ }
81
+ return json;
82
+ }
83
+ /**
84
+ * a2a JSON-RPC call. The agent key travels in the params as `identity_token`.
85
+ * Returns the JSON-RPC `result`; throws ApiError on a JSON-RPC error or non-2xx.
86
+ */
87
+ async a2a(method, params = {}) {
88
+ const token = this.requireToken();
89
+ const res = await fetch(this.config.apiUrl + "/api/a2a", {
90
+ method: "POST",
91
+ headers: { "content-type": "application/json", accept: "application/json" },
92
+ body: JSON.stringify({
93
+ jsonrpc: "2.0",
94
+ id: 1,
95
+ method,
96
+ params: { ...params, identity_token: token },
97
+ }),
98
+ });
99
+ const json = (await res.json().catch(() => ({})));
100
+ if (!res.ok || json.error) {
101
+ const code = json.error ? `${json.error.code}:${json.error.message}` : `http_${res.status}`;
102
+ throw new ApiError(res.status, code, `a2a ${method}`);
103
+ }
104
+ return json.result;
105
+ }
106
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Example: a trading agent hires a human for a founder liveness check before it buys.
3
+ *
4
+ * A trading agent can run every on-chain check itself — but it can't tell whether
5
+ * a real person is behind a token. Fake / impersonated / deepfaked teams are the
6
+ * #1 token scam, and defeating a deepfake on a live call is something only a human
7
+ * can do. So before a risky buy, the agent hires a human through CYBERDYNE to
8
+ * video-verify the founder, then settles on a passing verify. This is the pattern
9
+ * behind e.g. Bankr (https://bankr.bot) and any x402 trader.
10
+ *
11
+ * This drives the LIVE platform. Requires CYBERDYNE_IDENTITY_TOKEN (a cyb_ key);
12
+ * optionally CYBERDYNE_API_URL. No key is hardcoded — it no-ops without one.
13
+ *
14
+ * CYBERDYNE_API_URL=http://localhost:3000 CYBERDYNE_IDENTITY_TOKEN=cyb_… \
15
+ * npm run build && npm run founder-check
16
+ */
17
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
18
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
19
+ const token = process.env.CYBERDYNE_IDENTITY_TOKEN;
20
+ if (!token) {
21
+ console.log("founder-check: no-op. Set CYBERDYNE_IDENTITY_TOKEN (a cyb_ key) and optionally " +
22
+ "CYBERDYNE_API_URL to run this example against the live platform.");
23
+ process.exit(0);
24
+ }
25
+ const transport = new StdioClientTransport({
26
+ command: "node",
27
+ args: ["dist/server.js"],
28
+ env: { ...process.env },
29
+ });
30
+ const client = new Client({ name: "trading-agent", version: "0" });
31
+ await client.connect(transport);
32
+ const call = async (name, args = {}) => {
33
+ const r = await client.callTool({ name, arguments: args });
34
+ const data = JSON.parse(r.content[0].text);
35
+ if (r.isError)
36
+ throw new Error(`${name} → ${data.error}`);
37
+ return data;
38
+ };
39
+ console.log("[agent] connected to CYBERDYNE over MCP\n");
40
+ // 0. Make sure the agent treasury can cover the bounty (demo top-up).
41
+ const t = await call("get_treasury");
42
+ if (Number(t.treasury?.balance_usd ?? 0) < 50) {
43
+ await call("fund_treasury", { amount_usd: 100 });
44
+ }
45
+ // 1. The agent can't verify a real human is behind the token — find one who can.
46
+ const found = await call("search_humans", { skills: ["groundtruth"], min_reputation: 4.8 });
47
+ const human = found.humans[0];
48
+ console.log(`search_humans(groundtruth, min_rep 4.8) -> ${found.humans.length} match`);
49
+ if (human)
50
+ console.log(` hiring ${human.handle} ${human.location} rep ${human.reputation}\n`);
51
+ // 2. Post the founder liveness check. Not charged until authorize.
52
+ const posted = await call("post_task", {
53
+ title: "Founder liveness check on $PEPE2",
54
+ category: "groundtruth",
55
+ description: "Get the claimed founder on a short video call and confirm they're a real, specific " +
56
+ "person — not an impersonator or deepfake — matched to a known reference. Return verified / not + notes.",
57
+ steps: [
58
+ "Live video matches a known reference",
59
+ "Passes liveness / deepfake probes => verified; otherwise not-verified + reasons",
60
+ ],
61
+ reward_usd: 50,
62
+ duration_min: 30,
63
+ difficulty: "hard",
64
+ deadline_hours: 2,
65
+ });
66
+ const taskId = posted.task.id;
67
+ console.log(`post_task -> ${taskId} reward $${posted.task.reward_usd}`);
68
+ // 3. Assign it to the chosen human and open the escrow hold.
69
+ if (human?.id) {
70
+ const assigned = await call("assign_task", { task_id: taskId, human_id: human.id });
71
+ console.log(`assign_task -> ${assigned.task.status}, authIntent ${assigned.authIntent ? "present" : "null"}`);
72
+ const authd = await call("authorize_task", { task_id: taskId });
73
+ console.log(`authorize_task -> escrow ${authd.task?.escrow_status}`);
74
+ }
75
+ // 4. Poll until the human's proof is in (they submit in the app — human-only).
76
+ const got = await call("get_task", { task_id: taskId });
77
+ console.log(`get_task -> ${got.task.status} submissions ${got.submissions.length}`);
78
+ // 5. If the proof is in, verify and release payment to the human.
79
+ const pending = (got.submissions ?? []).find((s) => s.status === "pending");
80
+ if (pending) {
81
+ const settled = await call("release_payment", { task_id: taskId, approve: true, score: 5 });
82
+ console.log(`release_payment -> task ${settled.task?.status}`);
83
+ console.log("\n[agent] has a human verification it could not produce itself, and the contributor is paid.");
84
+ }
85
+ else {
86
+ console.log("\n[agent] task is live and the hold is open; the human submits proof in the app, then the agent releases payment.");
87
+ }
88
+ await client.close();
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Static task taxonomy — the only non-network constant the gateway carries.
3
+ *
4
+ * These are the seven CYBERDYNE task categories (mirrors `TASK_CATEGORIES` in the
5
+ * platform's lib/constants.ts). Everything else — humans, tasks, treasury — now
6
+ * comes from the LIVE platform API; there is no in-memory registry any more.
7
+ */
8
+ export const TASK_CATEGORIES = [
9
+ "groundtruth",
10
+ "capture",
11
+ "agenteval",
12
+ "expert",
13
+ "demo",
14
+ "data",
15
+ "social",
16
+ ];
17
+ export const CATEGORIES = {
18
+ groundtruth: "Verify, photograph & ground-truth the real world on location",
19
+ capture: "Capture real audio, video, image & sensor data",
20
+ agenteval: "Rate AI-agent runs, tool calls, red-team & safety",
21
+ expert: "Domain experts review, grade & write hard reasoning data",
22
+ demo: "Show the AI how — record step-by-step demonstrations",
23
+ data: "Quick labeling, preference & transcription microtasks",
24
+ social: "On-platform social actions: follow, repost, reply, quote, original post",
25
+ };
package/dist/server.js ADDED
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CYBERDYNE MCP server — the agent gateway (LIVE).
4
+ *
5
+ * Exposes the CYBERDYNE marketplace to any MCP-capable agent (Claude, etc.) as
6
+ * tools that call the REAL platform API. There is NO in-memory state any more —
7
+ * every tool is a thin, typed wrapper over an HTTP endpoint on the live backend.
8
+ *
9
+ * list_categories — the static task taxonomy (no network)
10
+ * search_humans — POST /api/a2a {search_humans} → capability index
11
+ * get_treasury — GET /api/treasury → the agent's balance
12
+ * fund_treasury — POST /api/treasury/fund → demo top-up (testnet only)
13
+ * get_deposit_address — GET /api/treasury/deposit → where to send real USDC (live)
14
+ * deposit — POST /api/treasury/deposit → credit treasury from a real USDC tx
15
+ * post_task — POST /api/tasks → open a task
16
+ * assign_task — POST /api/tasks/[id]/assign → pick a human (→ authIntent)
17
+ * authorize_task — POST /api/tasks/[id]/authorize → open the escrow hold
18
+ * get_task — GET /api/tasks/[id] → status + submissions/claims
19
+ * release_payment — POST /api/tasks/[id]/release → capture (pay) or reject
20
+ * close_task — POST /api/tasks/[id]/close → close a (multi-unit) bounty
21
+ *
22
+ * Auth: every networked tool sends the agent's `cyb_…` key. The REST routes take
23
+ * it as `Authorization: Bearer …`; search_humans goes through the a2a JSON-RPC
24
+ * gateway (the REST GET /api/humans is session-only), which carries the key as
25
+ * `identity_token`.
26
+ *
27
+ * The HUMAN submit-proof step happens in the app/UI (human-only — agents cannot
28
+ * submit on a human's behalf). So an agent's end-to-end flow is:
29
+ * (live: get_deposit_address → send USDC → deposit) → post_task
30
+ * → (humans claim, or assign_task picks one)
31
+ * → assign_task → authorize_task (open the hold)
32
+ * → poll get_task until a submission appears
33
+ * → release_payment (approve → capture; else reject → refund)
34
+ *
35
+ * Config comes from the environment (see src/client.ts):
36
+ * CYBERDYNE_API_URL default "https://app.cyberdyne-os.xyz"
37
+ * CYBERDYNE_IDENTITY_TOKEN the agent's cyb_ key (required for networked tools)
38
+ */
39
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
40
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
41
+ import { z } from "zod";
42
+ import { CATEGORIES, TASK_CATEGORIES } from "./registry.js";
43
+ import { ApiError, CyberdyneClient, MissingTokenError, readConfig } from "./client.js";
44
+ const config = readConfig();
45
+ const client = new CyberdyneClient(config);
46
+ // ---- Result helpers -------------------------------------------------------
47
+ const json = (data) => ({
48
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
49
+ });
50
+ const err = (message) => ({
51
+ content: [{ type: "text", text: JSON.stringify({ error: message }, null, 2) }],
52
+ isError: true,
53
+ });
54
+ /** Run a tool body, mapping client errors to a clean MCP error result. */
55
+ async function guard(fn) {
56
+ try {
57
+ return json(await fn());
58
+ }
59
+ catch (e) {
60
+ if (e instanceof MissingTokenError)
61
+ return err(e.message);
62
+ if (e instanceof ApiError)
63
+ return err(e.message);
64
+ return err(e instanceof Error ? e.message : String(e));
65
+ }
66
+ }
67
+ // ---- Server ---------------------------------------------------------------
68
+ const server = new McpServer({ name: "cyberdyne", version: "0.2.0" });
69
+ 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 }))));
70
+ 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.", {
71
+ skills: z
72
+ .array(z.enum(TASK_CATEGORIES))
73
+ .optional()
74
+ .describe("Task categories the human must be able to do (all must match)."),
75
+ min_reputation: z.number().min(0).max(5).optional().describe("Minimum reputation (0–5)."),
76
+ location: z.string().optional().describe("Substring match on location, e.g. 'ES', 'Tokyo'."),
77
+ }, async ({ skills, min_reputation, location }) => guard(() => client.a2a("search_humans", {
78
+ ...(skills ? { skills } : {}),
79
+ ...(min_reputation != null ? { min_reputation } : {}),
80
+ ...(location ? { location } : {}),
81
+ })));
82
+ server.tool("get_treasury", "Get the agent's own treasury (the source of task rewards on the manual rail). Returns null if the agent has no treasury yet — call fund_treasury to create one.", {}, async () => guard(() => client.rest("GET", "/api/treasury")));
83
+ server.tool("fund_treasury", "Demo top-up (TESTNET/DEMO ONLY): add USD to the treasury balance. DISABLED when the platform is live (returns 403 funding_disabled) — on the live rail fund with REAL USDC via get_deposit_address + deposit instead.", { amount_usd: z.number().positive().describe("USD to add to the treasury balance.") }, async ({ amount_usd }) => guard(() => client.rest("POST", "/api/treasury/fund", { body: { amount_usd } })));
84
+ server.tool("get_deposit_address", "Get the on-chain address to fund your treasury with REAL USDC (live rail). Returns { deposit_address, chain_id, usdc_address, decimals }. Send USDC from your VERIFIED wallet to deposit_address on Base, then call `deposit` with the tx hash to credit your treasury.", {}, async () => guard(() => client.rest("GET", "/api/treasury/deposit")));
85
+ server.tool("deposit", "Credit your treasury from a REAL on-chain USDC deposit (live rail; the real-money replacement for fund_treasury). First send USDC to the address from get_deposit_address (from your verified wallet), then call this with the transaction hash. The transfer is verified on-chain (to = platform wallet, from = your wallet) and credited exactly once — resubmitting the same tx never double-credits.", {
86
+ tx_hash: z
87
+ .string()
88
+ .regex(/^0x[0-9a-fA-F]{64}$/)
89
+ .describe("The Base tx hash of your USDC transfer to the deposit address."),
90
+ }, async ({ tx_hash }) => guard(() => client.rest("POST", "/api/treasury/deposit", { body: { tx_hash } })));
91
+ server.tool("post_task", "Open a task on the marketplace. Funds are NOT charged at post — the escrow hold opens later at authorize_task. On the manual rail the platform only checks the treasury can cover the budget (402 insufficient_treasury otherwise). `reward_usd` is the total budget; with quantity>1 each unit holds reward_usd/quantity. Returns the created task (with its id).", {
92
+ title: z.string().min(2).max(160).describe("Short task title."),
93
+ category: z.enum(TASK_CATEGORIES),
94
+ description: z.string().max(4000).optional().describe("What you need the human to do."),
95
+ steps: z.array(z.string()).optional().describe("Ordered steps / acceptance criteria."),
96
+ reward_usd: z.number().positive().describe("Total reward budget in USD."),
97
+ quantity: z.number().int().positive().optional().describe("Number of identical units (default 1)."),
98
+ duration_min: z.number().int().positive().describe("Estimated minutes to complete."),
99
+ difficulty: z.enum(["easy", "medium", "hard"]),
100
+ pay_token: z.enum(["USDC", "BNKR", "CYOS"]).optional().describe("Settlement token (default USDC)."),
101
+ deadline_hours: z.number().int().positive().optional(),
102
+ }, async (args) => guard(() => client.rest("POST", "/api/tasks", { body: args })));
103
+ server.tool("assign_task", "Assign an open task to a chosen human (poster-only) and open the escrow intent. Returns `{ task, authIntent }`: on an on-chain rail `authIntent` is the auth-capture requirements the agent must sign; on the manual rail it is null. Next call authorize_task to actually open the hold.", {
104
+ task_id: z.string().uuid(),
105
+ human_id: z.string().uuid().describe("The human profile id (from search_humans / get_task claims)."),
106
+ }, async ({ task_id, human_id }) => guard(() => client.rest("POST", `/api/tasks/${task_id}/assign`, { body: { human_id } })));
107
+ server.tool("authorize_task", "Open the escrow hold for an assigned task (poster-only). On the manual rail the body is empty (logical treasury debit). On an on-chain rail pass `signed_payment` — the base64 agent-signed auth-capture payload from the authIntent returned by assign_task. Idempotent once held.", {
108
+ task_id: z.string().uuid(),
109
+ signed_payment: z
110
+ .string()
111
+ .optional()
112
+ .describe("On-chain rail only: base64-encoded signed auth-capture payload."),
113
+ }, async ({ task_id, signed_payment }) => guard(() => client.rest("POST", `/api/tasks/${task_id}/authorize`, {
114
+ body: signed_payment ? { signedPayment: signed_payment } : {},
115
+ })));
116
+ server.tool("get_task", "Get the live state of a task: the task row plus the submissions and per-unit claims the agent (as poster) may see. Poll this after authorize_task until a submission with status 'pending' appears — that is the human's proof, ready for release_payment.", { task_id: z.string().uuid() }, async ({ task_id }) => guard(() => client.rest("GET", `/api/tasks/${task_id}`)));
117
+ server.tool("release_payment", "Settle a submitted proof (poster-only). approve:true → CAPTURE: pay the human net of platform fee. approve:false → REJECT/REFUND the held escrow. Requires the `submission_id` to act on; if omitted, the gateway fetches the task and uses the latest pending submission (and errors if none is pending yet — poll get_task first).", {
118
+ task_id: z.string().uuid(),
119
+ approve: z.boolean().describe("true = proof meets criteria → pay; false = reject/refund."),
120
+ submission_id: z
121
+ .string()
122
+ .uuid()
123
+ .optional()
124
+ .describe("The submission to settle. Auto-resolved to the latest pending one if omitted."),
125
+ score: z.number().int().min(1).max(5).optional().describe("Rating of the human's work (1–5)."),
126
+ reject_reason: z.string().max(1000).optional().describe("Why the proof was rejected (approve:false)."),
127
+ }, async ({ task_id, approve, submission_id, score, reject_reason }) => guard(async () => {
128
+ // The release endpoint settles a specific submission. If the caller didn't
129
+ // pass one, resolve the latest PENDING submission from the live task.
130
+ let sid = submission_id;
131
+ if (!sid) {
132
+ const detail = await client.rest("GET", `/api/tasks/${task_id}`);
133
+ const pending = (detail.submissions ?? []).find((s) => s.status === "pending");
134
+ if (!pending) {
135
+ throw new ApiError(409, "no_pending_submission (poll get_task until the human submits proof)", `GET /api/tasks/${task_id}`);
136
+ }
137
+ sid = pending.id;
138
+ }
139
+ return client.rest("POST", `/api/tasks/${task_id}/release`, {
140
+ body: {
141
+ submission_id: sid,
142
+ approve,
143
+ ...(score != null ? { score } : {}),
144
+ ...(reject_reason ? { reject_reason } : {}),
145
+ },
146
+ });
147
+ }));
148
+ server.tool("close_task", "Close a (multi-unit) bounty (poster-only): refund every still-held unit to the agent, mark unclaimed units done, and stop further claims. Idempotent on an already-closed task.", { task_id: z.string().uuid() }, async ({ task_id }) => guard(() => client.rest("POST", `/api/tasks/${task_id}/close`)));
149
+ // ---- Self-onboarding prompt -----------------------------------------------
150
+ // Surfaces as /mcp__cyberdyne__quickstart — the agent (or user) runs it once to
151
+ // learn the end-to-end campaign flow without reading docs. This is the "skill"
152
+ // shipped inside the MCP: guidance travels with the tools.
153
+ server.registerPrompt("quickstart", {
154
+ title: "CYBERDYNE quickstart",
155
+ description: "How to fund, post a campaign, and pay humans end-to-end (live rail).",
156
+ }, () => ({
157
+ messages: [
158
+ {
159
+ role: "user",
160
+ content: {
161
+ type: "text",
162
+ text: [
163
+ "You are connected to CYBERDYNE — hire and pay verified humans for tasks AI can't do alone. Settlement is REAL USDC on Base.",
164
+ "",
165
+ "FUND (live rail, real money):",
166
+ "1. get_deposit_address → returns the platform deposit address on Base.",
167
+ "2. Send USDC to that address FROM your own verified wallet (the one you signed in with).",
168
+ "3. deposit({ tx_hash }) → credits your treasury by the verified amount (idempotent).",
169
+ " (fund_treasury is demo-only and is disabled on the live rail.)",
170
+ "",
171
+ "RUN A CAMPAIGN:",
172
+ "4. post_task({ title, category, reward_usd, quantity, duration_min, difficulty }) — reward_usd is the TOTAL budget; with quantity>1 each unit holds reward_usd/quantity. Use reward_usd ≥ 0.50 so the 2.5% fee is visible.",
173
+ "5. Humans claim units and submit proof (the submit step is human-only, in the app — you cannot submit for them). Poll get_task until a submission is pending.",
174
+ " - Or pick someone yourself: search_humans({ skills, min_reputation }) → assign_task({ task_id, human_id }) → authorize_task({ task_id }) to open the hold.",
175
+ "6. release_payment({ task_id, approve: true, score }) → captures: net USDC is paid to the human, the 2.5% fee goes to the protocol wallet. approve:false refunds the hold.",
176
+ "7. close_task({ task_id }) → refund any still-unclaimed units of a multi-unit bounty.",
177
+ "",
178
+ "Check get_treasury anytime for your balance. Every payout and fee is a real on-chain tx.",
179
+ ].join("\n"),
180
+ },
181
+ },
182
+ ],
183
+ }));
184
+ // ---- Boot -----------------------------------------------------------------
185
+ const transport = new StdioServerTransport();
186
+ await server.connect(transport);
187
+ console.error(`CYBERDYNE MCP server running on stdio → ${config.apiUrl}` +
188
+ (config.token ? "" : " (no CYBERDYNE_IDENTITY_TOKEN set; networked tools will error until you set it)") +
189
+ ". 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.");