@humanops/mcp-server 0.2.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.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,1142 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
5
+ import { z } from "zod";
6
+ // ---------------------------------------------------------------------------
7
+ // Local constants and helpers (keep MCP server standalone and Node-runnable).
8
+ // ---------------------------------------------------------------------------
9
+ const MIN_TASK_VALUE_USD = 10;
10
+ const MAX_TASK_VALUE_USD = 10_000;
11
+ const MIN_DEPOSIT_USD = 5;
12
+ const MAX_DEPOSIT_USD = 10_000;
13
+ const MIN_PAYOUT_USD = 10;
14
+ const CREDENTIAL_CATEGORIES = [
15
+ "ACCOUNT_CREATION",
16
+ "API_KEY_PROCUREMENT",
17
+ "PHONE_VERIFICATION",
18
+ "SUBSCRIPTION_SETUP",
19
+ ];
20
+ const DIGITAL_CATEGORY_CONFIG = {
21
+ CAPTCHA_SOLVING: {
22
+ maxValueUsd: 10,
23
+ defaultDeadlineMinutes: 15,
24
+ requiredProofTypes: ["SCREENSHOT"],
25
+ requiredTrustTier: "T1_STANDARD",
26
+ description: "Solve CAPTCHAs and visual challenges that AI cannot handle",
27
+ },
28
+ FORM_FILLING: {
29
+ maxValueUsd: 50,
30
+ defaultDeadlineMinutes: 60,
31
+ requiredProofTypes: ["SCREENSHOT", "CONFIRMATION"],
32
+ requiredTrustTier: "T1_STANDARD",
33
+ description: "Fill out web forms, applications, or registration flows",
34
+ },
35
+ BROWSER_INTERACTION: {
36
+ maxValueUsd: 100,
37
+ defaultDeadlineMinutes: 60,
38
+ requiredProofTypes: ["SCREENSHOT"],
39
+ requiredTrustTier: "T1_STANDARD",
40
+ description: "Perform browser-based interactions requiring human judgment",
41
+ },
42
+ CONTENT_REVIEW: {
43
+ maxValueUsd: 25,
44
+ defaultDeadlineMinutes: 120,
45
+ requiredProofTypes: ["CONFIRMATION"],
46
+ requiredTrustTier: "T1_STANDARD",
47
+ description: "Review and assess content for quality, accuracy, or compliance",
48
+ },
49
+ DATA_VALIDATION: {
50
+ maxValueUsd: 50,
51
+ defaultDeadlineMinutes: 120,
52
+ requiredProofTypes: ["CONFIRMATION"],
53
+ requiredTrustTier: "T1_STANDARD",
54
+ description: "Validate data accuracy by cross-referencing real-world sources",
55
+ },
56
+ ACCOUNT_CREATION: {
57
+ maxValueUsd: 100,
58
+ defaultDeadlineMinutes: 60,
59
+ requiredProofTypes: ["ARTIFACT", "SCREENSHOT"],
60
+ requiredTrustTier: "T2_DIGITAL",
61
+ description: "Create accounts on third-party services",
62
+ },
63
+ API_KEY_PROCUREMENT: {
64
+ maxValueUsd: 200,
65
+ defaultDeadlineMinutes: 120,
66
+ requiredProofTypes: ["ARTIFACT"],
67
+ requiredTrustTier: "T2_DIGITAL",
68
+ description: "Sign up and retrieve API keys from services",
69
+ },
70
+ PHONE_VERIFICATION: {
71
+ maxValueUsd: 25,
72
+ defaultDeadlineMinutes: 30,
73
+ requiredProofTypes: ["CONFIRMATION"],
74
+ requiredTrustTier: "T2_DIGITAL",
75
+ description: "Receive SMS codes and verify phone numbers",
76
+ },
77
+ SUBSCRIPTION_SETUP: {
78
+ maxValueUsd: 500,
79
+ defaultDeadlineMinutes: 120,
80
+ requiredProofTypes: ["ARTIFACT", "SCREENSHOT"],
81
+ requiredTrustTier: "T2_DIGITAL",
82
+ description: "Configure paid service subscriptions",
83
+ },
84
+ };
85
+ const DIGITAL_CATEGORIES = [
86
+ "CAPTCHA_SOLVING",
87
+ "FORM_FILLING",
88
+ "BROWSER_INTERACTION",
89
+ "CONTENT_REVIEW",
90
+ "DATA_VALIDATION",
91
+ "ACCOUNT_CREATION",
92
+ "API_KEY_PROCUREMENT",
93
+ "PHONE_VERIFICATION",
94
+ "SUBSCRIPTION_SETUP",
95
+ ];
96
+ // ---------------------------------------------------------------------------
97
+ // Inline E2EE helpers (ECDH P-256 + HKDF-SHA-256 + AES-256-GCM)
98
+ // Inlined to keep MCP server standalone without importing @humanops/shared.
99
+ // ---------------------------------------------------------------------------
100
+ const HKDF_INFO = new TextEncoder().encode("humanops-credential-v1");
101
+ function toBase64(buf) {
102
+ const bytes = new Uint8Array(buf);
103
+ let binary = "";
104
+ for (let i = 0; i < bytes.length; i++) {
105
+ binary += String.fromCharCode(bytes[i]);
106
+ }
107
+ return btoa(binary);
108
+ }
109
+ function fromBase64(b64) {
110
+ const binary = atob(b64);
111
+ const bytes = new Uint8Array(binary.length);
112
+ for (let i = 0; i < binary.length; i++) {
113
+ bytes[i] = binary.charCodeAt(i);
114
+ }
115
+ return bytes.buffer;
116
+ }
117
+ async function generateKeyPair() {
118
+ const keyPair = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveKey", "deriveBits"]);
119
+ const [publicRaw, privateRaw] = await Promise.all([
120
+ crypto.subtle.exportKey("raw", keyPair.publicKey),
121
+ crypto.subtle.exportKey("pkcs8", keyPair.privateKey),
122
+ ]);
123
+ return { publicKey: toBase64(publicRaw), privateKey: toBase64(privateRaw) };
124
+ }
125
+ async function importPublicKey(base64) {
126
+ return crypto.subtle.importKey("raw", fromBase64(base64), { name: "ECDH", namedCurve: "P-256" }, false, []);
127
+ }
128
+ async function importPrivateKey(base64) {
129
+ return crypto.subtle.importKey("pkcs8", fromBase64(base64), { name: "ECDH", namedCurve: "P-256" }, false, ["deriveBits"]);
130
+ }
131
+ async function deriveSharedSecret(privateKeyBase64, publicKeyBase64) {
132
+ const [privateKey, publicKey] = await Promise.all([
133
+ importPrivateKey(privateKeyBase64),
134
+ importPublicKey(publicKeyBase64),
135
+ ]);
136
+ const sharedBits = await crypto.subtle.deriveBits({ name: "ECDH", public: publicKey }, privateKey, 256);
137
+ const hkdfKey = await crypto.subtle.importKey("raw", sharedBits, "HKDF", false, ["deriveKey"]);
138
+ return crypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(32), info: HKDF_INFO }, hkdfKey, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
139
+ }
140
+ async function decryptCredential(encrypted, privateKeyBase64) {
141
+ const aesKey = await deriveSharedSecret(privateKeyBase64, encrypted.ephemeralPublicKey);
142
+ const plainBuf = await crypto.subtle.decrypt({ name: "AES-GCM", iv: fromBase64(encrypted.iv) }, aesKey, fromBase64(encrypted.ciphertext));
143
+ return new TextDecoder().decode(plainBuf);
144
+ }
145
+ // ---------------------------------------------------------------------------
146
+ // SSRF protection
147
+ // ---------------------------------------------------------------------------
148
+ function isSSRFSafeUrl(urlStr, requireHttps = false) {
149
+ let parsed;
150
+ try {
151
+ parsed = new URL(urlStr);
152
+ }
153
+ catch {
154
+ return false;
155
+ }
156
+ if (!["http:", "https:"].includes(parsed.protocol))
157
+ return false;
158
+ if (requireHttps && parsed.protocol !== "https:")
159
+ return false;
160
+ const rawHostname = parsed.hostname.trim().toLowerCase().replace(/\.+$/, "");
161
+ if (!rawHostname)
162
+ return false;
163
+ const hostname = rawHostname.startsWith("[") && rawHostname.endsWith("]")
164
+ ? rawHostname.slice(1, -1)
165
+ : rawHostname;
166
+ if (hostname === "localhost" || hostname === "localhost.localdomain")
167
+ return false;
168
+ if (hostname.endsWith(".localhost"))
169
+ return false;
170
+ if (hostname.endsWith(".local") || hostname.endsWith(".localdomain"))
171
+ return false;
172
+ if (hostname === "metadata.google.internal")
173
+ return false;
174
+ if (hostname.endsWith(".internal"))
175
+ return false;
176
+ const ipv4 = parseIpv4(hostname);
177
+ if (ipv4 && isPrivateIpv4(ipv4))
178
+ return false;
179
+ const ipv6 = parseIpv6(hostname);
180
+ if (ipv6 && isPrivateIpv6(ipv6))
181
+ return false;
182
+ return true;
183
+ }
184
+ function parseIpv4(hostname) {
185
+ const dotted = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
186
+ if (dotted) {
187
+ const a = Number(dotted[1]);
188
+ const b = Number(dotted[2]);
189
+ const c = Number(dotted[3]);
190
+ const d = Number(dotted[4]);
191
+ if ([a, b, c, d].some((n) => !Number.isInteger(n) || n < 0 || n > 255))
192
+ return null;
193
+ return [a, b, c, d];
194
+ }
195
+ let n;
196
+ try {
197
+ if (/^0x[0-9a-f]+$/i.test(hostname)) {
198
+ n = BigInt(hostname);
199
+ }
200
+ else if (hostname === "0") {
201
+ n = 0n;
202
+ }
203
+ else if (/^0[0-7]+$/.test(hostname)) {
204
+ n = BigInt(`0o${hostname.slice(1)}`);
205
+ }
206
+ else if (/^\d+$/.test(hostname)) {
207
+ n = BigInt(hostname);
208
+ }
209
+ else {
210
+ return null;
211
+ }
212
+ }
213
+ catch {
214
+ return null;
215
+ }
216
+ if (n < 0n || n > 0xffffffffn)
217
+ return null;
218
+ const a = Number((n >> 24n) & 255n);
219
+ const b = Number((n >> 16n) & 255n);
220
+ const c = Number((n >> 8n) & 255n);
221
+ const d = Number(n & 255n);
222
+ return [a, b, c, d];
223
+ }
224
+ function isPrivateIpv4([a, b]) {
225
+ if (a === 127)
226
+ return true;
227
+ if (a === 10)
228
+ return true;
229
+ if (a === 172 && b >= 16 && b <= 31)
230
+ return true;
231
+ if (a === 192 && b === 168)
232
+ return true;
233
+ if (a === 169 && b === 254)
234
+ return true;
235
+ if (a === 100 && b >= 64 && b <= 127)
236
+ return true;
237
+ if (a === 0)
238
+ return true;
239
+ if (a === 198 && (b === 18 || b === 19))
240
+ return true;
241
+ if (a >= 224)
242
+ return true;
243
+ return false;
244
+ }
245
+ function parseIpv6(hostname) {
246
+ if (!hostname.includes(":"))
247
+ return null;
248
+ if (hostname.includes("%"))
249
+ return null;
250
+ const parts = hostname.split("::");
251
+ if (parts.length > 2)
252
+ return null;
253
+ const leftRaw = parts[0] ? parts[0].split(":") : [];
254
+ const rightRaw = parts.length === 2 && parts[1] ? parts[1].split(":") : [];
255
+ const left = [];
256
+ const right = [];
257
+ const pushToken = (arr, token) => {
258
+ if (!token)
259
+ return false;
260
+ if (token.includes(".")) {
261
+ const ipv4 = parseIpv4(token);
262
+ if (!ipv4)
263
+ return false;
264
+ arr.push((ipv4[0] << 8) | ipv4[1]);
265
+ arr.push((ipv4[2] << 8) | ipv4[3]);
266
+ return true;
267
+ }
268
+ if (!/^[0-9a-f]{1,4}$/i.test(token))
269
+ return false;
270
+ arr.push(parseInt(token, 16));
271
+ return true;
272
+ };
273
+ for (let i = 0; i < leftRaw.length; i++) {
274
+ if (!pushToken(left, leftRaw[i]))
275
+ return null;
276
+ }
277
+ for (let i = 0; i < rightRaw.length; i++) {
278
+ if (!pushToken(right, rightRaw[i]))
279
+ return null;
280
+ }
281
+ const total = left.length + right.length;
282
+ if (parts.length === 1) {
283
+ if (total !== 8)
284
+ return null;
285
+ return new Uint16Array([...left, ...right]);
286
+ }
287
+ if (total > 8)
288
+ return null;
289
+ const zerosToInsert = 8 - total;
290
+ return new Uint16Array([...left, ...Array(zerosToInsert).fill(0), ...right]);
291
+ }
292
+ function isPrivateIpv6(groups) {
293
+ if (groups.length !== 8)
294
+ return true;
295
+ if (groups.every((g) => g === 0))
296
+ return true;
297
+ if (groups[0] === 0 &&
298
+ groups[1] === 0 &&
299
+ groups[2] === 0 &&
300
+ groups[3] === 0 &&
301
+ groups[4] === 0 &&
302
+ groups[5] === 0 &&
303
+ groups[6] === 0 &&
304
+ groups[7] === 1) {
305
+ return true;
306
+ }
307
+ if ((groups[0] & 0xfe00) === 0xfc00)
308
+ return true;
309
+ if ((groups[0] & 0xffc0) === 0xfe80)
310
+ return true;
311
+ if ((groups[0] & 0xffc0) === 0xfec0)
312
+ return true;
313
+ if (groups[0] === 0 &&
314
+ groups[1] === 0 &&
315
+ groups[2] === 0 &&
316
+ groups[3] === 0 &&
317
+ groups[4] === 0 &&
318
+ groups[5] === 0xffff) {
319
+ const a = (groups[6] >> 8) & 0xff;
320
+ const b = groups[6] & 0xff;
321
+ const c = (groups[7] >> 8) & 0xff;
322
+ const d = groups[7] & 0xff;
323
+ return isPrivateIpv4([a, b, c, d]);
324
+ }
325
+ return false;
326
+ }
327
+ /**
328
+ * HumanOps MCP Server (safe mode)
329
+ *
330
+ * Design: This MCP server is a local process. To avoid bypassing the API's
331
+ * authz/rate-limits/audit logging, it talks to HumanOps via HTTP only.
332
+ */
333
+ const DEFAULT_API_URL = "https://api.humanops.io";
334
+ function getApiUrl() {
335
+ const raw = process.env.HUMANOPS_API_URL?.trim();
336
+ if (!raw)
337
+ return DEFAULT_API_URL;
338
+ const url = raw.replace(/\/+$/, "");
339
+ let parsed;
340
+ try {
341
+ parsed = new URL(url);
342
+ }
343
+ catch {
344
+ throw new Error("HUMANOPS_API_URL must be a valid URL.");
345
+ }
346
+ const allowAnyHost = process.env.HUMANOPS_ALLOW_ANY_API_HOST === "true";
347
+ const hostname = parsed.hostname.trim().toLowerCase().replace(/\.+$/, "");
348
+ if (!allowAnyHost) {
349
+ // Prevent accidental exfiltration of HUMANOPS_API_KEY to an unrelated domain.
350
+ if (hostname !== "api.humanops.io" && !hostname.endsWith(".humanops.io")) {
351
+ throw new Error("HUMANOPS_API_URL must be api.humanops.io (or *.humanops.io). To override, set HUMANOPS_ALLOW_ANY_API_HOST=true.");
352
+ }
353
+ }
354
+ // Prevent subtle base URL bugs like including paths, querystrings, or fragments.
355
+ if (parsed.pathname && parsed.pathname !== "/") {
356
+ throw new Error("HUMANOPS_API_URL must not include a path (use only the origin, e.g. https://api.humanops.io).");
357
+ }
358
+ if (parsed.search || parsed.hash) {
359
+ throw new Error("HUMANOPS_API_URL must not include query params or a fragment.");
360
+ }
361
+ const normalized = parsed.origin;
362
+ if (!isSSRFSafeUrl(normalized, true)) {
363
+ throw new Error("HUMANOPS_API_URL must be a public HTTPS URL (private/internal addresses are blocked).");
364
+ }
365
+ return normalized;
366
+ }
367
+ function getApiKey() {
368
+ const apiKey = process.env.HUMANOPS_API_KEY;
369
+ if (!apiKey) {
370
+ throw new Error("HUMANOPS_API_KEY environment variable is required.");
371
+ }
372
+ return apiKey;
373
+ }
374
+ async function apiRequest(path, init = {}) {
375
+ const baseUrl = getApiUrl();
376
+ const apiKey = getApiKey();
377
+ const url = `${baseUrl}${path}`;
378
+ const headers = new Headers(init.headers);
379
+ headers.set("X-API-Key", apiKey);
380
+ if (!headers.has("Content-Type") && init.body && typeof init.body === "string") {
381
+ headers.set("Content-Type", "application/json");
382
+ }
383
+ // Never follow redirects when sending X-API-Key (prevents key exfiltration).
384
+ const res = await fetch(url, { ...init, headers, redirect: "error" });
385
+ const contentType = res.headers.get("content-type") ?? "";
386
+ const raw = await res.text();
387
+ const json = contentType.includes("application/json") && raw ? JSON.parse(raw) : null;
388
+ if (!res.ok) {
389
+ const message = json?.error ||
390
+ json?.message ||
391
+ res.statusText ||
392
+ `HTTP ${res.status}`;
393
+ throw new Error(message);
394
+ }
395
+ return (json ?? raw);
396
+ }
397
+ // ---------------------------------------------------------------------------
398
+ // Tool schemas (validate inputs before hitting the API)
399
+ // ---------------------------------------------------------------------------
400
+ const TaskTypeEnum = z.enum([
401
+ "VERIFICATION",
402
+ "PHOTO",
403
+ "DELIVERY",
404
+ "INSPECTION",
405
+ "CAPTCHA_SOLVING",
406
+ "FORM_FILLING",
407
+ "BROWSER_INTERACTION",
408
+ "CONTENT_REVIEW",
409
+ "DATA_VALIDATION",
410
+ "ACCOUNT_CREATION",
411
+ "API_KEY_PROCUREMENT",
412
+ "PHONE_VERIFICATION",
413
+ "SUBSCRIPTION_SETUP",
414
+ ]);
415
+ const DigitalCategoryEnum = z.enum([
416
+ "CAPTCHA_SOLVING",
417
+ "FORM_FILLING",
418
+ "BROWSER_INTERACTION",
419
+ "CONTENT_REVIEW",
420
+ "DATA_VALIDATION",
421
+ ]);
422
+ const CredentialCategoryEnum = z.enum([
423
+ "ACCOUNT_CREATION",
424
+ "API_KEY_PROCUREMENT",
425
+ "PHONE_VERIFICATION",
426
+ "SUBSCRIPTION_SETUP",
427
+ ]);
428
+ const DispatchDigitalTaskInputSchema = z.object({
429
+ title: z.string().min(1).max(200),
430
+ description: z.string().min(10).max(5000),
431
+ digital_category: DigitalCategoryEnum,
432
+ reward_usd: z.number().min(MIN_TASK_VALUE_USD).max(MAX_TASK_VALUE_USD),
433
+ deadline: z.string().datetime(),
434
+ proof_requirements: z.array(z.string()).min(1).max(10),
435
+ digital_instructions: z.string().max(5000).optional(),
436
+ callback_url: z
437
+ .string()
438
+ .url()
439
+ .refine((url) => isSSRFSafeUrl(url, true), {
440
+ message: "callback_url must be a public HTTPS URL (private/internal addresses are blocked)",
441
+ })
442
+ .optional(),
443
+ callback_secret: z.string().min(16).max(128).optional(),
444
+ idempotency_key: z.string().min(1).max(200).optional(),
445
+ });
446
+ const DispatchCredentialTaskInputSchema = z.object({
447
+ title: z.string().min(1).max(200),
448
+ description: z.string().min(10).max(5000),
449
+ digital_category: CredentialCategoryEnum,
450
+ reward_usd: z.number().min(MIN_TASK_VALUE_USD).max(MAX_TASK_VALUE_USD),
451
+ deadline: z.string().datetime(),
452
+ proof_requirements: z.array(z.string()).min(1).max(10),
453
+ digital_instructions: z.string().max(5000).optional(),
454
+ callback_url: z
455
+ .string()
456
+ .url()
457
+ .refine((url) => isSSRFSafeUrl(url, true), {
458
+ message: "callback_url must be a public HTTPS URL (private/internal addresses are blocked)",
459
+ })
460
+ .optional(),
461
+ callback_secret: z.string().min(16).max(128).optional(),
462
+ idempotency_key: z.string().min(1).max(200).optional(),
463
+ });
464
+ const RetrieveCredentialInputSchema = z.object({
465
+ task_id: z.string().min(1),
466
+ private_key: z.string().min(1),
467
+ });
468
+ const SearchOperatorsInputSchema = z.object({
469
+ lat: z.number(),
470
+ lng: z.number(),
471
+ radius_km: z.number().min(1).max(500).optional(),
472
+ task_type: TaskTypeEnum.optional(),
473
+ min_rating: z.number().min(0).max(5).optional(),
474
+ });
475
+ const PostTaskInputSchema = z.object({
476
+ title: z.string().min(1).max(200),
477
+ description: z.string().min(10).max(5000),
478
+ location: z.object({
479
+ lat: z.number(),
480
+ lng: z.number(),
481
+ address: z.string().min(1),
482
+ }),
483
+ reward_usd: z.number().min(MIN_TASK_VALUE_USD).max(MAX_TASK_VALUE_USD),
484
+ deadline: z.string().datetime(),
485
+ proof_requirements: z.array(z.string()).min(1).max(10),
486
+ task_type: TaskTypeEnum,
487
+ callback_url: z
488
+ .string()
489
+ .url()
490
+ .refine((url) => isSSRFSafeUrl(url, true), {
491
+ message: "callback_url must be a public HTTPS URL (private/internal addresses are blocked)",
492
+ })
493
+ .optional(),
494
+ callback_secret: z.string().min(16).max(128).optional(),
495
+ idempotency_key: z.string().min(1).max(200).optional(),
496
+ });
497
+ const GetTaskResultInputSchema = z.object({
498
+ task_id: z.string().min(1),
499
+ });
500
+ const USDCChainEnum = z.enum(["base", "polygon", "ethereum", "arbitrum"]);
501
+ const FundAccountInputSchema = z.object({
502
+ amount_usd: z.number().min(MIN_DEPOSIT_USD).max(MAX_DEPOSIT_USD),
503
+ payment_method: z.enum(["usdc", "card", "bank_transfer"]).default("usdc"),
504
+ tx_hash: z.string().min(1).optional(),
505
+ chain: USDCChainEnum.optional(),
506
+ return_url: z
507
+ .string()
508
+ .url()
509
+ .refine((url) => isSSRFSafeUrl(url, true), {
510
+ message: "return_url must be a public HTTPS URL (private/internal addresses are blocked)",
511
+ })
512
+ .optional(),
513
+ });
514
+ const RequestPayoutInputSchema = z.object({
515
+ amount_usd: z.number().min(MIN_PAYOUT_USD),
516
+ });
517
+ const ALL_TASK_TYPES = [
518
+ "VERIFICATION", "PHOTO", "DELIVERY", "INSPECTION",
519
+ "CAPTCHA_SOLVING", "FORM_FILLING", "BROWSER_INTERACTION", "CONTENT_REVIEW", "DATA_VALIDATION",
520
+ "ACCOUNT_CREATION", "API_KEY_PROCUREMENT", "PHONE_VERIFICATION", "SUBSCRIPTION_SETUP",
521
+ ];
522
+ const server = new Server({
523
+ name: "humanops",
524
+ version: "0.2.0",
525
+ }, {
526
+ capabilities: { tools: {} },
527
+ });
528
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
529
+ tools: [
530
+ {
531
+ name: "search_operators",
532
+ description: "Search for available human operators near a location who can perform real-world tasks like verifications, photo documentation, deliveries, and inspections.",
533
+ inputSchema: {
534
+ type: "object",
535
+ properties: {
536
+ lat: { type: "number", description: "Latitude of the task location" },
537
+ lng: { type: "number", description: "Longitude of the task location" },
538
+ radius_km: { type: "number", description: "Search radius in kilometers (default: 50, max: 500)", default: 50 },
539
+ task_type: {
540
+ type: "string",
541
+ enum: [...ALL_TASK_TYPES],
542
+ },
543
+ min_rating: { type: "number", description: "Minimum operator rating (0-5)" },
544
+ },
545
+ required: ["lat", "lng"],
546
+ },
547
+ },
548
+ {
549
+ name: "post_task",
550
+ description: "Create a new task for a human operator. Supports physical tasks (VERIFICATION, PHOTO, DELIVERY, INSPECTION), digital tasks (CAPTCHA_SOLVING, FORM_FILLING, etc.), and credential tasks (ACCOUNT_CREATION, API_KEY_PROCUREMENT, etc.). For digital tasks prefer dispatch_digital_task; for credential tasks prefer dispatch_credential_task. Funds will be escrowed from your account (reward + platform fee).",
551
+ inputSchema: {
552
+ type: "object",
553
+ properties: {
554
+ title: { type: "string" },
555
+ description: { type: "string" },
556
+ location: {
557
+ type: "object",
558
+ properties: { lat: { type: "number" }, lng: { type: "number" }, address: { type: "string" } },
559
+ required: ["lat", "lng", "address"],
560
+ },
561
+ reward_usd: { type: "number" },
562
+ deadline: { type: "string", description: "ISO 8601 deadline for task completion" },
563
+ proof_requirements: { type: "array", items: { type: "string" } },
564
+ task_type: {
565
+ type: "string",
566
+ enum: [...ALL_TASK_TYPES],
567
+ },
568
+ callback_url: { type: "string" },
569
+ callback_secret: { type: "string", description: "Optional secret to sign webhook callbacks" },
570
+ idempotency_key: { type: "string", description: "Optional key to make task creation safe to retry" },
571
+ },
572
+ required: ["title", "description", "location", "reward_usd", "deadline", "proof_requirements", "task_type"],
573
+ },
574
+ },
575
+ {
576
+ name: "dispatch_digital_task",
577
+ description: "Create a Tier 1 digital task for a human operator. Includes CAPTCHA solving, form filling, browser interaction, content review, and data validation. No physical location needed — operators complete these tasks remotely.",
578
+ inputSchema: {
579
+ type: "object",
580
+ properties: {
581
+ title: { type: "string", description: "Short title for the task" },
582
+ description: { type: "string", description: "Detailed description of what needs to be done" },
583
+ digital_category: {
584
+ type: "string",
585
+ enum: ["CAPTCHA_SOLVING", "FORM_FILLING", "BROWSER_INTERACTION", "CONTENT_REVIEW", "DATA_VALIDATION"],
586
+ description: "Category of digital task",
587
+ },
588
+ reward_usd: { type: "number", description: "Reward in USD for the operator" },
589
+ deadline: { type: "string", description: "ISO 8601 deadline for task completion" },
590
+ proof_requirements: {
591
+ type: "array",
592
+ items: { type: "string" },
593
+ description: "List of proof requirements",
594
+ },
595
+ digital_instructions: {
596
+ type: "string",
597
+ description: "Specific step-by-step instructions for the digital task",
598
+ },
599
+ callback_url: { type: "string", description: "Optional webhook URL for status updates" },
600
+ callback_secret: { type: "string", description: "Optional secret to sign webhook callbacks" },
601
+ idempotency_key: { type: "string", description: "Optional key to make task creation safe to retry" },
602
+ },
603
+ required: ["title", "description", "digital_category", "reward_usd", "deadline", "proof_requirements"],
604
+ },
605
+ },
606
+ {
607
+ name: "dispatch_credential_task",
608
+ description: "Create a task requiring a human operator to deliver credentials (account login, API key, phone verification code, etc.) via an end-to-end encrypted channel. A unique ECDH keypair is generated automatically — only you can decrypt the result. The server never sees plaintext credentials. IMPORTANT: Save the returned private_key — you need it to call retrieve_credential later.",
609
+ inputSchema: {
610
+ type: "object",
611
+ properties: {
612
+ title: { type: "string", description: "Short title for the credential task" },
613
+ description: { type: "string", description: "Detailed description of what credentials are needed" },
614
+ digital_category: {
615
+ type: "string",
616
+ enum: ["ACCOUNT_CREATION", "API_KEY_PROCUREMENT", "PHONE_VERIFICATION", "SUBSCRIPTION_SETUP"],
617
+ description: "Category of credential task",
618
+ },
619
+ reward_usd: { type: "number", description: "Reward in USD for the operator" },
620
+ deadline: { type: "string", description: "ISO 8601 deadline for task completion" },
621
+ proof_requirements: {
622
+ type: "array",
623
+ items: { type: "string" },
624
+ description: "List of proof requirements (e.g., 'screenshot of account creation')",
625
+ },
626
+ digital_instructions: {
627
+ type: "string",
628
+ description: "Step-by-step instructions for the operator",
629
+ },
630
+ callback_url: { type: "string", description: "Optional webhook URL for status updates" },
631
+ callback_secret: { type: "string", description: "Optional secret to sign webhook callbacks" },
632
+ idempotency_key: { type: "string", description: "Optional key to make task creation safe to retry" },
633
+ },
634
+ required: ["title", "description", "digital_category", "reward_usd", "deadline", "proof_requirements"],
635
+ },
636
+ },
637
+ {
638
+ name: "retrieve_credential",
639
+ description: "Retrieve and decrypt the credential delivered by a human operator for a completed Tier 2 task. Requires the private_key returned by dispatch_credential_task. The decryption happens locally — the server never sees the plaintext.",
640
+ inputSchema: {
641
+ type: "object",
642
+ properties: {
643
+ task_id: { type: "string", description: "The task ID from dispatch_credential_task" },
644
+ private_key: { type: "string", description: "The base64-encoded private key returned by dispatch_credential_task" },
645
+ },
646
+ required: ["task_id", "private_key"],
647
+ },
648
+ },
649
+ {
650
+ name: "list_digital_categories",
651
+ description: "List available digital task categories with descriptions, pricing, and trust tier requirements. Returns both Tier 1 (standard digital) and Tier 2 (credential) categories.",
652
+ inputSchema: {
653
+ type: "object",
654
+ properties: {},
655
+ required: [],
656
+ },
657
+ },
658
+ {
659
+ name: "get_task_result",
660
+ description: "Get the current status, proof, and AI Guardian verification result for a task.",
661
+ inputSchema: {
662
+ type: "object",
663
+ properties: { task_id: { type: "string" } },
664
+ required: ["task_id"],
665
+ },
666
+ },
667
+ {
668
+ name: "check_verification_status",
669
+ description: "Check the AI Guardian verification status and result for a task. Returns focused verification details including decision and confidence.",
670
+ inputSchema: {
671
+ type: "object",
672
+ properties: { task_id: { type: "string" } },
673
+ required: ["task_id"],
674
+ },
675
+ },
676
+ {
677
+ name: "get_deposit_address",
678
+ description: "Get your USDC deposit address. Send USDC to this address on a supported chain (Base, Polygon, Ethereum, Arbitrum) to fund your HumanOps account. This is the recommended way to add funds.",
679
+ inputSchema: {
680
+ type: "object",
681
+ properties: {},
682
+ required: [],
683
+ },
684
+ },
685
+ {
686
+ 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.",
688
+ inputSchema: {
689
+ type: "object",
690
+ properties: {
691
+ amount_usd: { type: "number", description: "Amount in USD (min: $5, max: $10,000). For USDC deposits, this is the expected amount." },
692
+ payment_method: {
693
+ type: "string",
694
+ enum: ["usdc", "card", "bank_transfer"],
695
+ description: "Payment method. Defaults to 'usdc'. Card and bank_transfer are coming soon.",
696
+ default: "usdc",
697
+ },
698
+ tx_hash: { type: "string", description: "On-chain transaction hash (required for USDC deposits)" },
699
+ chain: {
700
+ type: "string",
701
+ enum: ["base", "polygon", "ethereum", "arbitrum"],
702
+ description: "Blockchain network the USDC was sent on (required for USDC deposits). Base recommended for lowest fees.",
703
+ },
704
+ return_url: { type: "string", description: "Optional return URL after fiat payment (not used for USDC)" },
705
+ },
706
+ required: ["amount_usd"],
707
+ },
708
+ },
709
+ {
710
+ name: "request_payout",
711
+ description: "Request a payout of available earnings. Currently only available through the HumanOps operator portal.",
712
+ inputSchema: {
713
+ type: "object",
714
+ properties: { amount_usd: { type: "number", description: "Amount to withdraw in USD (min: $10)" } },
715
+ required: ["amount_usd"],
716
+ },
717
+ },
718
+ {
719
+ name: "get_balance",
720
+ description: "Check your HumanOps account balances. Returns deposit balance (available to create tasks) and escrow balance (locked in active tasks).",
721
+ inputSchema: {
722
+ type: "object",
723
+ properties: {},
724
+ required: [],
725
+ },
726
+ },
727
+ {
728
+ name: "cancel_task",
729
+ description: "Cancel a pending or accepted task. The escrowed funds (reward + platform fee) will be refunded to your account.",
730
+ inputSchema: {
731
+ type: "object",
732
+ properties: { task_id: { type: "string", description: "The task ID to cancel" } },
733
+ required: ["task_id"],
734
+ },
735
+ },
736
+ {
737
+ name: "list_tasks",
738
+ description: "List your tasks with optional status filter. Returns paginated results ordered by creation date.",
739
+ inputSchema: {
740
+ type: "object",
741
+ properties: {
742
+ status: { type: "string", description: "Filter by status (e.g., PENDING, ACCEPTED, COMPLETED)" },
743
+ limit: { type: "number", description: "Max results (default: 20, max: 100)" },
744
+ offset: { type: "number", description: "Pagination offset (default: 0)" },
745
+ },
746
+ },
747
+ },
748
+ ],
749
+ }));
750
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
751
+ const { name, arguments: args } = request.params;
752
+ try {
753
+ switch (name) {
754
+ case "search_operators": {
755
+ const parsed = SearchOperatorsInputSchema.safeParse(args);
756
+ if (!parsed.success) {
757
+ return {
758
+ content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
759
+ isError: true,
760
+ };
761
+ }
762
+ const { lat, lng, radius_km, task_type, min_rating } = parsed.data;
763
+ const data = await apiRequest("/api/v1/operators/search", {
764
+ method: "POST",
765
+ body: JSON.stringify({
766
+ location: { lat, lng },
767
+ radius_km,
768
+ task_type,
769
+ min_rating,
770
+ }),
771
+ });
772
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
773
+ }
774
+ case "post_task": {
775
+ const parsed = PostTaskInputSchema.safeParse(args);
776
+ if (!parsed.success) {
777
+ return {
778
+ content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
779
+ isError: true,
780
+ };
781
+ }
782
+ const sandbox = process.env.HUMANOPS_SANDBOX === "true";
783
+ const headers = {};
784
+ if (sandbox) {
785
+ headers["X-Sandbox-Mode"] = "true";
786
+ }
787
+ const { idempotency_key, ...body } = parsed.data;
788
+ if (idempotency_key)
789
+ headers["Idempotency-Key"] = idempotency_key;
790
+ const result = await apiRequest("/api/v1/tasks", {
791
+ method: "POST",
792
+ body: JSON.stringify(body),
793
+ headers,
794
+ });
795
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
796
+ }
797
+ case "get_task_result": {
798
+ const parsed = GetTaskResultInputSchema.safeParse(args);
799
+ if (!parsed.success) {
800
+ return {
801
+ content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
802
+ isError: true,
803
+ };
804
+ }
805
+ const result = await apiRequest(`/api/v1/tasks/${encodeURIComponent(parsed.data.task_id)}`, {
806
+ method: "GET",
807
+ });
808
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
809
+ }
810
+ case "check_verification_status": {
811
+ const parsed = GetTaskResultInputSchema.safeParse(args);
812
+ if (!parsed.success) {
813
+ return {
814
+ content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
815
+ isError: true,
816
+ };
817
+ }
818
+ const task = await apiRequest(`/api/v1/tasks/${encodeURIComponent(parsed.data.task_id)}`, {
819
+ method: "GET",
820
+ });
821
+ const focused = {
822
+ task_id: task.task_id ?? parsed.data.task_id,
823
+ status: task.status,
824
+ guardian_result: task.guardian_result,
825
+ verified_at: task.verified_at ?? null,
826
+ completed_at: task.completed_at ?? null,
827
+ };
828
+ return { content: [{ type: "text", text: JSON.stringify(focused, null, 2) }] };
829
+ }
830
+ case "get_deposit_address": {
831
+ const result = await apiRequest("/api/v1/agents/deposit-address", {
832
+ method: "GET",
833
+ });
834
+ return {
835
+ content: [
836
+ {
837
+ type: "text",
838
+ text: JSON.stringify({
839
+ ...result,
840
+ _instructions: "Send USDC to this address on the specified chain. After sending, use fund_account with the tx_hash and chain to verify your deposit.",
841
+ }, null, 2),
842
+ },
843
+ ],
844
+ };
845
+ }
846
+ case "fund_account": {
847
+ const parsed = FundAccountInputSchema.safeParse(args);
848
+ if (!parsed.success) {
849
+ return {
850
+ content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
851
+ isError: true,
852
+ };
853
+ }
854
+ const { amount_usd, payment_method, tx_hash, chain, return_url } = parsed.data;
855
+ const method = payment_method ?? "usdc";
856
+ // Fiat methods are coming soon
857
+ if (method === "card" || method === "bank_transfer") {
858
+ return {
859
+ content: [
860
+ {
861
+ type: "text",
862
+ text: JSON.stringify({
863
+ status: "coming_soon",
864
+ payment_method: method,
865
+ amount_usd,
866
+ message: `Fiat deposits via ${method} are coming soon. Use USDC deposits instead — call get_deposit_address to get your deposit address, send USDC, then call fund_account with payment_method='usdc', tx_hash, and chain.`,
867
+ }, null, 2),
868
+ },
869
+ ],
870
+ };
871
+ }
872
+ // USDC deposit verification
873
+ if (!tx_hash) {
874
+ // No tx_hash — return deposit address instructions
875
+ const addressResult = await apiRequest("/api/v1/agents/deposit-address", {
876
+ method: "GET",
877
+ });
878
+ return {
879
+ content: [
880
+ {
881
+ type: "text",
882
+ text: JSON.stringify({
883
+ status: "awaiting_deposit",
884
+ ...addressResult,
885
+ message: "No tx_hash provided. Send USDC to the address above, then call fund_account again with the tx_hash and chain to verify your deposit.",
886
+ }, null, 2),
887
+ },
888
+ ],
889
+ };
890
+ }
891
+ const result = await apiRequest("/api/v1/agents/deposit/usdc", {
892
+ method: "POST",
893
+ body: JSON.stringify({
894
+ amount_usd,
895
+ tx_hash,
896
+ chain: chain ?? "base",
897
+ }),
898
+ });
899
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
900
+ }
901
+ case "request_payout": {
902
+ const parsed = RequestPayoutInputSchema.safeParse(args);
903
+ if (!parsed.success) {
904
+ return {
905
+ content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
906
+ isError: true,
907
+ };
908
+ }
909
+ return {
910
+ content: [
911
+ {
912
+ type: "text",
913
+ text: JSON.stringify({
914
+ status: "unavailable",
915
+ amount_requested: parsed.data.amount_usd,
916
+ message: "Payouts are currently only available through the HumanOps operator portal (https://humanops.io). MCP-based payouts will be available in a future release.",
917
+ }, null, 2),
918
+ },
919
+ ],
920
+ };
921
+ }
922
+ case "get_balance": {
923
+ const data = await apiRequest("/api/v1/agents/balance", { method: "GET" });
924
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
925
+ }
926
+ case "cancel_task": {
927
+ const parsed = GetTaskResultInputSchema.safeParse(args);
928
+ if (!parsed.success) {
929
+ return {
930
+ content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
931
+ isError: true,
932
+ };
933
+ }
934
+ const result = await apiRequest(`/api/v1/tasks/${encodeURIComponent(parsed.data.task_id)}/cancel`, {
935
+ method: "POST",
936
+ });
937
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
938
+ }
939
+ case "list_tasks": {
940
+ const params = new URLSearchParams();
941
+ if (args && typeof args === "object") {
942
+ if (args.status)
943
+ params.set("status", String(args.status));
944
+ if (args.limit)
945
+ params.set("limit", String(args.limit));
946
+ if (args.offset)
947
+ params.set("offset", String(args.offset));
948
+ }
949
+ const query = params.toString();
950
+ const data = await apiRequest(`/api/v1/tasks${query ? `?${query}` : ""}`, { method: "GET" });
951
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
952
+ }
953
+ case "dispatch_digital_task": {
954
+ const parsed = DispatchDigitalTaskInputSchema.safeParse(args);
955
+ if (!parsed.success) {
956
+ return {
957
+ content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
958
+ isError: true,
959
+ };
960
+ }
961
+ const { digital_category, digital_instructions, idempotency_key, ...rest } = parsed.data;
962
+ const categoryConfig = DIGITAL_CATEGORY_CONFIG[digital_category];
963
+ if (categoryConfig && rest.reward_usd > categoryConfig.maxValueUsd) {
964
+ return {
965
+ content: [
966
+ {
967
+ type: "text",
968
+ text: `Error: reward_usd ($${rest.reward_usd}) exceeds max for ${digital_category} ($${categoryConfig.maxValueUsd})`,
969
+ },
970
+ ],
971
+ isError: true,
972
+ };
973
+ }
974
+ const sandbox = process.env.HUMANOPS_SANDBOX === "true";
975
+ const headers = {};
976
+ if (sandbox) {
977
+ headers["X-Sandbox-Mode"] = "true";
978
+ }
979
+ if (idempotency_key)
980
+ headers["Idempotency-Key"] = idempotency_key;
981
+ const payload = {
982
+ ...rest,
983
+ task_type: digital_category,
984
+ task_domain: "DIGITAL",
985
+ location: { lat: 0, lng: 0, address: "Remote/Digital" },
986
+ ...(digital_instructions ? { digital_instructions } : {}),
987
+ };
988
+ const result = await apiRequest("/api/v1/tasks", {
989
+ method: "POST",
990
+ body: JSON.stringify(payload),
991
+ headers,
992
+ });
993
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
994
+ }
995
+ case "dispatch_credential_task": {
996
+ const parsed = DispatchCredentialTaskInputSchema.safeParse(args);
997
+ if (!parsed.success) {
998
+ return {
999
+ content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
1000
+ isError: true,
1001
+ };
1002
+ }
1003
+ const { digital_category, digital_instructions, idempotency_key, ...rest } = parsed.data;
1004
+ const credCategoryConfig = DIGITAL_CATEGORY_CONFIG[digital_category];
1005
+ if (credCategoryConfig && rest.reward_usd > credCategoryConfig.maxValueUsd) {
1006
+ return {
1007
+ content: [
1008
+ {
1009
+ type: "text",
1010
+ text: `Error: reward_usd ($${rest.reward_usd}) exceeds max for ${digital_category} ($${credCategoryConfig.maxValueUsd})`,
1011
+ },
1012
+ ],
1013
+ isError: true,
1014
+ };
1015
+ }
1016
+ // Generate ECDH keypair for E2EE credential delivery
1017
+ const keyPair = await generateKeyPair();
1018
+ const sandbox = process.env.HUMANOPS_SANDBOX === "true";
1019
+ const headers = {};
1020
+ if (sandbox) {
1021
+ headers["X-Sandbox-Mode"] = "true";
1022
+ }
1023
+ if (idempotency_key)
1024
+ headers["Idempotency-Key"] = idempotency_key;
1025
+ const payload = {
1026
+ ...rest,
1027
+ task_type: digital_category,
1028
+ task_domain: "DIGITAL",
1029
+ location: { lat: 0, lng: 0, address: "Remote/Digital" },
1030
+ agent_public_key: keyPair.publicKey,
1031
+ ...(digital_instructions ? { digital_instructions } : {}),
1032
+ };
1033
+ const result = await apiRequest("/api/v1/tasks", {
1034
+ method: "POST",
1035
+ body: JSON.stringify(payload),
1036
+ headers,
1037
+ });
1038
+ const response = {
1039
+ ...result,
1040
+ private_key: keyPair.privateKey,
1041
+ _notice: "IMPORTANT: Save the private_key securely. You need it to decrypt the credential using retrieve_credential.",
1042
+ };
1043
+ return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }] };
1044
+ }
1045
+ case "retrieve_credential": {
1046
+ const parsed = RetrieveCredentialInputSchema.safeParse(args);
1047
+ if (!parsed.success) {
1048
+ return {
1049
+ content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
1050
+ isError: true,
1051
+ };
1052
+ }
1053
+ const { task_id, private_key } = parsed.data;
1054
+ // Use the single-read POST endpoint (server clears credential after retrieval)
1055
+ let retrieveResult;
1056
+ try {
1057
+ retrieveResult = await apiRequest(`/api/v1/tasks/${encodeURIComponent(task_id)}/retrieve-credential`, {
1058
+ method: "POST",
1059
+ body: JSON.stringify({}),
1060
+ });
1061
+ }
1062
+ catch (retrieveErr) {
1063
+ // If no credential available, provide a helpful error
1064
+ if (retrieveErr.message?.includes("No encrypted credential")) {
1065
+ const task = await apiRequest(`/api/v1/tasks/${encodeURIComponent(task_id)}`, { method: "GET" });
1066
+ return {
1067
+ content: [
1068
+ {
1069
+ type: "text",
1070
+ text: JSON.stringify({
1071
+ task_id,
1072
+ status: task.status,
1073
+ error: "No encrypted credential available yet. The operator may not have submitted it, or the task is not in a completed state.",
1074
+ }, null, 2),
1075
+ },
1076
+ ],
1077
+ isError: true,
1078
+ };
1079
+ }
1080
+ throw retrieveErr;
1081
+ }
1082
+ const plaintext = await decryptCredential(retrieveResult.encrypted_credential, private_key);
1083
+ return {
1084
+ content: [
1085
+ {
1086
+ type: "text",
1087
+ text: JSON.stringify({
1088
+ task_id,
1089
+ credential: plaintext,
1090
+ retrieved_at: retrieveResult.retrieved_at,
1091
+ _notice: "This credential was decrypted locally. The HumanOps server never saw the plaintext. The encrypted credential has been cleared from the server (single-read).",
1092
+ }, null, 2),
1093
+ },
1094
+ ],
1095
+ };
1096
+ }
1097
+ case "list_digital_categories": {
1098
+ const categories = DIGITAL_CATEGORIES.map((cat) => {
1099
+ const config = DIGITAL_CATEGORY_CONFIG[cat];
1100
+ return {
1101
+ category: cat,
1102
+ tier: config.requiredTrustTier,
1103
+ description: config.description,
1104
+ maxValueUsd: config.maxValueUsd,
1105
+ defaultDeadlineMinutes: config.defaultDeadlineMinutes,
1106
+ requiredProofTypes: config.requiredProofTypes,
1107
+ isCredentialTask: CREDENTIAL_CATEGORIES.includes(cat),
1108
+ };
1109
+ });
1110
+ return { content: [{ type: "text", text: JSON.stringify(categories, null, 2) }] };
1111
+ }
1112
+ default:
1113
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
1114
+ }
1115
+ }
1116
+ catch (err) {
1117
+ const errorMessage = err.message || "Unknown error";
1118
+ const isNetworkError = errorMessage.includes("fetch failed") || errorMessage.includes("ECONNREFUSED");
1119
+ const hint = isNetworkError
1120
+ ? " (Network error — check your HUMANOPS_API_URL and HUMANOPS_API_KEY configuration.)"
1121
+ : "";
1122
+ return {
1123
+ content: [{ type: "text", text: `Error: ${errorMessage}${hint}` }],
1124
+ isError: true,
1125
+ };
1126
+ }
1127
+ });
1128
+ async function main() {
1129
+ const transport = new StdioServerTransport();
1130
+ await server.connect(transport);
1131
+ if (process.env.NODE_ENV !== "production") {
1132
+ console.error(`HumanOps MCP Server running on stdio (api=${getApiUrl()})`);
1133
+ }
1134
+ else {
1135
+ console.error("HumanOps MCP Server running on stdio");
1136
+ }
1137
+ }
1138
+ main().catch((err) => {
1139
+ console.error("Failed to start MCP server:", err);
1140
+ process.exit(1);
1141
+ });
1142
+ //# sourceMappingURL=index.js.map
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@humanops/mcp-server",
3
+ "version": "0.2.0",
4
+ "mcpName": "io.github.thepianistdirector/humanops",
5
+ "description": "MCP server for AI agents to dispatch real-world tasks to verified human operators via HumanOps",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "bin": {
9
+ "humanops-mcp": "./dist/index.js"
10
+ },
11
+ "files": ["dist/*.js", "dist/*.d.ts"],
12
+ "scripts": {
13
+ "start": "node dist/index.js",
14
+ "dev": "tsx watch src/index.ts",
15
+ "build": "tsc",
16
+ "prepublishOnly": "npm run build",
17
+ "lint": "tsc --noEmit"
18
+ },
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.0.0",
21
+ "zod": "^3.24.0"
22
+ },
23
+ "devDependencies": {
24
+ "tsx": "^4.19.0",
25
+ "typescript": "^5.7.0"
26
+ },
27
+ "license": "MIT",
28
+ "keywords": ["humanops", "mcp", "ai", "agents", "tasks", "model-context-protocol"],
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/ThePianistDirector/real-auto-code"
32
+ }
33
+ }