@purposebot/mcp 0.1.0 → 0.1.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 +23 -12
  2. package/dist/index.js +91 -26
  3. package/package.json +9 -4
package/README.md CHANGED
@@ -1,20 +1,26 @@
1
1
  # @purposebot/mcp
2
2
 
3
- A thin **stdio MCP server** that proxies to the hosted PurposeBot MCP
4
- (`https://purposebot.ai/mcp`). Use it to plug PurposeBot agent-native discovery,
5
- commerce, API Trust, and trust/reputation tools into any MCP client (Claude
6
- Desktop, Cursor, Claude Code, etc.) via `npx`, without writing a custom HTTP
7
- integration.
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`, ) appear automatically this package just forwards
13
- `tools/list` and `tools/call`.
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 18. Get an API key from your PurposeBot account.
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.1"],
27
33
  "env": {
28
34
  "PURPOSEBOT_API_KEY": "pb_xxx"
29
35
  }
@@ -35,15 +41,20 @@ 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.1
39
45
  ```
40
46
 
41
47
  ## Environment
42
48
 
43
49
  | Variable | Required | Default | Notes |
44
50
  |---|---|---|---|
45
- | `PURPOSEBOT_API_KEY` | yes | | sent as `X-API-Key` |
46
- | `PURPOSEBOT_API_URL` | no | `https://purposebot.ai` | point at a sandbox/self-host |
51
+ | `PURPOSEBOT_API_KEY` | yes | - | sent as `X-API-Key` |
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 the 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.
47
58
 
48
59
  ## Develop
49
60
 
package/dist/index.js CHANGED
@@ -1,20 +1,37 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * PurposeBot MCP server a thin stdio proxy to the hosted PurposeBot MCP
4
- * (JSON-RPC at <API_URL>/mcp). Lets agents in Claude Desktop, Cursor, etc. use
5
- * PurposeBot via `npx @purposebot/mcp` without a custom HTTP integration.
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
- * It forwards tools/list and tools/call straight through; the hosted endpoint is
8
- * the source of truth for the tool catalogue, so new tools appear automatically.
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
12
  * PURPOSEBOT_API_KEY (required) — 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
19
  const API_KEY = process.env.PURPOSEBOT_API_KEY;
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);
@@ -24,9 +41,12 @@ function die(msg) {
24
41
  // explicit opt-in for anything else; never send the key over plain http (except
25
42
  // 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
- "www.purposebot.ai",
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
  }
@@ -54,33 +77,75 @@ if (!API_KEY) {
54
77
  const API_URL = resolveApiBase(process.env.PURPOSEBOT_API_URL ?? "https://purposebot.ai");
55
78
  const MCP_ENDPOINT = `${API_URL}/mcp`;
56
79
  let rpcId = 0;
80
+ async function readDiagnosticBody(res) {
81
+ const reader = res.body?.getReader();
82
+ if (!reader)
83
+ return "";
84
+ const decoder = new TextDecoder();
85
+ let bytesRead = 0;
86
+ let text = "";
87
+ try {
88
+ while (bytesRead < MAX_DIAGNOSTIC_BYTES) {
89
+ const { done, value } = await reader.read();
90
+ if (done) {
91
+ text += decoder.decode();
92
+ return text;
93
+ }
94
+ const remaining = MAX_DIAGNOSTIC_BYTES - bytesRead;
95
+ const chunk = value.byteLength > remaining ? value.subarray(0, remaining) : value;
96
+ bytesRead += chunk.byteLength;
97
+ text += decoder.decode(chunk, { stream: true });
98
+ if (value.byteLength > remaining) {
99
+ await reader.cancel().catch(() => undefined);
100
+ return `${text}${decoder.decode()}...`;
101
+ }
102
+ }
103
+ await reader.cancel().catch(() => undefined);
104
+ return `${text}${decoder.decode()}...`;
105
+ }
106
+ finally {
107
+ reader.releaseLock();
108
+ }
109
+ }
57
110
  async function rpc(method, params) {
58
- const res = await fetch(MCP_ENDPOINT, {
59
- method: "POST",
60
- headers: {
61
- "content-type": "application/json",
62
- "x-api-key": API_KEY,
63
- },
64
- body: JSON.stringify({ jsonrpc: "2.0", id: ++rpcId, method, params }),
65
- // Never follow redirects: undici keeps custom headers (X-API-Key) on
66
- // cross-origin redirects, so a 30x from an allowlisted host could leak the
67
- // key. Fail instead — the host allowlist only guards the INITIAL request.
68
- redirect: "error",
69
- });
111
+ const controller = new AbortController();
112
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
113
+ let res;
114
+ try {
115
+ res = await fetch(MCP_ENDPOINT, {
116
+ method: "POST",
117
+ headers: {
118
+ "content-type": "application/json",
119
+ "x-api-key": API_KEY,
120
+ },
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.text().catch(() => "");
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.0" }, { capabilities: { tools: {} } });
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.1" }, { 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,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
96
161
  });
97
162
  async function main() {
98
163
  await server.connect(new StdioServerTransport());
99
- console.error(`[purposebot-mcp] connected proxying ${MCP_ENDPOINT}`);
164
+ console.error(`[purposebot-mcp] connected - proxying ${MCP_ENDPOINT}`);
100
165
  }
101
166
  main().catch((err) => {
102
167
  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.0",
4
- "description": "PurposeBot MCP server stdio proxy to the hosted PurposeBot MCP (https://purposebot.ai/mcp) so agents in Claude Desktop, Cursor, etc. can use it via npx.",
3
+ "version": "0.1.1",
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
- "trust"
31
+ "discovery"
27
32
  ],
28
33
  "license": "MIT",
29
34
  "dependencies": {
30
- "@modelcontextprotocol/sdk": "^1.0.0"
35
+ "@modelcontextprotocol/sdk": "1.29.0"
31
36
  },
32
37
  "devDependencies": {
33
38
  "@types/node": "^20.0.0",