plures-mcp 1.3.1 → 1.5.1

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.
Files changed (3) hide show
  1. package/README.md +10 -1
  2. package/dist/index.js +98 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,6 +2,8 @@
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
4
 
5
+ [![Plures MCP server](https://mcprush.com/badge/plures-mcp/card.svg)](https://mcprush.com/to/plures-mcp?ref=badge)
6
+
5
7
  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
8
 
7
9
  ## Set up
@@ -27,6 +29,8 @@ A `pk_test_` key answers instantly with a simulated result (always the first opt
27
29
  ## Tools
28
30
 
29
31
  **`ask_humans`**: send a judgment call to verified people.
32
+
33
+ Set `route: "team"` to send a decision straight to your own team instead of verified people (for money and account decisions, and confidential or policy calls), `team` to pick one of your named teams, `team_votes` to have several of them vote, and `category` to let your routing rules decide. Live, each internal vote is $0.05.
30
34
  - `question` (required): what people should decide, answerable from the context alone
31
35
  - `options` (required): 2 to 8 possible answers
32
36
  - `context`: the material to judge, as plain text
@@ -40,11 +44,15 @@ A `pk_test_` key answers instantly with a simulated result (always the first opt
40
44
 
41
45
  **`cancel_human_task`**: cancel a task that no longer matters.
42
46
 
47
+ **`get_mapping_guide`**, **`submit_agent_map`**, **`get_agent_map`**: ask your coding agent to "map this app for Plures". It reads the guide, reads your code where it runs, and sends Plures only the map: the steps, the actions with consequences, and a drafted check for each place a person should look. Open the map in your dashboard to try each check and add it.
48
+
43
49
  Each result tells the agent the answer, how many people agreed and whether it is safe to act on. If you've added your team at plures.si/dashboard/team, decisions verified people don't all agree on go to your team, and the agent gets their decision as the final answer.
44
50
 
45
51
  ## When agents should use it
46
52
 
47
- 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.
53
+ The server tells the agent to ask verified people before acting on judgment calls where a mistake is costly: whether an AI answer is correct and on policy, which of two model responses is better, a customer's tone or intent, whether a listing, document or screenshot matches, a moderation edge case. Not for anything it can check with code or a lookup.
54
+
55
+ Money and account decisions (refunds, payments, invoices, payouts, account access) go to your own team: the agent sends them with `route: "team"`, or your routing rules can match them by `category`.
48
56
 
49
57
  Tasks are shown to strangers, so the agent is told not to include secrets or more personal data than the decision needs.
50
58
 
@@ -52,5 +60,6 @@ Tasks are shown to strangers, so the agent is told not to include secrets or mor
52
60
 
53
61
  - `PLURES_API_KEY` (required)
54
62
  - `PLURES_BASE_URL` (optional, defaults to https://plures.si)
63
+ - `PLURES_APP` (optional): the app tasks and maps belong to, like `support-agent`
55
64
 
56
65
  MIT licensed. Source: [github.com/plureslabs/plures](https://github.com/plureslabs/plures).
package/dist/index.js CHANGED
@@ -5,21 +5,26 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import { z } from "zod";
7
7
  import { Plures, PluresError } from "plures";
8
- var VERSION = "1.3.1";
8
+ var VERSION = "1.5.1";
9
9
  var KEY = process.env.PLURES_API_KEY;
10
10
  var plures = KEY ? new Plures(KEY, { baseUrl: process.env.PLURES_BASE_URL }) : null;
11
11
  var MAX_WAIT = 240;
12
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."
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 trust or safety: grading whether an AI answer is correct, safe and on policy, picking the better of two model responses, reading a customer's tone or intent, checking that a listing, document or screenshot matches what it claims, 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. Money and account decisions (refunds, payments, invoices, payouts, account access) belong to the company's own people: set route to team for those, and for anything confidential, a policy call or regulated, so it goes straight to the developer's team instead of verified people. When the user asks to map their app or agent for Plures, call get_mapping_guide, read the code the way it says, then call submit_agent_map. Never put source code, secrets or customer data in a map."
14
14
  });
15
15
  var TEST_NOTE = "TEST MODE: this answer is simulated; no person saw the question. It always picks the first option, so don't act on it. Use a live key for real answers.";
16
16
  var fmt = (t) => {
17
17
  const text = describe(t);
18
+ if (t.mode === "test" && t.route === "team") return `TEST TASK, sent to your own team (marked as a test in their email).
19
+ ${text}`;
18
20
  return t.mode === "test" ? `${TEST_NOTE}
19
21
  ${text}` : text;
20
22
  };
21
- var describe = (t) => {
23
+ var describe = (task) => {
24
+ const t = task;
22
25
  const label = (id) => t.options.find((o) => o.id === id)?.label ?? id;
26
+ if (t.status === "pending" && t.escalation?.reason === "team_only")
27
+ return `Sent straight to ${t.team ? `your ${t.team} team` : "your team"}${(t.team_consensus?.votes ?? 1) > 1 ? `, where ${t.team_consensus.votes} people vote` : ""}. Task ${t.id}. Call get_human_answer with this task_id later; don't act until it's decided.`;
23
28
  if (t.status === "pending" && t.escalation)
24
29
  return `Verified people didn't all agree, so the decision went to your team, who decide within a day. Task ${t.id}. Call get_human_answer with this task_id later; don't act until it's decided.`;
25
30
  if (t.status === "pending")
@@ -28,6 +33,12 @@ var describe = (t) => {
28
33
  const r = t.result;
29
34
  const votes = r ? Object.entries(r.votes).map(([k, v]) => `${label(k)}: ${v}`).join(", ") : "none";
30
35
  if (t.status === "expired") return `Task ${t.id} expired before enough people answered. Votes so far: ${votes}. Treat this as unverified.`;
36
+ const tv = r?.team;
37
+ const teamVotes = tv && tv.voters.length > 1 ? ` ${tv.voters.filter((v) => !v.lead).length} people voted, ${Math.round(tv.agreement * 100)}% agreed${tv.decided_by_lead ? ", and the lead made the final call" : ""}.` : "";
38
+ if (r?.decided_by === "team" && r.answer !== null && t.route === "team")
39
+ return `Your team decided: ${label(r.answer)}${r.reviewer && !teamVotes ? ` (${r.reviewer})` : ""}.${teamVotes} This is the final answer; act on it.${r.note && !teamVotes ? ` Their note: "${r.note}"` : ""}`;
40
+ if (r?.decided_by === "team" && r.answer === null && tv)
41
+ return `Your team didn't agree (${Object.entries(tv.votes).filter(([, n]) => n > 0).map(([k, n]) => `${label(k)}: ${n}`).join(", ")}), so there's no answer. Don't act on it; ask a person on your team directly.`;
31
42
  if (r?.decided_by === "team" && r.answer !== null)
32
43
  return `Your team decided: ${label(r.answer)}${r.reviewer ? ` (${r.reviewer})` : ""}, after verified people didn't all agree (${votes}). This is the final answer; act on it.${r.note ? ` Their note: "${r.note}"` : ""}`;
33
44
  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.`;
@@ -36,7 +47,7 @@ var describe = (t) => {
36
47
  };
37
48
  var out = (t) => ({
38
49
  content: [{ type: "text", text: fmt(t) }],
39
- structuredContent: { task_id: t.id, status: t.status, mode: t.mode, decided_by: t.result?.decided_by ?? null, with_team: !!t.escalation && t.status === "pending", answer: t.result?.answer ?? null, label: t.result?.label ?? null, agreement: t.result?.agreement ?? null, answers: t.result?.answers ?? 0, votes: t.result?.votes ?? {} }
50
+ structuredContent: { task_id: t.id, status: t.status, mode: t.mode, decided_by: t.result?.decided_by ?? null, with_team: !!t.escalation && t.status === "pending", route: t.route ?? "verifiers", answer: t.result?.answer ?? null, label: t.result?.label ?? null, agreement: t.result?.agreement ?? null, answers: t.result?.answers ?? 0, votes: t.result?.votes ?? {} }
40
51
  });
41
52
  var fail = (e) => ({
42
53
  isError: true,
@@ -62,14 +73,14 @@ var progressFor = (extra, total) => {
62
73
  if (token === void 0) return void 0;
63
74
  return (n) => extra.sendNotification({ method: "notifications/progress", params: { progressToken: token, progress: n, total, message: "Waiting for verified people to answer" } });
64
75
  };
65
- var result = { task_id: z.string(), status: z.string(), mode: z.string(), decided_by: z.string().nullable(), with_team: z.boolean(), answer: z.string().nullable(), label: z.string().nullable(), agreement: z.number().nullable(), answers: z.number(), votes: z.record(z.number()) };
76
+ var result = { task_id: z.string(), status: z.string(), mode: z.string(), route: z.string(), decided_by: z.string().nullable(), with_team: z.boolean(), answer: z.string().nullable(), label: z.string().nullable(), agreement: z.number().nullable(), answers: z.number(), votes: z.record(z.number()) };
66
77
  server.registerTool("ask_humans", {
67
78
  title: "Ask verified humans",
68
- 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 use prepaid credit at $0.20 per answer (clips cost more by length); test keys are free and simulated.",
79
+ 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: evals of AI output, tone and intent, whether two things match, moderation edge cases. Send refunds, payments and other money calls with route team. 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 use prepaid credit at $0.20 per answer (clips cost more by length), or $0.05 per internal vote with route team; test keys are free and simulated.",
69
80
  inputSchema: {
70
81
  question: z.string().min(3).max(1e3).describe('What people should decide, e.g. "Is this customer upset?"'),
71
82
  options: z.array(z.string().min(1).max(200)).min(2).max(8).describe('2 to 8 possible answers, e.g. ["Yes", "No"]'),
72
- context: z.string().max(2e4).optional().describe("The material to judge, as plain text: the message, the invoice lines, the listing"),
83
+ context: z.string().max(2e4).optional().describe("The material to judge, as plain text: the AI answer and the policy it should follow, the message, the listing"),
73
84
  image_url: z.string().url().optional().describe("An https image shown with the question"),
74
85
  audio_url: z.string().url().optional().describe("An https recording people listen to in full before answering, such as a call your voice agent handled. Up to 3 minutes; needs media_seconds"),
75
86
  video_url: z.string().url().optional().describe("An https video (MP4) people watch in full before answering. Up to 3 minutes; needs media_seconds. Not with audio_url"),
@@ -77,6 +88,11 @@ server.registerTool("ask_humans", {
77
88
  sensitive: z.boolean().optional().describe("Set true for violent, graphic or disturbing content, so only people who opted in see it, behind a warning"),
78
89
  instructions: z.string().max(2e3).optional().describe("Short guidance shown under the question"),
79
90
  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"),
91
+ route: z.enum(["verifiers", "team"]).optional().describe("verifiers (default) asks ID verified people first. team sends it straight to the developer's own people, for money and account decisions and confidential or policy calls; $0.05 per internal vote live"),
92
+ category: z.string().max(64).optional().describe(`A label the developer's routing rules match on, like "refunds"; the rule may choose the route and team`),
93
+ team: z.string().max(40).optional().describe(`The name of one of the developer's teams, like "Finance", to decide instead of the main team`),
94
+ team_votes: z.number().int().min(1).max(10).optional().describe("With route team: how many people on the team vote; the majority decides"),
95
+ app: z.string().max(40).optional().describe(`Which of the developer's apps this is for, like "support-agent". Default: PLURES_APP, then the default app`),
80
96
  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}`)
81
97
  },
82
98
  outputSchema: result,
@@ -85,7 +101,8 @@ server.registerTool("ask_humans", {
85
101
  if (!plures) return noKey();
86
102
  try {
87
103
  const wait = wait_seconds ?? 45;
88
- const created = await plures.tasks.create({ ...params, metadata: { source: "mcp" } });
104
+ const app = params.app ?? process.env.PLURES_APP;
105
+ const created = await plures.tasks.create({ ...params, ...app ? { app } : {}, metadata: { source: "mcp" } });
89
106
  const t = created.status === "pending" && wait > 0 ? await settle(created.id, wait, progressFor(extra, Math.ceil(wait / 3))) : created;
90
107
  return out(t);
91
108
  } catch (e) {
@@ -123,4 +140,77 @@ server.registerTool("cancel_human_task", {
123
140
  return fail(e);
124
141
  }
125
142
  });
143
+ var BASE = (process.env.PLURES_BASE_URL ?? "https://plures.si").replace(/\/$/, "");
144
+ async function api(method, path, body) {
145
+ const r = await fetch(BASE + path, { method, headers: { authorization: `Bearer ${KEY}`, "user-agent": `plures-mcp/${VERSION}`, ...body ? { "content-type": "application/json" } : {} }, body: body ? JSON.stringify(body) : void 0 });
146
+ const data = await r.json().catch(() => ({}));
147
+ if (!r.ok) throw new PluresError(data?.error?.message ?? `HTTP ${r.status}`, r.status, data?.error?.type ?? "api_error");
148
+ return data;
149
+ }
150
+ var describeMap = (m) => {
151
+ const label = (id) => m.nodes.find((n) => n.id === id)?.label ?? id;
152
+ const actions = m.nodes.filter((n) => n.kind === "action");
153
+ const checked = new Set(m.checkpoints.filter((c) => c.status !== "dismissed").map((c) => c.at));
154
+ const lines = m.checkpoints.map((c) => `- ${label(c.at)} (${c.category}): ${c.status}${c.stats?.tasks ? `, ${c.stats.tasks} checks in 30 days${c.stats.agreement != null ? `, ${Math.round(c.stats.agreement * 100)}% agreement` : ""}` : ""}. ${c.why}`);
155
+ const open = actions.filter((a) => a.risk === "high" && !checked.has(a.id)).map((a) => `- ${a.label}: high risk, no check`);
156
+ return [
157
+ `${m.name ?? m.app}, version ${m.version}: ${m.nodes.length} steps, ${actions.length} actions with consequences.`,
158
+ ...lines,
159
+ ...open,
160
+ m.url ? `The developer can see it and try each check at ${m.url}` : ""
161
+ ].filter(Boolean).join("\n");
162
+ };
163
+ server.registerTool("get_mapping_guide", {
164
+ title: "Get the Plures mapping guide",
165
+ description: "Returns the steps for mapping the developer's AI agent for Plures: how to find its steps and actions, where to suggest a person checks, and how to draft each check. Call this first when asked to map an app for Plures.",
166
+ inputSchema: {},
167
+ annotations: { readOnlyHint: true, openWorldHint: true }
168
+ }, async () => {
169
+ try {
170
+ const r = await fetch(`${BASE}/api/v1/map_guide`);
171
+ return { content: [{ type: "text", text: await r.text() }] };
172
+ } catch (e) {
173
+ return fail(e);
174
+ }
175
+ });
176
+ server.registerTool("submit_agent_map", {
177
+ title: "Send an agent map to Plures",
178
+ description: "Uploads the map you made with get_mapping_guide: nodes, edges and checkpoints with drafted checks. Never include source code, secrets or customer data. If Plures names a problem, fix it and send again.",
179
+ inputSchema: {
180
+ map: z.object({
181
+ name: z.string().optional(),
182
+ summary: z.string().optional(),
183
+ source: z.object({ repo: z.string().optional(), commit: z.string().optional(), tool: z.string().optional() }).optional(),
184
+ nodes: z.array(z.record(z.any())),
185
+ edges: z.array(z.record(z.any())),
186
+ checkpoints: z.array(z.record(z.any()))
187
+ }).describe("The agent map, as the guide describes"),
188
+ app: z.string().optional().describe("The app id, like support-agent. Default: the PLURES_APP environment variable, then the account's default app")
189
+ },
190
+ annotations: { readOnlyHint: false, idempotentHint: false, openWorldHint: true }
191
+ }, async ({ map, app }) => {
192
+ if (!KEY) return noKey();
193
+ const id = app ?? process.env.PLURES_APP ?? "default";
194
+ try {
195
+ return { content: [{ type: "text", text: describeMap(await api("PUT", `/api/v1/apps/${encodeURIComponent(id)}/map`, map)) }] };
196
+ } catch (e) {
197
+ return fail(e);
198
+ }
199
+ });
200
+ server.registerTool("get_agent_map", {
201
+ title: "Get an agent map",
202
+ description: "Returns an app's agent map from Plures: its checkpoints, which are added, and live numbers for each (checks in 30 days, agreement, how many went to the team).",
203
+ inputSchema: { app: z.string().optional().describe("The app id. Default: PLURES_APP, then the default app") },
204
+ annotations: { readOnlyHint: true, openWorldHint: true }
205
+ }, async ({ app }) => {
206
+ if (!KEY) return noKey();
207
+ const id = app ?? process.env.PLURES_APP ?? "default";
208
+ try {
209
+ const m = await api("GET", `/api/v1/apps/${encodeURIComponent(id)}/map`);
210
+ if (!m.version) return { content: [{ type: "text", text: `${id} has no map yet. Call get_mapping_guide and follow it.` }] };
211
+ return { content: [{ type: "text", text: describeMap(m) }] };
212
+ } catch (e) {
213
+ return fail(e);
214
+ }
215
+ });
126
216
  await server.connect(new StdioServerTransport());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plures-mcp",
3
- "version": "1.3.1",
3
+ "version": "1.5.1",
4
4
  "mcpName": "io.github.plureslabs/plures",
5
5
  "description": "MCP server for Plures: lets any AI agent ask ID verified people to check a decision before it acts.",
6
6
  "keywords": [