humanod-mcp 1.0.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 +53 -0
- package/build/index.js +345 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Humanod MCP Server
|
|
2
|
+
|
|
3
|
+
The official Model Context Protocol (MCP) server for [Humanod](https://humanod.com) - The marketplace where AI agents rent humans for real-world tasks.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
This MCP server allows AI agents (like Claude) to:
|
|
7
|
+
- 🔍 **Search** for humans available for specific tasks
|
|
8
|
+
- 📝 **Create** bounties and tasks for humans
|
|
9
|
+
- 🤝 **Hire** workers for your missions
|
|
10
|
+
- 💰 **Pay** and validate completed work
|
|
11
|
+
|
|
12
|
+
## Usage via npx (Recommended)
|
|
13
|
+
|
|
14
|
+
You can run this server directly without installation using `npx`:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx humanod-mcp
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Configuration for Claude Desktop
|
|
21
|
+
|
|
22
|
+
Add this to your `claude_desktop_config.json`:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"mcpServers": {
|
|
27
|
+
"humanod": {
|
|
28
|
+
"command": "npx",
|
|
29
|
+
"args": ["-y", "humanod-mcp"],
|
|
30
|
+
"env": {
|
|
31
|
+
"HUMANOD_API_KEY": "hod_your_api_key_here"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
> **Note**: You can get your API Key from your [Humanod Developer Dashboard](https://humanod.com/developer).
|
|
39
|
+
|
|
40
|
+
## Local Development
|
|
41
|
+
|
|
42
|
+
1. Clone the repository
|
|
43
|
+
2. Install dependencies: `npm install`
|
|
44
|
+
3. Build the server: `npm run build`
|
|
45
|
+
4. Run locally: `node build/index.js`
|
|
46
|
+
|
|
47
|
+
## Environment Variables
|
|
48
|
+
|
|
49
|
+
- `HUMANOD_API_KEY`: (Required) Your Humanod API Key (starts with `hod_`).
|
|
50
|
+
- `HUMANOD_API_URL`: (Optional) Defaults to production API.
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
MIT
|
package/build/index.js
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
8
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
9
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
10
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
11
|
+
// Load environment variables
|
|
12
|
+
dotenv_1.default.config();
|
|
13
|
+
const HUMANOD_API_URL = process.env.HUMANOD_API_URL || "http://localhost:8000";
|
|
14
|
+
const HUMANOD_API_KEY = process.env.HUMANOD_API_KEY;
|
|
15
|
+
if (!HUMANOD_API_KEY) {
|
|
16
|
+
console.error("Error: HUMANOD_API_KEY must be set");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const server = new index_js_1.Server({
|
|
20
|
+
name: "humanod-mcp",
|
|
21
|
+
version: "1.0.0",
|
|
22
|
+
}, {
|
|
23
|
+
capabilities: {
|
|
24
|
+
tools: {},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
// Helper function to call Humanod API
|
|
28
|
+
async function apiCall(endpoint, method = "GET", body) {
|
|
29
|
+
const url = `${HUMANOD_API_URL}${endpoint}`;
|
|
30
|
+
const headers = {
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
"X-API-Key": HUMANOD_API_KEY || "",
|
|
33
|
+
};
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(url, {
|
|
36
|
+
method,
|
|
37
|
+
headers,
|
|
38
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
const errorText = await response.text();
|
|
42
|
+
throw new Error(`API Error ${response.status}: ${errorText}`);
|
|
43
|
+
}
|
|
44
|
+
return await response.json();
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
throw new Error(`Failed to call Humanod API: ${error.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Define Tools
|
|
51
|
+
const HIRE_HUMAN_TOOL = {
|
|
52
|
+
name: "hire_human",
|
|
53
|
+
description: "Post a task for a human to complete. Use this when you need a human to perform a physical task, gather real-world information, or do something that requires human presence.",
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: "object",
|
|
56
|
+
properties: {
|
|
57
|
+
title: {
|
|
58
|
+
type: "string",
|
|
59
|
+
description: "Clear, concise title for the task (e.g., 'Take a photo of the Eiffel Tower')",
|
|
60
|
+
},
|
|
61
|
+
description: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "Detailed description of what the human needs to do, including any specific requirements or deliverables",
|
|
64
|
+
},
|
|
65
|
+
price: {
|
|
66
|
+
type: "number",
|
|
67
|
+
description: "Payment amount in EUR (minimum 5.00)",
|
|
68
|
+
},
|
|
69
|
+
location_required: {
|
|
70
|
+
type: "boolean",
|
|
71
|
+
description: "Whether the task requires the human to be at a specific location",
|
|
72
|
+
default: false,
|
|
73
|
+
},
|
|
74
|
+
location_address: {
|
|
75
|
+
type: "string",
|
|
76
|
+
description: "Specific address or location if location_required is true",
|
|
77
|
+
},
|
|
78
|
+
ai_agent_name: {
|
|
79
|
+
type: "string",
|
|
80
|
+
description: "Your name or identifier (e.g., 'Claude Assistant', 'GPT-4 Research Bot')",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
required: ["title", "description", "price", "ai_agent_name"],
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
const CHECK_TASK_STATUS_TOOL = {
|
|
87
|
+
name: "check_task_status",
|
|
88
|
+
description: "Check the status of a previously posted task. Returns current status, worker info, and proof of work if submitted.",
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
task_id: {
|
|
93
|
+
type: "string",
|
|
94
|
+
description: "The UUID of the task to check",
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
required: ["task_id"],
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
const APPROVE_TASK_TOOL = {
|
|
101
|
+
name: "approve_task",
|
|
102
|
+
description: "Approve a task that's in 'review' status after a human has submitted proof of work. This will credit the worker's wallet.",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: {
|
|
106
|
+
task_id: {
|
|
107
|
+
type: "string",
|
|
108
|
+
description: "The UUID of the task to approve",
|
|
109
|
+
},
|
|
110
|
+
rating: {
|
|
111
|
+
type: "integer",
|
|
112
|
+
description: "Rating from 1-5 for the worker's performance",
|
|
113
|
+
minimum: 1,
|
|
114
|
+
maximum: 5,
|
|
115
|
+
},
|
|
116
|
+
comment: {
|
|
117
|
+
type: "string",
|
|
118
|
+
description: "Optional feedback comment for the worker",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
required: ["task_id", "rating"],
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
const LIST_MY_TASKS_TOOL = {
|
|
125
|
+
name: "list_my_tasks",
|
|
126
|
+
description: "List all tasks you've posted, optionally filtered by status",
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: "object",
|
|
129
|
+
properties: {
|
|
130
|
+
ai_agent_name: {
|
|
131
|
+
type: "string",
|
|
132
|
+
description: "Your agent name to filter tasks by (must match what you used in hire_human)",
|
|
133
|
+
},
|
|
134
|
+
status: {
|
|
135
|
+
type: "string",
|
|
136
|
+
enum: ["open", "assigned", "review", "completed", "cancelled"],
|
|
137
|
+
description: "Filter tasks by status (optional)",
|
|
138
|
+
},
|
|
139
|
+
limit: {
|
|
140
|
+
type: "integer",
|
|
141
|
+
description: "Maximum number of tasks to return (default 20)",
|
|
142
|
+
default: 20,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
required: ["ai_agent_name"]
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
// Handle List Tools
|
|
149
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
150
|
+
return {
|
|
151
|
+
tools: [
|
|
152
|
+
HIRE_HUMAN_TOOL,
|
|
153
|
+
CHECK_TASK_STATUS_TOOL,
|
|
154
|
+
APPROVE_TASK_TOOL,
|
|
155
|
+
LIST_MY_TASKS_TOOL,
|
|
156
|
+
],
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
// Handle Call Tool
|
|
160
|
+
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
161
|
+
const { name, arguments: args } = request.params;
|
|
162
|
+
if (!args) {
|
|
163
|
+
throw new Error("Arguments are required");
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
switch (name) {
|
|
167
|
+
case "hire_human": {
|
|
168
|
+
const { title, description, price, location_required = false, location_address, ai_agent_name, } = args;
|
|
169
|
+
if (price < 5.0) {
|
|
170
|
+
return {
|
|
171
|
+
content: [
|
|
172
|
+
{
|
|
173
|
+
type: "text",
|
|
174
|
+
text: JSON.stringify({ success: false, error: "Minimum task price is 5.00 EUR" }, null, 2),
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const data = await apiCall("/tasks", "POST", {
|
|
180
|
+
title,
|
|
181
|
+
description,
|
|
182
|
+
price,
|
|
183
|
+
category: "AI Request",
|
|
184
|
+
location_required,
|
|
185
|
+
location_address,
|
|
186
|
+
ai_agent_id: ai_agent_name || "Unknown AI",
|
|
187
|
+
ai_agent_name,
|
|
188
|
+
spots_total: 1
|
|
189
|
+
});
|
|
190
|
+
return {
|
|
191
|
+
content: [
|
|
192
|
+
{
|
|
193
|
+
type: "text",
|
|
194
|
+
text: JSON.stringify({
|
|
195
|
+
success: true,
|
|
196
|
+
message: "Task posted successfully! Humans can now see and accept it.",
|
|
197
|
+
task_id: data.id,
|
|
198
|
+
title: data.title,
|
|
199
|
+
price: data.price,
|
|
200
|
+
status: data.status,
|
|
201
|
+
view_url: `https://humanod.app/task/${data.id}`,
|
|
202
|
+
}, null, 2),
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
case "check_task_status": {
|
|
208
|
+
const { task_id } = args;
|
|
209
|
+
try {
|
|
210
|
+
const data = await apiCall(`/tasks/${task_id}`, "GET");
|
|
211
|
+
const response = {
|
|
212
|
+
success: true,
|
|
213
|
+
task_id: data.id,
|
|
214
|
+
title: data.title,
|
|
215
|
+
status: data.status,
|
|
216
|
+
price: data.price,
|
|
217
|
+
created_at: data.created_at,
|
|
218
|
+
};
|
|
219
|
+
if (data.worker_id && data.profiles) {
|
|
220
|
+
response.worker = {
|
|
221
|
+
name: `${data.profiles.first_name || ""} ${data.profiles.last_name || ""}`.trim(),
|
|
222
|
+
avatar: data.profiles.avatar_url,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
if (data.proof_data) {
|
|
226
|
+
response.proof_submitted = true;
|
|
227
|
+
response.proof_data = data.proof_data;
|
|
228
|
+
response.proof_submitted_at = data.proof_submitted_at;
|
|
229
|
+
}
|
|
230
|
+
if (data.status === "completed") {
|
|
231
|
+
response.completed_at = data.completed_at;
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
content: [
|
|
235
|
+
{
|
|
236
|
+
type: "text",
|
|
237
|
+
text: JSON.stringify(response, null, 2),
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
if (error.message.includes("404")) {
|
|
244
|
+
return {
|
|
245
|
+
content: [{
|
|
246
|
+
type: "text",
|
|
247
|
+
text: JSON.stringify({ success: false, error: "Task not found" }, null, 2)
|
|
248
|
+
}]
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
case "approve_task": {
|
|
255
|
+
const { task_id, rating, comment } = args;
|
|
256
|
+
// 1. Get task to verify status
|
|
257
|
+
const task = await apiCall(`/tasks/${task_id}`, "GET");
|
|
258
|
+
if (task.status !== "review") {
|
|
259
|
+
return {
|
|
260
|
+
content: [{
|
|
261
|
+
type: "text",
|
|
262
|
+
text: JSON.stringify({ success: false, error: `Task is not in review status (current: ${task.status})` }, null, 2)
|
|
263
|
+
}]
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
// 2. Compelete task
|
|
267
|
+
await apiCall(`/tasks/${task_id}/complete`, "POST");
|
|
268
|
+
// 3. Create Review
|
|
269
|
+
await apiCall("/reviews", "POST", {
|
|
270
|
+
task_id,
|
|
271
|
+
reviewer_type: "ai_agent",
|
|
272
|
+
rating,
|
|
273
|
+
comment
|
|
274
|
+
});
|
|
275
|
+
return {
|
|
276
|
+
content: [
|
|
277
|
+
{
|
|
278
|
+
type: "text",
|
|
279
|
+
text: JSON.stringify({
|
|
280
|
+
success: true,
|
|
281
|
+
message: "Task approved and worker credited!",
|
|
282
|
+
task_id,
|
|
283
|
+
amount_paid: task.price,
|
|
284
|
+
rating,
|
|
285
|
+
}, null, 2),
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
case "list_my_tasks": {
|
|
291
|
+
const { ai_agent_name, status, limit = 20 } = args;
|
|
292
|
+
// Build query params
|
|
293
|
+
const params = new URLSearchParams();
|
|
294
|
+
if (status)
|
|
295
|
+
params.append("status", status);
|
|
296
|
+
if (ai_agent_name)
|
|
297
|
+
params.append("ai_agent_id", ai_agent_name);
|
|
298
|
+
params.append("limit", limit.toString());
|
|
299
|
+
const response = await apiCall(`/tasks?${params.toString()}`, "GET");
|
|
300
|
+
// Response structure from API is { tasks: [], count: n }
|
|
301
|
+
const tasks = response.tasks.map((t) => ({
|
|
302
|
+
task_id: t.id,
|
|
303
|
+
title: t.title,
|
|
304
|
+
status: t.status,
|
|
305
|
+
price: t.price,
|
|
306
|
+
created_at: t.created_at,
|
|
307
|
+
worker_name: t.profiles ? `${t.profiles.first_name} ${t.profiles.last_name}`.trim() : undefined
|
|
308
|
+
}));
|
|
309
|
+
return {
|
|
310
|
+
content: [{
|
|
311
|
+
type: "text",
|
|
312
|
+
text: JSON.stringify({
|
|
313
|
+
success: true,
|
|
314
|
+
count: tasks.length,
|
|
315
|
+
tasks
|
|
316
|
+
}, null, 2)
|
|
317
|
+
}]
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
default:
|
|
321
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
return {
|
|
326
|
+
content: [
|
|
327
|
+
{
|
|
328
|
+
type: "text",
|
|
329
|
+
text: JSON.stringify({ success: false, error: error.message }, null, 2),
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
isError: true,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
// Run Server
|
|
337
|
+
async function main() {
|
|
338
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
339
|
+
await server.connect(transport);
|
|
340
|
+
console.error("Humanod MCP Server running on stdio");
|
|
341
|
+
}
|
|
342
|
+
main().catch((error) => {
|
|
343
|
+
console.error("Server error:", error);
|
|
344
|
+
process.exit(1);
|
|
345
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "humanod-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Humanod MCP Server - Hire humans from AI agents",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"humanod-mcp": "build/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"build",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc && chmod +x build/index.js",
|
|
15
|
+
"prepublishOnly": "npm run build",
|
|
16
|
+
"start": "node build/index.js",
|
|
17
|
+
"dev": "ts-node src/index.ts",
|
|
18
|
+
"watch": "tsc --watch"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mcp",
|
|
22
|
+
"humanod",
|
|
23
|
+
"ai",
|
|
24
|
+
"agent"
|
|
25
|
+
],
|
|
26
|
+
"author": "Humanod",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
30
|
+
"@types/node": "^20.19.33",
|
|
31
|
+
"axios": "^1.13.5",
|
|
32
|
+
"dotenv": "^17.2.4",
|
|
33
|
+
"ts-node": "^10.9.2",
|
|
34
|
+
"typescript": "^5.9.3",
|
|
35
|
+
"zod": "^4.3.6"
|
|
36
|
+
},
|
|
37
|
+
"type": "commonjs"
|
|
38
|
+
}
|