@getabrain/mcp-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +175 -0
- package/package.json +21 -0
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @getabrain/mcp-server
|
|
2
|
+
|
|
3
|
+
MCP server for [GetABrain.ai](https://getabrain.ai) — give your AI agent real human judgment as native tools.
|
|
4
|
+
|
|
5
|
+
## Use with Claude Desktop / Cursor
|
|
6
|
+
|
|
7
|
+
Add to your MCP client config (e.g. `claude_desktop_config.json`):
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"getabrain": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["-y", "@getabrain/mcp-server"],
|
|
15
|
+
"env": {
|
|
16
|
+
"GETABRAIN_API_KEY": "gab_k_…",
|
|
17
|
+
"GETABRAIN_API_SECRET": "gab_s_…"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Get your API key by signing up at https://getabrain.ai.
|
|
25
|
+
|
|
26
|
+
## Tools
|
|
27
|
+
|
|
28
|
+
- `get_balance` — check your prepaid balance.
|
|
29
|
+
- `submit_query` — ask real humans a question (13 query types: A/B test, rating, ranking, sentiment, yes/no, image/video/audio review, …). Returns a `query_id`.
|
|
30
|
+
- `get_responses` — fetch the human answers for a query.
|
|
31
|
+
- `wait_for_responses` — poll (bounded) until enough humans answer; call again while it returns `pending`.
|
|
32
|
+
- `list_queries` — your recent queries.
|
|
33
|
+
- `rate_response` — rate a worker's answer 1–5.
|
|
34
|
+
|
|
35
|
+
## Example agent flow
|
|
36
|
+
|
|
37
|
+
1. `get_balance` → ensure funds.
|
|
38
|
+
2. `submit_query` → get `query_id`.
|
|
39
|
+
3. `wait_for_responses` (repeat while `pending`) → read the human answers.
|
|
40
|
+
|
|
41
|
+
Full API docs: https://getabrain.ai/docs/api
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
|
|
6
|
+
// src/client.ts
|
|
7
|
+
import { GetABrain } from "@getabrain/sdk";
|
|
8
|
+
function createClient(env = process.env) {
|
|
9
|
+
const apiKey = env.GETABRAIN_API_KEY;
|
|
10
|
+
const apiSecret = env.GETABRAIN_API_SECRET;
|
|
11
|
+
if (!apiKey || !apiSecret) {
|
|
12
|
+
throw new Error("GETABRAIN_API_KEY and GETABRAIN_API_SECRET environment variables are required.");
|
|
13
|
+
}
|
|
14
|
+
return new GetABrain({ apiKey, apiSecret, baseUrl: env.GETABRAIN_BASE_URL });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/server.ts
|
|
18
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19
|
+
|
|
20
|
+
// src/tools.ts
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
import { QUERY_TYPES, TimeoutError } from "@getabrain/sdk";
|
|
23
|
+
|
|
24
|
+
// src/result.ts
|
|
25
|
+
import {
|
|
26
|
+
InsufficientBalanceError,
|
|
27
|
+
AuthError,
|
|
28
|
+
ValidationError,
|
|
29
|
+
RateLimitError,
|
|
30
|
+
NotFoundError,
|
|
31
|
+
GetABrainError
|
|
32
|
+
} from "@getabrain/sdk";
|
|
33
|
+
function ok(data) {
|
|
34
|
+
return { content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data) }] };
|
|
35
|
+
}
|
|
36
|
+
function fail(message) {
|
|
37
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
38
|
+
}
|
|
39
|
+
function toToolError(e) {
|
|
40
|
+
if (e instanceof InsufficientBalanceError)
|
|
41
|
+
return fail("Insufficient balance. Add funds at https://getabrain.ai before submitting more queries.");
|
|
42
|
+
if (e instanceof AuthError)
|
|
43
|
+
return fail("Invalid GetABrain API credentials \u2014 check GETABRAIN_API_KEY / GETABRAIN_API_SECRET.");
|
|
44
|
+
if (e instanceof RateLimitError)
|
|
45
|
+
return fail(`Rate limited. Retry in ${Math.ceil((e.retryAfterMs ?? 1e3) / 1e3)}s.`);
|
|
46
|
+
if (e instanceof ValidationError)
|
|
47
|
+
return fail(`Invalid request: ${e.message}`);
|
|
48
|
+
if (e instanceof NotFoundError)
|
|
49
|
+
return fail("Not found: the query or response does not exist.");
|
|
50
|
+
if (e instanceof GetABrainError)
|
|
51
|
+
return fail(`GetABrain request failed (${e.status}): ${e.message}`);
|
|
52
|
+
return fail(`Unexpected error: ${e instanceof Error ? e.message : String(e)}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/tools.ts
|
|
56
|
+
var tools = [
|
|
57
|
+
{
|
|
58
|
+
name: "get_balance",
|
|
59
|
+
description: "Get the prepaid balance (in cents) available to fund GetABrain queries.",
|
|
60
|
+
inputShape: {},
|
|
61
|
+
run: async (_args, client) => {
|
|
62
|
+
const b = await client.account.balance();
|
|
63
|
+
return ok({ balance_cents: b.balance_cents, company_name: b.company_name });
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "submit_query",
|
|
68
|
+
description: `Submit a human-judgment query to real human workers and get its id back. "type" is one of: ${QUERY_TYPES.join(", ")}. "content_data" is the type-specific payload (e.g. ab_test: {question, variant_a:{description}, variant_b:{description}}; yes_no: {question}; rating_scale: {question, scale_type, scale_min, scale_max}). Cost = (bid_amount_cents + bonus_amount_cents) * required_responses, deducted from your balance.`,
|
|
69
|
+
inputShape: {
|
|
70
|
+
type: z.enum(QUERY_TYPES),
|
|
71
|
+
title: z.string().min(5).max(255),
|
|
72
|
+
content_data: z.record(z.any()),
|
|
73
|
+
required_responses: z.number().int().min(1).max(1e3),
|
|
74
|
+
bid_amount_cents: z.number().int().min(5),
|
|
75
|
+
description: z.string().optional(),
|
|
76
|
+
bonus_amount_cents: z.number().int().min(0).optional(),
|
|
77
|
+
min_worker_quality: z.number().min(0).max(5).optional()
|
|
78
|
+
},
|
|
79
|
+
run: async (args, client) => ok(await client.queries.create(args))
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "get_responses",
|
|
83
|
+
description: "Get a query and the human responses submitted so far.",
|
|
84
|
+
inputShape: { query_id: z.string() },
|
|
85
|
+
run: async (args, client) => {
|
|
86
|
+
const q = await client.queries.get(args.query_id);
|
|
87
|
+
return ok({
|
|
88
|
+
status: q.status,
|
|
89
|
+
completed_responses: q.completed_responses,
|
|
90
|
+
required_responses: q.required_responses,
|
|
91
|
+
responses: q.responses ?? []
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "wait_for_responses",
|
|
97
|
+
description: 'Poll for up to max_wait_seconds (default 50) for human responses to a query. Returns status "ready" with the responses if enough arrived, otherwise status "pending" \u2014 call again to keep waiting.',
|
|
98
|
+
inputShape: {
|
|
99
|
+
query_id: z.string(),
|
|
100
|
+
min_responses: z.number().int().min(1).optional(),
|
|
101
|
+
max_wait_seconds: z.number().int().min(1).max(50).optional()
|
|
102
|
+
},
|
|
103
|
+
run: async (args, client) => {
|
|
104
|
+
const current = await client.queries.get(args.query_id);
|
|
105
|
+
const minResponses = args.min_responses ?? current.required_responses;
|
|
106
|
+
try {
|
|
107
|
+
const responses = await client.queries.waitForResponses(args.query_id, {
|
|
108
|
+
minResponses,
|
|
109
|
+
timeoutMs: (args.max_wait_seconds ?? 50) * 1e3,
|
|
110
|
+
pollIntervalMs: 5e3
|
|
111
|
+
});
|
|
112
|
+
return ok({ status: "ready", responses });
|
|
113
|
+
} catch (e) {
|
|
114
|
+
if (!(e instanceof TimeoutError)) throw e;
|
|
115
|
+
const q = await client.queries.get(args.query_id);
|
|
116
|
+
if (q.status === "completed" || (q.completed_responses ?? 0) >= minResponses) {
|
|
117
|
+
return ok({ status: "ready", responses: q.responses ?? [] });
|
|
118
|
+
}
|
|
119
|
+
return ok({
|
|
120
|
+
status: "pending",
|
|
121
|
+
completed_responses: q.completed_responses ?? 0,
|
|
122
|
+
required_responses: q.required_responses,
|
|
123
|
+
hint: "Call wait_for_responses again to keep waiting for more human answers."
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "list_queries",
|
|
130
|
+
description: "List your recent GetABrain queries (most recent first).",
|
|
131
|
+
inputShape: {
|
|
132
|
+
status: z.string().optional(),
|
|
133
|
+
limit: z.number().int().min(1).max(100).optional()
|
|
134
|
+
},
|
|
135
|
+
run: async (args, client) => ok(await client.queries.list(args))
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "rate_response",
|
|
139
|
+
description: "Rate a worker's response 1-5 (feeds the quality system). Optionally include feedback_text.",
|
|
140
|
+
inputShape: {
|
|
141
|
+
query_id: z.string(),
|
|
142
|
+
response_id: z.string(),
|
|
143
|
+
score: z.number().int().min(1).max(5),
|
|
144
|
+
feedback_text: z.string().optional()
|
|
145
|
+
},
|
|
146
|
+
run: async (args, client) => ok(await client.responses.rate(args.query_id, args.response_id, { score: args.score, feedback_text: args.feedback_text }))
|
|
147
|
+
}
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
// src/server.ts
|
|
151
|
+
function createServer(client) {
|
|
152
|
+
const server = new McpServer({ name: "getabrain", version: "0.1.0" });
|
|
153
|
+
for (const t of tools) {
|
|
154
|
+
server.tool(t.name, t.description, t.inputShape, async (args) => {
|
|
155
|
+
try {
|
|
156
|
+
return await t.run(args, client);
|
|
157
|
+
} catch (e) {
|
|
158
|
+
return toToolError(e);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return server;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/index.ts
|
|
166
|
+
async function main() {
|
|
167
|
+
const client = createClient();
|
|
168
|
+
const server = createServer(client);
|
|
169
|
+
const transport = new StdioServerTransport();
|
|
170
|
+
await server.connect(transport);
|
|
171
|
+
}
|
|
172
|
+
main().catch((e) => {
|
|
173
|
+
console.error(`getabrain-mcp failed to start: ${e instanceof Error ? e.message : String(e)}`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@getabrain/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for GetABrain.ai — real human judgment as agent tools",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "getabrain-mcp": "./dist/index.js" },
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"files": ["dist", "README.md"],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsup src/index.ts --format esm --dts --clean --tsconfig tsconfig.build.json",
|
|
11
|
+
"prepublishOnly": "npm run build"
|
|
12
|
+
},
|
|
13
|
+
"engines": { "node": ">=18" },
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@getabrain/sdk": "^0.1.0",
|
|
16
|
+
"@modelcontextprotocol/sdk": "^1.18.0",
|
|
17
|
+
"zod": "^3.25.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": { "tsup": "^8.0.0" },
|
|
20
|
+
"license": "MIT"
|
|
21
|
+
}
|