chore: initial commit with Phase 0 setup
This commit is contained in:
158
packages/mcp-server/src/index.ts
Normal file
158
packages/mcp-server/src/index.ts
Normal 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}`);
|
||||
});
|
||||
26
packages/mcp-server/src/proxy.ts
Normal file
26
packages/mcp-server/src/proxy.ts
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user