From 599764652a52ac224dcf709225085964b659d4df Mon Sep 17 00:00:00 2001 From: marko-durasic Date: Mon, 17 Aug 2026 22:57:10 +0800 Subject: [PATCH] feat: add read-only Taskmarket discovery plugin Give Claude Code agents a GET-only way to browse Taskmarket work, inspect a task, and list public submissions without spending, keys, or POST. Co-authored-by: Cursor --- .claude-plugin/marketplace.json | 11 ++ README.md | 13 ++ taskmarket/.claude-plugin/plugin.json | 10 ++ taskmarket/.mcp.json | 8 ++ taskmarket/README.md | 55 ++++++++ taskmarket/mcp-server/package.json | 15 +++ taskmarket/mcp-server/src/client.js | 76 +++++++++++ taskmarket/mcp-server/src/client.test.js | 85 ++++++++++++ taskmarket/mcp-server/src/index.js | 156 +++++++++++++++++++++++ taskmarket/mcp-server/src/safety.js | 49 +++++++ taskmarket/skills/taskmarket/SKILL.md | 37 ++++++ 11 files changed, 515 insertions(+) create mode 100644 taskmarket/.claude-plugin/plugin.json create mode 100644 taskmarket/.mcp.json create mode 100644 taskmarket/README.md create mode 100644 taskmarket/mcp-server/package.json create mode 100644 taskmarket/mcp-server/src/client.js create mode 100644 taskmarket/mcp-server/src/client.test.js create mode 100644 taskmarket/mcp-server/src/index.js create mode 100644 taskmarket/mcp-server/src/safety.js create mode 100644 taskmarket/skills/taskmarket/SKILL.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 49ddb04..2ba2a04 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -39,6 +39,17 @@ "source": "./bankr-agent-dev", "category": "development", "homepage": "https://bankr.bot" + }, + { + "name": "taskmarket", + "description": "Read-only Taskmarket discovery: browse open work, inspect a task, and list public submissions. Create-task is a CLI preview only — no spend, no keys.", + "author": { + "name": "DuReef (community)", + "email": "hello@dureef.com" + }, + "source": "./taskmarket", + "category": "tools", + "homepage": "https://taskmarket.dev" } ] } diff --git a/README.md b/README.md index af6336a..3aa0cc4 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,16 @@ _Maintained by the Bankr team._ [View Plugin →](./x402-sdk-dev/) +### taskmarket + +**Read-only Taskmarket discovery (community)** + +- Browse open Taskmarket work and inspect a task +- List public submissions for human review +- Create-task is a first-party CLI preview only — no spend, no keys, no POST + +[View Plugin →](./taskmarket/) + ## Installation ### Claude Code @@ -63,6 +73,9 @@ claude plugin install bankr-agent-dev@bankr-claude-plugins # For bankr-x402-sdk-dev (Web3 development SDK) claude plugin install bankr-x402-sdk-dev@bankr-claude-plugins + +# For taskmarket (read-only Taskmarket discovery) +claude plugin install taskmarket@bankr-claude-plugins ``` ### Other Coding Tools (Cursor, OpenCode, Gemini CLI, Antigravity, etc.) diff --git a/taskmarket/.claude-plugin/plugin.json b/taskmarket/.claude-plugin/plugin.json new file mode 100644 index 0000000..b5b66bc --- /dev/null +++ b/taskmarket/.claude-plugin/plugin.json @@ -0,0 +1,10 @@ +{ + "name": "taskmarket", + "version": "1.0.0", + "description": "Read-only Taskmarket discovery for Bankr/Claude agents: browse open work, inspect a task, and track public submissions. Creating a task is a CLI preview only — no spend, no keys.", + "author": { + "name": "DuReef (community)", + "email": "hello@dureef.com" + }, + "keywords": ["taskmarket", "bounty", "agents", "delegation", "base", "usdc"] +} diff --git a/taskmarket/.mcp.json b/taskmarket/.mcp.json new file mode 100644 index 0000000..9d931b1 --- /dev/null +++ b/taskmarket/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "taskmarket": { + "command": "node", + "args": ["${CLAUDE_PLUGIN_ROOT}/mcp-server/src/index.js"] + } + } +} diff --git a/taskmarket/README.md b/taskmarket/README.md new file mode 100644 index 0000000..c8f0c54 --- /dev/null +++ b/taskmarket/README.md @@ -0,0 +1,55 @@ +# Taskmarket plugin for Claude Code + +Read-only [Taskmarket](https://taskmarket.dev) discovery inside Bankr's Claude plugin marketplace. + +Agents can **browse open work**, **inspect a task**, and **list public submissions**. Creating a bounty is a **CLI preview only** — this plugin never spends USDC, never holds keys, and never POSTs to the API. + +This is a community contribution. It does not impersonate Bankr or Taskmarket, and it is not affiliated with Base or Coinbase. + +## Why it belongs here + +Bankr already gives Claude Code agents wallets, DeFi, and x402. Taskmarket is the complementary **delegation** surface: when a request is better done by an external worker than by burning more local inference, the agent can discover funded work (and a human can post work via the first-party CLI). + +Pre-check (2026-08-17): this marketplace had `bankr-agent`, `bankr-agent-dev`, and `bankr-x402-sdk-dev` only — no Taskmarket plugin. + +## Tools + +| Tool | HTTP | Spend | +|------|------|--------| +| `taskmarket_list_tasks` | GET `/api/tasks` | none | +| `taskmarket_get_task` | GET `/api/tasks/{id}` | none | +| `taskmarket_list_submissions` | GET `/api/tasks/{id}/submissions` | none | +| `taskmarket_create_preview` | none (prints CLI) | none | + +Host allowlist: `https://api.taskmarket.dev` only. + +## Install + +```bash +claude plugin marketplace add BankrBot/claude-plugins +claude plugin install taskmarket@bankr-claude-plugins +``` + +Requires Node.js 18+. No API key. No bun. No `BANKR_API_KEY`. + +## Tests + +```bash +cd taskmarket/mcp-server +node --test +``` + +## Create a task (human + official CLI) + +```bash +npm i -g @lucid-agents/taskmarket +taskmarket task create --description "..." --reward 50 --duration-hours 72 --mode bounty +``` + +Wallet import and x402 stay in that CLI. Do not paste keys into Claude. + +## Links + +- Taskmarket: https://taskmarket.dev +- Docs: https://docs.taskmarket.dev +- Bankr: https://bankr.bot diff --git a/taskmarket/mcp-server/package.json b/taskmarket/mcp-server/package.json new file mode 100644 index 0000000..466718e --- /dev/null +++ b/taskmarket/mcp-server/package.json @@ -0,0 +1,15 @@ +{ + "name": "taskmarket-mcp-server", + "version": "1.0.0", + "description": "GET-only Taskmarket MCP server (no wallet, no spend)", + "type": "module", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "test": "node --test src/*.test.js" + }, + "engines": { + "node": ">=18.0.0" + }, + "license": "MIT" +} diff --git a/taskmarket/mcp-server/src/client.js b/taskmarket/mcp-server/src/client.js new file mode 100644 index 0000000..7b67f78 --- /dev/null +++ b/taskmarket/mcp-server/src/client.js @@ -0,0 +1,76 @@ +import { + assertGetOnly, + assertTaskId, + clampLimit, + resolveApiOrigin, +} from "./safety.js"; + +async function getJson(origin, path, { fetchImpl = fetch } = {}) { + assertGetOnly("GET"); + const url = `${origin}${path}`; + const response = await fetchImpl(url, { + method: "GET", + headers: { Accept: "application/json" }, + }); + const text = await response.text(); + if (!response.ok) { + throw new Error(`Taskmarket GET ${path} failed: ${response.status} ${text.slice(0, 240)}`); + } + try { + return JSON.parse(text); + } catch { + throw new Error(`Taskmarket GET ${path} returned non-JSON`); + } +} + +export function createClient({ env = process.env, fetchImpl = fetch } = {}) { + const origin = resolveApiOrigin(env); + + return { + origin, + async listTasks({ status = "open", limit = 20 } = {}) { + const capped = clampLimit(limit); + const params = new URLSearchParams({ + status: String(status || "open"), + limit: String(capped), + }); + return getJson(origin, `/api/tasks?${params}`, { fetchImpl }); + }, + async getTask(taskId) { + const id = assertTaskId(taskId); + return getJson(origin, `/api/tasks/${id}`, { fetchImpl }); + }, + async listSubmissions(taskId) { + const id = assertTaskId(taskId); + return getJson(origin, `/api/tasks/${id}/submissions`, { fetchImpl }); + }, + createPreview({ description, rewardUsdc, durationHours = 72 } = {}) { + const desc = String(description || "").trim(); + const reward = Number(rewardUsdc); + const hours = Number(durationHours); + if (!desc) { + throw new Error("description is required for create_preview"); + } + if (!Number.isFinite(reward) || reward <= 0) { + throw new Error("rewardUsdc must be a positive number"); + } + if (!Number.isFinite(hours) || hours < 1) { + throw new Error("durationHours must be >= 1"); + } + const cmd = [ + "taskmarket task create", + `--description ${JSON.stringify(desc)}`, + `--reward ${reward}`, + `--duration-hours ${Math.trunc(hours)}`, + "--mode bounty", + ].join(" "); + return { + previewOnly: true, + fetched: false, + instruction: + "This plugin never creates tasks or spends USDC. Run the first-party CLI yourself after an explicit human confirm. Wallet keys stay in the CLI.", + command: cmd, + }; + }, + }; +} diff --git a/taskmarket/mcp-server/src/client.test.js b/taskmarket/mcp-server/src/client.test.js new file mode 100644 index 0000000..792a93b --- /dev/null +++ b/taskmarket/mcp-server/src/client.test.js @@ -0,0 +1,85 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { createClient } from "./client.js"; +import { assertHttpsApiOrigin, assertGetOnly } from "./safety.js"; + +function mockFetch(handler) { + return async (url, init = {}) => { + assert.equal(init.method, "GET"); + return handler(String(url), init); + }; +} + +function jsonResponse(body, status = 200) { + return { + ok: status >= 200 && status < 300, + status, + async text() { + return JSON.stringify(body); + }, + }; +} + +test("listTasks uses public GET /api/tasks and caps limit", async () => { + const seen = []; + const client = createClient({ + fetchImpl: mockFetch((url) => { + seen.push(url); + return jsonResponse({ tasks: [{ id: "0x" + "ab".repeat(32) }] }); + }), + }); + const out = await client.listTasks({ status: "open", limit: 99 }); + assert.equal(out.tasks.length, 1); + assert.match(seen[0], /^https:\/\/api\.taskmarket\.dev\/api\/tasks\?/); + assert.match(seen[0], /limit=20/); +}); + +test("getTask and listSubmissions hit allowlisted GET paths", async () => { + const id = "0x" + "cd".repeat(32); + const seen = []; + const client = createClient({ + fetchImpl: mockFetch((url) => { + seen.push(url); + return jsonResponse({ ok: true, url }); + }), + }); + await client.getTask(id); + await client.listSubmissions(id); + assert.deepEqual(seen, [ + `https://api.taskmarket.dev/api/tasks/${id}`, + `https://api.taskmarket.dev/api/tasks/${id}/submissions`, + ]); +}); + +test("createPreview never fetches", () => { + let fetches = 0; + const client = createClient({ + fetchImpl: async () => { + fetches += 1; + throw new Error("should not fetch"); + }, + }); + const preview = client.createPreview({ + description: "Audit one GitHub Actions pipeline", + rewardUsdc: 50, + durationHours: 48, + }); + assert.equal(preview.previewOnly, true); + assert.equal(preview.fetched, false); + assert.equal(fetches, 0); + assert.match(preview.command, /taskmarket task create/); + assert.doesNotMatch(preview.command, /PRIVATE|0x[0-9a-fA-F]{64}/); +}); + +test("rejects http and foreign hosts", () => { + assert.throws(() => assertHttpsApiOrigin("http://api.taskmarket.dev"), /https/); + assert.throws(() => assertHttpsApiOrigin("https://evil.example"), /host/); + assert.throws(() => assertGetOnly("POST"), /GET-only/); + assert.throws( + () => + createClient({ + env: { TASKMARKET_API_URL: "https://example.com" }, + }), + /host/, + ); +}); diff --git a/taskmarket/mcp-server/src/index.js b/taskmarket/mcp-server/src/index.js new file mode 100644 index 0000000..36ac6b0 --- /dev/null +++ b/taskmarket/mcp-server/src/index.js @@ -0,0 +1,156 @@ +#!/usr/bin/env node + +import { stdin, stdout } from "node:process"; +import { createClient } from "./client.js"; + +const TOOLS = [ + { + name: "taskmarket_list_tasks", + description: + "Browse open Taskmarket work (public GET). Default status=open, limit capped at 20.", + inputSchema: { + type: "object", + properties: { + status: { type: "string", description: "Task status filter (default open)" }, + limit: { type: "number", description: "Max rows, capped at 20" }, + }, + }, + }, + { + name: "taskmarket_get_task", + description: "Inspect one Taskmarket task by 0x id (public GET).", + inputSchema: { + type: "object", + required: ["taskId"], + properties: { + taskId: { type: "string", description: "0x-prefixed 32-byte task id" }, + }, + }, + }, + { + name: "taskmarket_list_submissions", + description: "List public submissions for a task so a human can review them (GET).", + inputSchema: { + type: "object", + required: ["taskId"], + properties: { + taskId: { type: "string" }, + }, + }, + }, + { + name: "taskmarket_create_preview", + description: + "Print a first-party CLI command to create a bounty. Does not fetch, sign, or spend. Human must run the CLI after explicit confirm.", + inputSchema: { + type: "object", + required: ["description", "rewardUsdc"], + properties: { + description: { type: "string" }, + rewardUsdc: { type: "number" }, + durationHours: { type: "number" }, + }, + }, + }, +]; + +function send(message) { + const json = JSON.stringify(message); + stdout.write(`Content-Length: ${Buffer.byteLength(json)}\r\n\r\n${json}`); +} + +function textResult(payload) { + return { + content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], + }; +} + +function errorResult(err) { + return { + content: [{ type: "text", text: String(err?.message || err) }], + isError: true, + }; +} + +async function callTool(name, args) { + const client = createClient(); + switch (name) { + case "taskmarket_list_tasks": + return textResult(await client.listTasks(args || {})); + case "taskmarket_get_task": + return textResult(await client.getTask(args?.taskId)); + case "taskmarket_list_submissions": + return textResult(await client.listSubmissions(args?.taskId)); + case "taskmarket_create_preview": + return textResult(client.createPreview(args || {})); + default: + throw new Error(`Unknown tool: ${name}`); + } +} + +async function handle(message) { + if (message.method === "initialize") { + send({ + jsonrpc: "2.0", + id: message.id, + result: { + protocolVersion: "2024-11-05", + capabilities: { tools: {} }, + serverInfo: { name: "taskmarket", version: "1.0.0" }, + }, + }); + return; + } + if (message.method === "notifications/initialized") { + return; + } + if (message.method === "tools/list") { + send({ jsonrpc: "2.0", id: message.id, result: { tools: TOOLS } }); + return; + } + if (message.method === "tools/call") { + try { + const result = await callTool(message.params?.name, message.params?.arguments || {}); + send({ jsonrpc: "2.0", id: message.id, result }); + } catch (err) { + send({ jsonrpc: "2.0", id: message.id, result: errorResult(err) }); + } + return; + } + if (message.id !== undefined) { + send({ + jsonrpc: "2.0", + id: message.id, + error: { code: -32601, message: `Method not found: ${message.method}` }, + }); + } +} + +let buf = Buffer.alloc(0); +stdin.on("data", (chunk) => { + buf = Buffer.concat([buf, chunk]); + while (true) { + const headerEnd = buf.indexOf("\r\n\r\n"); + if (headerEnd === -1) { + return; + } + const header = buf.slice(0, headerEnd).toString("utf8"); + const match = header.match(/Content-Length:\s*(\d+)/i); + if (!match) { + buf = buf.slice(headerEnd + 4); + continue; + } + const len = Number(match[1]); + const bodyStart = headerEnd + 4; + if (buf.length < bodyStart + len) { + return; + } + const body = buf.slice(bodyStart, bodyStart + len).toString("utf8"); + buf = buf.slice(bodyStart + len); + handle(JSON.parse(body)).catch((err) => { + process.stderr.write(String(err?.stack || err) + "\n"); + }); + } +}); + +stdin.resume(); diff --git a/taskmarket/mcp-server/src/safety.js b/taskmarket/mcp-server/src/safety.js new file mode 100644 index 0000000..2a44105 --- /dev/null +++ b/taskmarket/mcp-server/src/safety.js @@ -0,0 +1,49 @@ +export const DEFAULT_API_ORIGIN = "https://api.taskmarket.dev"; +export const ALLOWED_HOST = "api.taskmarket.dev"; +export const MAX_LIST_LIMIT = 20; + +export function assertHttpsApiOrigin(origin) { + let url; + try { + url = new URL(origin); + } catch { + throw new Error("TASKMARKET_API_URL must be a valid URL"); + } + if (url.protocol !== "https:") { + throw new Error("TASKMARKET_API_URL must use https"); + } + if (url.hostname !== ALLOWED_HOST) { + throw new Error(`TASKMARKET_API_URL host must be ${ALLOWED_HOST}`); + } + if (url.username || url.password) { + throw new Error("TASKMARKET_API_URL must not include credentials"); + } + return `${url.protocol}//${url.host}`; +} + +export function resolveApiOrigin(env = process.env) { + const raw = env.TASKMARKET_API_URL || DEFAULT_API_ORIGIN; + return assertHttpsApiOrigin(raw); +} + +export function assertGetOnly(method) { + if (String(method).toUpperCase() !== "GET") { + throw new Error("Taskmarket plugin client is GET-only"); + } +} + +export function clampLimit(limit) { + const n = Number.parseInt(String(limit ?? MAX_LIST_LIMIT), 10); + if (!Number.isFinite(n) || n < 1) { + return MAX_LIST_LIMIT; + } + return Math.min(n, MAX_LIST_LIMIT); +} + +export function assertTaskId(taskId) { + const id = String(taskId || "").trim(); + if (!/^0x[0-9a-fA-F]{64}$/.test(id)) { + throw new Error("taskId must be a 0x-prefixed 32-byte hex id"); + } + return id; +} diff --git a/taskmarket/skills/taskmarket/SKILL.md b/taskmarket/skills/taskmarket/SKILL.md new file mode 100644 index 0000000..d681c2b --- /dev/null +++ b/taskmarket/skills/taskmarket/SKILL.md @@ -0,0 +1,37 @@ +--- +name: Taskmarket +description: Browse and inspect Taskmarket work (USDC on Base) from Claude Code. Discover open bounties, inspect a task, and list public submissions. Never spend, claim, or create tasks from this plugin. +version: 1.0.0 +--- + +# Taskmarket (read-only) + +Use this skill when a request is better **delegated to external workers** on [Taskmarket](https://taskmarket.dev) than burned as local inference. + +## What this plugin does + +- `taskmarket_list_tasks` — browse open work +- `taskmarket_get_task` — inspect one task +- `taskmarket_list_submissions` — present public submissions for human review +- `taskmarket_create_preview` — print a first-party CLI command; **does not submit** + +## What this plugin never does + +- Spend USDC, import wallets, or touch private keys +- POST/PUT/PATCH/DELETE against the API +- Auto-claim, auto-accept, or create tasks for third parties +- Bypass wallet permissions or x402 payment flow + +## Create-task rule + +If the human wants to post work, call `taskmarket_create_preview`, show the command, and **wait for an explicit confirm**. Then they run the official CLI themselves: + +```bash +npm i -g @lucid-agents/taskmarket +taskmarket task create --description "..." --reward 50 --duration-hours 72 --mode bounty +``` + +## Docs + +- https://taskmarket.dev/ +- https://docs.taskmarket.dev/