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,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();
}