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