@humanops/mcp-server 0.3.1 → 0.3.2

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 +209 -13
  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
  }
@@ -545,6 +552,20 @@ const FundAccountInputSchema = z.object({
545
552
  })
546
553
  .optional(),
547
554
  });
555
+ const GetWalletChallengeInputSchema = z.object({
556
+ chain: z.enum(["base"]).optional(),
557
+ });
558
+ const BindWalletInputSchema = z.object({
559
+ wallet_address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, "Invalid wallet_address (must be a 0x-prefixed address)"),
560
+ nonce: z.string().min(1).max(200),
561
+ signature: z.string().min(10).max(5000),
562
+ chain: z.enum(["base"]).optional(),
563
+ });
564
+ const RegisterAgentInputSchema = z.object({
565
+ name: z.string().min(1).max(200),
566
+ email: z.string().email(),
567
+ company: z.string().min(1).max(200).optional(),
568
+ });
548
569
  const RequestPayoutInputSchema = z.object({
549
570
  amount_usd: z.number().min(MIN_PAYOUT_USD),
550
571
  });
@@ -717,6 +738,28 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
717
738
  required: ["task_id"],
718
739
  },
719
740
  },
741
+ {
742
+ name: "register_agent",
743
+ 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.",
744
+ inputSchema: {
745
+ type: "object",
746
+ properties: {
747
+ name: { type: "string", description: "Agent name" },
748
+ email: { type: "string", description: "Agent email (must be unique)" },
749
+ company: { type: "string", description: "Optional company name" },
750
+ },
751
+ required: ["name", "email"],
752
+ },
753
+ },
754
+ {
755
+ name: "get_agent_status",
756
+ description: "Check your current agent status including tier, email verification, wallet binding, and balances. Also returns clear upgrade steps.",
757
+ inputSchema: {
758
+ type: "object",
759
+ properties: {},
760
+ required: [],
761
+ },
762
+ },
720
763
  {
721
764
  name: "get_deposit_address",
722
765
  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 +769,31 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
726
769
  required: [],
727
770
  },
728
771
  },
772
+ {
773
+ name: "get_wallet_challenge",
774
+ 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.",
775
+ inputSchema: {
776
+ type: "object",
777
+ properties: {
778
+ chain: { type: "string", enum: ["base"], description: "Blockchain network (default: base)" },
779
+ },
780
+ required: [],
781
+ },
782
+ },
783
+ {
784
+ name: "bind_wallet",
785
+ description: "Bind your EVM wallet after signing the challenge message. This verifies wallet ownership. Required once before confirming USDC deposits in production.",
786
+ inputSchema: {
787
+ type: "object",
788
+ properties: {
789
+ wallet_address: { type: "string", description: "0x-prefixed EVM address (sender wallet)" },
790
+ nonce: { type: "string", description: "Nonce from get_wallet_challenge" },
791
+ signature: { type: "string", description: "Signature over the challenge message (personal_sign)" },
792
+ chain: { type: "string", enum: ["base"], description: "Blockchain network (default: base)" },
793
+ },
794
+ required: ["wallet_address", "nonce", "signature"],
795
+ },
796
+ },
729
797
  {
730
798
  name: "fund_account",
731
799
  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 +820,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
752
820
  },
753
821
  {
754
822
  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.",
823
+ description: "Operator-only: agents cannot request payouts. Use this tool to understand payout options (withdrawals happen in the operator portal / operator API).",
756
824
  inputSchema: {
757
825
  type: "object",
758
826
  properties: { amount_usd: { type: "number", description: "Amount to withdraw in USD (min: $10)" } },
@@ -895,6 +963,60 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
895
963
  }
896
964
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
897
965
  }
966
+ case "register_agent": {
967
+ const parsed = RegisterAgentInputSchema.safeParse(args);
968
+ if (!parsed.success) {
969
+ return {
970
+ content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
971
+ isError: true,
972
+ };
973
+ }
974
+ try {
975
+ const result = await apiRequest("/api/v1/agents/register", {
976
+ method: "POST",
977
+ body: JSON.stringify({
978
+ name: parsed.data.name,
979
+ email: parsed.data.email,
980
+ ...(parsed.data.company ? { company: parsed.data.company } : {}),
981
+ }),
982
+ }, { auth: false });
983
+ const apiKey = result && typeof result.api_key === "string" ? result.api_key : null;
984
+ const banner = apiKey
985
+ ? `CRITICAL — SAVE THIS API KEY:\nHUMANOPS_API_KEY=${apiKey}\n\nRestart your MCP server with this environment variable to use authenticated tools.`
986
+ : "Registration succeeded.";
987
+ return {
988
+ content: [{ type: "text", text: `${banner}\n\n${JSON.stringify(result, null, 2)}` }],
989
+ };
990
+ }
991
+ catch (err) {
992
+ return {
993
+ content: [
994
+ {
995
+ type: "text",
996
+ text: `Error: ${err instanceof Error ? err.message : String(err)}\n\nTip: If you already registered with this email, use a different email address.`,
997
+ },
998
+ ],
999
+ isError: true,
1000
+ };
1001
+ }
1002
+ }
1003
+ case "get_agent_status": {
1004
+ try {
1005
+ const result = await apiRequest("/api/v1/agents/status", { method: "GET" });
1006
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1007
+ }
1008
+ catch (err) {
1009
+ return {
1010
+ content: [
1011
+ {
1012
+ type: "text",
1013
+ 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.`,
1014
+ },
1015
+ ],
1016
+ isError: true,
1017
+ };
1018
+ }
1019
+ }
898
1020
  case "check_verification_status": {
899
1021
  const parsed = GetTaskResultInputSchema.safeParse(args);
900
1022
  if (!parsed.success) {
@@ -920,13 +1042,80 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
920
1042
  const result = await apiRequest("/api/v1/agents/deposit-address", {
921
1043
  method: "GET",
922
1044
  });
1045
+ const obj = result;
1046
+ const walletVerified = Boolean(obj.wallet_verified);
1047
+ const walletAddress = typeof obj.wallet_address === "string" ? obj.wallet_address : null;
1048
+ const walletBound = walletVerified || Boolean(walletAddress);
1049
+ const walletBindingRequired = Boolean(obj.wallet_verification?.required);
1050
+ const instructions = walletBindingRequired
1051
+ ? (walletBound
1052
+ ? "Send USDC to this address, then call fund_account with payment_method='usdc', tx_hash, and chain to confirm the deposit."
1053
+ : "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.")
1054
+ : (walletBound
1055
+ ? "Send USDC to this address, then call fund_account with tx_hash + chain to confirm the deposit."
1056
+ : "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.");
1057
+ return {
1058
+ content: [
1059
+ {
1060
+ type: "text",
1061
+ text: JSON.stringify({
1062
+ ...obj,
1063
+ wallet_bound: walletBound,
1064
+ wallet_binding_required: walletBindingRequired,
1065
+ _instructions: instructions,
1066
+ }, null, 2),
1067
+ },
1068
+ ],
1069
+ };
1070
+ }
1071
+ case "get_wallet_challenge": {
1072
+ const parsed = GetWalletChallengeInputSchema.safeParse(args ?? {});
1073
+ if (!parsed.success) {
1074
+ return {
1075
+ content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
1076
+ isError: true,
1077
+ };
1078
+ }
1079
+ const result = await apiRequest("/api/v1/agents/wallet/challenge", {
1080
+ method: "POST",
1081
+ body: JSON.stringify({ chain: parsed.data.chain ?? "base" }),
1082
+ });
1083
+ return {
1084
+ content: [
1085
+ {
1086
+ type: "text",
1087
+ text: JSON.stringify({
1088
+ ...result,
1089
+ _instructions: "Sign the returned message with your wallet using personal_sign. Then call bind_wallet with wallet_address + nonce + signature (and chain if needed).",
1090
+ }, null, 2),
1091
+ },
1092
+ ],
1093
+ };
1094
+ }
1095
+ case "bind_wallet": {
1096
+ const parsed = BindWalletInputSchema.safeParse(args);
1097
+ if (!parsed.success) {
1098
+ return {
1099
+ content: [{ type: "text", text: `Error: invalid input: ${parsed.error.message}` }],
1100
+ isError: true,
1101
+ };
1102
+ }
1103
+ const result = await apiRequest("/api/v1/agents/wallet", {
1104
+ method: "PUT",
1105
+ body: JSON.stringify({
1106
+ wallet_address: parsed.data.wallet_address,
1107
+ nonce: parsed.data.nonce,
1108
+ signature: parsed.data.signature,
1109
+ chain: parsed.data.chain ?? "base",
1110
+ }),
1111
+ });
923
1112
  return {
924
1113
  content: [
925
1114
  {
926
1115
  type: "text",
927
1116
  text: JSON.stringify({
928
1117
  ...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.",
1118
+ _instructions: "Wallet bound. You can now deposit USDC: call get_deposit_address, send USDC, then call fund_account with tx_hash + chain.",
930
1119
  }, null, 2),
931
1120
  },
932
1121
  ],
@@ -955,6 +1144,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
955
1144
  }, null, 2),
956
1145
  },
957
1146
  ],
1147
+ isError: true,
958
1148
  };
959
1149
  }
960
1150
  // USDC deposit verification
@@ -970,7 +1160,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
970
1160
  text: JSON.stringify({
971
1161
  status: "awaiting_deposit",
972
1162
  ...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.",
1163
+ 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
1164
  }, null, 2),
975
1165
  },
976
1166
  ],
@@ -1005,6 +1195,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1005
1195
  }, null, 2),
1006
1196
  },
1007
1197
  ],
1198
+ isError: true,
1008
1199
  };
1009
1200
  }
1010
1201
  case "get_balance": {
@@ -1186,17 +1377,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1186
1377
  const response = {
1187
1378
  ...result,
1188
1379
  private_key: keyPair.privateKey,
1189
- _notice: "IMPORTANT: Save the private_key securely. You need it to decrypt the credential using retrieve_credential.",
1380
+ _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
1381
  };
1382
+ const warningText = `CRITICAL — SAVE THIS PRIVATE KEY:\n` +
1383
+ `private_key: ${keyPair.privateKey}\n\n` +
1384
+ `You MUST store this key securely. It is required to decrypt the credential\n` +
1385
+ `via retrieve_credential. If lost, the credential is unrecoverable (single-read).\n\n` +
1386
+ `${JSON.stringify(response, null, 2)}`;
1191
1387
  const credResultObj = result;
1192
1388
  if (credResultObj.sandbox) {
1193
1389
  return {
1194
1390
  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)}` },
1391
+ { 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
1392
  ],
1197
1393
  };
1198
1394
  }
1199
- return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }] };
1395
+ return { content: [{ type: "text", text: warningText }] };
1200
1396
  }
1201
1397
  case "retrieve_credential": {
1202
1398
  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.2",
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",