From bc325fd6506f31a546ecb65a724d2661b4920d28 Mon Sep 17 00:00:00 2001 From: OG T Date: Sun, 7 Jun 2026 21:15:56 +0800 Subject: [PATCH] feat(mcp): support vw_beta_promo_2026 public token for external agents --- apps/scout-bot/src/index.ts | 2 +- apps/web/public/llms.txt | 22 ++++++- apps/web/src/app/api/mcp/[tool]/route.ts | 7 +- apps/web/src/lib/payment.ts | 14 ++++ .../web/src/lib/traffic-conversion-monitor.ts | 27 +++++++- docker-compose.yml | 1 + packages/contracts/src/schemas/index.ts | 2 +- packages/contracts/tests/schemas.test.ts | 5 ++ scripts/test_external_agent.sh | 65 +++++++++++++++++++ 9 files changed, 139 insertions(+), 6 deletions(-) create mode 100755 scripts/test_external_agent.sh diff --git a/apps/scout-bot/src/index.ts b/apps/scout-bot/src/index.ts index 177ce9d..0a56190 100644 --- a/apps/scout-bot/src/index.ts +++ b/apps/scout-bot/src/index.ts @@ -267,7 +267,7 @@ async function processIssue(owner: string, repo: string, issue: any) { AI Agent 可以直接透過以下指令接入並接案: \`\`\`bash -npx -y @agent-bounty/mcp-server --endpoint https://agent.wooo.work +API_KEY=vw_beta_promo_2026 npx -y @agent-bounty/mcp-server --endpoint https://agent.wooo.work \`\`\` 🔗 [查看任務與獎金池狀態 (${draft.status})](${draft.checkout_url}) diff --git a/apps/web/public/llms.txt b/apps/web/public/llms.txt index 42916c2..cbd2f8b 100644 --- a/apps/web/public/llms.txt +++ b/apps/web/public/llms.txt @@ -14,10 +14,10 @@ Use `/api/open-tasks` to find currently open bounties and their payout mode. You do not need to register on any website or create a human account. You can interact with our task pool entirely through our official MCP Server. ### Instructions for AI Agents: -Execute the following MCP server command in your environment (or instruct your human operator to configure it for you): +Execute the following MCP server command in your environment (or instruct your human operator to configure it for you). Make sure to pass the public beta token: ```bash -API_KEY= API_BASE_URL=https://agent.wooo.work npx -y @agent-bounty/mcp-server +API_KEY=vw_beta_promo_2026 npx -y @agent-bounty/mcp-server --endpoint https://agent.wooo.work ``` ### Standard Claim Contract (MCP) @@ -30,6 +30,24 @@ API_KEY= API_BASE_URL=https://agent.wooo.work npx -y @agent-bounty } ``` +### MCP `list_open_tasks` Example Payload + +You can send empty payload to discover open tasks (the endpoint will fallback to defaults): + +```json +{} +``` + +### MCP `claim_task` Example Payload + +```json +{ + "task_id": "", + "agent_id": "external-gpt-02", + "developer_wallet": "acct_1234567890" +} +``` + ### API Key Note Without the `API_KEY` header (`Authorization: Bearer `), claim and submission APIs are protected. diff --git a/apps/web/src/app/api/mcp/[tool]/route.ts b/apps/web/src/app/api/mcp/[tool]/route.ts index e169245..f6ba5d5 100644 --- a/apps/web/src/app/api/mcp/[tool]/route.ts +++ b/apps/web/src/app/api/mcp/[tool]/route.ts @@ -276,7 +276,12 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool } const token = authHeader.split(" ")[1]; - if (process.env.API_KEY && token !== process.env.API_KEY) { + + // 試營運推廣期間,允許外部 Agent 使用這組公版 Token 來接案 + const isBetaToken = token === "vw_beta_promo_2026"; + const isValidServerKey = process.env.API_KEY && token === process.env.API_KEY; + + if (!isValidServerKey && !isBetaToken) { void sendTrafficAlert({ level: "warning", action: "EXTERNAL_MCP_AUTH_FORBIDDEN", diff --git a/apps/web/src/lib/payment.ts b/apps/web/src/lib/payment.ts index 238fd73..dca4b05 100644 --- a/apps/web/src/lib/payment.ts +++ b/apps/web/src/lib/payment.ts @@ -5,6 +5,7 @@ import Stripe from "stripe"; const stripe = process.env.STRIPE_SECRET_KEY ? new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-05-27.dahlia", // Use latest or your specific API version }) : null; +const ALLOW_MCP_CLAIM_WITHOUT_STRIPE = process.env.ALLOW_MCP_CLAIM_WITHOUT_STRIPE === "true"; export async function authHold( tx: Prisma.TransactionClient, @@ -26,6 +27,19 @@ export async function authHold( // Check if we already have a payment intent for this task const task = await tx.task.findUnique({ where: { id: taskId } }); if (!task || !task.stripe_payment_intent_id) { + if (ALLOW_MCP_CLAIM_WITHOUT_STRIPE) { + return await tx.ledgerEntry.create({ + data: { + task_id: taskId, + phase: "AUTH_HOLD", + idempotency_key: idempotencyKey, + stripe_object_id: null, + response_status: "SKIPPED_NO_STRIPE_INTENT", + http_status: 200, + }, + }); + } + throw new Error("Task does not have a stripe_payment_intent_id. It must be created via Scout checkout first."); } diff --git a/apps/web/src/lib/traffic-conversion-monitor.ts b/apps/web/src/lib/traffic-conversion-monitor.ts index d03fa46..9b9bedb 100644 --- a/apps/web/src/lib/traffic-conversion-monitor.ts +++ b/apps/web/src/lib/traffic-conversion-monitor.ts @@ -10,6 +10,8 @@ type FunnelSummary = { judgeFailEvents: number; openTaskCount: number; sampleOpenTasks: string[]; + mcpAuthMissingEvents: number; + mcpAuthForbiddenEvents: number; externalOpenedActors: number; externalClaimingActors: number; externalSubmittingActors: number; @@ -172,6 +174,8 @@ async function fetchFunnelSummary(minutes: number): Promise { (actionSummary["EXTERNAL_LIST_OPEN_TASKS_MCP"] || 0); const claimEvents = actionSummary["EXTERNAL_CLAIM_TASK_SUCCESS"] || 0; const submitEvents = actionSummary["EXTERNAL_SUBMIT_SOLUTION_SUCCESS"] || 0; + const mcpAuthMissingEvents = actionSummary["EXTERNAL_MCP_AUTH_MISSING"] || 0; + const mcpAuthForbiddenEvents = actionSummary["EXTERNAL_MCP_AUTH_FORBIDDEN"] || 0; const judgePassEvents = judgeRows.filter((row) => { const metadata = asRecordJson(row.metadata); @@ -232,6 +236,8 @@ async function fetchFunnelSummary(minutes: number): Promise { judgeFailEvents, openTaskCount, sampleOpenTasks, + mcpAuthMissingEvents, + mcpAuthForbiddenEvents, externalOpenedActors, externalClaimingActors, externalSubmittingActors, @@ -250,6 +256,8 @@ function buildAlertMessage(rule: string, summary: FunnelSummary) { claimEvents, submitEvents, judgePassEvents, + mcpAuthMissingEvents, + mcpAuthForbiddenEvents, payoutCaptured, openTaskCount, sampleOpenTasks, @@ -260,10 +268,24 @@ function buildAlertMessage(rule: string, summary: FunnelSummary) { topOpenOnlyActors, } = summary; + const authBarrierEvents = mcpAuthMissingEvents + mcpAuthForbiddenEvents; + const topActorSummary = topOpenOnlyActors + .slice(0, 3) + .map((actor) => `${actor.actorId}(${actor.opens})`) + .join(", "); + const authBarrierHint = + authBarrierEvents > 0 + ? `偵測到 MCP 權限攔截: AUTH missing=${mcpAuthMissingEvents},FORBIDDEN=${mcpAuthForbiddenEvents}。` + + `請先確認外部 agent 是否已帶 ` + + "`Authorization: Bearer `。" + : ""; + switch (rule) { case "EXTERNAL_FUNNEL_CLAIM_STALL": return `外部曝光已達 ${discoveryEvents}(最近 ${periodMinutes} 分鐘),待接任務 ${openTaskCount} 筆,但尚無接案(EXTERNAL_CLAIM_TASK_SUCCESS = ${claimEvents})。` + `${sampleOpenTasks.length > 0 ? `可用任務樣本: ${sampleOpenTasks.join(", ")}。` : ""}` + + `${topActorSummary ? `高活躍 Actor(尚未接案): ${topActorSummary}。` : ""}` + + `${authBarrierHint}` + `請檢查任務是否包含可直接執行的 npx 指令與明確交付條件。`; case "EXTERNAL_FUNNEL_SUBMIT_STALL": return `外部已有 ${claimEvents} 筆接案,但近期 ${periodMinutes} 分鐘無任何提交(EXTERNAL_SUBMIT_SOLUTION_SUCCESS = ${submitEvents})。請先加速回傳格式與驗收測試規格。`; @@ -276,7 +298,8 @@ function buildAlertMessage(rule: string, summary: FunnelSummary) { `外部 Actor= ${externalOpenedActors} 位,` + `已接案=${externalClaimingActors}、已提交=${externalSubmittingActors},` + `仍停在曝光僅曝光階段 ${externalOnlyOpenActors} 位。` + - `${topOpenOnlyActors.length ? `先看未進一步的熱門 Actor:${topOpenOnlyActors.map((actor) => `${actor.actorId}(${actor.opens})`).join(", ")}。` : ""}`; + `${topActorSummary ? `先看未進一步的熱門 Actor:${topActorSummary}。` : ""}` + + `${authBarrierHint}`; default: return "外部 AI 流量轉化斷崖異常。"; } @@ -374,6 +397,8 @@ export async function evaluateExternalFunnelHealth(input: MonitorInput): Promise external_claiming_actors: summary.externalClaimingActors, external_submitting_actors: summary.externalSubmittingActors, external_only_open_actors: summary.externalOnlyOpenActors, + mcp_auth_missing_events: summary.mcpAuthMissingEvents, + mcp_auth_forbidden_events: summary.mcpAuthForbiddenEvents, payout_captured: summary.payoutCaptured, payout_released: summary.payoutReleased, period_minutes: summary.periodMinutes, diff --git a/docker-compose.yml b/docker-compose.yml index afdd375..0f48ade 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -51,6 +51,7 @@ services: - API_KEY=${API_KEY:-super-secret-mcp-key} - E2B_API_KEY=${E2B_API_KEY:-""} - AUTO_WHITELIST_EXTERNAL_AGENTS=${AUTO_WHITELIST_EXTERNAL_AGENTS:-true} + - ALLOW_MCP_CLAIM_WITHOUT_STRIPE=${ALLOW_MCP_CLAIM_WITHOUT_STRIPE:-false} - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-} - TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-} - TRAFFIC_MONITOR_TOKEN=${TRAFFIC_MONITOR_TOKEN:-} diff --git a/packages/contracts/src/schemas/index.ts b/packages/contracts/src/schemas/index.ts index f3248f7..facc6cd 100644 --- a/packages/contracts/src/schemas/index.ts +++ b/packages/contracts/src/schemas/index.ts @@ -349,7 +349,7 @@ export const LeadSchema = z.object({ export const ListOpenTasksRequestSchema = z.object({ skills: z .array(z.string().min(1).max(50)) - .min(1, "至少需要指定一個技能"), + .default([]), limit: z.number().int().min(1).max(20).default(5), difficulty: z .enum([ diff --git a/packages/contracts/tests/schemas.test.ts b/packages/contracts/tests/schemas.test.ts index 561bec7..90d07c4 100644 --- a/packages/contracts/tests/schemas.test.ts +++ b/packages/contracts/tests/schemas.test.ts @@ -12,6 +12,11 @@ describe("Contracts Zod Validation", () => { expect(result.success).toBe(true); }); + it("should validate empty ListOpenTasks payload", () => { + const result = ListOpenTasksRequestSchema.safeParse({}); + expect(result.success).toBe(true); + }); + it("should reject invalid ListOpenTasks payload", () => { const payload = { // limit is a string instead of number diff --git a/scripts/test_external_agent.sh b/scripts/test_external_agent.sh new file mode 100755 index 0000000..16485d0 --- /dev/null +++ b/scripts/test_external_agent.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +# VibeWork External Agent MCP API Test Script +# This script simulates an external AI agent discovering the network via the Beta Promo token. + +API_URL=${1:-"https://agent.wooo.work"} +API_KEY="vw_beta_promo_2026" +AGENT_ID="test_agent_$(date +%s)" + +echo "🚀 Simulating External AI Agent connecting to $API_URL" +echo "🔑 Using Public Beta Token: $API_KEY" +echo "🤖 Agent ID: $AGENT_ID" +echo "---------------------------------------------------" + +# 1. Fetch Open Tasks +echo "👉 1. Fetching Open Tasks (list_open_tasks)..." +TASKS_RESPONSE=$(curl -s -X POST "$API_URL/api/mcp/list_open_tasks" \ + -H "Authorization: Bearer $API_KEY" \ + -H "Content-Type: application/json" \ + -H "x-agent-id: $AGENT_ID" \ + -d '{"skills": []}') + +TOTAL_OPEN=$(echo $TASKS_RESPONSE | grep -o '"total_open":[0-9]*' | cut -d':' -f2) + +if [ -z "$TOTAL_OPEN" ]; then + echo "❌ Error parsing list_open_tasks response:" + echo $TASKS_RESPONSE + exit 1 +fi + +echo "✅ Found $TOTAL_OPEN open tasks." + +if [ "$TOTAL_OPEN" -eq "0" ]; then + echo "⚠️ No open tasks available to claim. Stopping test." + exit 0 +fi + +# 2. Extract first task ID +TASK_ID=$(echo $TASKS_RESPONSE | grep -o '"task_id":"[^"]*"' | head -n 1 | cut -d'"' -f4) +echo "🎯 Selecting Task ID: $TASK_ID" + +# 3. Claim the task +echo "👉 2. Claiming Task (claim_task)..." +CLAIM_RESPONSE=$(curl -s -X POST "$API_URL/api/mcp/claim_task" \ + -H "Authorization: Bearer $API_KEY" \ + -H "Content-Type: application/json" \ + -H "x-agent-id: $AGENT_ID" \ + -d '{ + "task_id": "'"$TASK_ID"'", + "agent_id": "'"$AGENT_ID"'", + "developer_wallet": "0xTestWalletExternalAgent999" + }') + +CLAIM_STATUS=$(echo $CLAIM_RESPONSE | grep -o '"status":"[^"]*"' | cut -d'"' -f4) +ERROR_MSG=$(echo $CLAIM_RESPONSE | grep -o '"error":"[^"]*"' | cut -d'"' -f4) + +if [ -n "$ERROR_MSG" ]; then + echo "❌ Failed to claim task: $ERROR_MSG" + exit 1 +fi + +echo "✅ Task claimed successfully! Status: $CLAIM_STATUS" + +# We won't submit solution because we don't have the real sandbox context. +echo "🎉 Test pipeline passed!"