ouro-mcp 1.0.0 → 1.1.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 +4 -3
- package/dist/index.js +115 -8
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ Add to your MCP client config (works with Cursor, Claude Code, Claude Desktop, V
|
|
|
31
31
|
}
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
> **Claude Code
|
|
34
|
+
> **Claude Code CLI:** `claude mcp add ouro --transport stdio -e WALLET_PRIVATE_KEY=0x... -- npx -y ouro-mcp`
|
|
35
35
|
|
|
36
36
|
## Usage Examples
|
|
37
37
|
|
|
@@ -64,10 +64,11 @@ User: Run a Python script that computes the first 1000 primes
|
|
|
64
64
|
← { "job_id": "def456", "price_usdc": "0.03", "status": "pending" }
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
###
|
|
67
|
+
### Wait for results
|
|
68
68
|
|
|
69
69
|
```
|
|
70
70
|
→ get_job_status(job_id: "abc123")
|
|
71
|
+
# Streams SSE events internally, returns when the job finishes
|
|
71
72
|
|
|
72
73
|
← { "status": "completed", "output": "[2, 3, 5, 7, 11, ...]", "runtime_seconds": 4.2 }
|
|
73
74
|
```
|
|
@@ -97,7 +98,7 @@ Submit a compute job and pay automatically. Returns `job_id` when accepted.
|
|
|
97
98
|
|
|
98
99
|
### `get_job_status`
|
|
99
100
|
|
|
100
|
-
Check the status of a submitted job.
|
|
101
|
+
Check the status of a submitted job. Uses SSE streaming to wait for the job to finish — call it once and it returns when the job reaches a terminal state (`completed` or `failed`). No manual polling needed.
|
|
101
102
|
|
|
102
103
|
| Parameter | Type | Required | Description |
|
|
103
104
|
|-----------|------|----------|-------------|
|
package/dist/index.js
CHANGED
|
@@ -82,7 +82,7 @@ Payment is automatic — your wallet signs USDC payments locally.
|
|
|
82
82
|
|
|
83
83
|
Typical flow:
|
|
84
84
|
1. run_job(script="echo hello") → { job_id, price }
|
|
85
|
-
2. get_job_status(job_id) →
|
|
85
|
+
2. get_job_status(job_id) → waits for completion and returns result
|
|
86
86
|
|
|
87
87
|
Submission modes:
|
|
88
88
|
- script: single shell script (simplest)
|
|
@@ -107,6 +107,7 @@ server.tool("run_job", "Submit a compute job and pay automatically. Returns job_
|
|
|
107
107
|
cpus: z.number().int().min(1).max(8).default(1).describe("CPU cores (1-8)"),
|
|
108
108
|
time_limit_min: z.number().int().min(1).default(1).describe("Max runtime in minutes"),
|
|
109
109
|
builder_code: z.string().optional().describe("Builder code for ERC-8021 attribution"),
|
|
110
|
+
webhook_url: z.string().url().optional().describe("URL to receive a POST notification when the job completes or fails"),
|
|
110
111
|
}, async (params) => {
|
|
111
112
|
// Validate: exactly one of script or files
|
|
112
113
|
if (params.script && params.files) {
|
|
@@ -128,6 +129,8 @@ server.tool("run_job", "Submit a compute job and pay automatically. Returns job_
|
|
|
128
129
|
const headers = { "Content-Type": "application/json" };
|
|
129
130
|
if (params.builder_code)
|
|
130
131
|
headers["X-BUILDER-CODE"] = params.builder_code;
|
|
132
|
+
if (params.webhook_url)
|
|
133
|
+
body.webhook_url = params.webhook_url;
|
|
131
134
|
try {
|
|
132
135
|
const res = await apiFetch("/api/compute/submit", {
|
|
133
136
|
method: "POST",
|
|
@@ -147,22 +150,126 @@ server.tool("run_job", "Submit a compute job and pay automatically. Returns job_
|
|
|
147
150
|
}
|
|
148
151
|
});
|
|
149
152
|
// ---------------------------------------------------------------------------
|
|
153
|
+
// Polling fallback (used when SSE is unavailable)
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
async function pollUntilDone(jobId) {
|
|
156
|
+
const maxPolls = 780; // 65 min at 5s intervals (matches SSE timeout)
|
|
157
|
+
for (let i = 0; i < maxPolls; i++) {
|
|
158
|
+
try {
|
|
159
|
+
const res = await apiFetch(`/api/jobs/${jobId}`);
|
|
160
|
+
if (!res.ok) {
|
|
161
|
+
if (res.status === 404) {
|
|
162
|
+
const text = await res.text();
|
|
163
|
+
return { content: [{ type: "text", text: errorText(res.status, text) }] };
|
|
164
|
+
}
|
|
165
|
+
await new Promise((r) => setTimeout(r, 5_000));
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const job = await res.json();
|
|
169
|
+
if (job.status === "completed" || job.status === "failed") {
|
|
170
|
+
return { content: [{ type: "text", text: JSON.stringify(job, null, 2) }] };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// Network error — continue polling
|
|
175
|
+
}
|
|
176
|
+
await new Promise((r) => setTimeout(r, 5_000));
|
|
177
|
+
}
|
|
178
|
+
return { content: [{ type: "text", text: `Job ${jobId} still running after 65 minutes` }] };
|
|
179
|
+
}
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
150
181
|
// Tool: get_job_status
|
|
151
182
|
// ---------------------------------------------------------------------------
|
|
152
|
-
server.tool("get_job_status", "Check the status of a job. Returns
|
|
183
|
+
server.tool("get_job_status", "Check the status of a job. Returns immediately if already done, otherwise waits for completion and returns the final result with output.", {
|
|
153
184
|
job_id: z.string().describe("Job ID to check"),
|
|
154
185
|
}, async (params) => {
|
|
186
|
+
// Step 1: Check current status — job may already be done
|
|
155
187
|
try {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return { content: [{ type: "text", text: errorText(
|
|
188
|
+
const statusRes = await apiFetch(`/api/jobs/${params.job_id}`);
|
|
189
|
+
if (!statusRes.ok) {
|
|
190
|
+
const text = await statusRes.text();
|
|
191
|
+
return { content: [{ type: "text", text: errorText(statusRes.status, text) }] };
|
|
160
192
|
}
|
|
161
|
-
|
|
193
|
+
const job = await statusRes.json();
|
|
194
|
+
if (job.status === "completed" || job.status === "failed") {
|
|
195
|
+
return { content: [{ type: "text", text: JSON.stringify(job, null, 2) }] };
|
|
196
|
+
}
|
|
197
|
+
console.error(`Job ${params.job_id.slice(0, 8)} is ${job.status}, streaming events...`);
|
|
162
198
|
}
|
|
163
199
|
catch (err) {
|
|
164
200
|
const msg = err instanceof Error ? err.message : String(err);
|
|
165
|
-
return { content: [{ type: "text", text: msg }] };
|
|
201
|
+
return { content: [{ type: "text", text: `Status check failed: ${msg}` }] };
|
|
202
|
+
}
|
|
203
|
+
// Step 2: Connect to SSE for live events
|
|
204
|
+
try {
|
|
205
|
+
const sseRes = await apiFetch(`/api/jobs/${params.job_id}/events`, {
|
|
206
|
+
timeout: 3_900_000, // 65 minutes
|
|
207
|
+
});
|
|
208
|
+
if (!sseRes.ok) {
|
|
209
|
+
console.error(`SSE unavailable (${sseRes.status}), falling back to polling`);
|
|
210
|
+
return await pollUntilDone(params.job_id);
|
|
211
|
+
}
|
|
212
|
+
const reader = sseRes.body?.getReader();
|
|
213
|
+
if (!reader) {
|
|
214
|
+
console.error("SSE stream body unavailable, falling back to polling");
|
|
215
|
+
return await pollUntilDone(params.job_id);
|
|
216
|
+
}
|
|
217
|
+
const decoder = new TextDecoder();
|
|
218
|
+
let buffer = "";
|
|
219
|
+
while (true) {
|
|
220
|
+
const { done, value } = await reader.read();
|
|
221
|
+
if (done)
|
|
222
|
+
break;
|
|
223
|
+
buffer += decoder.decode(value, { stream: true });
|
|
224
|
+
const lines = buffer.split("\n");
|
|
225
|
+
buffer = lines.pop() || "";
|
|
226
|
+
let terminalMessage = null;
|
|
227
|
+
for (const rawLine of lines) {
|
|
228
|
+
const line = rawLine.trim();
|
|
229
|
+
if (!line.startsWith("data: "))
|
|
230
|
+
continue;
|
|
231
|
+
try {
|
|
232
|
+
const event = JSON.parse(line.slice(6));
|
|
233
|
+
if (event.type === "job" && event.message) {
|
|
234
|
+
const msg = event.message.toLowerCase();
|
|
235
|
+
if (msg.includes("completed") || msg.includes("failed")) {
|
|
236
|
+
terminalMessage = event.message;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// Skip malformed SSE lines
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (terminalMessage) {
|
|
246
|
+
console.error(`Job ${params.job_id.slice(0, 8)}: ${terminalMessage}`);
|
|
247
|
+
try {
|
|
248
|
+
reader.cancel();
|
|
249
|
+
}
|
|
250
|
+
catch { /* best effort stream cleanup */ }
|
|
251
|
+
const finalRes = await apiFetch(`/api/jobs/${params.job_id}`);
|
|
252
|
+
const finalText = await finalRes.text();
|
|
253
|
+
return { content: [{ type: "text", text: finalText }] };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Stream ended without terminal event — fetch current status
|
|
257
|
+
console.error("SSE stream ended, fetching final status");
|
|
258
|
+
const fallbackRes = await apiFetch(`/api/jobs/${params.job_id}`);
|
|
259
|
+
const fallbackText = await fallbackRes.text();
|
|
260
|
+
return { content: [{ type: "text", text: fallbackText }] };
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
264
|
+
console.error(`SSE error: ${msg}, fetching current status`);
|
|
265
|
+
try {
|
|
266
|
+
const fallbackRes = await apiFetch(`/api/jobs/${params.job_id}`);
|
|
267
|
+
const fallbackText = await fallbackRes.text();
|
|
268
|
+
return { content: [{ type: "text", text: fallbackText }] };
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return { content: [{ type: "text", text: `Job status unavailable: ${msg}` }] };
|
|
272
|
+
}
|
|
166
273
|
}
|
|
167
274
|
});
|
|
168
275
|
// ---------------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ouro-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Run HPC compute from any AI agent — pay with USDC via x402 on Base",
|
|
5
5
|
"bin": {
|
|
6
|
-
"ouro-mcp": "
|
|
6
|
+
"ouro-mcp": "dist/index.js"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
9
|
"scripts": {
|