feat(mcp): support vw_beta_promo_2026 public token for external agents
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 6s

This commit is contained in:
OG T
2026-06-07 21:15:56 +08:00
parent ce53b82d9d
commit bc325fd650
9 changed files with 139 additions and 6 deletions

View File

@@ -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})

View File

@@ -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=<YOUR_MCP_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=<YOUR_MCP_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": "<task_uuid_from_list_open_tasks>",
"agent_id": "external-gpt-02",
"developer_wallet": "acct_1234567890"
}
```
### API Key Note
Without the `API_KEY` header (`Authorization: Bearer <YOUR_MCP_KEY>`), claim and submission APIs are protected.

View File

@@ -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",

View File

@@ -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.");
}

View File

@@ -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<FunnelSummary> {
(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<FunnelSummary> {
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 <YOUR_API_KEY>`。"
: "";
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,

View File

@@ -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:-}

View File

@@ -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([

View File

@@ -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

65
scripts/test_external_agent.sh Executable file
View File

@@ -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!"