@purposebot/mcp 0.1.0 → 0.1.2
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 +25 -12
- package/dist/index.js +101 -35
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
# @purposebot/mcp
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
PurposeBot is an agent trust layer for the next web: agents and businesses can
|
|
4
|
+
register identity, prove provenance, discover counterparties by earned trust,
|
|
5
|
+
ask for API access decisions, transact through commerce workflows, and build
|
|
6
|
+
reputation from verified interaction evidence.
|
|
7
|
+
|
|
8
|
+
This package connects MCP clients (Claude Desktop, Cursor, Claude Code, etc.)
|
|
9
|
+
to PurposeBot with `npx @purposebot/mcp`. It exposes the hosted PurposeBot MCP
|
|
10
|
+
tools for trust-ranked discovery, API Trust provider registration, API access
|
|
11
|
+
decisions, signed conduct events, pending feedback, commerce recommendations,
|
|
12
|
+
and interaction/settlement-backed reputation.
|
|
8
13
|
|
|
9
14
|
The hosted endpoint is the source of truth for the tool catalogue, so new tools
|
|
10
15
|
(`search_tools`, `submit_interaction_report`, `list_pending_feedback`,
|
|
11
16
|
`recommend_listings`, `create_api_trust_provider`, `request_api_trust_decision`,
|
|
12
|
-
`submit_api_trust_event`,
|
|
13
|
-
|
|
17
|
+
`submit_api_trust_event`, ...) appear automatically. This package is only the
|
|
18
|
+
stdio transport bridge; the trust, identity, payment, and provenance checks run
|
|
19
|
+
server-side in PurposeBot.
|
|
14
20
|
|
|
15
21
|
## Usage
|
|
16
22
|
|
|
17
|
-
Requires Node
|
|
23
|
+
Requires Node >= 18. Get an API key from your PurposeBot account.
|
|
18
24
|
|
|
19
25
|
### Claude Desktop / Cursor (`mcpServers` config)
|
|
20
26
|
|
|
@@ -23,7 +29,7 @@ Requires Node ≥ 18. Get an API key from your PurposeBot account.
|
|
|
23
29
|
"mcpServers": {
|
|
24
30
|
"purposebot": {
|
|
25
31
|
"command": "npx",
|
|
26
|
-
"args": ["-y", "@purposebot/mcp"],
|
|
32
|
+
"args": ["-y", "@purposebot/mcp@0.1.2"],
|
|
27
33
|
"env": {
|
|
28
34
|
"PURPOSEBOT_API_KEY": "pb_xxx"
|
|
29
35
|
}
|
|
@@ -35,15 +41,22 @@ Requires Node ≥ 18. Get an API key from your PurposeBot account.
|
|
|
35
41
|
### Claude Code
|
|
36
42
|
|
|
37
43
|
```bash
|
|
38
|
-
claude mcp add purposebot --env PURPOSEBOT_API_KEY=pb_xxx -- npx -y @purposebot/mcp
|
|
44
|
+
claude mcp add purposebot --env PURPOSEBOT_API_KEY=pb_xxx -- npx -y @purposebot/mcp@0.1.2
|
|
39
45
|
```
|
|
40
46
|
|
|
41
47
|
## Environment
|
|
42
48
|
|
|
43
49
|
| Variable | Required | Default | Notes |
|
|
44
50
|
|---|---|---|---|
|
|
45
|
-
| `PURPOSEBOT_API_KEY` |
|
|
46
|
-
| `PURPOSEBOT_API_URL` | no | `https://purposebot.ai` |
|
|
51
|
+
| `PURPOSEBOT_API_KEY` | no | - | sent as `X-API-Key`; keyless mode supports first-key registration |
|
|
52
|
+
| `PURPOSEBOT_API_URL` | no | `https://purposebot.ai` | origin only; use `https://api.purposebot.ai` or sandbox/self-host origin |
|
|
53
|
+
| `PURPOSEBOT_ALLOW_CUSTOM_HOST` | no | unset | set to `1` for non-PurposeBot self-hosts |
|
|
54
|
+
|
|
55
|
+
The wrapper validates the target URL before sending an API key. It only sends
|
|
56
|
+
keys to PurposeBot hosts by default, allows `http://localhost` for development,
|
|
57
|
+
rejects embedded credentials, and refuses HTTP redirects. Without
|
|
58
|
+
`PURPOSEBOT_API_KEY`, the wrapper can still call keyless MCP tools such as
|
|
59
|
+
`begin_registration`; protected tools return the hosted PurposeBot auth error.
|
|
47
60
|
|
|
48
61
|
## Develop
|
|
49
62
|
|
package/dist/index.js
CHANGED
|
@@ -1,32 +1,52 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* PurposeBot
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* PurposeBot agent trust layer for MCP. Agents use this bridge to reach hosted
|
|
4
|
+
* PurposeBot tools for identity, provenance, API Trust, commerce reputation,
|
|
5
|
+
* and trust-ranked discovery from Claude Desktop, Cursor, Claude Code, etc.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* the source of truth for the tool catalogue
|
|
7
|
+
* The wrapper forwards tools/list and tools/call to the hosted MCP endpoint.
|
|
8
|
+
* PurposeBot remains the source of truth for the tool catalogue and trust
|
|
9
|
+
* checks, so new tools appear automatically.
|
|
9
10
|
*
|
|
10
11
|
* Env:
|
|
11
|
-
* PURPOSEBOT_API_KEY (
|
|
12
|
+
* PURPOSEBOT_API_KEY (optional) - your PurposeBot API key (sent as X-API-Key)
|
|
12
13
|
* PURPOSEBOT_API_URL (optional) — defaults to https://purposebot.ai
|
|
14
|
+
* PURPOSEBOT_ALLOW_CUSTOM_HOST (optional) — set to 1 for self-hosts
|
|
13
15
|
*/
|
|
14
16
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
15
17
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
18
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
17
|
-
const API_KEY = process.env.PURPOSEBOT_API_KEY;
|
|
19
|
+
const API_KEY = process.env.PURPOSEBOT_API_KEY?.trim() || undefined;
|
|
20
|
+
const REQUEST_TIMEOUT_MS = 60_000;
|
|
21
|
+
const MAX_DIAGNOSTIC_BYTES = 2048;
|
|
22
|
+
const PURPOSEBOT_TOKEN_RE = /\bpb_[A-Za-z0-9_=-]{12,}\b/g;
|
|
23
|
+
const CONTROL_CHARS_RE = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g;
|
|
24
|
+
function sanitizeDiagnostic(raw) {
|
|
25
|
+
let text = typeof raw === "string" ? raw : String(raw ?? "");
|
|
26
|
+
if (API_KEY) {
|
|
27
|
+
text = text.split(API_KEY).join("[redacted-api-key]");
|
|
28
|
+
}
|
|
29
|
+
text = text.replace(PURPOSEBOT_TOKEN_RE, "[redacted-purposebot-token]");
|
|
30
|
+
text = text.replace(CONTROL_CHARS_RE, "?");
|
|
31
|
+
return text.length > MAX_DIAGNOSTIC_BYTES
|
|
32
|
+
? `${text.slice(0, MAX_DIAGNOSTIC_BYTES)}...`
|
|
33
|
+
: text;
|
|
34
|
+
}
|
|
18
35
|
function die(msg) {
|
|
19
36
|
console.error(`[purposebot-mcp] ${msg}`);
|
|
20
37
|
process.exit(1);
|
|
21
38
|
}
|
|
22
|
-
// The wrapper's trust boundary:
|
|
23
|
-
//
|
|
24
|
-
// explicit opt-in for anything else; never send the key over plain
|
|
25
|
-
// localhost dev); reject embedded credentials.
|
|
39
|
+
// The wrapper's trust boundary: if an API key is configured, it is sent to this
|
|
40
|
+
// host. Validate before any request. Default + allowlist PurposeBot hosts;
|
|
41
|
+
// require an explicit opt-in for anything else; never send the key over plain
|
|
42
|
+
// http (except localhost dev); reject embedded credentials.
|
|
26
43
|
const ALLOWED_HOSTS = new Set([
|
|
44
|
+
"api.purposebot.ai",
|
|
45
|
+
"api-sandbox.purposebot.ai",
|
|
27
46
|
"purposebot.ai",
|
|
28
|
-
"
|
|
47
|
+
"purposebot-api.fly.dev",
|
|
29
48
|
"purposebot-api-sandbox.fly.dev",
|
|
49
|
+
"www.purposebot.ai",
|
|
30
50
|
]);
|
|
31
51
|
function resolveApiBase(raw) {
|
|
32
52
|
let u;
|
|
@@ -34,11 +54,14 @@ function resolveApiBase(raw) {
|
|
|
34
54
|
u = new URL(raw);
|
|
35
55
|
}
|
|
36
56
|
catch {
|
|
37
|
-
die(`PURPOSEBOT_API_URL is not a valid URL: ${raw}`);
|
|
57
|
+
die(`PURPOSEBOT_API_URL is not a valid URL: ${sanitizeDiagnostic(raw)}`);
|
|
38
58
|
}
|
|
39
59
|
const isLocal = u.hostname === "localhost" || u.hostname === "127.0.0.1";
|
|
40
60
|
if (u.username || u.password)
|
|
41
61
|
die("PURPOSEBOT_API_URL must not contain embedded credentials");
|
|
62
|
+
if ((u.pathname && u.pathname !== "/") || u.search || u.hash) {
|
|
63
|
+
die("PURPOSEBOT_API_URL must be an origin only, for example https://api.purposebot.ai");
|
|
64
|
+
}
|
|
42
65
|
if (u.protocol !== "https:" && !(u.protocol === "http:" && isLocal)) {
|
|
43
66
|
die(`PURPOSEBOT_API_URL must use https (http allowed only for localhost), got ${u.protocol}`);
|
|
44
67
|
}
|
|
@@ -48,39 +71,81 @@ function resolveApiBase(raw) {
|
|
|
48
71
|
}
|
|
49
72
|
return `${u.protocol}//${u.host}`;
|
|
50
73
|
}
|
|
51
|
-
if (!API_KEY) {
|
|
52
|
-
die("PURPOSEBOT_API_KEY is required. Set it in your MCP client config (env).");
|
|
53
|
-
}
|
|
54
74
|
const API_URL = resolveApiBase(process.env.PURPOSEBOT_API_URL ?? "https://purposebot.ai");
|
|
55
75
|
const MCP_ENDPOINT = `${API_URL}/mcp`;
|
|
56
76
|
let rpcId = 0;
|
|
77
|
+
async function readDiagnosticBody(res) {
|
|
78
|
+
const reader = res.body?.getReader();
|
|
79
|
+
if (!reader)
|
|
80
|
+
return "";
|
|
81
|
+
const decoder = new TextDecoder();
|
|
82
|
+
let bytesRead = 0;
|
|
83
|
+
let text = "";
|
|
84
|
+
try {
|
|
85
|
+
while (bytesRead < MAX_DIAGNOSTIC_BYTES) {
|
|
86
|
+
const { done, value } = await reader.read();
|
|
87
|
+
if (done) {
|
|
88
|
+
text += decoder.decode();
|
|
89
|
+
return text;
|
|
90
|
+
}
|
|
91
|
+
const remaining = MAX_DIAGNOSTIC_BYTES - bytesRead;
|
|
92
|
+
const chunk = value.byteLength > remaining ? value.subarray(0, remaining) : value;
|
|
93
|
+
bytesRead += chunk.byteLength;
|
|
94
|
+
text += decoder.decode(chunk, { stream: true });
|
|
95
|
+
if (value.byteLength > remaining) {
|
|
96
|
+
await reader.cancel().catch(() => undefined);
|
|
97
|
+
return `${text}${decoder.decode()}...`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
await reader.cancel().catch(() => undefined);
|
|
101
|
+
return `${text}${decoder.decode()}...`;
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
reader.releaseLock();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
57
107
|
async function rpc(method, params) {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
108
|
+
const controller = new AbortController();
|
|
109
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
110
|
+
let res;
|
|
111
|
+
const headers = {
|
|
112
|
+
"content-type": "application/json",
|
|
113
|
+
};
|
|
114
|
+
if (API_KEY) {
|
|
115
|
+
headers["x-api-key"] = API_KEY;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
res = await fetch(MCP_ENDPOINT, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
headers,
|
|
121
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: ++rpcId, method, params }),
|
|
122
|
+
// Never follow redirects: undici keeps custom headers (X-API-Key) on
|
|
123
|
+
// cross-origin redirects, so a 30x from an allowlisted host could leak the
|
|
124
|
+
// key. Fail instead - the host allowlist only guards the INITIAL request.
|
|
125
|
+
redirect: "error",
|
|
126
|
+
signal: controller.signal,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
throw new Error(`PurposeBot MCP ${method} request failed: ${sanitizeDiagnostic(err instanceof Error ? err.message : err)}`);
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
clearTimeout(timeout);
|
|
134
|
+
}
|
|
70
135
|
if (!res.ok) {
|
|
71
|
-
const text = await res
|
|
136
|
+
const text = sanitizeDiagnostic(await readDiagnosticBody(res).catch(() => ""));
|
|
72
137
|
throw new Error(`PurposeBot MCP ${method} -> HTTP ${res.status}: ${text}`);
|
|
73
138
|
}
|
|
74
139
|
const body = (await res.json());
|
|
75
140
|
if (body.error) {
|
|
76
|
-
throw new Error(`PurposeBot MCP ${method} error ${body.error.code}: ${body.error.message}`);
|
|
141
|
+
throw new Error(`PurposeBot MCP ${method} error ${body.error.code}: ${sanitizeDiagnostic(body.error.message)}`);
|
|
77
142
|
}
|
|
78
143
|
return body.result;
|
|
79
144
|
}
|
|
80
|
-
const server = new Server({ name: "purposebot", version: "0.1.
|
|
81
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
82
|
-
const result = await rpc("tools/list");
|
|
83
|
-
return { tools: Array.isArray(result?.tools) ? result.tools : [] };
|
|
145
|
+
const server = new Server({ name: "purposebot", version: "0.1.2" }, { capabilities: { tools: {} } });
|
|
146
|
+
server.setRequestHandler(ListToolsRequestSchema, async (request) => {
|
|
147
|
+
const result = await rpc("tools/list", request.params);
|
|
148
|
+
return { ...(result ?? {}), tools: Array.isArray(result?.tools) ? result.tools : [] };
|
|
84
149
|
});
|
|
85
150
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
86
151
|
const result = await rpc("tools/call", {
|
|
@@ -96,7 +161,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
96
161
|
});
|
|
97
162
|
async function main() {
|
|
98
163
|
await server.connect(new StdioServerTransport());
|
|
99
|
-
|
|
164
|
+
const authMode = API_KEY ? "authenticated" : "keyless";
|
|
165
|
+
console.error(`[purposebot-mcp] connected - proxying ${MCP_ENDPOINT} (${authMode})`);
|
|
100
166
|
}
|
|
101
167
|
main().catch((err) => {
|
|
102
168
|
console.error("[purposebot-mcp] fatal:", err);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@purposebot/mcp",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "PurposeBot
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "PurposeBot agent trust layer for MCP: agent identity, provenance, API access decisions, commerce reputation, and trust-ranked discovery.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"purposebot-mcp": "dist/index.js"
|
|
@@ -22,12 +22,17 @@
|
|
|
22
22
|
"mcp",
|
|
23
23
|
"model-context-protocol",
|
|
24
24
|
"purposebot",
|
|
25
|
+
"agent-trust",
|
|
26
|
+
"agent-identity",
|
|
27
|
+
"api-trust",
|
|
28
|
+
"provenance",
|
|
29
|
+
"reputation",
|
|
25
30
|
"agent-commerce",
|
|
26
|
-
"
|
|
31
|
+
"discovery"
|
|
27
32
|
],
|
|
28
33
|
"license": "MIT",
|
|
29
34
|
"dependencies": {
|
|
30
|
-
"@modelcontextprotocol/sdk": "
|
|
35
|
+
"@modelcontextprotocol/sdk": "1.29.0"
|
|
31
36
|
},
|
|
32
37
|
"devDependencies": {
|
|
33
38
|
"@types/node": "^20.0.0",
|