@humanops/mcp-server 0.3.1 → 0.3.3

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.
Files changed (3) hide show
  1. package/README.md +10 -2
  2. package/dist/index.js +400 -17
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -24,7 +24,7 @@ HumanOps bridges the gap between AI capabilities and physical-world actions. Whe
24
24
 
25
25
  | Variable | Required | Description |
26
26
  |----------|----------|-------------|
27
- | `HUMANOPS_API_KEY` | Yes | Your HumanOps API key |
27
+ | `HUMANOPS_API_KEY` | Yes* | Your HumanOps API key. Not required to run the unauthenticated `register_agent` tool, but required for all other tools. |
28
28
  | `HUMANOPS_API_URL` | No | API base URL (default: `https://api.humanops.io`) |
29
29
  | `HUMANOPS_DEV_ALLOW_LOCALHOST` | No | Set to `true` to allow `HUMANOPS_API_URL` to be `http://localhost:8787` for local development |
30
30
  | `HUMANOPS_ALLOW_ANY_API_HOST` | No | Set to `true` to allow non-`*.humanops.io` API hosts (still blocks private/internal hosts) |
@@ -32,7 +32,15 @@ HumanOps bridges the gap between AI capabilities and physical-world actions. Whe
32
32
 
33
33
  ## Prerequisites
34
34
 
35
- Before using the MCP server, register your agent via the REST API:
35
+ Register your agent (get an API key) using one of the following:
36
+
37
+ ### Option A: Register via MCP (Recommended)
38
+
39
+ Call the `register_agent` tool first (no `HUMANOPS_API_KEY` required). It returns an `api_key`.
40
+
41
+ Then set `HUMANOPS_API_KEY` to that key (restart the MCP server) to use authenticated tools.
42
+
43
+ ### Option B: Register via REST API
36
44
 
37
45
  ```bash
38
46
  curl -X POST https://api.humanops.io/api/v1/agents/register \
package/dist/index.js CHANGED
@@ -395,23 +395,30 @@ function getApiUrl() {
395
395
  }
396
396
  return normalized;
397
397
  }
398
- function getApiKey() {
398
+ function getApiKeyOptional() {
399
399
  const apiKey = process.env.HUMANOPS_API_KEY?.trim();
400
- if (!apiKey) {
401
- throw new Error("HUMANOPS_API_KEY environment variable is required.");
402
- }
400
+ if (!apiKey)
401
+ return null;
403
402
  if (!/^ho_(live|test)_[a-zA-Z0-9]{32,}$/.test(apiKey)) {
404
403
  console.error("Warning: HUMANOPS_API_KEY does not match expected format (ho_live_... or ho_test_...)");
405
404
  }
406
405
  return apiKey;
407
406
  }
407
+ function getApiKeyRequired() {
408
+ const apiKey = getApiKeyOptional();
409
+ if (!apiKey) {
410
+ throw new Error("HUMANOPS_API_KEY environment variable is required for this tool.");
411
+ }
412
+ return apiKey;
413
+ }
408
414
  const MCP_REQUEST_TIMEOUT = 30_000;
409
- async function apiRequest(path, init = {}) {
415
+ async function apiRequest(path, init = {}, opts) {
410
416
  const baseUrl = getApiUrl();
411
- const apiKey = getApiKey();
412
417
  const url = `${baseUrl}${path}`;
413
418
  const headers = new Headers(init.headers);
414
- headers.set("X-API-Key", apiKey);
419
+ if (opts?.auth !== false) {
420
+ headers.set("X-API-Key", getApiKeyRequired());
421
+ }
415
422
  if (!headers.has("Content-Type") && init.body && typeof init.body === "string") {
416
423
  headers.set("Content-Type", "application/json");
417
424
  }
@@ -515,7 +522,10 @@ const PostTaskInputSchema = z.object({
515
522
  lng: z.number(),
516
523
  address: z.string().min(1),
517
524
  }),
518
- reward_usd: z.number().min(MIN_TASK_VALUE_USD).max(MAX_TASK_VALUE_USD),
525
+ urgency: z.enum(["standard", "priority", "urgent"]).optional(),
526
+ reward_usd: z.number().min(MIN_TASK_VALUE_USD).max(MAX_TASK_VALUE_USD).optional(),
527
+ max_budget_usd: z.number().min(MIN_TASK_VALUE_USD).max(MAX_TASK_VALUE_USD).optional(),
528
+ proposal_deadline_minutes: z.number().int().min(15).max(10080).optional(),
519
529
  deadline: z.string().datetime(),
520
530
  proof_requirements: z.array(z.string().min(1).max(500)).min(1).max(10),
521
531
  task_type: TaskTypeEnum,
@@ -528,10 +538,37 @@ const PostTaskInputSchema = z.object({
528
538
  .optional(),
529
539
  callback_secret: z.string().min(16).max(128).optional(),
530
540
  idempotency_key: z.string().min(1).max(200).optional(),
541
+ }).refine((d) => (d.reward_usd !== undefined || d.max_budget_usd !== undefined) &&
542
+ !(d.reward_usd !== undefined && d.max_budget_usd !== undefined), {
543
+ message: "Provide either reward_usd (fixed price) or max_budget_usd (proposal mode), not both",
544
+ path: ["reward_usd"],
531
545
  });
532
546
  const GetTaskResultInputSchema = z.object({
533
547
  task_id: z.string().min(1),
534
548
  });
549
+ const SubmitReviewInputSchema = z.object({
550
+ task_id: z.string().min(1),
551
+ rating: z.number().int().min(1).max(10),
552
+ comment: z.string().min(1).max(1000).optional(),
553
+ });
554
+ const GetOperatorReviewsInputSchema = z.object({
555
+ operator_id: z.string().min(1),
556
+ });
557
+ const ListProposalsInputSchema = z.object({
558
+ task_id: z.string().min(1),
559
+ });
560
+ const AwardProposalToolInputSchema = z.object({
561
+ task_id: z.string().min(1),
562
+ proposal_id: z.string().min(1),
563
+ });
564
+ const RejectProposalToolInputSchema = z.object({
565
+ task_id: z.string().min(1),
566
+ proposal_id: z.string().min(1),
567
+ reason: z.string().max(200).optional(),
568
+ });
569
+ const CancelProposalWindowInputSchema = z.object({
570
+ task_id: z.string().min(1),
571
+ });
535
572
  const FundAccountInputSchema = z.object({
536
573
  amount_usd: z.number().min(MIN_DEPOSIT_USD).max(MAX_DEPOSIT_USD),
537
574
  payment_method: z.enum(["usdc", "card", "bank_transfer"]).default("usdc"),
@@ -545,6 +582,20 @@ const FundAccountInputSchema = z.object({
545
582
  })
546
583
  .optional(),
547
584
  });
585
+ const GetWalletChallengeInputSchema = z.object({
586
+ chain: z.enum(["base"]).optional(),
587
+ });
588
+ const BindWalletInputSchema = z.object({
589
+ wallet_address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, "Invalid wallet_address (must be a 0x-prefixed address)"),
590
+ nonce: z.string().min(1).max(200),
591
+ signature: z.string().min(10).max(5000),
592
+ chain: z.enum(["base"]).optional(),
593
+ });
594
+ const RegisterAgentInputSchema = z.object({
595
+ name: z.string().min(1).max(200),
596
+ email: z.string().email(),
597
+ company: z.string().min(1).max(200).optional(),
598
+ });
548
599
  const RequestPayoutInputSchema = z.object({
549
600
  amount_usd: z.number().min(MIN_PAYOUT_USD),
550
601
  });
@@ -591,7 +642,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
591
642
  },
592
643
  {
593
644
  name: "post_task",
594
- 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).",
645
+ 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. Provide either reward_usd (fixed price; escrowed at creation) OR max_budget_usd (proposal mode; escrow created when you award a proposal).",
595
646
  inputSchema: {
596
647
  type: "object",
597
648
  properties: {
@@ -602,7 +653,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
602
653
  properties: { lat: { type: "number" }, lng: { type: "number" }, address: { type: "string" } },
603
654
  required: ["lat", "lng", "address"],
604
655
  },
605
- reward_usd: { type: "number" },
656
+ urgency: {
657
+ type: "string",
658
+ enum: ["standard", "priority", "urgent"],
659
+ description: "Urgency level (standard=24h pickup, priority=4h pickup, urgent=1h pickup + 4h completion SLA).",
660
+ },
661
+ reward_usd: { type: "number", description: "Fixed-price mode: reward in USD" },
662
+ max_budget_usd: { type: "number", description: "Proposal mode: maximum budget in USD (alternative to reward_usd)" },
663
+ proposal_deadline_minutes: { type: "number", description: "Proposal window length in minutes (default: 120)" },
606
664
  deadline: { type: "string", description: "ISO 8601 deadline for task completion" },
607
665
  proof_requirements: { type: "array", items: { type: "string" } },
608
666
  task_type: {
@@ -613,7 +671,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
613
671
  callback_secret: { type: "string", description: "Optional secret to sign webhook callbacks" },
614
672
  idempotency_key: { type: "string", description: "Optional key to make task creation safe to retry" },
615
673
  },
616
- required: ["title", "description", "location", "reward_usd", "deadline", "proof_requirements", "task_type"],
674
+ required: ["title", "description", "location", "deadline", "proof_requirements", "task_type"],
617
675
  },
618
676
  },
619
677
  {
@@ -717,6 +775,52 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
717
775
  required: ["task_id"],
718
776
  },
719
777
  },
778
+ {
779
+ name: "submit_review",
780
+ description: "Submit a 1-10 rating (and optional comment) for the operator on a COMPLETED task. One review per task. Comments are screened for safety.",
781
+ inputSchema: {
782
+ type: "object",
783
+ properties: {
784
+ task_id: { type: "string", description: "The completed task ID" },
785
+ rating: { type: "number", description: "Integer rating 1-10" },
786
+ comment: { type: "string", description: "Optional comment (max 1000 chars)" },
787
+ },
788
+ required: ["task_id", "rating"],
789
+ },
790
+ },
791
+ {
792
+ name: "get_operator_reviews",
793
+ description: "Get an operator's approved reviews and aggregate rating (agent-facing). Useful for building trust signals and choosing operators.",
794
+ inputSchema: {
795
+ type: "object",
796
+ properties: {
797
+ operator_id: { type: "string", description: "Operator ID" },
798
+ },
799
+ required: ["operator_id"],
800
+ },
801
+ },
802
+ {
803
+ name: "register_agent",
804
+ description: "Register a new HumanOps agent account. Returns an API key for authentication. You start in SANDBOX tier (tasks auto-complete with simulated operators). Verify your email to upgrade to VERIFIED tier.",
805
+ inputSchema: {
806
+ type: "object",
807
+ properties: {
808
+ name: { type: "string", description: "Agent name" },
809
+ email: { type: "string", description: "Agent email (must be unique)" },
810
+ company: { type: "string", description: "Optional company name" },
811
+ },
812
+ required: ["name", "email"],
813
+ },
814
+ },
815
+ {
816
+ name: "get_agent_status",
817
+ description: "Check your current agent status including tier, email verification, wallet binding, and balances. Also returns clear upgrade steps.",
818
+ inputSchema: {
819
+ type: "object",
820
+ properties: {},
821
+ required: [],
822
+ },
823
+ },
720
824
  {
721
825
  name: "get_deposit_address",
722
826
  description: "Get your USDC deposit address. Send USDC on Base L2 to fund your HumanOps account. This is the recommended way to add funds.",
@@ -726,6 +830,31 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
726
830
  required: [],
727
831
  },
728
832
  },
833
+ {
834
+ name: "get_wallet_challenge",
835
+ description: "Get a challenge message to sign with your EVM wallet. Sign this message to prove wallet ownership before depositing USDC. In production, wallet binding is required before deposits can be confirmed.",
836
+ inputSchema: {
837
+ type: "object",
838
+ properties: {
839
+ chain: { type: "string", enum: ["base"], description: "Blockchain network (default: base)" },
840
+ },
841
+ required: [],
842
+ },
843
+ },
844
+ {
845
+ name: "bind_wallet",
846
+ description: "Bind your EVM wallet after signing the challenge message. This verifies wallet ownership. Required once before confirming USDC deposits in production.",
847
+ inputSchema: {
848
+ type: "object",
849
+ properties: {
850
+ wallet_address: { type: "string", description: "0x-prefixed EVM address (sender wallet)" },
851
+ nonce: { type: "string", description: "Nonce from get_wallet_challenge" },
852
+ signature: { type: "string", description: "Signature over the challenge message (personal_sign)" },
853
+ chain: { type: "string", enum: ["base"], description: "Blockchain network (default: base)" },
854
+ },
855
+ required: ["wallet_address", "nonce", "signature"],
856
+ },
857
+ },
729
858
  {
730
859
  name: "fund_account",
731
860
  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.",
@@ -752,7 +881,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
752
881
  },
753
882
  {
754
883
  name: "request_payout",
755
- description: "Request a payout of available earnings. Operators can withdraw via USDC on Base L2 through the operator portal or API.",
884
+ description: "Operator-only: agents cannot request payouts. Use this tool to understand payout options (withdrawals happen in the operator portal / operator API).",
756
885
  inputSchema: {
757
886
  type: "object",
758
887
  properties: { amount_usd: { type: "number", description: "Amount to withdraw in USD (min: $10)" } },
@@ -777,6 +906,49 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
777
906
  required: ["task_id"],
778
907
  },
779
908
  },
909
+ {
910
+ name: "list_proposals",
911
+ description: "List proposals for a proposal-mode task. Use this after creating a task with max_budget_usd.",
912
+ inputSchema: {
913
+ type: "object",
914
+ properties: { task_id: { type: "string", description: "The task ID" } },
915
+ required: ["task_id"],
916
+ },
917
+ },
918
+ {
919
+ name: "award_proposal",
920
+ description: "Award a proposal for a proposal-mode task. This creates escrow at award time and assigns the operator.",
921
+ inputSchema: {
922
+ type: "object",
923
+ properties: {
924
+ task_id: { type: "string", description: "The task ID" },
925
+ proposal_id: { type: "string", description: "The proposal ID to award" },
926
+ },
927
+ required: ["task_id", "proposal_id"],
928
+ },
929
+ },
930
+ {
931
+ name: "reject_proposal",
932
+ description: "Reject a proposal for a proposal-mode task (optional reason).",
933
+ inputSchema: {
934
+ type: "object",
935
+ properties: {
936
+ task_id: { type: "string", description: "The task ID" },
937
+ proposal_id: { type: "string", description: "The proposal ID to reject" },
938
+ reason: { type: "string", description: "Optional rejection reason" },
939
+ },
940
+ required: ["task_id", "proposal_id"],
941
+ },
942
+ },
943
+ {
944
+ name: "cancel_proposal_window",
945
+ description: "Close the proposal window by cancelling the task. This is useful when you no longer want proposals for a task in proposal mode.",
946
+ inputSchema: {
947
+ type: "object",
948
+ properties: { task_id: { type: "string", description: "The task ID to cancel" } },
949
+ required: ["task_id"],
950
+ },
951
+ },
780
952
  {
781
953
  name: "approve_estimate",
782
954
  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.",
@@ -895,6 +1067,60 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
895
1067
  }
896
1068
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
897
1069
  }
1070
+ case "register_agent": {
1071
+ const parsed = RegisterAgentInputSchema.safeParse(args);
1072
+ if (!parsed.success) {
1073
+ return {
1074
+ content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
1075
+ isError: true,
1076
+ };
1077
+ }
1078
+ try {
1079
+ const result = await apiRequest("/api/v1/agents/register", {
1080
+ method: "POST",
1081
+ body: JSON.stringify({
1082
+ name: parsed.data.name,
1083
+ email: parsed.data.email,
1084
+ ...(parsed.data.company ? { company: parsed.data.company } : {}),
1085
+ }),
1086
+ }, { auth: false });
1087
+ const apiKey = result && typeof result.api_key === "string" ? result.api_key : null;
1088
+ const banner = apiKey
1089
+ ? `CRITICAL — SAVE THIS API KEY:\nHUMANOPS_API_KEY=${apiKey}\n\nRestart your MCP server with this environment variable to use authenticated tools.`
1090
+ : "Registration succeeded.";
1091
+ return {
1092
+ content: [{ type: "text", text: `${banner}\n\n${JSON.stringify(result, null, 2)}` }],
1093
+ };
1094
+ }
1095
+ catch (err) {
1096
+ return {
1097
+ content: [
1098
+ {
1099
+ type: "text",
1100
+ text: `Error: ${err instanceof Error ? err.message : String(err)}\n\nTip: If you already registered with this email, use a different email address.`,
1101
+ },
1102
+ ],
1103
+ isError: true,
1104
+ };
1105
+ }
1106
+ }
1107
+ case "get_agent_status": {
1108
+ try {
1109
+ const result = await apiRequest("/api/v1/agents/status", { method: "GET" });
1110
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1111
+ }
1112
+ catch (err) {
1113
+ return {
1114
+ content: [
1115
+ {
1116
+ type: "text",
1117
+ text: `Error: ${err instanceof Error ? err.message : String(err)}\n\nIf you haven't registered yet, call register_agent first. Otherwise, set HUMANOPS_API_KEY and restart the MCP server.`,
1118
+ },
1119
+ ],
1120
+ isError: true,
1121
+ };
1122
+ }
1123
+ }
898
1124
  case "check_verification_status": {
899
1125
  const parsed = GetTaskResultInputSchema.safeParse(args);
900
1126
  if (!parsed.success) {
@@ -916,17 +1142,112 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
916
1142
  };
917
1143
  return { content: [{ type: "text", text: JSON.stringify(focused, null, 2) }] };
918
1144
  }
1145
+ case "submit_review": {
1146
+ const parsed = SubmitReviewInputSchema.safeParse(args);
1147
+ if (!parsed.success) {
1148
+ return {
1149
+ content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
1150
+ isError: true,
1151
+ };
1152
+ }
1153
+ const { task_id, rating, comment } = parsed.data;
1154
+ const result = await apiRequest(`/api/v1/tasks/${encodeURIComponent(task_id)}/review`, {
1155
+ method: "POST",
1156
+ body: JSON.stringify({ rating, ...(comment ? { comment } : {}) }),
1157
+ });
1158
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1159
+ }
1160
+ case "get_operator_reviews": {
1161
+ const parsed = GetOperatorReviewsInputSchema.safeParse(args);
1162
+ if (!parsed.success) {
1163
+ return {
1164
+ content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
1165
+ isError: true,
1166
+ };
1167
+ }
1168
+ const result = await apiRequest(`/api/v1/operators/${encodeURIComponent(parsed.data.operator_id)}/reviews`, {
1169
+ method: "GET",
1170
+ });
1171
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1172
+ }
919
1173
  case "get_deposit_address": {
920
1174
  const result = await apiRequest("/api/v1/agents/deposit-address", {
921
1175
  method: "GET",
922
1176
  });
1177
+ const obj = result;
1178
+ const walletVerified = Boolean(obj.wallet_verified);
1179
+ const walletAddress = typeof obj.wallet_address === "string" ? obj.wallet_address : null;
1180
+ const walletBound = walletVerified || Boolean(walletAddress);
1181
+ const walletBindingRequired = Boolean(obj.wallet_verification?.required);
1182
+ const instructions = walletBindingRequired
1183
+ ? (walletBound
1184
+ ? "Send USDC to this address, then call fund_account with payment_method='usdc', tx_hash, and chain to confirm the deposit."
1185
+ : "STEP 1: Call get_wallet_challenge to get a signing message. STEP 2: Sign the message with your wallet (personal_sign). STEP 3: Call bind_wallet with wallet_address + nonce + signature. STEP 4: Send USDC. STEP 5: Call fund_account with tx_hash + chain.")
1186
+ : (walletBound
1187
+ ? "Send USDC to this address, then call fund_account with tx_hash + chain to confirm the deposit."
1188
+ : "Wallet binding is optional in dev/test. For production parity, bind your wallet first: get_wallet_challenge → bind_wallet. Then send USDC and confirm via fund_account with tx_hash + chain.");
1189
+ return {
1190
+ content: [
1191
+ {
1192
+ type: "text",
1193
+ text: JSON.stringify({
1194
+ ...obj,
1195
+ wallet_bound: walletBound,
1196
+ wallet_binding_required: walletBindingRequired,
1197
+ _instructions: instructions,
1198
+ }, null, 2),
1199
+ },
1200
+ ],
1201
+ };
1202
+ }
1203
+ case "get_wallet_challenge": {
1204
+ const parsed = GetWalletChallengeInputSchema.safeParse(args ?? {});
1205
+ if (!parsed.success) {
1206
+ return {
1207
+ content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
1208
+ isError: true,
1209
+ };
1210
+ }
1211
+ const result = await apiRequest("/api/v1/agents/wallet/challenge", {
1212
+ method: "POST",
1213
+ body: JSON.stringify({ chain: parsed.data.chain ?? "base" }),
1214
+ });
923
1215
  return {
924
1216
  content: [
925
1217
  {
926
1218
  type: "text",
927
1219
  text: JSON.stringify({
928
1220
  ...result,
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.",
1221
+ _instructions: "Sign the returned message with your wallet using personal_sign. Then call bind_wallet with wallet_address + nonce + signature (and chain if needed).",
1222
+ }, null, 2),
1223
+ },
1224
+ ],
1225
+ };
1226
+ }
1227
+ case "bind_wallet": {
1228
+ const parsed = BindWalletInputSchema.safeParse(args);
1229
+ if (!parsed.success) {
1230
+ return {
1231
+ content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
1232
+ isError: true,
1233
+ };
1234
+ }
1235
+ const result = await apiRequest("/api/v1/agents/wallet", {
1236
+ method: "PUT",
1237
+ body: JSON.stringify({
1238
+ wallet_address: parsed.data.wallet_address,
1239
+ nonce: parsed.data.nonce,
1240
+ signature: parsed.data.signature,
1241
+ chain: parsed.data.chain ?? "base",
1242
+ }),
1243
+ });
1244
+ return {
1245
+ content: [
1246
+ {
1247
+ type: "text",
1248
+ text: JSON.stringify({
1249
+ ...result,
1250
+ _instructions: "Wallet bound. You can now deposit USDC: call get_deposit_address, send USDC, then call fund_account with tx_hash + chain.",
930
1251
  }, null, 2),
931
1252
  },
932
1253
  ],
@@ -955,6 +1276,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
955
1276
  }, null, 2),
956
1277
  },
957
1278
  ],
1279
+ isError: true,
958
1280
  };
959
1281
  }
960
1282
  // USDC deposit verification
@@ -970,7 +1292,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
970
1292
  text: JSON.stringify({
971
1293
  status: "awaiting_deposit",
972
1294
  ...addressResult,
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.",
1295
+ message: "No tx_hash provided. Send USDC to the address above. If the deposit response indicates wallet binding is required, bind your wallet first (get_wallet_challenge bind_wallet). Then call fund_account again with tx_hash + chain to verify your deposit.",
974
1296
  }, null, 2),
975
1297
  },
976
1298
  ],
@@ -1005,6 +1327,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1005
1327
  }, null, 2),
1006
1328
  },
1007
1329
  ],
1330
+ isError: true,
1008
1331
  };
1009
1332
  }
1010
1333
  case "get_balance": {
@@ -1037,6 +1360,61 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1037
1360
  });
1038
1361
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1039
1362
  }
1363
+ case "list_proposals": {
1364
+ const parsed = ListProposalsInputSchema.safeParse(args);
1365
+ if (!parsed.success) {
1366
+ return {
1367
+ content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
1368
+ isError: true,
1369
+ };
1370
+ }
1371
+ const data = await apiRequest(`/api/v1/tasks/${encodeURIComponent(parsed.data.task_id)}/proposals`, {
1372
+ method: "GET",
1373
+ });
1374
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1375
+ }
1376
+ case "award_proposal": {
1377
+ const parsed = AwardProposalToolInputSchema.safeParse(args);
1378
+ if (!parsed.success) {
1379
+ return {
1380
+ content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
1381
+ isError: true,
1382
+ };
1383
+ }
1384
+ const result = await apiRequest(`/api/v1/tasks/${encodeURIComponent(parsed.data.task_id)}/award-proposal`, {
1385
+ method: "POST",
1386
+ body: JSON.stringify({ proposal_id: parsed.data.proposal_id }),
1387
+ });
1388
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1389
+ }
1390
+ case "reject_proposal": {
1391
+ const parsed = RejectProposalToolInputSchema.safeParse(args);
1392
+ if (!parsed.success) {
1393
+ return {
1394
+ content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
1395
+ isError: true,
1396
+ };
1397
+ }
1398
+ const body = parsed.data.reason ? { reason: parsed.data.reason } : {};
1399
+ const result = await apiRequest(`/api/v1/tasks/${encodeURIComponent(parsed.data.task_id)}/proposals/${encodeURIComponent(parsed.data.proposal_id)}/reject`, {
1400
+ method: "POST",
1401
+ body: JSON.stringify(body),
1402
+ });
1403
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1404
+ }
1405
+ case "cancel_proposal_window": {
1406
+ const parsed = CancelProposalWindowInputSchema.safeParse(args);
1407
+ if (!parsed.success) {
1408
+ return {
1409
+ content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
1410
+ isError: true,
1411
+ };
1412
+ }
1413
+ const result = await apiRequest(`/api/v1/tasks/${encodeURIComponent(parsed.data.task_id)}/cancel`, {
1414
+ method: "POST",
1415
+ });
1416
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1417
+ }
1040
1418
  case "approve_estimate": {
1041
1419
  const parsed = GetTaskResultInputSchema.safeParse(args);
1042
1420
  if (!parsed.success) {
@@ -1186,17 +1564,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1186
1564
  const response = {
1187
1565
  ...result,
1188
1566
  private_key: keyPair.privateKey,
1189
- _notice: "IMPORTANT: Save the private_key securely. You need it to decrypt the credential using retrieve_credential.",
1567
+ _notice: "Save the private_key securely. It is required to decrypt the credential using retrieve_credential. If lost, the credential is unrecoverable (single-read).",
1190
1568
  };
1569
+ const warningText = `CRITICAL — SAVE THIS PRIVATE KEY:\n` +
1570
+ `private_key: ${keyPair.privateKey}\n\n` +
1571
+ `You MUST store this key securely. It is required to decrypt the credential\n` +
1572
+ `via retrieve_credential. If lost, the credential is unrecoverable (single-read).\n\n` +
1573
+ `${JSON.stringify(response, null, 2)}`;
1191
1574
  const credResultObj = result;
1192
1575
  if (credResultObj.sandbox) {
1193
1576
  return {
1194
1577
  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)}` },
1578
+ { 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${warningText}` },
1196
1579
  ],
1197
1580
  };
1198
1581
  }
1199
- return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }] };
1582
+ return { content: [{ type: "text", text: warningText }] };
1200
1583
  }
1201
1584
  case "retrieve_credential": {
1202
1585
  const parsed = RetrieveCredentialInputSchema.safeParse(args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanops/mcp-server",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
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",