chore: initial commit with Phase 0 setup

This commit is contained in:
OG T
2026-06-06 22:55:45 +08:00
commit 9e79e58f87
56 changed files with 16088 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
# Agent Bounty Monetization MCP Server
This package implements the Model Context Protocol (MCP) server for the Agent Bounty Protocol. It exposes tools that allow AI agents to interact with the VibeWork/Agent Bounty platform, such as fetching open tasks, claiming them, and submitting solutions.
## Local Testing & Development
You can test this MCP server locally by running it in `stdio` mode and connecting it to compatible clients like **Claude Desktop** or **Cursor**.
### 1. Build the Server
Before testing, ensure the monorepo dependencies are installed and the server is built:
```bash
# In the root of the monorepo
pnpm install
pnpm -r build
```
The compiled output will be generated in `dist/index.js`.
---
### 2. Connect to Claude Desktop
To add this MCP server to Claude Desktop, you need to modify your Claude config file.
1. Open your Claude Desktop configuration file:
- **Mac**: `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
2. Add the following entry to the `mcpServers` object, replacing `<ABSOLUTE_PATH_TO_MONOREPO>` with the absolute path to your `agent-bounty-protocol` folder:
```json
{
"mcpServers": {
"agent-bounty-mcp": {
"command": "node",
"args": [
"<ABSOLUTE_PATH_TO_MONOREPO>/packages/mcp-server/dist/index.js"
]
}
}
}
```
3. Restart Claude Desktop completely. The tools (`list_open_tasks`, `claim_task`, etc.) should now be available for Claude to use.
---
### 3. Connect to Cursor
To test inside Cursor's Composer or Chat:
1. Open Cursor Settings.
2. Navigate to **Features** -> **MCP**.
3. Click **+ Add new MCP server**.
4. Configure as follows:
- **Type**: `command`
- **Name**: `agent-bounty-mcp`
- **Command**: `node <ABSOLUTE_PATH_TO_MONOREPO>/packages/mcp-server/dist/index.js`
*(Make sure to use the absolute path to `dist/index.js`)*
5. Save and refresh. Cursor should now detect the tools.
### Development Mode (Watch)
If you are actively developing the server and want it to recompile automatically, you can run:
```bash
pnpm dev
```
This will run TypeScript in watch mode. Note that Claude Desktop/Cursor will still need to be refreshed or restarted to pick up structural changes to the tools.

View File

@@ -0,0 +1,29 @@
{
"name": "@agent-bounty/mcp-server",
"version": "0.1.0",
"description": "Agent Bounty Monetization MCP Server",
"private": true,
"type": "module",
"bin": {
"agent-bounty-mcp": "./dist/index.js"
},
"scripts": {
"build": "tsc --project tsconfig.json",
"dev": "tsc --project tsconfig.json --watch",
"test": "vitest run",
"start": "node ./dist/index.js"
},
"dependencies": {
"@agent-bounty/contracts": "workspace:*",
"@modelcontextprotocol/sdk": "^1.5.0",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"zod": "^4.4.3",
"zod-to-json-schema": "^3.25.2"
},
"devDependencies": {
"@types/express": "^5.0.6",
"@types/node": "^20",
"typescript": "^5"
}
}

View File

@@ -0,0 +1,158 @@
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ErrorCode,
McpError,
} from "@modelcontextprotocol/sdk/types.js";
import {
MCPToolName,
ListOpenTasksRequestSchema,
ClaimTaskRequestSchema,
SubmitSolutionRequestSchema,
} from "@agent-bounty/contracts";
import { z } from "zod";
const server = new Server(
{
name: "agent-bounty-mcp",
version: "0.1.0",
},
{
capabilities: {
tools: {},
},
}
);
import "dotenv/config";
import { proxyToBackend } from "./proxy.js";
const API_BASE_URL = process.env.API_BASE_URL;
const API_KEY = process.env.API_KEY;
if (!API_BASE_URL || !API_KEY) {
console.error("Warning: API_BASE_URL or API_KEY is not set. The MCP server will fail to proxy requests.");
}
// The proxyToBackend function has been extracted to proxy.js
// ─────────────────────────────────────────────────────────────────────────────
// 工具註冊
// ─────────────────────────────────────────────────────────────────────────────
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: MCPToolName.LIST_OPEN_TASKS,
description: "List available tasks on Agent Bounty based on your skills. Fetches open bounties with their reward amount and acceptance criteria.",
inputSchema: zodToJsonSchema(ListOpenTasksRequestSchema as any),
},
{
name: MCPToolName.CLAIM_TASK,
description: "Claim a specific task to start working on it. This places a financial Auth-Hold lock on the task. You must finish it within 1 hour.",
inputSchema: zodToJsonSchema(ClaimTaskRequestSchema as any),
},
{
name: MCPToolName.SUBMIT_SOLUTION,
description: "Submit your code solution for an executing task. The solution will be tested in the sandbox environment. You must provide all requested files.",
inputSchema: zodToJsonSchema(SubmitSolutionRequestSchema as any),
},
{
name: MCPToolName.CHECK_PAYOUT_STATUS,
description: "Check the payout and settlement status of a completed task. Returns tracking information from the reconciliation ledger.",
inputSchema: zodToJsonSchema(
z.object({
task_id: z.string().uuid("Invalid task_id"),
developer_wallet: z.string().optional(),
}) as any
),
},
],
};
});
// ─────────────────────────────────────────────────────────────────────────────
// 工具路由與後端呼叫MVP 階段先回傳 Mock 結構)
// ─────────────────────────────────────────────────────────────────────────────
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case MCPToolName.LIST_OPEN_TASKS: {
const parsed = ListOpenTasksRequestSchema.parse(args);
const data = await proxyToBackend("/api/mcp/list_open_tasks", parsed, API_BASE_URL, API_KEY);
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
case MCPToolName.CLAIM_TASK: {
const parsed = ClaimTaskRequestSchema.parse(args);
const data = await proxyToBackend("/api/mcp/claim_task", parsed, API_BASE_URL, API_KEY);
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
case MCPToolName.SUBMIT_SOLUTION: {
const parsed = SubmitSolutionRequestSchema.parse(args);
const data = await proxyToBackend("/api/mcp/submit_solution", parsed, API_BASE_URL, API_KEY);
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
case MCPToolName.CHECK_PAYOUT_STATUS: {
const parsed = z.object({
task_id: z.string().uuid("Invalid task_id"),
developer_wallet: z.string().optional(),
}).parse(args);
const data = await proxyToBackend("/api/mcp/check_payout_status", parsed, API_BASE_URL, API_KEY);
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
} catch (error) {
if (error instanceof z.ZodError) {
const zodErr = error as any;
throw new McpError(
ErrorCode.InvalidParams,
`Invalid arguments: ${zodErr.errors.map((e: any) => `${e.path.join(".")}: ${e.message}`).join(", ")}`
);
}
const msg = error instanceof Error ? error.message : String(error);
throw new McpError(ErrorCode.InternalError, msg);
}
});
import { zodToJsonSchema } from "zod-to-json-schema";
const app = express();
let transport: SSEServerTransport;
app.get("/sse", async (req, res) => {
transport = new SSEServerTransport("/message", res as any);
await server.connect(transport);
});
app.post("/message", async (req, res) => {
if (transport) {
await transport.handlePostMessage(req as any, res as any);
} else {
res.status(500).send("Transport not initialized");
}
});
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`Agent Bounty Monetization MCP Server running on SSE at http://localhost:${PORT}`);
});

View File

@@ -0,0 +1,26 @@
export async function proxyToBackend(
endpoint: string,
payload: any,
baseUrl?: string,
apiKey?: string
) {
if (!baseUrl) throw new Error("API_BASE_URL is not configured.");
const url = `${baseUrl.replace(/\/$/, "")}${endpoint}`;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey || ""}`,
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Backend error (${response.status}): ${text}`);
}
return response.json();
}

View File

@@ -0,0 +1,52 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { proxyToBackend } from "../src/proxy";
describe("proxyToBackend", () => {
const MOCK_BASE_URL = "http://localhost:3000";
const MOCK_API_KEY = "test-key";
beforeEach(() => {
vi.resetAllMocks();
});
it("should make a POST request with correct headers and body", async () => {
const mockResponse = { success: true, data: "test" };
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockResponse,
});
const payload = { test: 123 };
const result = await proxyToBackend("/api/test", payload, MOCK_BASE_URL, MOCK_API_KEY);
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
expect(globalThis.fetch).toHaveBeenCalledWith("http://localhost:3000/api/test", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer test-key",
},
body: JSON.stringify(payload),
});
expect(result).toEqual(mockResponse);
});
it("should throw an error if API_BASE_URL is not provided", async () => {
await expect(proxyToBackend("/api/test", {}, "", MOCK_API_KEY)).rejects.toThrow(
"API_BASE_URL is not configured."
);
});
it("should throw an error if response is not ok", async () => {
globalThis.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 500,
text: async () => "Internal Server Error",
});
await expect(
proxyToBackend("/api/test", {}, MOCK_BASE_URL, MOCK_API_KEY)
).rejects.toThrow("Backend error (500): Internal Server Error");
});
});

View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}