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.
Files changed (2) hide show
  1. package/build/index.js +177 -282
  2. 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 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");
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.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
- 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",
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
- 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",
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
- rating: {
111
- type: "integer",
112
- description: "Rating from 1-5 for the worker's performance",
113
- minimum: 1,
114
- maximum: 5,
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
- comment: {
117
- type: "string",
118
- description: "Optional feedback comment for the worker",
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
- 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)",
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
- status: {
135
- type: "string",
136
- enum: ["open", "assigned", "review", "completed", "cancelled"],
137
- description: "Filter tasks by status (optional)",
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
- limit: {
140
- type: "integer",
141
- description: "Maximum number of tasks to return (default 20)",
142
- default: 20,
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 Call Tool
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 "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
- });
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 "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
- }
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 "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
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 "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
- }));
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
- type: "text",
312
- text: JSON.stringify({
313
- success: true,
314
- count: tasks.length,
315
- tasks
316
- }, null, 2)
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
- // Run Server
231
+ // Start Server
337
232
  async function main() {
338
233
  const transport = new stdio_js_1.StdioServerTransport();
339
234
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "humanod-mcp",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "Humanod MCP Server - Hire humans from AI agents",
5
5
  "main": "build/index.js",
6
6
  "bin": {