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/dist/smoke.js ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Live smoke test: drive the MCP server against the REAL platform API like an
3
+ * agent would. Not shipped.
4
+ *
5
+ * Requires both env vars to run for real:
6
+ * CYBERDYNE_API_URL e.g. http://localhost:3000 or https://app.cyberdyne-os.xyz
7
+ * CYBERDYNE_IDENTITY_TOKEN the agent's cyb_ key
8
+ * If either is missing it no-ops with a clear message (no key is ever hardcoded).
9
+ *
10
+ * The server inherits this process's env over stdio, so the same creds drive it.
11
+ */
12
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
13
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
14
+ const apiUrl = process.env.CYBERDYNE_API_URL;
15
+ const token = process.env.CYBERDYNE_IDENTITY_TOKEN;
16
+ if (!token) {
17
+ console.log("smoke: no-op. Set CYBERDYNE_IDENTITY_TOKEN (a cyb_ key) and optionally " +
18
+ "CYBERDYNE_API_URL to run the live flow against the platform.\n" +
19
+ " e.g. CYBERDYNE_API_URL=http://localhost:3000 CYBERDYNE_IDENTITY_TOKEN=cyb_… npm run smoke");
20
+ process.exit(0);
21
+ }
22
+ const transport = new StdioClientTransport({
23
+ command: "node",
24
+ args: ["dist/server.js"],
25
+ env: { ...process.env },
26
+ });
27
+ const client = new Client({ name: "smoke", version: "0" });
28
+ await client.connect(transport);
29
+ const call = async (name, args = {}) => {
30
+ const r = await client.callTool({ name, arguments: args });
31
+ const data = JSON.parse(r.content[0].text);
32
+ if (r.isError)
33
+ throw new Error(`${name} → ${data.error}`);
34
+ return data;
35
+ };
36
+ console.log(`smoke → ${apiUrl ?? "https://app.cyberdyne-os.xyz"}`);
37
+ console.log("tools:", (await client.listTools()).tools.map((t) => t.name).join(", "));
38
+ // 1. Categories (static, no network).
39
+ const cats = await call("list_categories");
40
+ console.log(`list_categories → ${cats.length} categories`);
41
+ // 2. Treasury — ensure it can cover a small task; top up if needed.
42
+ let treasury = await call("get_treasury");
43
+ const balance = Number(treasury.treasury?.balance_usd ?? 0);
44
+ console.log(`get_treasury → balance $${balance}`);
45
+ if (balance < 5) {
46
+ treasury = await call("fund_treasury", { amount_usd: 25 });
47
+ console.log(`fund_treasury → balance $${treasury.treasury?.balance_usd}`);
48
+ }
49
+ // 3. Discover a human via the live capability index.
50
+ const found = await call("search_humans", { skills: ["capture"] });
51
+ console.log(`search_humans(capture) → ${found.humans.length} match`);
52
+ const human = found.humans[0];
53
+ // 4. Post a task. reward_usd is the budget; not charged until authorize.
54
+ const posted = await call("post_task", {
55
+ title: "Read 10 phrases (smoke)",
56
+ category: "capture",
57
+ description: "Record 10 short phrases clearly in a quiet room.",
58
+ reward_usd: 3.5,
59
+ duration_min: 10,
60
+ difficulty: "easy",
61
+ });
62
+ const taskId = posted.task.id;
63
+ console.log(`post_task → ${taskId} (status ${posted.task.status})`);
64
+ // 5. If we found a human, assign + authorize (open the escrow hold).
65
+ if (human?.id) {
66
+ const assigned = await call("assign_task", { task_id: taskId, human_id: human.id });
67
+ console.log(`assign_task → status ${assigned.task.status}, authIntent ${assigned.authIntent ? "present" : "null (manual rail)"}`);
68
+ const authd = await call("authorize_task", { task_id: taskId });
69
+ console.log(`authorize_task → escrow_status ${authd.task?.escrow_status}`);
70
+ }
71
+ // 6. Poll the live task. The human submits proof in the app (human-only), so on a
72
+ // fresh task there is typically no submission yet — that is expected here.
73
+ const got = await call("get_task", { task_id: taskId });
74
+ console.log(`get_task → status ${got.task.status}, submissions ${got.submissions.length}, claims ${got.claims.length}`);
75
+ const pending = (got.submissions ?? []).find((s) => s.status === "pending");
76
+ if (pending) {
77
+ const settled = await call("release_payment", { task_id: taskId, approve: true, score: 5 });
78
+ console.log(`release_payment → settled, task status ${settled.task?.status}`);
79
+ }
80
+ else {
81
+ console.log("release_payment → skipped: no pending submission yet (a human submits proof in the app). " +
82
+ "Closing the task to release the hold.");
83
+ const closed = await call("close_task", { task_id: taskId });
84
+ console.log(`close_task → status ${closed.task?.status}`);
85
+ }
86
+ await client.close();
87
+ console.log("OK");
package/llms.txt ADDED
@@ -0,0 +1,66 @@
1
+ # CYBERDYNE MCP — agent gateway
2
+
3
+ > The agent-facing side of CYBERDYNE. An open Model Context Protocol (MCP)
4
+ > server that lets an AI agent discover, hire, verify and pay verified humans
5
+ > for real-world tasks an AI can't do alone — voice, on-location observation,
6
+ > physical judgment, data work. Tagline: Get Paid by AI.
7
+
8
+ This file helps AI agents understand and use this repository accurately.
9
+
10
+ ## What this is
11
+
12
+ A runnable MCP server (TypeScript, stdio). Each tool is a thin, typed wrapper
13
+ over the LIVE CYBERDYNE platform API — there is no in-memory demo state. The
14
+ agent authenticates with its own API key, read from the environment. It is the
15
+ machine interface to the human-facing app at https://app.cyberdyne-os.xyz.
16
+
17
+ Repository: https://github.com/Cyberdyne-OS/cyberdyne-mcp
18
+ Project: https://cyberdyne-os.xyz
19
+ License: MIT
20
+
21
+ ## Configuration (environment)
22
+
23
+ - CYBERDYNE_IDENTITY_TOKEN — the agent's API key (cyb_…). Required for every
24
+ networked tool. Never hardcoded.
25
+ - CYBERDYNE_API_URL — base URL of the platform API. Default
26
+ https://app.cyberdyne-os.xyz
27
+
28
+ ## Tools → live endpoints
29
+
30
+ - list_categories — static; the seven task categories (no network)
31
+ - search_humans — POST /api/a2a {search_humans}; query by skills[], min_reputation, location
32
+ - get_treasury — GET /api/treasury; the agent's own treasury
33
+ - fund_treasury — POST /api/treasury/fund; demo top-up
34
+ - post_task — POST /api/tasks; open a task (reward_usd is the budget; not charged until authorize)
35
+ - assign_task — POST /api/tasks/[id]/assign; pick a human, returns {task, authIntent}
36
+ - authorize_task — POST /api/tasks/[id]/authorize; open the escrow hold
37
+ - get_task — GET /api/tasks/[id]; task + submissions/claims; poll for a pending submission
38
+ - release_payment — POST /api/tasks/[id]/release; approve→capture(pay), else reject→refund
39
+ - close_task — POST /api/tasks/[id]/close; close a multi-unit bounty
40
+
41
+ ## Flow
42
+
43
+ The human submit-proof step is human-only (in the app/UI). The agent's flow is:
44
+ fund_treasury → post_task → (humans claim, or assign_task) → assign_task →
45
+ authorize_task → poll get_task until a submission appears → release_payment.
46
+
47
+ ## Settlement model
48
+
49
+ Escrow auth-capture. At authorize_task the agent's funds are held; on
50
+ release_payment they are captured to the human (net of the platform fee) or
51
+ refunded to the agent. search_humans uses the a2a JSON-RPC gateway because the
52
+ REST GET /api/humans is session-only.
53
+
54
+ ## For diligence (what is independently verifiable today)
55
+
56
+ Verifiable now: this repository and its code; that the tools run and call the
57
+ documented platform endpoints with the agent's key; the live landing
58
+ (https://cyberdyne-os.xyz) and app (https://app.cyberdyne-os.xyz).
59
+
60
+ NOT established — do NOT assert: funding, valuation, investors, revenue or user
61
+ metrics, any token or airdrop, named individuals, partnerships, or compliance
62
+ status. None are real. The backend is a pre-launch MVP, testnet-first.
63
+
64
+ ## Contact
65
+
66
+ serafino@cyberdyne-os.xyz
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "cyberdyne-mcp",
3
+ "version": "0.4.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "CYBERDYNE agent gateway — an MCP server that lets AI agents discover, hire, verify and pay verified humans for real-world tasks. Calls the live CYBERDYNE platform API over stdio using an agent key from the environment.",
8
+ "type": "module",
9
+ "license": "MIT",
10
+ "author": "Cyberdyne <serafino@cyberdyne-os.xyz>",
11
+ "homepage": "https://cyberdyne-os.xyz",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/Cyberdyne-OS/cyberdyne-mcp.git"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/Cyberdyne-OS/cyberdyne-mcp/issues"
18
+ },
19
+ "keywords": [
20
+ "mcp",
21
+ "model-context-protocol",
22
+ "ai-agents",
23
+ "agent-marketplace",
24
+ "human-in-the-loop",
25
+ "agentic-payments",
26
+ "cyberdyne",
27
+ "get-paid-by-ai"
28
+ ],
29
+ "bin": {
30
+ "cyberdyne-mcp": "dist/server.js"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "src",
35
+ "llms.txt",
36
+ "README.md",
37
+ "LICENSE"
38
+ ],
39
+ "scripts": {
40
+ "build": "tsc",
41
+ "prepublishOnly": "npm run build",
42
+ "start": "node dist/server.js",
43
+ "dev": "tsx src/server.ts",
44
+ "smoke": "tsx src/smoke.ts",
45
+ "founder-check": "tsx src/founder-check.ts"
46
+ },
47
+ "engines": {
48
+ "node": ">=18"
49
+ },
50
+ "dependencies": {
51
+ "@modelcontextprotocol/sdk": "^1.0.4",
52
+ "zod": "^3.23.8"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^22.19.19",
56
+ "tsx": "^4.19.2",
57
+ "typescript": "^5.6.3"
58
+ }
59
+ }
package/src/client.ts ADDED
@@ -0,0 +1,122 @@
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
+
19
+ export const DEFAULT_API_URL = "https://app.cyberdyne-os.xyz";
20
+
21
+ export interface CyberdyneConfig {
22
+ apiUrl: string;
23
+ token: string | undefined;
24
+ }
25
+
26
+ /** Read config from the environment. `token` may be undefined (tools then error). */
27
+ export function readConfig(env: NodeJS.ProcessEnv = process.env): CyberdyneConfig {
28
+ const apiUrl = (env.CYBERDYNE_API_URL || DEFAULT_API_URL).replace(/\/+$/, "");
29
+ const token = env.CYBERDYNE_IDENTITY_TOKEN?.trim() || undefined;
30
+ return { apiUrl, token };
31
+ }
32
+
33
+ /** An API error surfaced to the caller — carries the HTTP status + the API's error code. */
34
+ export class ApiError extends Error {
35
+ constructor(
36
+ public readonly status: number,
37
+ public readonly code: string,
38
+ public readonly path: string,
39
+ ) {
40
+ super(`${path} → ${status} ${code}`);
41
+ this.name = "ApiError";
42
+ }
43
+ }
44
+
45
+ /** Thrown when a tool is invoked without CYBERDYNE_IDENTITY_TOKEN set. */
46
+ export class MissingTokenError extends Error {
47
+ constructor() {
48
+ super(
49
+ "CYBERDYNE_IDENTITY_TOKEN is not set. Export your agent key (cyb_…) in the " +
50
+ "environment before calling this tool.",
51
+ );
52
+ this.name = "MissingTokenError";
53
+ }
54
+ }
55
+
56
+ export class CyberdyneClient {
57
+ constructor(private readonly config: CyberdyneConfig) {}
58
+
59
+ private requireToken(): string {
60
+ if (!this.config.token) throw new MissingTokenError();
61
+ return this.config.token;
62
+ }
63
+
64
+ /** REST call with `Authorization: Bearer`. Returns parsed JSON; throws ApiError on !ok. */
65
+ async rest<T = unknown>(
66
+ method: "GET" | "POST",
67
+ path: string,
68
+ opts: { query?: Record<string, string | number | boolean | undefined>; body?: unknown } = {},
69
+ ): Promise<T> {
70
+ const token = this.requireToken();
71
+ const url = new URL(this.config.apiUrl + path);
72
+ if (opts.query) {
73
+ for (const [k, v] of Object.entries(opts.query)) {
74
+ if (v !== undefined && v !== null && v !== "") url.searchParams.set(k, String(v));
75
+ }
76
+ }
77
+ const res = await fetch(url, {
78
+ method,
79
+ headers: {
80
+ authorization: `Bearer ${token}`,
81
+ ...(opts.body !== undefined ? { "content-type": "application/json" } : {}),
82
+ accept: "application/json",
83
+ },
84
+ ...(opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}),
85
+ });
86
+ const json = await res.json().catch(() => ({}) as Record<string, unknown>);
87
+ if (!res.ok) {
88
+ const code =
89
+ (json && typeof json === "object" && "error" in json && String((json as { error: unknown }).error)) ||
90
+ `http_${res.status}`;
91
+ throw new ApiError(res.status, code, `${method} ${path}`);
92
+ }
93
+ return json as T;
94
+ }
95
+
96
+ /**
97
+ * a2a JSON-RPC call. The agent key travels in the params as `identity_token`.
98
+ * Returns the JSON-RPC `result`; throws ApiError on a JSON-RPC error or non-2xx.
99
+ */
100
+ async a2a<T = unknown>(method: string, params: Record<string, unknown> = {}): Promise<T> {
101
+ const token = this.requireToken();
102
+ const res = await fetch(this.config.apiUrl + "/api/a2a", {
103
+ method: "POST",
104
+ headers: { "content-type": "application/json", accept: "application/json" },
105
+ body: JSON.stringify({
106
+ jsonrpc: "2.0",
107
+ id: 1,
108
+ method,
109
+ params: { ...params, identity_token: token },
110
+ }),
111
+ });
112
+ const json = (await res.json().catch(() => ({}))) as {
113
+ result?: T;
114
+ error?: { code: number; message: string };
115
+ };
116
+ if (!res.ok || json.error) {
117
+ const code = json.error ? `${json.error.code}:${json.error.message}` : `http_${res.status}`;
118
+ throw new ApiError(res.status, code, `a2a ${method}`);
119
+ }
120
+ return json.result as T;
121
+ }
122
+ }
@@ -0,0 +1,99 @@
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
+
20
+ const token = process.env.CYBERDYNE_IDENTITY_TOKEN;
21
+ if (!token) {
22
+ console.log(
23
+ "founder-check: no-op. Set CYBERDYNE_IDENTITY_TOKEN (a cyb_ key) and optionally " +
24
+ "CYBERDYNE_API_URL to run this example against the live platform.",
25
+ );
26
+ process.exit(0);
27
+ }
28
+
29
+ const transport = new StdioClientTransport({
30
+ command: "node",
31
+ args: ["dist/server.js"],
32
+ env: { ...process.env } as Record<string, string>,
33
+ });
34
+ const client = new Client({ name: "trading-agent", version: "0" });
35
+ await client.connect(transport);
36
+
37
+ const call = async (name: string, args: Record<string, unknown> = {}) => {
38
+ const r: any = await client.callTool({ name, arguments: args });
39
+ const data = JSON.parse(r.content[0].text);
40
+ if (r.isError) throw new Error(`${name} → ${data.error}`);
41
+ return data;
42
+ };
43
+
44
+ console.log("[agent] connected to CYBERDYNE over MCP\n");
45
+
46
+ // 0. Make sure the agent treasury can cover the bounty (demo top-up).
47
+ const t = await call("get_treasury");
48
+ if (Number(t.treasury?.balance_usd ?? 0) < 50) {
49
+ await call("fund_treasury", { amount_usd: 100 });
50
+ }
51
+
52
+ // 1. The agent can't verify a real human is behind the token — find one who can.
53
+ const found = await call("search_humans", { skills: ["groundtruth"], min_reputation: 4.8 });
54
+ const human = found.humans[0];
55
+ console.log(`search_humans(groundtruth, min_rep 4.8) -> ${found.humans.length} match`);
56
+ if (human) console.log(` hiring ${human.handle} ${human.location} rep ${human.reputation}\n`);
57
+
58
+ // 2. Post the founder liveness check. Not charged until authorize.
59
+ const posted = await call("post_task", {
60
+ title: "Founder liveness check on $PEPE2",
61
+ category: "groundtruth",
62
+ description:
63
+ "Get the claimed founder on a short video call and confirm they're a real, specific " +
64
+ "person — not an impersonator or deepfake — matched to a known reference. Return verified / not + notes.",
65
+ steps: [
66
+ "Live video matches a known reference",
67
+ "Passes liveness / deepfake probes => verified; otherwise not-verified + reasons",
68
+ ],
69
+ reward_usd: 50,
70
+ duration_min: 30,
71
+ difficulty: "hard",
72
+ deadline_hours: 2,
73
+ });
74
+ const taskId = posted.task.id;
75
+ console.log(`post_task -> ${taskId} reward $${posted.task.reward_usd}`);
76
+
77
+ // 3. Assign it to the chosen human and open the escrow hold.
78
+ if (human?.id) {
79
+ const assigned = await call("assign_task", { task_id: taskId, human_id: human.id });
80
+ console.log(`assign_task -> ${assigned.task.status}, authIntent ${assigned.authIntent ? "present" : "null"}`);
81
+ const authd = await call("authorize_task", { task_id: taskId });
82
+ console.log(`authorize_task -> escrow ${authd.task?.escrow_status}`);
83
+ }
84
+
85
+ // 4. Poll until the human's proof is in (they submit in the app — human-only).
86
+ const got = await call("get_task", { task_id: taskId });
87
+ console.log(`get_task -> ${got.task.status} submissions ${got.submissions.length}`);
88
+
89
+ // 5. If the proof is in, verify and release payment to the human.
90
+ const pending = (got.submissions ?? []).find((s: any) => s.status === "pending");
91
+ if (pending) {
92
+ const settled = await call("release_payment", { task_id: taskId, approve: true, score: 5 });
93
+ console.log(`release_payment -> task ${settled.task?.status}`);
94
+ console.log("\n[agent] has a human verification it could not produce itself, and the contributor is paid.");
95
+ } else {
96
+ console.log("\n[agent] task is live and the hold is open; the human submits proof in the app, then the agent releases payment.");
97
+ }
98
+
99
+ await client.close();
@@ -0,0 +1,29 @@
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
+
9
+ export const TASK_CATEGORIES = [
10
+ "groundtruth",
11
+ "capture",
12
+ "agenteval",
13
+ "expert",
14
+ "demo",
15
+ "data",
16
+ "social",
17
+ ] as const;
18
+
19
+ export type Category = (typeof TASK_CATEGORIES)[number];
20
+
21
+ export const CATEGORIES: Record<Category, string> = {
22
+ groundtruth: "Verify, photograph & ground-truth the real world on location",
23
+ capture: "Capture real audio, video, image & sensor data",
24
+ agenteval: "Rate AI-agent runs, tool calls, red-team & safety",
25
+ expert: "Domain experts review, grade & write hard reasoning data",
26
+ demo: "Show the AI how — record step-by-step demonstrations",
27
+ data: "Quick labeling, preference & transcription microtasks",
28
+ social: "On-platform social actions: follow, repost, reply, quote, original post",
29
+ };