@humanops/mcp-server 0.2.0 → 0.3.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 +193 -0
- package/dist/index.js +219 -46
- package/package.json +13 -4
package/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# @humanops/mcp-server
|
|
2
|
+
|
|
3
|
+
MCP server for AI agents to dispatch real-world tasks to verified human operators.
|
|
4
|
+
|
|
5
|
+
HumanOps bridges the gap between AI capabilities and physical-world actions. When your agent needs something done in the real world — verifying a business address, filling out a form, solving a CAPTCHA, procuring an API key — it dispatches a task to a verified human operator through this MCP server.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"humanops": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["@humanops/mcp-server"],
|
|
15
|
+
"env": {
|
|
16
|
+
"HUMANOPS_API_KEY": "your-api-key"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Environment Variables
|
|
24
|
+
|
|
25
|
+
| Variable | Required | Description |
|
|
26
|
+
|----------|----------|-------------|
|
|
27
|
+
| `HUMANOPS_API_KEY` | Yes | Your HumanOps API key |
|
|
28
|
+
| `HUMANOPS_API_URL` | No | API base URL (default: `https://api.humanops.io`) |
|
|
29
|
+
| `HUMANOPS_DEV_ALLOW_LOCALHOST` | No | Set to `true` to allow `HUMANOPS_API_URL` to be `http://localhost:8787` for local development |
|
|
30
|
+
| `HUMANOPS_ALLOW_ANY_API_HOST` | No | Set to `true` to allow non-`*.humanops.io` API hosts (still blocks private/internal hosts) |
|
|
31
|
+
| `HUMANOPS_SANDBOX` | No | Set to `true` to force sandbox mode for non-SANDBOX tier agents (useful for integration testing with VERIFIED/STANDARD accounts) |
|
|
32
|
+
|
|
33
|
+
## Prerequisites
|
|
34
|
+
|
|
35
|
+
Before using the MCP server, register your agent via the REST API:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
curl -X POST https://api.humanops.io/api/v1/agents/register \
|
|
39
|
+
-H "Content-Type: application/json" \
|
|
40
|
+
-d '{"name": "my-agent", "email": "agent@example.com"}'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This returns your API key and starts you at **SANDBOX** tier (tasks auto-complete with synthetic proof). To create real tasks:
|
|
44
|
+
|
|
45
|
+
1. **Verify your email** — click the link in the registration email to upgrade to VERIFIED tier
|
|
46
|
+
2. **Deposit USDC** — send $50+ USDC on Base L2 to upgrade to STANDARD tier
|
|
47
|
+
|
|
48
|
+
### Agent Tiers
|
|
49
|
+
|
|
50
|
+
| Tier | Daily Tasks | Max Task Value | Daily Spend | Real Tasks? |
|
|
51
|
+
|------|------------|---------------|-------------|-------------|
|
|
52
|
+
| SANDBOX | 50 | $10 | $10 | No (auto-complete) |
|
|
53
|
+
| VERIFIED | 10 | $100 | $200 | Yes |
|
|
54
|
+
| STANDARD | 100 | $10,000 | $50,000 | Yes |
|
|
55
|
+
|
|
56
|
+
### Sandbox Mode
|
|
57
|
+
|
|
58
|
+
SANDBOX tier agents operate in sandbox mode. Every task auto-completes through a simulated lifecycle:
|
|
59
|
+
|
|
60
|
+
1. Task is created (PENDING) — no funds are escrowed
|
|
61
|
+
2. A simulated operator auto-accepts the task within seconds
|
|
62
|
+
3. Synthetic proof is generated and submitted automatically
|
|
63
|
+
4. A synthetic Guardian verification auto-approves
|
|
64
|
+
5. Task completes — no real money moves, no real human is involved
|
|
65
|
+
|
|
66
|
+
All API responses for sandbox tasks include `sandbox: true` and a `sandbox_notice` field explaining the simulation. MCP tool outputs prefix sandbox responses with `SANDBOX MODE:` or `SANDBOX TASK:` so your agent clearly understands the results are simulated.
|
|
67
|
+
|
|
68
|
+
**To exit sandbox mode:** verify your email (upgrades to VERIFIED) and deposit USDC (upgrades to STANDARD).
|
|
69
|
+
|
|
70
|
+
## Available Tools
|
|
71
|
+
|
|
72
|
+
### Physical Tasks
|
|
73
|
+
|
|
74
|
+
| Tool | Description |
|
|
75
|
+
|------|-------------|
|
|
76
|
+
| `search_operators` | Find available operators near a location |
|
|
77
|
+
| `post_task` | Create a physical task (verification, photo, delivery, inspection) |
|
|
78
|
+
|
|
79
|
+
### Digital Tasks (Tier 1)
|
|
80
|
+
|
|
81
|
+
| Tool | Description |
|
|
82
|
+
|------|-------------|
|
|
83
|
+
| `dispatch_digital_task` | Create a remote digital task |
|
|
84
|
+
| `list_digital_categories` | List available categories with pricing |
|
|
85
|
+
|
|
86
|
+
Categories: `CAPTCHA_SOLVING`, `FORM_FILLING`, `BROWSER_INTERACTION`, `CONTENT_REVIEW`, `DATA_VALIDATION`
|
|
87
|
+
|
|
88
|
+
### Credential Tasks (Tier 2, E2EE)
|
|
89
|
+
|
|
90
|
+
| Tool | Description |
|
|
91
|
+
|------|-------------|
|
|
92
|
+
| `dispatch_credential_task` | Create an encrypted credential delivery task |
|
|
93
|
+
| `retrieve_credential` | Decrypt and retrieve delivered credentials |
|
|
94
|
+
|
|
95
|
+
Categories: `ACCOUNT_CREATION`, `API_KEY_PROCUREMENT`, `PHONE_VERIFICATION`, `SUBSCRIPTION_SETUP`
|
|
96
|
+
|
|
97
|
+
Credential tasks use end-to-end encryption (P-256 ECDH + AES-256-GCM). The server never sees plaintext credentials.
|
|
98
|
+
|
|
99
|
+
### Task Management
|
|
100
|
+
|
|
101
|
+
| Tool | Description |
|
|
102
|
+
|------|-------------|
|
|
103
|
+
| `approve_estimate` | Approve an operator's time estimate for an ESTIMATE_PENDING task |
|
|
104
|
+
| `reject_estimate` | Reject an estimate, returning the task to the available pool |
|
|
105
|
+
| `get_task_result` | Get task status, proof, and verification result |
|
|
106
|
+
| `check_verification_status` | Check AI Guardian verification status |
|
|
107
|
+
| `cancel_task` | Cancel a pending/estimate-pending/accepted task (refunds escrowed funds) |
|
|
108
|
+
| `list_tasks` | List your tasks with optional status filter |
|
|
109
|
+
|
|
110
|
+
### Payments (USDC)
|
|
111
|
+
|
|
112
|
+
Production safety note: HumanOps uses a shared USDC deposit address on Base L2. To safely
|
|
113
|
+
confirm deposits in production, the API may require you to bind (verify) the wallet you
|
|
114
|
+
deposit from via `POST /api/v1/agents/wallet/challenge` + `PUT /api/v1/agents/wallet`.
|
|
115
|
+
|
|
116
|
+
| Tool | Description |
|
|
117
|
+
|------|-------------|
|
|
118
|
+
| `get_deposit_address` | Get your USDC deposit address |
|
|
119
|
+
| `fund_account` | Verify a USDC deposit (Base L2) |
|
|
120
|
+
| `get_balance` | Check deposit and escrow balances |
|
|
121
|
+
| `request_payout` | Request operator USDC payout (gas fee deducted from amount) |
|
|
122
|
+
|
|
123
|
+
## Example Usage
|
|
124
|
+
|
|
125
|
+
### Dispatch a physical verification task
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
post_task({
|
|
129
|
+
title: "Verify business is open",
|
|
130
|
+
description: "Visit 123 Main St and confirm the restaurant is operating",
|
|
131
|
+
location: { lat: 40.7128, lng: -74.0060, address: "123 Main St, New York, NY" },
|
|
132
|
+
reward_usd: 15,
|
|
133
|
+
deadline: "2026-02-08T18:00:00Z",
|
|
134
|
+
proof_requirements: ["photo of storefront", "photo of business hours sign"],
|
|
135
|
+
task_type: "VERIFICATION"
|
|
136
|
+
})
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Dispatch a digital task
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
dispatch_digital_task({
|
|
143
|
+
title: "Fill out vendor registration form",
|
|
144
|
+
description: "Complete the vendor registration at example.com/register",
|
|
145
|
+
digital_category: "FORM_FILLING",
|
|
146
|
+
reward_usd: 12,
|
|
147
|
+
deadline: "2026-02-08T12:00:00Z",
|
|
148
|
+
proof_requirements: ["screenshot of confirmation page"],
|
|
149
|
+
digital_instructions: "1. Go to example.com/register\n2. Fill in company details\n3. Submit and screenshot confirmation"
|
|
150
|
+
})
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Dispatch a credential task (E2EE)
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
dispatch_credential_task({
|
|
157
|
+
title: "Create API account on DataService",
|
|
158
|
+
description: "Sign up for a free account and retrieve the API key",
|
|
159
|
+
digital_category: "API_KEY_PROCUREMENT",
|
|
160
|
+
reward_usd: 20,
|
|
161
|
+
deadline: "2026-02-08T12:00:00Z",
|
|
162
|
+
proof_requirements: ["screenshot of account dashboard"],
|
|
163
|
+
digital_instructions: "1. Go to dataservice.io/signup\n2. Create account\n3. Navigate to API keys\n4. Generate and submit the key"
|
|
164
|
+
})
|
|
165
|
+
// Returns { task_id, private_key } — save the private_key!
|
|
166
|
+
|
|
167
|
+
// Later, retrieve the credential:
|
|
168
|
+
retrieve_credential({ task_id: "...", private_key: "..." })
|
|
169
|
+
// Returns { credential: "sk-abc123..." }
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Task Lifecycle
|
|
173
|
+
|
|
174
|
+
1. **PENDING** — Task created, funds escrowed
|
|
175
|
+
2. **ESTIMATE_PENDING** — Operator submitted a time/cost estimate, awaiting agent approval (24h deadline)
|
|
176
|
+
3. **ACCEPTED** — Operator picked up the task (or estimate approved)
|
|
177
|
+
4. **SUBMITTED** — Operator submitted proof
|
|
178
|
+
5. **VERIFIED** / **REJECTED** — AI Guardian reviewed the proof
|
|
179
|
+
6. **COMPLETED** — Task finished, operator paid
|
|
180
|
+
|
|
181
|
+
## Security
|
|
182
|
+
|
|
183
|
+
- All inputs validated with Zod schemas
|
|
184
|
+
- SSRF protection on callback URLs
|
|
185
|
+
- API key sent only to `*.humanops.io` (configurable)
|
|
186
|
+
- Redirects blocked to prevent key exfiltration
|
|
187
|
+
- Credential tasks use client-side E2EE (server never sees plaintext)
|
|
188
|
+
- Single-read credential retrieval (cleared after first read)
|
|
189
|
+
- AI Guardian pre-screens task content at creation time (VERIFIED/STANDARD tiers) — blocks illegal, fraudulent, or abusive tasks
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -123,7 +123,12 @@ async function generateKeyPair() {
|
|
|
123
123
|
return { publicKey: toBase64(publicRaw), privateKey: toBase64(privateRaw) };
|
|
124
124
|
}
|
|
125
125
|
async function importPublicKey(base64) {
|
|
126
|
-
|
|
126
|
+
const raw = fromBase64(base64);
|
|
127
|
+
// P-256 uncompressed public key is exactly 65 bytes (0x04 || x || y)
|
|
128
|
+
if (raw.byteLength !== 65) {
|
|
129
|
+
throw new Error("Invalid public key length (expected 65 bytes for P-256 uncompressed)");
|
|
130
|
+
}
|
|
131
|
+
return crypto.subtle.importKey("raw", raw, { name: "ECDH", namedCurve: "P-256" }, false, []);
|
|
127
132
|
}
|
|
128
133
|
async function importPrivateKey(base64) {
|
|
129
134
|
return crypto.subtle.importKey("pkcs8", fromBase64(base64), { name: "ECDH", namedCurve: "P-256" }, false, ["deriveBits"]);
|
|
@@ -145,6 +150,16 @@ async function decryptCredential(encrypted, privateKeyBase64) {
|
|
|
145
150
|
// ---------------------------------------------------------------------------
|
|
146
151
|
// SSRF protection
|
|
147
152
|
// ---------------------------------------------------------------------------
|
|
153
|
+
// DNS rebinding services that resolve to arbitrary IPs (including private ranges)
|
|
154
|
+
const DNS_REBINDING_BLOCKLIST = [
|
|
155
|
+
"nip.io", "sslip.io", "xip.io", "nip.test",
|
|
156
|
+
"localtest.me", "lvh.me", "vcap.me",
|
|
157
|
+
"lacolhost.com", "yoogle.com",
|
|
158
|
+
"beweb.com", "servebeer.com", "servecounterstrike.com",
|
|
159
|
+
"servehalflife.com", "servehttp.com", "serveirc.com",
|
|
160
|
+
"serveminecraft.net", "servemp3.com", "servepics.com",
|
|
161
|
+
"servequake.com", "sytes.net",
|
|
162
|
+
];
|
|
148
163
|
function isSSRFSafeUrl(urlStr, requireHttps = false) {
|
|
149
164
|
let parsed;
|
|
150
165
|
try {
|
|
@@ -173,6 +188,11 @@ function isSSRFSafeUrl(urlStr, requireHttps = false) {
|
|
|
173
188
|
return false;
|
|
174
189
|
if (hostname.endsWith(".internal"))
|
|
175
190
|
return false;
|
|
191
|
+
// Block DNS rebinding services (resolve to attacker-controlled IPs including private ranges)
|
|
192
|
+
for (const domain of DNS_REBINDING_BLOCKLIST) {
|
|
193
|
+
if (hostname === domain || hostname.endsWith(`.${domain}`))
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
176
196
|
const ipv4 = parseIpv4(hostname);
|
|
177
197
|
if (ipv4 && isPrivateIpv4(ipv4))
|
|
178
198
|
return false;
|
|
@@ -331,6 +351,9 @@ function isPrivateIpv6(groups) {
|
|
|
331
351
|
* authz/rate-limits/audit logging, it talks to HumanOps via HTTP only.
|
|
332
352
|
*/
|
|
333
353
|
const DEFAULT_API_URL = "https://api.humanops.io";
|
|
354
|
+
function isAllowedLocalhost(hostname) {
|
|
355
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
356
|
+
}
|
|
334
357
|
function getApiUrl() {
|
|
335
358
|
const raw = process.env.HUMANOPS_API_URL?.trim();
|
|
336
359
|
if (!raw)
|
|
@@ -344,11 +367,15 @@ function getApiUrl() {
|
|
|
344
367
|
throw new Error("HUMANOPS_API_URL must be a valid URL.");
|
|
345
368
|
}
|
|
346
369
|
const allowAnyHost = process.env.HUMANOPS_ALLOW_ANY_API_HOST === "true";
|
|
370
|
+
const allowLocalhost = process.env.HUMANOPS_DEV_ALLOW_LOCALHOST === "true";
|
|
347
371
|
const hostname = parsed.hostname.trim().toLowerCase().replace(/\.+$/, "");
|
|
348
|
-
|
|
372
|
+
const isHumanopsHost = hostname === "api.humanops.io" || hostname.endsWith(".humanops.io");
|
|
373
|
+
const isLocalhost = isAllowedLocalhost(hostname);
|
|
374
|
+
if (!allowAnyHost && !isHumanopsHost) {
|
|
349
375
|
// Prevent accidental exfiltration of HUMANOPS_API_KEY to an unrelated domain.
|
|
350
|
-
|
|
351
|
-
|
|
376
|
+
// For local development, allow explicit localhost-only override.
|
|
377
|
+
if (!(allowLocalhost && isLocalhost)) {
|
|
378
|
+
throw new Error("HUMANOPS_API_URL must be api.humanops.io (or *.humanops.io). To override, set HUMANOPS_ALLOW_ANY_API_HOST=true. For local dev, set HUMANOPS_DEV_ALLOW_LOCALHOST=true and use http://localhost:8787.");
|
|
352
379
|
}
|
|
353
380
|
}
|
|
354
381
|
// Prevent subtle base URL bugs like including paths, querystrings, or fragments.
|
|
@@ -359,18 +386,26 @@ function getApiUrl() {
|
|
|
359
386
|
throw new Error("HUMANOPS_API_URL must not include query params or a fragment.");
|
|
360
387
|
}
|
|
361
388
|
const normalized = parsed.origin;
|
|
389
|
+
if (allowLocalhost && isLocalhost) {
|
|
390
|
+
// Explicit opt-in to allow local testing (still blocks redirects, and only applies to localhost).
|
|
391
|
+
return normalized;
|
|
392
|
+
}
|
|
362
393
|
if (!isSSRFSafeUrl(normalized, true)) {
|
|
363
394
|
throw new Error("HUMANOPS_API_URL must be a public HTTPS URL (private/internal addresses are blocked).");
|
|
364
395
|
}
|
|
365
396
|
return normalized;
|
|
366
397
|
}
|
|
367
398
|
function getApiKey() {
|
|
368
|
-
const apiKey = process.env.HUMANOPS_API_KEY;
|
|
399
|
+
const apiKey = process.env.HUMANOPS_API_KEY?.trim();
|
|
369
400
|
if (!apiKey) {
|
|
370
401
|
throw new Error("HUMANOPS_API_KEY environment variable is required.");
|
|
371
402
|
}
|
|
403
|
+
if (!/^ho_(live|test)_[a-zA-Z0-9]{32,}$/.test(apiKey)) {
|
|
404
|
+
console.error("Warning: HUMANOPS_API_KEY does not match expected format (ho_live_... or ho_test_...)");
|
|
405
|
+
}
|
|
372
406
|
return apiKey;
|
|
373
407
|
}
|
|
408
|
+
const MCP_REQUEST_TIMEOUT = 30_000;
|
|
374
409
|
async function apiRequest(path, init = {}) {
|
|
375
410
|
const baseUrl = getApiUrl();
|
|
376
411
|
const apiKey = getApiKey();
|
|
@@ -381,7 +416,7 @@ async function apiRequest(path, init = {}) {
|
|
|
381
416
|
headers.set("Content-Type", "application/json");
|
|
382
417
|
}
|
|
383
418
|
// Never follow redirects when sending X-API-Key (prevents key exfiltration).
|
|
384
|
-
const res = await fetch(url, { ...init, headers, redirect: "error" });
|
|
419
|
+
const res = await fetch(url, { ...init, headers, redirect: "error", signal: AbortSignal.timeout(MCP_REQUEST_TIMEOUT) });
|
|
385
420
|
const contentType = res.headers.get("content-type") ?? "";
|
|
386
421
|
const raw = await res.text();
|
|
387
422
|
const json = contentType.includes("application/json") && raw ? JSON.parse(raw) : null;
|
|
@@ -431,7 +466,7 @@ const DispatchDigitalTaskInputSchema = z.object({
|
|
|
431
466
|
digital_category: DigitalCategoryEnum,
|
|
432
467
|
reward_usd: z.number().min(MIN_TASK_VALUE_USD).max(MAX_TASK_VALUE_USD),
|
|
433
468
|
deadline: z.string().datetime(),
|
|
434
|
-
proof_requirements: z.array(z.string()).min(1).max(10),
|
|
469
|
+
proof_requirements: z.array(z.string().min(1).max(500)).min(1).max(10),
|
|
435
470
|
digital_instructions: z.string().max(5000).optional(),
|
|
436
471
|
callback_url: z
|
|
437
472
|
.string()
|
|
@@ -449,7 +484,7 @@ const DispatchCredentialTaskInputSchema = z.object({
|
|
|
449
484
|
digital_category: CredentialCategoryEnum,
|
|
450
485
|
reward_usd: z.number().min(MIN_TASK_VALUE_USD).max(MAX_TASK_VALUE_USD),
|
|
451
486
|
deadline: z.string().datetime(),
|
|
452
|
-
proof_requirements: z.array(z.string()).min(1).max(10),
|
|
487
|
+
proof_requirements: z.array(z.string().min(1).max(500)).min(1).max(10),
|
|
453
488
|
digital_instructions: z.string().max(5000).optional(),
|
|
454
489
|
callback_url: z
|
|
455
490
|
.string()
|
|
@@ -466,8 +501,8 @@ const RetrieveCredentialInputSchema = z.object({
|
|
|
466
501
|
private_key: z.string().min(1),
|
|
467
502
|
});
|
|
468
503
|
const SearchOperatorsInputSchema = z.object({
|
|
469
|
-
lat: z.number(),
|
|
470
|
-
lng: z.number(),
|
|
504
|
+
lat: z.number().min(-90).max(90),
|
|
505
|
+
lng: z.number().min(-180).max(180),
|
|
471
506
|
radius_km: z.number().min(1).max(500).optional(),
|
|
472
507
|
task_type: TaskTypeEnum.optional(),
|
|
473
508
|
min_rating: z.number().min(0).max(5).optional(),
|
|
@@ -482,7 +517,7 @@ const PostTaskInputSchema = z.object({
|
|
|
482
517
|
}),
|
|
483
518
|
reward_usd: z.number().min(MIN_TASK_VALUE_USD).max(MAX_TASK_VALUE_USD),
|
|
484
519
|
deadline: z.string().datetime(),
|
|
485
|
-
proof_requirements: z.array(z.string()).min(1).max(10),
|
|
520
|
+
proof_requirements: z.array(z.string().min(1).max(500)).min(1).max(10),
|
|
486
521
|
task_type: TaskTypeEnum,
|
|
487
522
|
callback_url: z
|
|
488
523
|
.string()
|
|
@@ -497,12 +532,11 @@ const PostTaskInputSchema = z.object({
|
|
|
497
532
|
const GetTaskResultInputSchema = z.object({
|
|
498
533
|
task_id: z.string().min(1),
|
|
499
534
|
});
|
|
500
|
-
const USDCChainEnum = z.enum(["base", "polygon", "ethereum", "arbitrum"]);
|
|
501
535
|
const FundAccountInputSchema = z.object({
|
|
502
536
|
amount_usd: z.number().min(MIN_DEPOSIT_USD).max(MAX_DEPOSIT_USD),
|
|
503
537
|
payment_method: z.enum(["usdc", "card", "bank_transfer"]).default("usdc"),
|
|
504
|
-
tx_hash: z.string().
|
|
505
|
-
chain:
|
|
538
|
+
tx_hash: z.string().regex(/^0x[a-fA-F0-9]{64}$/, "Invalid transaction hash (must be 0x-prefixed 64 hex chars)").optional(),
|
|
539
|
+
chain: z.enum(["base"]).optional(),
|
|
506
540
|
return_url: z
|
|
507
541
|
.string()
|
|
508
542
|
.url()
|
|
@@ -514,6 +548,16 @@ const FundAccountInputSchema = z.object({
|
|
|
514
548
|
const RequestPayoutInputSchema = z.object({
|
|
515
549
|
amount_usd: z.number().min(MIN_PAYOUT_USD),
|
|
516
550
|
});
|
|
551
|
+
const TaskStatusEnum = z.enum([
|
|
552
|
+
"PENDING", "ESTIMATE_PENDING", "ACCEPTED", "SUBMITTED",
|
|
553
|
+
"VERIFIED", "REJECTED", "COMPLETED", "CANCELLED", "EXPIRED", "DISPUTED",
|
|
554
|
+
]);
|
|
555
|
+
const ListTasksInputSchema = z.object({
|
|
556
|
+
status: TaskStatusEnum.optional(),
|
|
557
|
+
domain: z.enum(["physical", "digital", "all"]).optional(),
|
|
558
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
559
|
+
offset: z.number().int().min(0).optional(),
|
|
560
|
+
});
|
|
517
561
|
const ALL_TASK_TYPES = [
|
|
518
562
|
"VERIFICATION", "PHOTO", "DELIVERY", "INSPECTION",
|
|
519
563
|
"CAPTCHA_SOLVING", "FORM_FILLING", "BROWSER_INTERACTION", "CONTENT_REVIEW", "DATA_VALIDATION",
|
|
@@ -521,7 +565,7 @@ const ALL_TASK_TYPES = [
|
|
|
521
565
|
];
|
|
522
566
|
const server = new Server({
|
|
523
567
|
name: "humanops",
|
|
524
|
-
version: "0.
|
|
568
|
+
version: "0.3.0",
|
|
525
569
|
}, {
|
|
526
570
|
capabilities: { tools: {} },
|
|
527
571
|
});
|
|
@@ -675,7 +719,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
675
719
|
},
|
|
676
720
|
{
|
|
677
721
|
name: "get_deposit_address",
|
|
678
|
-
description: "Get your USDC deposit address. Send USDC
|
|
722
|
+
description: "Get your USDC deposit address. Send USDC on Base L2 to fund your HumanOps account. This is the recommended way to add funds.",
|
|
679
723
|
inputSchema: {
|
|
680
724
|
type: "object",
|
|
681
725
|
properties: {},
|
|
@@ -684,7 +728,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
684
728
|
},
|
|
685
729
|
{
|
|
686
730
|
name: "fund_account",
|
|
687
|
-
description: "Add funds to your HumanOps account. **Recommended: use USDC deposits** (default). Send USDC to your deposit address (get it via get_deposit_address), then call this with the tx_hash to verify. Fiat methods (card, bank_transfer) are coming soon.",
|
|
731
|
+
description: "Add funds to your HumanOps account. **Recommended: use USDC deposits** (default). Send USDC on Base L2 to your deposit address (get it via get_deposit_address), then call this with the tx_hash to verify. Fiat methods (card, bank_transfer) are coming soon.",
|
|
688
732
|
inputSchema: {
|
|
689
733
|
type: "object",
|
|
690
734
|
properties: {
|
|
@@ -698,8 +742,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
698
742
|
tx_hash: { type: "string", description: "On-chain transaction hash (required for USDC deposits)" },
|
|
699
743
|
chain: {
|
|
700
744
|
type: "string",
|
|
701
|
-
enum: ["base"
|
|
702
|
-
description: "Blockchain network the USDC was sent on (required for USDC deposits). Base
|
|
745
|
+
enum: ["base"],
|
|
746
|
+
description: "Blockchain network the USDC was sent on (required for USDC deposits). Only Base L2 is supported.",
|
|
703
747
|
},
|
|
704
748
|
return_url: { type: "string", description: "Optional return URL after fiat payment (not used for USDC)" },
|
|
705
749
|
},
|
|
@@ -708,7 +752,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
708
752
|
},
|
|
709
753
|
{
|
|
710
754
|
name: "request_payout",
|
|
711
|
-
description: "Request a payout of available earnings.
|
|
755
|
+
description: "Request a payout of available earnings. Operators can withdraw via USDC on Base L2 through the operator portal or API.",
|
|
712
756
|
inputSchema: {
|
|
713
757
|
type: "object",
|
|
714
758
|
properties: { amount_usd: { type: "number", description: "Amount to withdraw in USD (min: $10)" } },
|
|
@@ -733,13 +777,39 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
733
777
|
required: ["task_id"],
|
|
734
778
|
},
|
|
735
779
|
},
|
|
780
|
+
{
|
|
781
|
+
name: "approve_estimate",
|
|
782
|
+
description: "Approve an operator's time estimate for a task. The operator will be notified and can start working. Only works when task status is ESTIMATE_PENDING.",
|
|
783
|
+
inputSchema: {
|
|
784
|
+
type: "object",
|
|
785
|
+
properties: { task_id: { type: "string", description: "The task ID whose estimate to approve" } },
|
|
786
|
+
required: ["task_id"],
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
name: "reject_estimate",
|
|
791
|
+
description: "Reject an operator's time estimate for a task. The task returns to the available pool so another operator can claim it. Only works when task status is ESTIMATE_PENDING.",
|
|
792
|
+
inputSchema: {
|
|
793
|
+
type: "object",
|
|
794
|
+
properties: {
|
|
795
|
+
task_id: { type: "string", description: "The task ID whose estimate to reject" },
|
|
796
|
+
reason: { type: "string", description: "Optional reason for rejection (shown to operator)" },
|
|
797
|
+
},
|
|
798
|
+
required: ["task_id"],
|
|
799
|
+
},
|
|
800
|
+
},
|
|
736
801
|
{
|
|
737
802
|
name: "list_tasks",
|
|
738
803
|
description: "List your tasks with optional status filter. Returns paginated results ordered by creation date.",
|
|
739
804
|
inputSchema: {
|
|
740
805
|
type: "object",
|
|
741
806
|
properties: {
|
|
742
|
-
status: {
|
|
807
|
+
status: {
|
|
808
|
+
type: "string",
|
|
809
|
+
enum: ["PENDING", "ESTIMATE_PENDING", "ACCEPTED", "SUBMITTED", "VERIFIED", "REJECTED", "COMPLETED", "CANCELLED", "EXPIRED", "DISPUTED"],
|
|
810
|
+
description: "Filter by task status",
|
|
811
|
+
},
|
|
812
|
+
domain: { type: "string", enum: ["physical", "digital", "all"], description: "Filter by task domain (default: all)" },
|
|
743
813
|
limit: { type: "number", description: "Max results (default: 20, max: 100)" },
|
|
744
814
|
offset: { type: "number", description: "Pagination offset (default: 0)" },
|
|
745
815
|
},
|
|
@@ -792,6 +862,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
792
862
|
body: JSON.stringify(body),
|
|
793
863
|
headers,
|
|
794
864
|
});
|
|
865
|
+
// Surface sandbox_notice prominently so the agent understands what happened
|
|
866
|
+
const resultObj = result;
|
|
867
|
+
if (resultObj.sandbox) {
|
|
868
|
+
return {
|
|
869
|
+
content: [
|
|
870
|
+
{ type: "text", text: `SANDBOX MODE: ${resultObj.sandbox_notice ?? "This task will auto-complete with simulated proof. No real operator is involved."}\n\n${JSON.stringify(result, null, 2)}` },
|
|
871
|
+
],
|
|
872
|
+
};
|
|
873
|
+
}
|
|
795
874
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
796
875
|
}
|
|
797
876
|
case "get_task_result": {
|
|
@@ -805,6 +884,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
805
884
|
const result = await apiRequest(`/api/v1/tasks/${encodeURIComponent(parsed.data.task_id)}`, {
|
|
806
885
|
method: "GET",
|
|
807
886
|
});
|
|
887
|
+
// Surface sandbox notice prominently
|
|
888
|
+
const taskObj = result;
|
|
889
|
+
if (taskObj.sandbox) {
|
|
890
|
+
return {
|
|
891
|
+
content: [
|
|
892
|
+
{ type: "text", text: `SANDBOX TASK: ${taskObj.sandbox_notice ?? "This task was simulated — operator, proof, and verification are all synthetic."}\n\n${JSON.stringify(result, null, 2)}` },
|
|
893
|
+
],
|
|
894
|
+
};
|
|
895
|
+
}
|
|
808
896
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
809
897
|
}
|
|
810
898
|
case "check_verification_status": {
|
|
@@ -824,6 +912,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
824
912
|
guardian_result: task.guardian_result,
|
|
825
913
|
verified_at: task.verified_at ?? null,
|
|
826
914
|
completed_at: task.completed_at ?? null,
|
|
915
|
+
...(task.sandbox && { sandbox: true, sandbox_notice: "Verification result is synthetic — this was a simulated sandbox task." }),
|
|
827
916
|
};
|
|
828
917
|
return { content: [{ type: "text", text: JSON.stringify(focused, null, 2) }] };
|
|
829
918
|
}
|
|
@@ -837,7 +926,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
837
926
|
type: "text",
|
|
838
927
|
text: JSON.stringify({
|
|
839
928
|
...result,
|
|
840
|
-
_instructions: "Send USDC to this address on the specified chain. After sending, use fund_account with
|
|
929
|
+
_instructions: "Send USDC to this address on the specified chain. Production safety: you may need to bind the wallet you deposit from (POST /api/v1/agents/wallet/challenge + PUT /api/v1/agents/wallet) before verifying deposits. After sending, use fund_account with tx_hash + chain to verify.",
|
|
841
930
|
}, null, 2),
|
|
842
931
|
},
|
|
843
932
|
],
|
|
@@ -851,8 +940,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
851
940
|
isError: true,
|
|
852
941
|
};
|
|
853
942
|
}
|
|
854
|
-
const { amount_usd, payment_method, tx_hash, chain, return_url } = parsed.data;
|
|
855
|
-
const method = payment_method ?? "usdc";
|
|
943
|
+
const { amount_usd, payment_method: method, tx_hash, chain, return_url } = parsed.data;
|
|
856
944
|
// Fiat methods are coming soon
|
|
857
945
|
if (method === "card" || method === "bank_transfer") {
|
|
858
946
|
return {
|
|
@@ -882,7 +970,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
882
970
|
text: JSON.stringify({
|
|
883
971
|
status: "awaiting_deposit",
|
|
884
972
|
...addressResult,
|
|
885
|
-
message: "No tx_hash provided. Send USDC to the address above,
|
|
973
|
+
message: "No tx_hash provided. Send USDC to the address above. If the API says your deposit wallet is not verified, bind it first via POST /api/v1/agents/wallet/challenge + PUT /api/v1/agents/wallet. Then call fund_account again with tx_hash + chain to verify your deposit.",
|
|
886
974
|
}, null, 2),
|
|
887
975
|
},
|
|
888
976
|
],
|
|
@@ -891,7 +979,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
891
979
|
const result = await apiRequest("/api/v1/agents/deposit/usdc", {
|
|
892
980
|
method: "POST",
|
|
893
981
|
body: JSON.stringify({
|
|
894
|
-
amount_usd,
|
|
982
|
+
amount_usdc: amount_usd,
|
|
895
983
|
tx_hash,
|
|
896
984
|
chain: chain ?? "base",
|
|
897
985
|
}),
|
|
@@ -911,9 +999,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
911
999
|
{
|
|
912
1000
|
type: "text",
|
|
913
1001
|
text: JSON.stringify({
|
|
914
|
-
status: "
|
|
1002
|
+
status: "operator_only",
|
|
915
1003
|
amount_requested: parsed.data.amount_usd,
|
|
916
|
-
message: "Payouts are
|
|
1004
|
+
message: "Payouts are processed via the operator portal. Operators can withdraw USDC on Base L2 by setting a wallet address and requesting a payout through the operator API (PUT /operator/wallet + POST /operator/payout).",
|
|
917
1005
|
}, null, 2),
|
|
918
1006
|
},
|
|
919
1007
|
],
|
|
@@ -921,6 +1009,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
921
1009
|
}
|
|
922
1010
|
case "get_balance": {
|
|
923
1011
|
const data = await apiRequest("/api/v1/agents/balance", { method: "GET" });
|
|
1012
|
+
const balanceObj = data;
|
|
1013
|
+
const depositBalance = Number(balanceObj.deposit_balance ?? 0);
|
|
1014
|
+
const hints = [];
|
|
1015
|
+
if (depositBalance === 0) {
|
|
1016
|
+
hints.push("Your balance is $0. To create real tasks, deposit USDC via get_deposit_address + fund_account.");
|
|
1017
|
+
}
|
|
1018
|
+
if (hints.length > 0) {
|
|
1019
|
+
return {
|
|
1020
|
+
content: [
|
|
1021
|
+
{ type: "text", text: `${hints.join(" ")}\n\n${JSON.stringify(data, null, 2)}` },
|
|
1022
|
+
],
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
924
1025
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
925
1026
|
}
|
|
926
1027
|
case "cancel_task": {
|
|
@@ -936,16 +1037,55 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
936
1037
|
});
|
|
937
1038
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
938
1039
|
}
|
|
1040
|
+
case "approve_estimate": {
|
|
1041
|
+
const parsed = GetTaskResultInputSchema.safeParse(args);
|
|
1042
|
+
if (!parsed.success) {
|
|
1043
|
+
return {
|
|
1044
|
+
content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
|
|
1045
|
+
isError: true,
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
const result = await apiRequest(`/api/v1/tasks/${encodeURIComponent(parsed.data.task_id)}/estimate/approve`, {
|
|
1049
|
+
method: "POST",
|
|
1050
|
+
});
|
|
1051
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1052
|
+
}
|
|
1053
|
+
case "reject_estimate": {
|
|
1054
|
+
const rejectSchema = z.object({
|
|
1055
|
+
task_id: z.string().min(1),
|
|
1056
|
+
reason: z.string().max(1000).optional(),
|
|
1057
|
+
});
|
|
1058
|
+
const parsed = rejectSchema.safeParse(args);
|
|
1059
|
+
if (!parsed.success) {
|
|
1060
|
+
return {
|
|
1061
|
+
content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
|
|
1062
|
+
isError: true,
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
const body = parsed.data.reason ? JSON.stringify({ reason: parsed.data.reason }) : undefined;
|
|
1066
|
+
const result = await apiRequest(`/api/v1/tasks/${encodeURIComponent(parsed.data.task_id)}/estimate/reject`, {
|
|
1067
|
+
method: "POST",
|
|
1068
|
+
...(body ? { body } : {}),
|
|
1069
|
+
});
|
|
1070
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1071
|
+
}
|
|
939
1072
|
case "list_tasks": {
|
|
940
|
-
const
|
|
941
|
-
if (
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
if (args.offset)
|
|
947
|
-
params.set("offset", String(args.offset));
|
|
1073
|
+
const parsed = ListTasksInputSchema.safeParse(args ?? {});
|
|
1074
|
+
if (!parsed.success) {
|
|
1075
|
+
return {
|
|
1076
|
+
content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
|
|
1077
|
+
isError: true,
|
|
1078
|
+
};
|
|
948
1079
|
}
|
|
1080
|
+
const params = new URLSearchParams();
|
|
1081
|
+
if (parsed.data.status)
|
|
1082
|
+
params.set("status", parsed.data.status);
|
|
1083
|
+
if (parsed.data.domain)
|
|
1084
|
+
params.set("domain", parsed.data.domain);
|
|
1085
|
+
if (parsed.data.limit !== undefined)
|
|
1086
|
+
params.set("limit", String(parsed.data.limit));
|
|
1087
|
+
if (parsed.data.offset !== undefined)
|
|
1088
|
+
params.set("offset", String(parsed.data.offset));
|
|
949
1089
|
const query = params.toString();
|
|
950
1090
|
const data = await apiRequest(`/api/v1/tasks${query ? `?${query}` : ""}`, { method: "GET" });
|
|
951
1091
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
@@ -990,6 +1130,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
990
1130
|
body: JSON.stringify(payload),
|
|
991
1131
|
headers,
|
|
992
1132
|
});
|
|
1133
|
+
const digitalResultObj = result;
|
|
1134
|
+
if (digitalResultObj.sandbox) {
|
|
1135
|
+
return {
|
|
1136
|
+
content: [
|
|
1137
|
+
{ type: "text", text: `SANDBOX MODE: ${digitalResultObj.sandbox_notice ?? "This task will auto-complete with simulated proof."}\n\n${JSON.stringify(result, null, 2)}` },
|
|
1138
|
+
],
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
993
1141
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
994
1142
|
}
|
|
995
1143
|
case "dispatch_credential_task": {
|
|
@@ -1040,6 +1188,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1040
1188
|
private_key: keyPair.privateKey,
|
|
1041
1189
|
_notice: "IMPORTANT: Save the private_key securely. You need it to decrypt the credential using retrieve_credential.",
|
|
1042
1190
|
};
|
|
1191
|
+
const credResultObj = result;
|
|
1192
|
+
if (credResultObj.sandbox) {
|
|
1193
|
+
return {
|
|
1194
|
+
content: [
|
|
1195
|
+
{ type: "text", text: `SANDBOX MODE: ${credResultObj.sandbox_notice ?? "This credential task will auto-complete with simulated data. No real credentials will be delivered."}\n\n${JSON.stringify(response, null, 2)}` },
|
|
1196
|
+
],
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1043
1199
|
return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }] };
|
|
1044
1200
|
}
|
|
1045
1201
|
case "retrieve_credential": {
|
|
@@ -1079,7 +1235,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1079
1235
|
}
|
|
1080
1236
|
throw retrieveErr;
|
|
1081
1237
|
}
|
|
1082
|
-
|
|
1238
|
+
let plaintext;
|
|
1239
|
+
try {
|
|
1240
|
+
plaintext = await decryptCredential(retrieveResult.encrypted_credential, private_key);
|
|
1241
|
+
}
|
|
1242
|
+
catch (decryptErr) {
|
|
1243
|
+
return {
|
|
1244
|
+
content: [
|
|
1245
|
+
{
|
|
1246
|
+
type: "text",
|
|
1247
|
+
text: JSON.stringify({
|
|
1248
|
+
task_id,
|
|
1249
|
+
error: "Decryption failed. This usually means the private_key does not match the keypair used when creating the task. Ensure you are using the exact private_key returned by dispatch_credential_task.",
|
|
1250
|
+
}, null, 2),
|
|
1251
|
+
},
|
|
1252
|
+
],
|
|
1253
|
+
isError: true,
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1083
1256
|
return {
|
|
1084
1257
|
content: [
|
|
1085
1258
|
{
|
|
@@ -1114,13 +1287,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1114
1287
|
}
|
|
1115
1288
|
}
|
|
1116
1289
|
catch (err) {
|
|
1117
|
-
const
|
|
1118
|
-
const isNetworkError =
|
|
1290
|
+
const rawMessage = err.message || "Unknown error";
|
|
1291
|
+
const isNetworkError = rawMessage.includes("fetch failed") || rawMessage.includes("ECONNREFUSED");
|
|
1292
|
+
// Sanitize: strip stack traces, internal paths, and limit length
|
|
1293
|
+
let safeMessage = rawMessage
|
|
1294
|
+
.replace(/\bat\s+.+\(.+\)/g, "")
|
|
1295
|
+
.replace(/\/[^\s:]+\.(ts|js):\d+/g, "[internal]")
|
|
1296
|
+
.slice(0, 500);
|
|
1119
1297
|
const hint = isNetworkError
|
|
1120
1298
|
? " (Network error — check your HUMANOPS_API_URL and HUMANOPS_API_KEY configuration.)"
|
|
1121
1299
|
: "";
|
|
1122
1300
|
return {
|
|
1123
|
-
content: [{ type: "text", text: `Error: ${
|
|
1301
|
+
content: [{ type: "text", text: `Error: ${safeMessage}${hint}` }],
|
|
1124
1302
|
isError: true,
|
|
1125
1303
|
};
|
|
1126
1304
|
}
|
|
@@ -1128,12 +1306,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1128
1306
|
async function main() {
|
|
1129
1307
|
const transport = new StdioServerTransport();
|
|
1130
1308
|
await server.connect(transport);
|
|
1131
|
-
|
|
1132
|
-
console.error(`HumanOps MCP Server running on stdio (api=${getApiUrl()})`);
|
|
1133
|
-
}
|
|
1134
|
-
else {
|
|
1135
|
-
console.error("HumanOps MCP Server running on stdio");
|
|
1136
|
-
}
|
|
1309
|
+
console.error("HumanOps MCP Server running on stdio");
|
|
1137
1310
|
}
|
|
1138
1311
|
main().catch((err) => {
|
|
1139
1312
|
console.error("Failed to start MCP server:", err);
|
package/package.json
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanops/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"mcpName": "io.github.thepianistdirector/humanops",
|
|
5
5
|
"description": "MCP server for AI agents to dispatch real-world tasks to verified human operators via HumanOps",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"bin": {
|
|
9
|
-
"humanops-mcp": "
|
|
9
|
+
"humanops-mcp": "dist/index.js"
|
|
10
10
|
},
|
|
11
|
-
"files": ["dist/*.js", "dist/*.d.ts"],
|
|
11
|
+
"files": ["dist/*.js", "dist/*.d.ts", "README.md"],
|
|
12
12
|
"scripts": {
|
|
13
13
|
"start": "node dist/index.js",
|
|
14
14
|
"dev": "tsx watch src/index.ts",
|
|
15
15
|
"build": "tsc",
|
|
16
|
+
"smoke:local": "node scripts/smoke-local.mjs",
|
|
16
17
|
"prepublishOnly": "npm run build",
|
|
17
18
|
"lint": "tsc --noEmit"
|
|
18
19
|
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
19
23
|
"dependencies": {
|
|
20
24
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
21
25
|
"zod": "^3.24.0"
|
|
@@ -26,8 +30,13 @@
|
|
|
26
30
|
},
|
|
27
31
|
"license": "MIT",
|
|
28
32
|
"keywords": ["humanops", "mcp", "ai", "agents", "tasks", "model-context-protocol"],
|
|
33
|
+
"author": "HumanOps <hello@humanops.io>",
|
|
34
|
+
"homepage": "https://humanops.io",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/thepianistdirector/humanops/issues"
|
|
37
|
+
},
|
|
29
38
|
"repository": {
|
|
30
39
|
"type": "git",
|
|
31
|
-
"url": "https://github.com/
|
|
40
|
+
"url": "https://github.com/thepianistdirector/humanops"
|
|
32
41
|
}
|
|
33
42
|
}
|