ouro-mcp 1.0.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 ADDED
@@ -0,0 +1,155 @@
1
+ # ouro-mcp
2
+
3
+ [![npm](https://img.shields.io/npm/v/ouro-mcp)](https://www.npmjs.com/package/ouro-mcp)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ Run HPC compute jobs from any AI agent — paid in USDC on Base via [x402](https://www.x402.org/).
7
+
8
+ ## What is Ouro?
9
+
10
+ Ouro is an autonomous agent that sells high-performance compute on a Slurm cluster. This MCP server lets any AI agent submit jobs, poll for results, and pay automatically — your wallet signs USDC payments locally via x402, so your private key never leaves your machine.
11
+
12
+ ## Quick Start
13
+
14
+ You need Node.js 18+ and a wallet with USDC on Base.
15
+
16
+ ```bash
17
+ npx -y ouro-mcp
18
+ ```
19
+
20
+ Add to your MCP client config (works with Cursor, Claude Code, Claude Desktop, VS Code, Windsurf, and any MCP-compatible client):
21
+
22
+ ```json
23
+ {
24
+ "mcpServers": {
25
+ "ouro": {
26
+ "command": "npx",
27
+ "args": ["-y", "ouro-mcp"],
28
+ "env": { "WALLET_PRIVATE_KEY": "0x..." }
29
+ }
30
+ }
31
+ }
32
+ ```
33
+
34
+ > **Claude Code** also supports: `claude mcp add ouro -- npx -y ouro-mcp`
35
+
36
+ ## Usage Examples
37
+
38
+ ### Run a simple script
39
+
40
+ ```
41
+ User: Run a Python script that computes the first 1000 primes
42
+
43
+ → run_job(
44
+ script: "python3 -c \"primes = []; n = 2\nwhile len(primes) < 1000:\n if all(n % p for p in primes): primes.append(n)\n n += 1\nprint(primes)\"",
45
+ image: "ouro-python"
46
+ )
47
+
48
+ ← { "job_id": "abc123", "price_usdc": "0.01", "status": "pending" }
49
+ ```
50
+
51
+ ### Multi-file project with custom Dockerfile
52
+
53
+ ```
54
+ → run_job(
55
+ files: [
56
+ { "path": "main.py", "content": "import numpy as np\nprint(np.random.rand(3, 3))" },
57
+ { "path": "Dockerfile", "content": "FROM python:3.12-slim\nRUN pip install numpy\nENTRYPOINT [\"python\", \"main.py\"]" }
58
+ ],
59
+ image: "python:3.12-slim",
60
+ cpus: 2,
61
+ time_limit_min: 5
62
+ )
63
+
64
+ ← { "job_id": "def456", "price_usdc": "0.03", "status": "pending" }
65
+ ```
66
+
67
+ ### Poll for results
68
+
69
+ ```
70
+ → get_job_status(job_id: "abc123")
71
+
72
+ ← { "status": "completed", "output": "[2, 3, 5, 7, 11, ...]", "runtime_seconds": 4.2 }
73
+ ```
74
+
75
+ ### Check pricing first
76
+
77
+ ```
78
+ → get_price_quote(cpus: 4, time_limit_min: 10)
79
+
80
+ ← { "price_usdc": "0.12", "breakdown": { "base": 0.01, "cpu_multiplier": 4, ... } }
81
+ ```
82
+
83
+ ## Tools
84
+
85
+ ### `run_job`
86
+
87
+ Submit a compute job and pay automatically. Returns `job_id` when accepted.
88
+
89
+ | Parameter | Type | Required | Default | Description |
90
+ |-----------|------|----------|---------|-------------|
91
+ | `script` | string | One of `script` or `files` | — | Shell script to execute |
92
+ | `files` | array | One of `script` or `files` | — | Array of `{path, content}` objects (can include a Dockerfile) |
93
+ | `image` | string | No | `ouro-ubuntu` | Container image to use |
94
+ | `cpus` | integer | No | `1` | CPU cores (1–8) |
95
+ | `time_limit_min` | integer | No | `1` | Max runtime in minutes |
96
+ | `builder_code` | string | No | — | Builder code for [ERC-8021](https://eips.ethereum.org/EIPS/eip-8021) attribution |
97
+
98
+ ### `get_job_status`
99
+
100
+ Check the status of a submitted job. Returns output when completed.
101
+
102
+ | Parameter | Type | Required | Description |
103
+ |-----------|------|----------|-------------|
104
+ | `job_id` | string | Yes | Job ID returned by `run_job` |
105
+
106
+ ### `get_price_quote`
107
+
108
+ Get a price quote without submitting or paying.
109
+
110
+ | Parameter | Type | Required | Default | Description |
111
+ |-----------|------|----------|---------|-------------|
112
+ | `cpus` | integer | No | `1` | CPU cores |
113
+ | `time_limit_min` | integer | No | `1` | Max runtime in minutes |
114
+ | `submission_mode` | string | No | `script` | `script` or `multi_file` |
115
+
116
+ ### `get_allowed_images`
117
+
118
+ List available container images. No parameters.
119
+
120
+ ## Container Images
121
+
122
+ **Prebuilt (instant start):**
123
+ - `ouro-ubuntu` — Ubuntu 22.04
124
+ - `ouro-python` — Python 3.12 with pip
125
+ - `ouro-nodejs` — Node.js 20 LTS
126
+
127
+ **Custom:** Include a `Dockerfile` in your `files` array to use any Docker Hub image:
128
+ ```dockerfile
129
+ FROM python:3.12-slim
130
+ RUN pip install numpy pandas
131
+ ENTRYPOINT ["python", "main.py"]
132
+ ```
133
+
134
+ ## Pricing
135
+
136
+ Jobs start at ~$0.01 USDC (1 CPU, 1 minute). Price scales with CPUs and time limit. Ouro uses dynamic pricing — use `get_price_quote` to check the current price before submitting.
137
+
138
+ Jobs that finish early receive proportional credits for unused compute time, automatically applied to future jobs.
139
+
140
+ ## Environment Variables
141
+
142
+ | Variable | Required | Description |
143
+ |----------|----------|-------------|
144
+ | `WALLET_PRIVATE_KEY` | Yes | Hex private key (starting with `0x`) for USDC payment signing |
145
+ | `OURO_API_URL` | No | API base URL (default: `https://api.ourocompute.com`) |
146
+
147
+ ## Security
148
+
149
+ Your private key **never leaves your machine**. The MCP server runs locally as a stdio process and only uses the key to sign USDC payment authorizations via x402. No keys are sent to any remote server.
150
+
151
+ ## Links
152
+
153
+ - [Dashboard](https://ourocompute.com) — live P&L, job stats, submit jobs
154
+ - [API Docs](https://github.com/richtan/ouro/blob/main/docs/api-reference.md)
155
+ - [GitHub](https://github.com/richtan/ouro)
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { privateKeyToAccount } from "viem/accounts";
6
+ import { createPublicClient, http } from "viem";
7
+ import { base } from "viem/chains";
8
+ import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
9
+ import { ExactEvmScheme, toClientEvmSigner } from "@x402/evm";
10
+ // ---------------------------------------------------------------------------
11
+ // Environment
12
+ // ---------------------------------------------------------------------------
13
+ const WALLET_PRIVATE_KEY = process.env.WALLET_PRIVATE_KEY;
14
+ if (!WALLET_PRIVATE_KEY) {
15
+ console.error("Error: WALLET_PRIVATE_KEY environment variable is required.\n" +
16
+ 'Set it in your MCP config\'s env section (hex string starting with "0x").');
17
+ process.exit(1);
18
+ }
19
+ if (!WALLET_PRIVATE_KEY.startsWith("0x")) {
20
+ console.error("Error: WALLET_PRIVATE_KEY must be a hex string starting with 0x");
21
+ process.exit(1);
22
+ }
23
+ const API_URL = (process.env.OURO_API_URL || "https://api.ourocompute.com").replace(/\/$/, "");
24
+ // ---------------------------------------------------------------------------
25
+ // Wallet & x402
26
+ // ---------------------------------------------------------------------------
27
+ const account = privateKeyToAccount(WALLET_PRIVATE_KEY);
28
+ const walletAddress = account.address;
29
+ const publicClient = createPublicClient({ chain: base, transport: http() });
30
+ const signer = toClientEvmSigner(account, publicClient);
31
+ const payFetch = wrapFetchWithPaymentFromConfig(fetch, {
32
+ schemes: [{ network: "eip155:8453", client: new ExactEvmScheme(signer) }],
33
+ });
34
+ const truncAddr = `${walletAddress.slice(0, 6)}...${walletAddress.slice(-4)}`;
35
+ console.error(`Ouro MCP server running · wallet: ${truncAddr}`);
36
+ // ---------------------------------------------------------------------------
37
+ // Helpers
38
+ // ---------------------------------------------------------------------------
39
+ async function apiFetch(path, opts = {}) {
40
+ const { paid = false, timeout = 30_000, ...init } = opts;
41
+ const fetchFn = paid ? payFetch : fetch;
42
+ const url = `${API_URL}${path}`;
43
+ try {
44
+ return await fetchFn(url, {
45
+ ...init,
46
+ signal: AbortSignal.timeout(timeout),
47
+ });
48
+ }
49
+ catch (err) {
50
+ if (err instanceof DOMException && err.name === "TimeoutError") {
51
+ throw new Error(`Request to ${path} timed out after ${timeout}ms`);
52
+ }
53
+ throw new Error(`Cannot reach ${API_URL}. Check internet connection. (${err instanceof Error ? err.message : err})`);
54
+ }
55
+ }
56
+ function errorText(status, body) {
57
+ switch (status) {
58
+ case 403:
59
+ return "Payment verification failed. Check USDC balance on Base.";
60
+ case 422:
61
+ try {
62
+ const parsed = JSON.parse(body);
63
+ return parsed.detail || parsed.message || body;
64
+ }
65
+ catch {
66
+ return body;
67
+ }
68
+ case 429:
69
+ return "Rate limited or max active jobs reached. Wait and retry.";
70
+ case 503:
71
+ return "Payment facilitator temporarily unavailable. Retry shortly.";
72
+ default:
73
+ return `API returned ${status}: ${body}`;
74
+ }
75
+ }
76
+ // ---------------------------------------------------------------------------
77
+ // MCP Server
78
+ // ---------------------------------------------------------------------------
79
+ const server = new McpServer({ name: "ouro", version: "1.0.0" }, {
80
+ instructions: `Ouro runs HPC jobs on a Slurm cluster, paid in USDC via x402 on Base.
81
+ Payment is automatic — your wallet signs USDC payments locally.
82
+
83
+ Typical flow:
84
+ 1. run_job(script="echo hello") → { job_id, price }
85
+ 2. get_job_status(job_id) → poll until status is "completed"
86
+
87
+ Submission modes:
88
+ - script: single shell script (simplest)
89
+ - files: list of {path, content} objects (multi-file workspace, can include a Dockerfile)
90
+
91
+ Use get_price_quote to check pricing before committing.
92
+ Use get_allowed_images to see available container images.
93
+
94
+ Prebuilt images (instant): ouro-ubuntu, ouro-python, ouro-nodejs.
95
+ Any Docker Hub image works via Dockerfile (e.g., FROM python:3.12-slim).`,
96
+ });
97
+ // ---------------------------------------------------------------------------
98
+ // Tool: run_job
99
+ // ---------------------------------------------------------------------------
100
+ server.tool("run_job", "Submit a compute job and pay automatically. Returns job_id when accepted.", {
101
+ script: z.string().optional().describe("Shell script to execute (use this OR files)"),
102
+ files: z
103
+ .array(z.object({ path: z.string(), content: z.string() }))
104
+ .optional()
105
+ .describe("Array of {path, content} file objects (can include a Dockerfile)"),
106
+ image: z.string().default("ouro-ubuntu").describe("Container image (default: ouro-ubuntu)"),
107
+ cpus: z.number().int().min(1).max(8).default(1).describe("CPU cores (1-8)"),
108
+ time_limit_min: z.number().int().min(1).default(1).describe("Max runtime in minutes"),
109
+ builder_code: z.string().optional().describe("Builder code for ERC-8021 attribution"),
110
+ }, async (params) => {
111
+ // Validate: exactly one of script or files
112
+ if (params.script && params.files) {
113
+ return { content: [{ type: "text", text: "Provide either script or files, not both." }] };
114
+ }
115
+ if (!params.script && !params.files) {
116
+ return { content: [{ type: "text", text: "Provide either script or files." }] };
117
+ }
118
+ const body = {
119
+ cpus: params.cpus,
120
+ time_limit_min: params.time_limit_min,
121
+ image: params.image,
122
+ submitter_address: walletAddress,
123
+ };
124
+ if (params.script)
125
+ body.script = params.script;
126
+ if (params.files)
127
+ body.files = params.files;
128
+ const headers = { "Content-Type": "application/json" };
129
+ if (params.builder_code)
130
+ headers["X-BUILDER-CODE"] = params.builder_code;
131
+ try {
132
+ const res = await apiFetch("/api/compute/submit", {
133
+ method: "POST",
134
+ headers,
135
+ body: JSON.stringify(body),
136
+ paid: true,
137
+ });
138
+ const text = await res.text();
139
+ if (!res.ok) {
140
+ return { content: [{ type: "text", text: errorText(res.status, text) }] };
141
+ }
142
+ return { content: [{ type: "text", text }] };
143
+ }
144
+ catch (err) {
145
+ const msg = err instanceof Error ? err.message : String(err);
146
+ return { content: [{ type: "text", text: msg }] };
147
+ }
148
+ });
149
+ // ---------------------------------------------------------------------------
150
+ // Tool: get_job_status
151
+ // ---------------------------------------------------------------------------
152
+ server.tool("get_job_status", "Check the status of a job. Returns output when completed.", {
153
+ job_id: z.string().describe("Job ID to check"),
154
+ }, async (params) => {
155
+ try {
156
+ const res = await apiFetch(`/api/jobs/${params.job_id}`);
157
+ const text = await res.text();
158
+ if (!res.ok) {
159
+ return { content: [{ type: "text", text: errorText(res.status, text) }] };
160
+ }
161
+ return { content: [{ type: "text", text }] };
162
+ }
163
+ catch (err) {
164
+ const msg = err instanceof Error ? err.message : String(err);
165
+ return { content: [{ type: "text", text: msg }] };
166
+ }
167
+ });
168
+ // ---------------------------------------------------------------------------
169
+ // Tool: get_price_quote
170
+ // ---------------------------------------------------------------------------
171
+ server.tool("get_price_quote", "Get a price quote without paying. Check pricing before committing.", {
172
+ cpus: z.number().int().min(1).max(8).default(1).describe("CPU cores"),
173
+ time_limit_min: z.number().int().min(1).default(1).describe("Max runtime in minutes"),
174
+ submission_mode: z
175
+ .string()
176
+ .default("script")
177
+ .describe("Submission mode: script, multi_file"),
178
+ }, async (params) => {
179
+ try {
180
+ const qs = new URLSearchParams({
181
+ cpus: String(params.cpus),
182
+ time_limit_min: String(params.time_limit_min),
183
+ submission_mode: params.submission_mode,
184
+ });
185
+ const res = await apiFetch(`/api/price?${qs}`);
186
+ const text = await res.text();
187
+ if (!res.ok) {
188
+ return { content: [{ type: "text", text: errorText(res.status, text) }] };
189
+ }
190
+ return { content: [{ type: "text", text }] };
191
+ }
192
+ catch (err) {
193
+ const msg = err instanceof Error ? err.message : String(err);
194
+ return { content: [{ type: "text", text: msg }] };
195
+ }
196
+ });
197
+ // ---------------------------------------------------------------------------
198
+ // Tool: get_allowed_images
199
+ // ---------------------------------------------------------------------------
200
+ server.tool("get_allowed_images", "List available container images for compute jobs.", {}, async () => {
201
+ try {
202
+ const res = await apiFetch("/api/capabilities");
203
+ const text = await res.text();
204
+ if (!res.ok) {
205
+ return { content: [{ type: "text", text: errorText(res.status, text) }] };
206
+ }
207
+ return { content: [{ type: "text", text }] };
208
+ }
209
+ catch (err) {
210
+ const msg = err instanceof Error ? err.message : String(err);
211
+ return { content: [{ type: "text", text: msg }] };
212
+ }
213
+ });
214
+ // ---------------------------------------------------------------------------
215
+ // Start
216
+ // ---------------------------------------------------------------------------
217
+ const transport = new StdioServerTransport();
218
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "ouro-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Run HPC compute from any AI agent — pay with USDC via x402 on Base",
5
+ "bin": {
6
+ "ouro-mcp": "./dist/index.js"
7
+ },
8
+ "type": "module",
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "prepublishOnly": "npm run build",
12
+ "start": "node dist/index.js",
13
+ "dev": "tsx watch src/index.ts"
14
+ },
15
+ "dependencies": {
16
+ "@modelcontextprotocol/sdk": "^1.25",
17
+ "@x402/evm": "^2.6",
18
+ "@x402/fetch": "^2.6",
19
+ "viem": "^2.0",
20
+ "zod": "^3.23"
21
+ },
22
+ "devDependencies": {
23
+ "tsx": "^4.21.0",
24
+ "typescript": "^5.5"
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/richtan/ouro"
33
+ },
34
+ "keywords": [
35
+ "mcp",
36
+ "compute",
37
+ "x402",
38
+ "usdc",
39
+ "base",
40
+ "hpc",
41
+ "slurm"
42
+ ]
43
+ }