plures-mcp 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +53 -2
- package/dist/index.js +112 -0
- package/package.json +48 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Plures Labs LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,54 @@
|
|
|
1
|
-
#
|
|
1
|
+
# plures-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An MCP server that gives any AI agent a human in the loop: an `ask_humans` tool that sends a decision to ID verified people and returns their consensus before the agent acts. Built on [Plures](https://plures.si).
|
|
4
|
+
|
|
5
|
+
Works with Claude Desktop, Claude Code, Cursor, VS Code, and agent frameworks that support MCP (OpenAI Agents SDK, LangChain, Vercel AI SDK and more).
|
|
6
|
+
|
|
7
|
+
## Set up
|
|
8
|
+
|
|
9
|
+
Get a free test key at [plures.si](https://plures.si/?for=developers#join), then add the server to your client:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"mcpServers": {
|
|
14
|
+
"plures": {
|
|
15
|
+
"command": "npx",
|
|
16
|
+
"args": ["-y", "plures-mcp"],
|
|
17
|
+
"env": { "PLURES_API_KEY": "pk_test_..." }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Claude Code: `claude mcp add plures --env PLURES_API_KEY=pk_test_... -- npx -y plures-mcp`
|
|
24
|
+
|
|
25
|
+
A `pk_test_` key answers instantly with a simulated result, so you can try the flow for free. A `pk_live_` key sends tasks to real verified people and is billed per answer.
|
|
26
|
+
|
|
27
|
+
## Tools
|
|
28
|
+
|
|
29
|
+
**`ask_humans`**: send a judgment call to verified people.
|
|
30
|
+
- `question` (required): what people should decide, answerable from the context alone
|
|
31
|
+
- `options` (required): 2 to 8 possible answers
|
|
32
|
+
- `context`: the material to judge, as plain text
|
|
33
|
+
- `image_url`, `instructions`
|
|
34
|
+
- `verifiers`: 1, 3, 5, 7 or 9 (default 3)
|
|
35
|
+
- `wait_seconds`: how long to wait for the answer, 0 to 240 (default 45). If people haven't answered by then, the tool returns a `task_id` to check later.
|
|
36
|
+
|
|
37
|
+
**`get_human_answer`**: check a task by `task_id`, optionally waiting.
|
|
38
|
+
|
|
39
|
+
**`cancel_human_task`**: cancel a task that no longer matters.
|
|
40
|
+
|
|
41
|
+
Each result tells the agent the answer, how many people agreed and whether it is safe to act on: unanimous answers are, anything less should be escalated.
|
|
42
|
+
|
|
43
|
+
## When agents should use it
|
|
44
|
+
|
|
45
|
+
The server tells the agent to ask people before acting on judgment calls where a mistake is costly: a customer's tone or intent, a refund or payment, whether an invoice, listing or document matches, a moderation edge case. Not for anything it can check with code or a lookup.
|
|
46
|
+
|
|
47
|
+
Tasks are shown to strangers, so the agent is told not to include secrets or more personal data than the decision needs.
|
|
48
|
+
|
|
49
|
+
## Environment
|
|
50
|
+
|
|
51
|
+
- `PLURES_API_KEY` (required)
|
|
52
|
+
- `PLURES_BASE_URL` (optional, defaults to https://plures.si)
|
|
53
|
+
|
|
54
|
+
MIT licensed. Source: [github.com/plureslabs/plures](https://github.com/plureslabs/plures).
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { Plures, PluresError } from "plures";
|
|
8
|
+
var VERSION = "1.0.0";
|
|
9
|
+
var KEY = process.env.PLURES_API_KEY;
|
|
10
|
+
var plures = KEY ? new Plures(KEY, { baseUrl: process.env.PLURES_BASE_URL }) : null;
|
|
11
|
+
var MAX_WAIT = 240;
|
|
12
|
+
var server = new McpServer({ name: "plures", version: VERSION }, {
|
|
13
|
+
instructions: "Plures sends a decision to ID verified people, who answer independently, and returns their consensus. Use ask_humans before you act on a judgment call you are not sure about, where a wrong call costs money, trust or safety: reading a customer's tone or intent, approving a refund or payment, checking that an invoice, listing or document matches, moderating an edge case, or confirming a fact that needs human eyes. Don't use it for things you can check with code or a lookup. Act on the answer when agreement is 1; below that, treat it as uncertain and escalate or ask for more verifiers."
|
|
14
|
+
});
|
|
15
|
+
var fmt = (t) => {
|
|
16
|
+
const label = (id) => t.options.find((o) => o.id === id)?.label ?? id;
|
|
17
|
+
if (t.status === "pending")
|
|
18
|
+
return `Still waiting for people to answer. Task ${t.id}. Call get_human_answer with this task_id later.`;
|
|
19
|
+
if (t.status === "canceled") return `Task ${t.id} was canceled.`;
|
|
20
|
+
const r = t.result;
|
|
21
|
+
const votes = r ? Object.entries(r.votes).map(([k, v]) => `${label(k)}: ${v}`).join(", ") : "none";
|
|
22
|
+
if (t.status === "expired") return `Task ${t.id} expired before enough people answered. Votes so far: ${votes}. Treat this as unverified.`;
|
|
23
|
+
if (!r || r.answer === null) return `People were split${r?.tie ? " (a tie)" : ""}. Votes: ${votes}. Treat this as uncertain and escalate to a person on your team.`;
|
|
24
|
+
const strength = r.agreement >= 1 ? "Everyone agreed, so it is safe to act on." : `Agreement is ${Math.round(r.agreement * 100)}%, so treat it as uncertain.`;
|
|
25
|
+
return `Verified people answered: ${label(r.answer)}. ${Math.round(r.agreement * r.answers)} of ${r.answers} agreed. ${strength} Votes: ${votes}.${t.mode === "test" ? " (Test mode: this answer is simulated, no real people saw it.)" : ""}`;
|
|
26
|
+
};
|
|
27
|
+
var out = (t) => ({
|
|
28
|
+
content: [{ type: "text", text: fmt(t) }],
|
|
29
|
+
structuredContent: { task_id: t.id, status: t.status, mode: t.mode, answer: t.result?.answer ?? null, label: t.result?.label ?? null, agreement: t.result?.agreement ?? null, answers: t.result?.answers ?? 0, votes: t.result?.votes ?? {} }
|
|
30
|
+
});
|
|
31
|
+
var fail = (e) => ({
|
|
32
|
+
isError: true,
|
|
33
|
+
content: [{ type: "text", text: e instanceof PluresError ? `Plures said ${e.status}: ${e.message}` : `Couldn't reach Plures: ${e instanceof Error ? e.message : String(e)}` }]
|
|
34
|
+
});
|
|
35
|
+
var noKey = () => ({
|
|
36
|
+
isError: true,
|
|
37
|
+
content: [{ type: "text", text: "PLURES_API_KEY isn't set. Add it to this MCP server's environment. A free test key is at https://plures.si/?for=developers#join" }]
|
|
38
|
+
});
|
|
39
|
+
async function settle(id, seconds, progress) {
|
|
40
|
+
const end = Date.now() + seconds * 1e3;
|
|
41
|
+
let t = await plures.tasks.retrieve(id);
|
|
42
|
+
let n = 0;
|
|
43
|
+
while (t.status === "pending" && Date.now() < end) {
|
|
44
|
+
await new Promise((r) => setTimeout(r, Math.min(3e3, Math.max(0, end - Date.now()))));
|
|
45
|
+
await progress?.(++n);
|
|
46
|
+
t = await plures.tasks.retrieve(id);
|
|
47
|
+
}
|
|
48
|
+
return t;
|
|
49
|
+
}
|
|
50
|
+
var progressFor = (extra, total) => {
|
|
51
|
+
const token = extra._meta?.progressToken;
|
|
52
|
+
if (token === void 0) return void 0;
|
|
53
|
+
return (n) => extra.sendNotification({ method: "notifications/progress", params: { progressToken: token, progress: n, total, message: "Waiting for verified people to answer" } });
|
|
54
|
+
};
|
|
55
|
+
var result = { task_id: z.string(), status: z.string(), mode: z.string(), answer: z.string().nullable(), label: z.string().nullable(), agreement: z.number().nullable(), answers: z.number(), votes: z.record(z.number()) };
|
|
56
|
+
server.registerTool("ask_humans", {
|
|
57
|
+
title: "Ask verified humans",
|
|
58
|
+
description: "Send a decision to ID verified people before you act on it. Several people answer independently, each confirmed with Face ID, and you get back the consensus answer and how strongly they agreed. Use it for judgment calls where being wrong is costly: tone and intent, refunds and payments, whether two things match, moderation edge cases. Write the question so a stranger can answer it from the context alone, and never include secrets or more personal data than the decision needs. Live keys are billed per answer; test keys are free and simulated.",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
question: z.string().min(3).max(1e3).describe('What people should decide, e.g. "Is this customer upset?"'),
|
|
61
|
+
options: z.array(z.string().min(1).max(200)).min(2).max(8).describe('2 to 8 possible answers, e.g. ["Yes", "No"]'),
|
|
62
|
+
context: z.string().max(2e4).optional().describe("The material to judge, as plain text: the message, the invoice lines, the listing"),
|
|
63
|
+
image_url: z.string().url().optional().describe("An https image shown with the question"),
|
|
64
|
+
instructions: z.string().max(2e3).optional().describe("Short guidance shown under the question"),
|
|
65
|
+
verifiers: z.union([z.literal(1), z.literal(3), z.literal(5), z.literal(7), z.literal(9)]).optional().describe("How many people answer. Default 3; use 5 for higher stakes"),
|
|
66
|
+
wait_seconds: z.number().int().min(0).max(MAX_WAIT).optional().describe(`How long to wait for the answer before returning a task_id to check later. Default 45, up to ${MAX_WAIT}`)
|
|
67
|
+
},
|
|
68
|
+
outputSchema: result,
|
|
69
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
70
|
+
}, async ({ wait_seconds, ...params }, extra) => {
|
|
71
|
+
if (!plures) return noKey();
|
|
72
|
+
try {
|
|
73
|
+
const wait = wait_seconds ?? 45;
|
|
74
|
+
const created = await plures.tasks.create({ ...params, metadata: { source: "mcp" } });
|
|
75
|
+
const t = created.status === "pending" && wait > 0 ? await settle(created.id, wait, progressFor(extra, Math.ceil(wait / 3))) : created;
|
|
76
|
+
return out(t);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
return fail(e);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
server.registerTool("get_human_answer", {
|
|
82
|
+
title: "Get the human answer",
|
|
83
|
+
description: "Check on a task sent with ask_humans and return the verified people's answer once it's ready.",
|
|
84
|
+
inputSchema: {
|
|
85
|
+
task_id: z.string().describe("The task_id ask_humans returned"),
|
|
86
|
+
wait_seconds: z.number().int().min(0).max(MAX_WAIT).optional().describe("How long to wait if it's still pending. Default 0")
|
|
87
|
+
},
|
|
88
|
+
outputSchema: result,
|
|
89
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
90
|
+
}, async ({ task_id, wait_seconds }, extra) => {
|
|
91
|
+
if (!plures) return noKey();
|
|
92
|
+
try {
|
|
93
|
+
return out(await settle(task_id, wait_seconds ?? 0, progressFor(extra, Math.ceil((wait_seconds ?? 0) / 3))));
|
|
94
|
+
} catch (e) {
|
|
95
|
+
return fail(e);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
server.registerTool("cancel_human_task", {
|
|
99
|
+
title: "Cancel a human task",
|
|
100
|
+
description: "Cancel a task that is still waiting for answers, for example when the decision no longer matters. Answers already given are still billed.",
|
|
101
|
+
inputSchema: { task_id: z.string().describe("The task_id ask_humans returned") },
|
|
102
|
+
outputSchema: result,
|
|
103
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
|
|
104
|
+
}, async ({ task_id }) => {
|
|
105
|
+
if (!plures) return noKey();
|
|
106
|
+
try {
|
|
107
|
+
return out(await plures.tasks.cancel(task_id));
|
|
108
|
+
} catch (e) {
|
|
109
|
+
return fail(e);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
await server.connect(new StdioServerTransport());
|
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plures-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"mcpName": "io.github.plureslabs/plures",
|
|
5
|
+
"description": "MCP server for Plures: lets any AI agent ask ID verified people to check a decision before it acts.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"mcp",
|
|
8
|
+
"mcp-server",
|
|
9
|
+
"model-context-protocol",
|
|
10
|
+
"human-in-the-loop",
|
|
11
|
+
"hitl",
|
|
12
|
+
"ai-agents",
|
|
13
|
+
"human-review",
|
|
14
|
+
"human-approval",
|
|
15
|
+
"plures"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://plures.si/docs#mcp",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/plureslabs/plures.git",
|
|
21
|
+
"directory": "mcp"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Plures Labs LLC <hello@plures.si>",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"bin": {
|
|
27
|
+
"plures-mcp": "dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup src/index.ts --format esm --clean",
|
|
38
|
+
"prepublishOnly": "npm run build"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.32.0",
|
|
42
|
+
"plures": "^1.0.0",
|
|
43
|
+
"zod": "^3.25.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^22.10.0",
|
|
47
|
+
"tsup": "^8.3.5",
|
|
48
|
+
"typescript": "^5.6.3"
|
|
49
|
+
}
|
|
50
|
+
}
|