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 +155 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +218 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# ouro-mcp
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/ouro-mcp)
|
|
4
|
+
[](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)
|
package/dist/index.d.ts
ADDED
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
|
+
}
|