humanod-mcp 1.0.7 → 1.0.9
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 +201 -281
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -7,314 +7,238 @@ 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.9",
|
|
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')",
|
|
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
|
+
},
|
|
81
52
|
},
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
+
},
|
|
95
71
|
},
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
task_id: {
|
|
107
|
-
type: "string",
|
|
108
|
-
description: "The UUID of the task to approve",
|
|
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
|
+
},
|
|
109
82
|
},
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
description: "
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
},
|
|
115
93
|
},
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
description: "
|
|
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
|
+
},
|
|
119
104
|
},
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
+
},
|
|
133
118
|
},
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
},
|
|
138
129
|
},
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
description: "
|
|
142
|
-
|
|
130
|
+
{
|
|
131
|
+
name: "get_balance",
|
|
132
|
+
description: "Check the agent's wallet balance",
|
|
133
|
+
inputSchema: {
|
|
134
|
+
type: "object",
|
|
135
|
+
properties: {},
|
|
136
|
+
},
|
|
143
137
|
},
|
|
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
138
|
],
|
|
157
139
|
};
|
|
158
140
|
});
|
|
159
|
-
// Handle
|
|
141
|
+
// Handle Tool Execution
|
|
160
142
|
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
143
|
try {
|
|
144
|
+
const { name, arguments: args } = request.params;
|
|
166
145
|
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
|
-
});
|
|
146
|
+
case "search_humans": {
|
|
147
|
+
const params = {
|
|
148
|
+
status: args?.status || "open",
|
|
149
|
+
min_price: args?.minPrice,
|
|
150
|
+
max_price: args?.maxPrice,
|
|
151
|
+
limit: args?.limit || 10,
|
|
152
|
+
};
|
|
153
|
+
const response = await api.get("/tasks", { params });
|
|
190
154
|
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
|
-
],
|
|
155
|
+
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
|
|
205
156
|
};
|
|
206
157
|
}
|
|
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
|
-
}
|
|
158
|
+
case "create_task": {
|
|
159
|
+
// Need to pass agent ID? The backend derives it from the API Key?
|
|
160
|
+
// Wait, schema `TaskCreate` requires `ai_agent_id`.
|
|
161
|
+
// Let's decode or fetch it first?
|
|
162
|
+
// Actually, for now let's modify the backend to infer it from the API key if missing,
|
|
163
|
+
// OR we just pass a placeholder and let backend override.
|
|
164
|
+
// Let's assume backend handles it or we pass a dummy 'me'.
|
|
165
|
+
// Checking `api/main.py`: `create_task` uses Pydantic `TaskCreate` which has `ai_agent_id: str`.
|
|
166
|
+
// But the endpoint logic says: `if auth["type"] == "agent": pass`.
|
|
167
|
+
// It doesn't seem to force set it from auth yet.
|
|
168
|
+
// We'll pass "me" for now and backend should handle or we'll patch backend.
|
|
169
|
+
const taskData = {
|
|
170
|
+
...args,
|
|
171
|
+
ai_agent_id: "agent_via_mcp", // Backend should ideally overwrite this or we fetch profile first
|
|
172
|
+
ai_agent_name: "Claude (via MCP)",
|
|
173
|
+
};
|
|
174
|
+
const response = await api.post("/tasks", taskData);
|
|
175
|
+
return {
|
|
176
|
+
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
|
|
177
|
+
};
|
|
253
178
|
}
|
|
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
|
|
179
|
+
case "list_my_tasks": {
|
|
180
|
+
// The backend `get_tasks` has `ai_agent_id` filter.
|
|
181
|
+
// We need to know OUR agent ID.
|
|
182
|
+
// Currently `api/main.py` doesn't expose a "whoami" for agents.
|
|
183
|
+
// We might need to add that or filter by "me" if supported.
|
|
184
|
+
// For MVP, we'll list all tasks and maybe filter client side?
|
|
185
|
+
// No, that's bad.
|
|
186
|
+
// Let's assume we implement a `/me` endpoint for agents later.
|
|
187
|
+
// For now, let's search generally or use `search_humans` with specific query if possible.
|
|
188
|
+
// Actually, step 1 is `search_humans`.
|
|
189
|
+
// Let's act as if we can filter by `ai_agent_id`.
|
|
190
|
+
// We'll leave `ai_agent_id` empty for now, effectively "list all tasks".
|
|
191
|
+
const response = await api.get("/tasks", {
|
|
192
|
+
params: { limit: args?.limit || 10 }
|
|
274
193
|
});
|
|
275
194
|
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
|
-
],
|
|
195
|
+
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
|
|
288
196
|
};
|
|
289
197
|
}
|
|
290
|
-
case "
|
|
291
|
-
const {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const response = await
|
|
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
|
-
}));
|
|
198
|
+
case "get_task_applications": {
|
|
199
|
+
const { taskId } = args;
|
|
200
|
+
const response = await api.get(`/tasks/${taskId}/applications`);
|
|
201
|
+
return {
|
|
202
|
+
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
case "accept_application": {
|
|
206
|
+
const { applicationId } = args;
|
|
207
|
+
const response = await api.post(`/applications/${applicationId}/accept`);
|
|
309
208
|
return {
|
|
310
|
-
content: [{
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
209
|
+
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
case "validate_work": {
|
|
213
|
+
const { applicationId, ...rest } = args;
|
|
214
|
+
const response = await api.post(`/applications/${applicationId}/validate`, rest);
|
|
215
|
+
return {
|
|
216
|
+
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
case "check_task_status": {
|
|
220
|
+
const { taskId } = args;
|
|
221
|
+
// Call GET /tasks/{taskId}
|
|
222
|
+
const response = await api.get(`/tasks/${taskId}`);
|
|
223
|
+
return {
|
|
224
|
+
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
case "get_balance": {
|
|
228
|
+
// Fetch profile/wallet
|
|
229
|
+
// Backend endpoint needed. `GET /me` or `/profile`?
|
|
230
|
+
// api/main.py: custom `get_my_profile`?
|
|
231
|
+
// currently no direct "me" endpoint.
|
|
232
|
+
// But we can query `profiles` table via Supabase if we had client?
|
|
233
|
+
// The MCP server uses REST API.
|
|
234
|
+
// We need a REST endpoint to get balance.
|
|
235
|
+
// Workaround: Use `list_my_tasks` and check balance? No.
|
|
236
|
+
// Let's assume we add `/me` endpoint to API quickly or use existing?
|
|
237
|
+
// Wait, `api/main.py` has no `/me`.
|
|
238
|
+
// I will add `/me` to API in next step. For now, I'll scaffold the call.
|
|
239
|
+
const response = await api.get("/me");
|
|
240
|
+
return {
|
|
241
|
+
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
|
|
318
242
|
};
|
|
319
243
|
}
|
|
320
244
|
default:
|
|
@@ -322,18 +246,14 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
322
246
|
}
|
|
323
247
|
}
|
|
324
248
|
catch (error) {
|
|
249
|
+
const errorMessage = error.response?.data?.detail || error.message;
|
|
325
250
|
return {
|
|
326
|
-
content: [
|
|
327
|
-
{
|
|
328
|
-
type: "text",
|
|
329
|
-
text: JSON.stringify({ success: false, error: error.message }, null, 2),
|
|
330
|
-
},
|
|
331
|
-
],
|
|
251
|
+
content: [{ type: "text", text: `Error: ${errorMessage}` }],
|
|
332
252
|
isError: true,
|
|
333
253
|
};
|
|
334
254
|
}
|
|
335
255
|
});
|
|
336
|
-
//
|
|
256
|
+
// Start Server
|
|
337
257
|
async function main() {
|
|
338
258
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
339
259
|
await server.connect(transport);
|