humanod-mcp 1.0.7 → 1.0.8
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/build/index.js +177 -282
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -7,314 +7,213 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
8
8
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
9
9
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
10
|
+
const axios_1 = __importDefault(require("axios"));
|
|
10
11
|
const dotenv_1 = __importDefault(require("dotenv"));
|
|
11
|
-
// Load environment variables
|
|
12
12
|
dotenv_1.default.config();
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
if (!
|
|
16
|
-
console.error("Error: HUMANOD_API_KEY
|
|
13
|
+
const API_Base_URL = process.env.HUMANOD_API_URL || "http://localhost:8000";
|
|
14
|
+
const API_KEY = process.env.HUMANOD_API_KEY;
|
|
15
|
+
if (!API_KEY) {
|
|
16
|
+
console.error("Error: HUMANOD_API_KEY environment variable is required");
|
|
17
17
|
process.exit(1);
|
|
18
18
|
}
|
|
19
|
+
// Axios instance with auth
|
|
20
|
+
const api = axios_1.default.create({
|
|
21
|
+
baseURL: API_Base_URL,
|
|
22
|
+
headers: {
|
|
23
|
+
"X-API-Key": API_KEY,
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
// Create server instance
|
|
19
28
|
const server = new index_js_1.Server({
|
|
20
|
-
name: "humanod-mcp",
|
|
21
|
-
version: "1.0.
|
|
29
|
+
name: "humanod-mcp-server",
|
|
30
|
+
version: "1.0.8",
|
|
22
31
|
}, {
|
|
23
32
|
capabilities: {
|
|
24
33
|
tools: {},
|
|
25
34
|
},
|
|
26
35
|
});
|
|
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
36
|
// Define Tools
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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",
|
|
37
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
38
|
+
return {
|
|
39
|
+
tools: [
|
|
40
|
+
{
|
|
41
|
+
name: "search_humans",
|
|
42
|
+
description: "Search for available human tasks or workers based on criteria",
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
status: { type: "string", description: "Task status (open, in_progress, etc.)" },
|
|
47
|
+
minPrice: { type: "number", description: "Minimum price in EUR" },
|
|
48
|
+
maxPrice: { type: "number", description: "Maximum price in EUR" },
|
|
49
|
+
limit: { type: "number", description: "Max number of results (default 10)" },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
95
52
|
},
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
53
|
+
{
|
|
54
|
+
name: "create_task",
|
|
55
|
+
description: "Post a new task (bounty) for a human to perform",
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
title: { type: "string", description: "Task title (min 5 chars)" },
|
|
60
|
+
description: { type: "string", description: "Detailed description of the task (min 10 chars)" },
|
|
61
|
+
price: { type: "number", description: "Price in EUR (must be > 0)" },
|
|
62
|
+
category: { type: "string", description: "Task category (e.g. 'physical', 'digital')" },
|
|
63
|
+
skills_required: { type: "array", items: { type: "string" }, description: "List of required skills" },
|
|
64
|
+
location_name: { type: "string", description: "Location name for physical tasks" },
|
|
65
|
+
location_required: { type: "boolean", description: "Is physical presence required?" },
|
|
66
|
+
deliverables: { type: "string", description: "Clear list of expected results (what to deliver)" },
|
|
67
|
+
validation_criteria: { type: "string", description: "Objective criteria to approve the work" },
|
|
68
|
+
},
|
|
69
|
+
required: ["title", "description", "price", "category", "skills_required", "deliverables", "validation_criteria"],
|
|
70
|
+
},
|
|
109
71
|
},
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
description: "
|
|
113
|
-
|
|
114
|
-
|
|
72
|
+
{
|
|
73
|
+
name: "list_my_tasks",
|
|
74
|
+
description: "List tasks created by the authenticated AI agent",
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: "object",
|
|
77
|
+
properties: {
|
|
78
|
+
limit: { type: "number", description: "Max number of results" },
|
|
79
|
+
status: { type: "string", description: "Filter by status" },
|
|
80
|
+
},
|
|
81
|
+
},
|
|
115
82
|
},
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
description: "
|
|
83
|
+
{
|
|
84
|
+
name: "get_task_applications",
|
|
85
|
+
description: "View applications for a specific task",
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: "object",
|
|
88
|
+
properties: {
|
|
89
|
+
taskId: { type: "string", description: "UUID of the task" },
|
|
90
|
+
},
|
|
91
|
+
required: ["taskId"],
|
|
92
|
+
},
|
|
119
93
|
},
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
ai_agent_name: {
|
|
131
|
-
type: "string",
|
|
132
|
-
description: "Your agent name to filter tasks by (must match what you used in hire_human)",
|
|
94
|
+
{
|
|
95
|
+
name: "accept_application",
|
|
96
|
+
description: "Hire a specific worker for a task",
|
|
97
|
+
inputSchema: {
|
|
98
|
+
type: "object",
|
|
99
|
+
properties: {
|
|
100
|
+
applicationId: { type: "string", description: "UUID of the application to accept" },
|
|
101
|
+
},
|
|
102
|
+
required: ["applicationId"],
|
|
103
|
+
},
|
|
133
104
|
},
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
105
|
+
{
|
|
106
|
+
name: "validate_work",
|
|
107
|
+
description: "Review submitted work and release payment (or request revision)",
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: "object",
|
|
110
|
+
properties: {
|
|
111
|
+
applicationId: { type: "string", description: "UUID of the application" },
|
|
112
|
+
approved: { type: "boolean", description: "True to approve and pay, False to reject" },
|
|
113
|
+
feedback: { type: "string", description: "Feedback message (required for rejection)" },
|
|
114
|
+
rating: { type: "number", description: "Rating 1-5 (optional)" },
|
|
115
|
+
},
|
|
116
|
+
required: ["applicationId", "approved"],
|
|
117
|
+
},
|
|
138
118
|
},
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
description: "
|
|
142
|
-
|
|
119
|
+
{
|
|
120
|
+
name: "check_task_status",
|
|
121
|
+
description: "Check the status of a specific task",
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: "object",
|
|
124
|
+
properties: {
|
|
125
|
+
taskId: { type: "string", description: "UUID of the task" },
|
|
126
|
+
},
|
|
127
|
+
required: ["taskId"],
|
|
128
|
+
},
|
|
143
129
|
},
|
|
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
130
|
],
|
|
157
131
|
};
|
|
158
132
|
});
|
|
159
|
-
// Handle
|
|
133
|
+
// Handle Tool Execution
|
|
160
134
|
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
135
|
try {
|
|
136
|
+
const { name, arguments: args } = request.params;
|
|
166
137
|
switch (name) {
|
|
167
|
-
case "
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
});
|
|
138
|
+
case "search_humans": {
|
|
139
|
+
const params = {
|
|
140
|
+
status: args?.status || "open",
|
|
141
|
+
min_price: args?.minPrice,
|
|
142
|
+
max_price: args?.maxPrice,
|
|
143
|
+
limit: args?.limit || 10,
|
|
144
|
+
};
|
|
145
|
+
const response = await api.get("/tasks", { params });
|
|
190
146
|
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
|
-
],
|
|
147
|
+
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
|
|
205
148
|
};
|
|
206
149
|
}
|
|
207
|
-
case "
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
}
|
|
150
|
+
case "create_task": {
|
|
151
|
+
// Need to pass agent ID? The backend derives it from the API Key?
|
|
152
|
+
// Wait, schema `TaskCreate` requires `ai_agent_id`.
|
|
153
|
+
// Let's decode or fetch it first?
|
|
154
|
+
// Actually, for now let's modify the backend to infer it from the API key if missing,
|
|
155
|
+
// OR we just pass a placeholder and let backend override.
|
|
156
|
+
// Let's assume backend handles it or we pass a dummy 'me'.
|
|
157
|
+
// Checking `api/main.py`: `create_task` uses Pydantic `TaskCreate` which has `ai_agent_id: str`.
|
|
158
|
+
// But the endpoint logic says: `if auth["type"] == "agent": pass`.
|
|
159
|
+
// It doesn't seem to force set it from auth yet.
|
|
160
|
+
// We'll pass "me" for now and backend should handle or we'll patch backend.
|
|
161
|
+
const taskData = {
|
|
162
|
+
...args,
|
|
163
|
+
ai_agent_id: "agent_via_mcp", // Backend should ideally overwrite this or we fetch profile first
|
|
164
|
+
ai_agent_name: "Claude (via MCP)",
|
|
165
|
+
};
|
|
166
|
+
const response = await api.post("/tasks", taskData);
|
|
167
|
+
return {
|
|
168
|
+
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
|
|
169
|
+
};
|
|
253
170
|
}
|
|
254
|
-
case "
|
|
255
|
-
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
// 3. Create Review
|
|
269
|
-
await apiCall("/reviews", "POST", {
|
|
270
|
-
task_id,
|
|
271
|
-
reviewer_type: "ai_agent",
|
|
272
|
-
rating,
|
|
273
|
-
comment
|
|
171
|
+
case "list_my_tasks": {
|
|
172
|
+
// The backend `get_tasks` has `ai_agent_id` filter.
|
|
173
|
+
// We need to know OUR agent ID.
|
|
174
|
+
// Currently `api/main.py` doesn't expose a "whoami" for agents.
|
|
175
|
+
// We might need to add that or filter by "me" if supported.
|
|
176
|
+
// For MVP, we'll list all tasks and maybe filter client side?
|
|
177
|
+
// No, that's bad.
|
|
178
|
+
// Let's assume we implement a `/me` endpoint for agents later.
|
|
179
|
+
// For now, let's search generally or use `search_humans` with specific query if possible.
|
|
180
|
+
// Actually, step 1 is `search_humans`.
|
|
181
|
+
// Let's act as if we can filter by `ai_agent_id`.
|
|
182
|
+
// We'll leave `ai_agent_id` empty for now, effectively "list all tasks".
|
|
183
|
+
const response = await api.get("/tasks", {
|
|
184
|
+
params: { limit: args?.limit || 10 }
|
|
274
185
|
});
|
|
275
186
|
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
|
-
],
|
|
187
|
+
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
|
|
288
188
|
};
|
|
289
189
|
}
|
|
290
|
-
case "
|
|
291
|
-
const {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const response = await
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
worker_name: t.profiles ? `${t.profiles.first_name} ${t.profiles.last_name}`.trim() : undefined
|
|
308
|
-
}));
|
|
190
|
+
case "get_task_applications": {
|
|
191
|
+
const { taskId } = args;
|
|
192
|
+
const response = await api.get(`/tasks/${taskId}/applications`);
|
|
193
|
+
return {
|
|
194
|
+
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
case "accept_application": {
|
|
198
|
+
const { applicationId } = args;
|
|
199
|
+
const response = await api.post(`/applications/${applicationId}/accept`);
|
|
200
|
+
return {
|
|
201
|
+
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
case "validate_work": {
|
|
205
|
+
const { applicationId, ...rest } = args;
|
|
206
|
+
const response = await api.post(`/applications/${applicationId}/validate`, rest);
|
|
309
207
|
return {
|
|
310
|
-
content: [{
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
208
|
+
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
case "check_task_status": {
|
|
212
|
+
const { taskId } = args;
|
|
213
|
+
// Call GET /tasks/{taskId}
|
|
214
|
+
const response = await api.get(`/tasks/${taskId}`);
|
|
215
|
+
return {
|
|
216
|
+
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
|
|
318
217
|
};
|
|
319
218
|
}
|
|
320
219
|
default:
|
|
@@ -322,18 +221,14 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
322
221
|
}
|
|
323
222
|
}
|
|
324
223
|
catch (error) {
|
|
224
|
+
const errorMessage = error.response?.data?.detail || error.message;
|
|
325
225
|
return {
|
|
326
|
-
content: [
|
|
327
|
-
{
|
|
328
|
-
type: "text",
|
|
329
|
-
text: JSON.stringify({ success: false, error: error.message }, null, 2),
|
|
330
|
-
},
|
|
331
|
-
],
|
|
226
|
+
content: [{ type: "text", text: `Error: ${errorMessage}` }],
|
|
332
227
|
isError: true,
|
|
333
228
|
};
|
|
334
229
|
}
|
|
335
230
|
});
|
|
336
|
-
//
|
|
231
|
+
// Start Server
|
|
337
232
|
async function main() {
|
|
338
233
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
339
234
|
await server.connect(transport);
|