Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions mcp_modules/taskmarket/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Taskmarket MCP Module

Delegate real work to **Taskmarket** from inside the Profullstack MCP server. This
module lets a user or agent *recognize that a request is better delegated to external
workers* and, with **explicit authorization**, create or discover a Taskmarket task
instead of repeatedly spending inference or forcing an unreliable solution.

- **Browse** open Taskmarket tasks
- **Create** a funded Taskmarket task after showing exact description, reward, deadline,
deliverables, Base network, and a maximum spend — and only after **fresh, explicit user
authorization**
- **Track** a task's live status by ID
- **Retrieve submissions and present them for human review** — the module never silently
accepts or rejects work
- **Accept** a submission only after explicit confirmation (and only by a human reviewer)

Target product: **Profullstack MCP server** (established, actively maintained, public repo
with an `mcp_modules/` extension system). This module is a *new* `taskmarket` extension that
the server did not previously have.

## Why this is a real integration

The module is a first-party `mcp_modules/taskmarket` package that registers HTTP routes and
agent tools on the official server. It shells out to the **official `taskmarket` CLI**
(first-party Taskmarket tooling) to perform every operation, so it behaves exactly like a
user would on the command line — no reimplemented protocol, no mock interface.

## Security model (required by the integration bounty)

- **No secrets handled here.** The module never requests, stores, logs, or commits private
keys, seed phrases, tokens, cookies, or other secrets. The `taskmarket` CLI reads the
operator's configured wallet from its own secure store. This module only ever passes
public task parameters to the CLI.
- **Explicit authorization gate.** Every fund-moving call — `createTask` (funds the reward)
and `acceptSubmission` (costs 0.001 USDC) — requires the caller to send `confirm: true`.
Without it the server refuses and returns `requireConfirmation: true`. The server never
spends on its own initiative.
- **Network + spending checks.** Tasks may only be created on **Base mainnet**; any other
`network` is rejected. `reward` must not exceed the caller-supplied `maxSpend` ceiling.
- **No blind retries.** If a CLI call fails (e.g. unknown settlement status), the error is
surfaced to the caller. The module never auto-retries a payment whose outcome is unknown.
- **Human-in-the-loop review.** Submissions are retrieved and returned for a human to read;
acceptance is a separate, confirmed call. Work is never silently auto-accepted.

## Setup

```bash
# prerequisites: Node >= 18, the official taskmarket CLI on PATH, a configured wallet
cd profullstack-mcp-server
npm install
# the module is auto-discovered from mcp_modules/taskmarket (see src/core/moduleLoader.js)
npm start
```

Configure the CLI (one time, on the host — not in this repo):

```bash
taskmarket wallet status # confirms a configured Base wallet
```

Point the module at a specific binary if needed:

```bash
export TASKMARKET_BIN=/usr/local/bin/taskmarket
```

## HTTP API

| Method | Path | Purpose | Auth |
| ------ | ---- | ------- | ---- |
| GET | `/taskmarket` | Module info | none |
| GET | `/taskmarket/capabilities` | Capabilities | none |
| GET | `/taskmarket/tasks?limit=20&mode=bounty` | Browse tasks | none |
| GET | `/taskmarket/tasks/:id` | Task details / live status | none |
| POST | `/taskmarket/tasks` | **Create** a funded task | `confirm:true` |
| GET | `/taskmarket/tasks/:id/submissions` | List submissions (review) | none |
| POST | `/taskmarket/tasks/:id/submissions/:subId/accept` | **Accept** submission | `confirm:true` |

### Create a task (explicit authorization)

```bash
curl -X POST http://localhost:3000/taskmarket/tasks \
-H 'content-type: application/json' \
-d '{
"confirm": true,
"description": "Build a landing page for our launch",
"reward": 5,
"durationHours": 48,
"mode": "bounty",
"network": "base",
"maxSpend": 5
}'
# -> { "created": { "data": { "taskId": "0x...", "link": "https://taskmarket.dev/task/0x..." } } }
```

Omit `confirm` (or set it `false`) and the server answers:

```json
{ "error": "Explicit user authorization required: send confirm:true to create a funded task.",
"requireConfirmation": true }
```

### Review submissions

```bash
curl http://localhost:3000/taskmarket/tasks/0x.../submissions
# -> { "submissions": { "data": [ { "submissionId": "0xSUB1", "worker": "0xW1" }, ... ] } }
```

### Accept a reviewed submission (explicit authorization)

```bash
curl -X POST http://localhost:3000/taskmarket/tasks/0x.../submissions/0xSUB1/accept \
-H 'content-type: application/json' -d '{"confirm": true}'
```

## CLI usage (same flows, directly)

```bash
taskmarket task list --limit 20
taskmarket task get 0x...
taskmarket task submissions 0x...
```

## Run the tests

```bash
npm test
```

The suite unit-tests every authorization/spending/network gate and exercises the CLI
round-trip against an injected fake binary (no real wallet, network, or funds required).

## Repository & upstream

- Upstream (target): https://github.com/profullstack/mcp-server
- Module path: `mcp_modules/taskmarket/`
- Taskmarket: https://taskmarket.dev/ · Docs: https://docs.taskmarket.dev/
54 changes: 54 additions & 0 deletions mcp_modules/taskmarket/docs/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Taskmarket Module — API Reference

All routes are mounted by `register(app)` in `index.js`. JSON bodies use
`content-type: application/json`.

## GET /taskmarket
Module status and version.

```json
{ "module": "taskmarket", "status": "active", "network": "base",
"message": "Delegate work to Taskmarket — browse, create (authorized), and review submissions on Base." }
```

## GET /taskmarket/capabilities
Returns supported version, network, and the list of available tools.

## GET /taskmarket/tasks
Browse open tasks.
- Query: `limit` (default 20), `mode` (optional: bounty|claim|pitch|benchmark|auction)
- Response: `{ "tasks": <cli json> }`

## GET /taskmarket/tasks/:id
Get a single task's details / live status. `:id` must be a `0x`-prefixed hex string.

## POST /taskmarket/tasks
Create a funded task. **Requires `confirm: true`.**

Body:
```json
{
"confirm": true,
"description": "string (required)",
"reward": 5,
"durationHours": 48,
"mode": "bounty",
"visibility": "public",
"network": "base",
"maxSpend": 5
}
```
Validation (all enforced before any spend):
- `confirm === true` else `400 requireConfirmation`
- `network === "base"` else `400 Unsupported network`
- `reward > 0` and `reward <= maxSpend` else `400`
- `durationHours > 0` else `400`
Response: `{ "created": <cli json with taskId/link> }`

## GET /taskmarket/tasks/:id/submissions
List submissions for a task, returned verbatim for **human review**. Never auto-accepted.

## POST /taskmarket/tasks/:id/submissions/:subId/accept
Accept a submission. **Requires `confirm: true`.** Costs 0.001 USDC.
- Body: `{ "confirm": true }`
- Response: `{ "accepted": <cli json> }`
34 changes: 34 additions & 0 deletions mcp_modules/taskmarket/examples/basic-usage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Basic usage example for the Taskmarket module.
*
* Real CLI: node examples/basic-usage.js
* Injected fake CLI: TASKMARKET_BIN=./fake.sh node examples/basic-usage.js
*
* The demo never spends funds: createTask is shown with confirm:false so the
* authorization gate rejects it. Flip to true only with a funded, authorized wallet.
*/
import { listTasks, getTask, listSubmissions, createTask } from "../src/taskmarket.js";

async function main() {
console.log("== Browse open tasks ==");
const tasks = await listTasks({ limit: 5 });
console.log(JSON.stringify(tasks, null, 2));

const firstId = tasks?.data?.tasks?.[0]?.id;
if (firstId) {
console.log("\n== Get task details ==");
console.log(JSON.stringify(await getTask(firstId), null, 2));

console.log("\n== List submissions for human review ==");
console.log(JSON.stringify(await listSubmissions(firstId), null, 2));
}

console.log("\n== Create a funded task (authorization gate) ==");
try {
await createTask({ description: "Demo task", reward: 1, durationHours: 24, network: "base", maxSpend: 1, confirm: false });
} catch (e) {
console.log("Authorization gate worked as designed ->", e.message);
}
}

main().catch((e) => { console.error(e); process.exit(1); });
50 changes: 50 additions & 0 deletions mcp_modules/taskmarket/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Taskmarket Module
*
* Delegate real work to Taskmarket from inside the Profullstack MCP server.
* Browse open tasks, create a funded task after explicit user authorization,
* and retrieve submissions for human review — all on Base mainnet via the
* official taskmarket CLI (first-party tooling; no secrets handled here).
*/

import { logger } from "../../src/utils/logger.js";
import {
listTasksHandler, getTaskHandler, createTaskHandler,
listSubmissionsHandler, acceptSubmissionHandler, capabilities,
} from "./src/controller.js";
import { listCapabilities } from "./src/service.js";

export async function register(app) {
logger.info("Registering taskmarket module");

app.get("/taskmarket", (c) => c.json({
module: "taskmarket",
status: "active",
message: "Delegate work to Taskmarket — browse, create (authorized), and review submissions on Base.",
version: listCapabilities().version,
network: listCapabilities().network,
}));

app.get("/taskmarket/capabilities", capabilities);
app.get("/taskmarket/tasks", listTasksHandler);
app.get("/taskmarket/tasks/:id", getTaskHandler);
app.post("/taskmarket/tasks", createTaskHandler);
app.get("/taskmarket/tasks/:id/submissions", listSubmissionsHandler);
app.post("/taskmarket/tasks/:id/submissions/:subId/accept", acceptSubmissionHandler);

app.get("/tools/taskmarket/info", (c) => c.json({
name: "taskmarket",
description:
"Browse Taskmarket tasks, create a funded task with explicit user authorization, and review " +
"submissions for human approval — all on Base mainnet via first-party taskmarket CLI.",
examples: [
"GET /taskmarket/tasks",
"GET /taskmarket/tasks/:id",
"POST /taskmarket/tasks {confirm:true, description, reward, durationHours, network:'base', maxSpend}",
"GET /taskmarket/tasks/:id/submissions",
],
}));
}

export { listCapabilities } from "./src/service.js";
export const metadata = { version: "1.0.0" };
22 changes: 22 additions & 0 deletions mcp_modules/taskmarket/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "mcp-module-taskmarket",
"version": "1.0.0",
"description": "Taskmarket delegation module for the Profullstack MCP server — browse, create (authorized), and review submissions on Base via first-party taskmarket CLI",
"main": "index.js",
"type": "module",
"scripts": {
"test": "node --test test/"
},
"keywords": [
"mcp",
"module",
"taskmarket",
"task",
"bounty",
"base",
"delegation",
"agent"
],
"license": "MIT",
"dependencies": {}
}
78 changes: 78 additions & 0 deletions mcp_modules/taskmarket/src/controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* HTTP handlers for the taskmarket module.
*
* Authorization model: every fund-moving action (create a task, accept a
* submission) requires the caller to send confirm:true. The server never
* spends on its own — it only forwards a deliberate, authorized request to the
* first-party taskmarket CLI.
*/
import {
listTasks, getTask, createTask, listSubmissions, acceptSubmission, listCapabilities,
} from "./service.js";

export async function listTasksHandler(c) {
try {
const limit = Number(c.req.query("limit") || 20);
const mode = c.req.query("mode") || null;
const data = await listTasks({ limit, mode });
return c.json({ tasks: data });
} catch (err) {
return c.json({ error: err.message }, 400);
}
}

export async function getTaskHandler(c) {
try {
const taskId = c.req.param("id");
const data = await getTask(taskId);
return c.json(data);
} catch (err) {
return c.json({ error: err.message }, 400);
}
}

export async function createTaskHandler(c) {
try {
const params = await c.req.json();
if (params.confirm !== true) {
return c.json(
{ error: "Explicit user authorization required: send confirm:true to create a funded task.",
requireConfirmation: true }, 400);
}
const data = await createTask(params);
return c.json({ created: data });
} catch (err) {
return c.json({ error: err.message }, 400);
}
}

export async function listSubmissionsHandler(c) {
try {
const taskId = c.req.param("id");
const data = await listSubmissions(taskId);
return c.json({ submissions: data });
} catch (err) {
return c.json({ error: err.message }, 400);
}
}

export async function acceptSubmissionHandler(c) {
try {
const taskId = c.req.param("id");
const submissionId = c.req.param("subId");
const body = await c.req.json().catch(() => ({}));
if (body.confirm !== true) {
return c.json(
{ error: "Explicit user authorization required: send confirm:true to accept a submission.",
requireConfirmation: true }, 400);
}
const data = await acceptSubmission(taskId, submissionId, true);
return c.json({ accepted: data });
} catch (err) {
return c.json({ error: err.message }, 400);
}
}

export async function capabilities(c) {
return c.json(listCapabilities());
}
Loading
Loading