@paidfor/mcp 0.3.1 → 0.3.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/dist/index.js +3 -3
- package/dist/server.d.ts +26 -3
- package/dist/server.js +95 -16
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { createServer as createHttpServer } from "node:http";
|
|
12
12
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
13
|
-
import {
|
|
13
|
+
import { createServer, DEFAULT_BASE, HOSTED_CLIENT_HEADER, HOSTED_INSTRUCTIONS, TOOL_NAMES, VERSION } from "./server.js";
|
|
14
14
|
const base = (process.env.PAIDFOR_API_BASE ?? process.env.PAIDFOR_URL ?? DEFAULT_BASE).replace(/\/$/, "");
|
|
15
15
|
const port = Number(process.env.PORT ?? 3000);
|
|
16
16
|
const host = process.env.HOST ?? "0.0.0.0";
|
|
@@ -21,7 +21,7 @@ const noteGeneratedAt = (g) => {
|
|
|
21
21
|
if (g)
|
|
22
22
|
dataGeneratedAt = g;
|
|
23
23
|
};
|
|
24
|
-
fetch(`${base}/v1/summary`, { headers: { Accept: "application/json", "User-Agent": `paidfor-mcp/${VERSION}`, "X-Paidfor-Client":
|
|
24
|
+
fetch(`${base}/v1/summary`, { headers: { Accept: "application/json", "User-Agent": `paidfor-mcp/${VERSION}`, "X-Paidfor-Client": HOSTED_CLIENT_HEADER } })
|
|
25
25
|
.then((r) => (r.ok ? r.json() : null))
|
|
26
26
|
.then((j) => noteGeneratedAt(j?.generated_at))
|
|
27
27
|
.catch(() => undefined);
|
|
@@ -62,7 +62,7 @@ createHttpServer(async (req, res) => {
|
|
|
62
62
|
}
|
|
63
63
|
for (const [k, v] of Object.entries(CORS))
|
|
64
64
|
res.setHeader(k, v);
|
|
65
|
-
const server = createServer({ baseUrl: base, onResponse: noteGeneratedAt });
|
|
65
|
+
const server = createServer({ baseUrl: base, onResponse: noteGeneratedAt, clientHeader: HOSTED_CLIENT_HEADER, instructions: HOSTED_INSTRUCTIONS });
|
|
66
66
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
67
67
|
res.on("close", () => {
|
|
68
68
|
void transport.close();
|
package/dist/server.d.ts
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
8
|
export * from "./rules.js";
|
|
9
|
-
export declare const VERSION = "0.3.
|
|
9
|
+
export declare const VERSION = "0.3.2";
|
|
10
10
|
/** Sent on every request to the index so usage can be counted by channel (never anything else). */
|
|
11
|
-
export declare const CLIENT_HEADER = "mcp/0.3.
|
|
11
|
+
export declare const CLIENT_HEADER = "mcp/0.3.2";
|
|
12
|
+
/** What the hosted service (index.ts) sends instead, so the index can count hosted and stdio use apart. */
|
|
13
|
+
export declare const HOSTED_CLIENT_HEADER = "mcp-hosted/0.3.2";
|
|
12
14
|
export declare const DEFAULT_BASE = "https://paidfor.dev";
|
|
13
15
|
export declare const TOOL_NAMES: readonly ["check_x402_seller", "list_reliable_sellers", "recent_changes"];
|
|
14
16
|
/**
|
|
@@ -19,6 +21,12 @@ export declare const NO_ACCOUNT_PROMISE: string;
|
|
|
19
21
|
/** Sent on initialize by every transport (stdio, the hosted service, the site's /mcp route), so clients call the tools unprompted.
|
|
20
22
|
* Generated from rules.ts, like the tool descriptions below and the skill at paidfor.dev/skill.md. */
|
|
21
23
|
export declare const INSTRUCTIONS: string;
|
|
24
|
+
/**
|
|
25
|
+
* What the hosted transports (index.ts, the site's /mcp route) send instead of INSTRUCTIONS: the same facts, described
|
|
26
|
+
* rather than directed, because the Connectors Directory review rejects text that tells Claude how to behave or to call
|
|
27
|
+
* tools unprompted. stdio and the skill keep INSTRUCTIONS; the tool descriptions are shared by both.
|
|
28
|
+
*/
|
|
29
|
+
export declare const HOSTED_INSTRUCTIONS: string;
|
|
22
30
|
export declare const CHECK_DESCRIPTION: string;
|
|
23
31
|
export declare const LIST_DESCRIPTION: string;
|
|
24
32
|
export declare const CHANGES_DESCRIPTION: string;
|
|
@@ -29,7 +37,22 @@ export type ServerOptions = {
|
|
|
29
37
|
onToolCall?: (tool: string) => void;
|
|
30
38
|
/** Called with the export's generated_at from every successful response (the hosted service shows it on /health). */
|
|
31
39
|
onResponse?: (generated_at: string) => void;
|
|
32
|
-
/**
|
|
40
|
+
/** The X-Paidfor-Client header on every index call; defaults to CLIENT_HEADER (stdio). */
|
|
41
|
+
clientHeader?: string;
|
|
42
|
+
/** Replaces INSTRUCTIONS; stdio.ts passes INSTRUCTIONS_WITH_PAY when it registers pay_x402, the hosted transports HOSTED_INSTRUCTIONS. */
|
|
33
43
|
instructions?: string;
|
|
34
44
|
};
|
|
45
|
+
/** Advertised on initialize (Implementation in the MCP schema) for clients and directories. */
|
|
46
|
+
export declare const SERVER_INFO: {
|
|
47
|
+
name: string;
|
|
48
|
+
title: string;
|
|
49
|
+
version: string;
|
|
50
|
+
description: string;
|
|
51
|
+
websiteUrl: string;
|
|
52
|
+
icons: {
|
|
53
|
+
src: string;
|
|
54
|
+
mimeType: string;
|
|
55
|
+
sizes: string[];
|
|
56
|
+
}[];
|
|
57
|
+
};
|
|
35
58
|
export declare function createServer(opts?: ServerOptions): McpServer;
|
package/dist/server.js
CHANGED
|
@@ -8,9 +8,11 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { CHANGES_TRIGGER, CHECK_TRIGGERS, CHOOSE_TRIGGER, NEVER_PAY_TWICE, SENDS_ONLY_URL, VERDICT_SENTENCE, cap, listAnd, listOr } from "./rules.js";
|
|
10
10
|
export * from "./rules.js";
|
|
11
|
-
export const VERSION = "0.3.
|
|
11
|
+
export const VERSION = "0.3.2";
|
|
12
12
|
/** Sent on every request to the index so usage can be counted by channel (never anything else). */
|
|
13
13
|
export const CLIENT_HEADER = `mcp/${VERSION}`;
|
|
14
|
+
/** What the hosted service (index.ts) sends instead, so the index can count hosted and stdio use apart. */
|
|
15
|
+
export const HOSTED_CLIENT_HEADER = `mcp-hosted/${VERSION}`;
|
|
14
16
|
export const DEFAULT_BASE = "https://paidfor.dev";
|
|
15
17
|
export const TOOL_NAMES = ["check_x402_seller", "list_reliable_sellers", "recent_changes"];
|
|
16
18
|
const CATEGORIES = ["inference", "data", "scraping", "compute", "other"];
|
|
@@ -31,30 +33,103 @@ export const INSTRUCTIONS = "paidfor rates x402 sellers by paying them and verif
|
|
|
31
33
|
SENDS_ONLY_URL +
|
|
32
34
|
"\n" +
|
|
33
35
|
NO_ACCOUNT_PROMISE;
|
|
36
|
+
/**
|
|
37
|
+
* What the hosted transports (index.ts, the site's /mcp route) send instead of INSTRUCTIONS: the same facts, described
|
|
38
|
+
* rather than directed, because the Connectors Directory review rejects text that tells Claude how to behave or to call
|
|
39
|
+
* tools unprompted. stdio and the skill keep INSTRUCTIONS; the tool descriptions are shared by both.
|
|
40
|
+
*/
|
|
41
|
+
export const HOSTED_INSTRUCTIONS = "paidfor rates x402 sellers by paying them and verifying delivery. " +
|
|
42
|
+
"check_x402_seller returns a verdict for an x402 resource URL or seller domain, useful before an x402 payment or before retrying a request that answered HTTP 402: " +
|
|
43
|
+
'"avoid" means the seller failed paid calls (the evidence link shows how), "caution" carries the failure rate, "ok" means no failures across its paid calls, and "unknown" means the index has no data on it. ' +
|
|
44
|
+
"list_reliable_sellers returns sellers with zero failures across at least 30 paid calls, for choosing among sellers. " +
|
|
45
|
+
"recent_changes returns what changed in the index, which is diffed hourly. " +
|
|
46
|
+
`${SENDS_ONLY_URL}\n` +
|
|
47
|
+
NO_ACCOUNT_PROMISE;
|
|
34
48
|
export const CHECK_DESCRIPTION = `Call this ${listOr(CHECK_TRIGGERS)}. ` +
|
|
35
49
|
"Returns a verdict (avoid, caution, ok, unknown), one sentence to act on, the seller's paid-call count and failure rate with a 95% interval, " +
|
|
36
50
|
"listed price, modal quote and max overcharge ratio when known, and links to the seller page and raw evidence.";
|
|
37
51
|
export const LIST_DESCRIPTION = `Call this ${CHOOSE_TRIGGER}, or when the user asks for a safe or reliable API. ` +
|
|
38
|
-
"Returns sellers with zero failures across at least 30 paid calls, most-called first
|
|
52
|
+
"Returns sellers with zero failures across at least 30 paid calls, most-called first. For each: domain, category, paid-call count, " +
|
|
53
|
+
"the 95% upper bound on its failure rate, median quoted price in USDC, USDC paid to it in the last 30 days, when it was last seen, and its paidfor seller page.";
|
|
39
54
|
export const CHANGES_DESCRIPTION = `Call this ${CHANGES_TRIGGER}. ` +
|
|
40
55
|
"Returns recent changes, newest first: verdict changes, new failures by type, moved quotes, changed pay-to addresses, sellers added or reaching 30 paid calls, published seller replies, " +
|
|
41
|
-
"the operator-reported series maturing or disagreeing with ours
|
|
56
|
+
"the operator-reported series maturing or disagreeing with ours. Each change has its time, type, seller domain, seller page and type-specific details (for example the old and new quote).";
|
|
57
|
+
/** Advertised on initialize (Implementation in the MCP schema) for clients and directories. */
|
|
58
|
+
export const SERVER_INFO = {
|
|
59
|
+
name: "paidfor",
|
|
60
|
+
title: "paidfor: x402 seller reliability",
|
|
61
|
+
version: VERSION,
|
|
62
|
+
description: "Helps an agent decide whether to pay an x402 API seller on Base: verdicts, failure rates and evidence from real paid calls, " +
|
|
63
|
+
"a list of sellers that have never failed, and a feed of recent changes. No account or API key needed.",
|
|
64
|
+
websiteUrl: DEFAULT_BASE,
|
|
65
|
+
icons: [{ src: `${DEFAULT_BASE}/icon.png`, mimeType: "image/png", sizes: ["512x512"] }],
|
|
66
|
+
};
|
|
67
|
+
const BY_TYPE = z.record(z.string(), z.number()).describe("Failure counts by type (non_delivery, overcharge, duplicate, wrong_goods)");
|
|
68
|
+
/** The structured result of check_x402_seller: the same object the text content carries after its lead sentence. */
|
|
69
|
+
const CHECK_OUTPUT = {
|
|
70
|
+
verdict: z.enum(["avoid", "caution", "ok", "unknown"]),
|
|
71
|
+
verdict_text: z.string().describe("One sentence to act on, with the evidence link"),
|
|
72
|
+
host: z.string().nullable(),
|
|
73
|
+
seller: z
|
|
74
|
+
.object({
|
|
75
|
+
domain: z.string(), calls: z.number(), failures: z.number(), failure_rate: z.number(),
|
|
76
|
+
wilson_low: z.number().describe("95% interval lower bound on the failure rate"),
|
|
77
|
+
wilson_high: z.number().describe("95% interval upper bound on the failure rate"),
|
|
78
|
+
by_type: BY_TYPE, low_sample: z.boolean(), last_seen: z.string(),
|
|
79
|
+
})
|
|
80
|
+
.nullable()
|
|
81
|
+
.describe("Measured paid calls to this seller; null when the study never paid it"),
|
|
82
|
+
resource: z
|
|
83
|
+
.object({
|
|
84
|
+
url: z.string(), method: z.string(), listed_price_usdc: z.string().nullable(), modal_quote_usdc: z.string().nullable(),
|
|
85
|
+
max_quote_usdc: z.string().nullable(), max_overcharge_ratio: z.number().nullable(), calls: z.number(), failure_rate: z.number(),
|
|
86
|
+
})
|
|
87
|
+
.nullable()
|
|
88
|
+
.describe("Measured paid calls to this exact URL; null when not measured"),
|
|
89
|
+
probe: z.looseObject({}).nullable().describe("Hourly unpaid probes of the seller's 402 quote; null when not probed"),
|
|
90
|
+
candidate: z.looseObject({}).nullable().describe("Queue state for a seller not yet measured; null otherwise"),
|
|
91
|
+
seller_url: z.string().nullable(),
|
|
92
|
+
evidence_url: z.string().nullable(),
|
|
93
|
+
methodology_url: z.string(),
|
|
94
|
+
generated_at: z.string().describe("When the index was generated"),
|
|
95
|
+
};
|
|
96
|
+
const LIST_OUTPUT = {
|
|
97
|
+
generated_at: z.string().describe("When the index was generated"),
|
|
98
|
+
sellers: z.array(z.object({
|
|
99
|
+
domain: z.string(), category: z.string(), calls: z.number(),
|
|
100
|
+
failure_rate_upper_bound: z.number().describe("95% interval upper bound on the failure rate"),
|
|
101
|
+
median_quote_usdc: z.string().nullable(), inflow_30d_usdc: z.string().nullable(), last_seen: z.string(),
|
|
102
|
+
url: z.string().describe("The seller's paidfor page"),
|
|
103
|
+
})),
|
|
104
|
+
};
|
|
105
|
+
const CHANGES_OUTPUT = {
|
|
106
|
+
count: z.number().describe("Matching changes the index returned (it caps the feed at the newest 500); events holds at most the newest 100"),
|
|
107
|
+
events: z.array(z.object({
|
|
108
|
+
id: z.string(), at: z.string(), type: z.string(), host: z.string(), url: z.string().describe("The seller's paidfor page"),
|
|
109
|
+
data: z.record(z.string(), z.unknown()).describe("Type-specific details"),
|
|
110
|
+
})),
|
|
111
|
+
};
|
|
42
112
|
/** The read tools only read the public index; the directories ask for these hints. */
|
|
43
113
|
const READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true };
|
|
44
|
-
async function
|
|
45
|
-
const res = await fetch(url, { headers: { Accept: "application/json", "User-Agent": `paidfor-mcp/${VERSION}`, "X-Paidfor-Client":
|
|
46
|
-
if (!res.ok)
|
|
47
|
-
|
|
114
|
+
async function getJsonAs(url, client = CLIENT_HEADER) {
|
|
115
|
+
const res = await fetch(url, { headers: { Accept: "application/json", "User-Agent": `paidfor-mcp/${VERSION}`, "X-Paidfor-Client": client } });
|
|
116
|
+
if (!res.ok) {
|
|
117
|
+
// Pass the API's own reason through (e.g. "since must be an ISO 8601 time"), so the caller can fix the input.
|
|
118
|
+
const reason = await res.json().then((j) => j.error, () => undefined);
|
|
119
|
+
throw new Error(`${url} returned HTTP ${res.status}${reason ? `: ${reason}` : ""}`);
|
|
120
|
+
}
|
|
48
121
|
return (await res.json());
|
|
49
122
|
}
|
|
50
123
|
export function createServer(opts = {}) {
|
|
51
124
|
const base = (opts.baseUrl ?? process.env.PAIDFOR_API_BASE ?? process.env.PAIDFOR_URL ?? DEFAULT_BASE).replace(/\/$/, "");
|
|
52
|
-
const
|
|
125
|
+
const getJson = (url) => getJsonAs(url, opts.clientHeader);
|
|
126
|
+
const server = new McpServer(SERVER_INFO, { instructions: opts.instructions ?? INSTRUCTIONS });
|
|
53
127
|
server.registerTool("check_x402_seller", {
|
|
54
128
|
title: "Check an x402 seller before paying",
|
|
55
129
|
description: CHECK_DESCRIPTION,
|
|
56
130
|
annotations: READ_ONLY,
|
|
57
131
|
inputSchema: { url: z.string().min(1).describe("x402 resource URL, or a bare seller domain such as api.example.com") },
|
|
132
|
+
outputSchema: CHECK_OUTPUT,
|
|
58
133
|
}, async ({ url }) => {
|
|
59
134
|
opts.onToolCall?.("check_x402_seller");
|
|
60
135
|
const r = await getJson(`${base}/v1/check?url=${encodeURIComponent(url)}`);
|
|
@@ -70,21 +145,24 @@ export function createServer(opts = {}) {
|
|
|
70
145
|
? { url: r.resource.url, method: r.resource.method, listed_price_usdc: r.resource.listed_price_usdc, modal_quote_usdc: r.resource.modal_quote_usdc, max_quote_usdc: r.resource.max_quote_usdc, max_overcharge_ratio: r.resource.overcharge_ratio, calls: r.resource.calls, failure_rate: r.resource.failure_rate }
|
|
71
146
|
: null,
|
|
72
147
|
probe: r.probe,
|
|
148
|
+
candidate: r.candidate ?? null,
|
|
73
149
|
seller_url: r.seller_url,
|
|
74
150
|
evidence_url: r.evidence_url,
|
|
75
151
|
methodology_url: r.methodology_url,
|
|
76
152
|
generated_at: r.generated_at,
|
|
77
153
|
};
|
|
78
|
-
|
|
154
|
+
const lead = r.candidate ? `${r.verdict_text} ${r.candidate.note}` : r.verdict_text;
|
|
155
|
+
return { content: [{ type: "text", text: `${lead}\n\n${JSON.stringify(short, null, 2)}` }], structuredContent: short };
|
|
79
156
|
});
|
|
80
157
|
server.registerTool("list_reliable_sellers", {
|
|
81
|
-
title: "List reliable
|
|
158
|
+
title: "List reliable pay-per-call API sellers",
|
|
82
159
|
description: LIST_DESCRIPTION,
|
|
83
160
|
annotations: READ_ONLY,
|
|
84
161
|
inputSchema: {
|
|
85
|
-
category: z.enum(CATEGORIES).optional().describe("
|
|
86
|
-
limit: z.number().int().min(1).max(500).optional().describe("
|
|
162
|
+
category: z.enum(CATEGORIES).optional().describe("Kind of service to list sellers for; omit to list every kind"),
|
|
163
|
+
limit: z.number().int().min(1).max(500).optional().describe("How many sellers to return, most-called first; 50 when omitted"),
|
|
87
164
|
},
|
|
165
|
+
outputSchema: LIST_OUTPUT,
|
|
88
166
|
}, async ({ category, limit }) => {
|
|
89
167
|
opts.onToolCall?.("list_reliable_sellers");
|
|
90
168
|
const q = new URLSearchParams({ min_calls: "30", zero_failures: "1", sort: "calls", limit: String(limit ?? 50) });
|
|
@@ -94,16 +172,17 @@ export function createServer(opts = {}) {
|
|
|
94
172
|
opts.onResponse?.(r.generated_at);
|
|
95
173
|
const rows = r.sellers.map((s) => ({ domain: s.domain, category: s.category, calls: s.calls, failure_rate_upper_bound: s.wilson_high, median_quote_usdc: s.median_quote_usdc, inflow_30d_usdc: s.inflow_30d_usdc, last_seen: s.last_seen, url: s.url }));
|
|
96
174
|
const head = `${rows.length} sellers with zero failures across at least 30 paid calls${category ? ` in ${category}` : ""} (index generated ${r.generated_at}).`;
|
|
97
|
-
return { content: [{ type: "text", text: `${head}\n\n${JSON.stringify(rows, null, 2)}` }] };
|
|
175
|
+
return { content: [{ type: "text", text: `${head}\n\n${JSON.stringify(rows, null, 2)}` }], structuredContent: { generated_at: r.generated_at, sellers: rows } };
|
|
98
176
|
});
|
|
99
177
|
server.registerTool("recent_changes", {
|
|
100
178
|
title: "Recent changes to x402 sellers",
|
|
101
179
|
description: CHANGES_DESCRIPTION,
|
|
102
180
|
annotations: READ_ONLY,
|
|
103
181
|
inputSchema: {
|
|
104
|
-
host: z.string().min(1).optional().describe("
|
|
105
|
-
since: z.string().min(1).optional().describe("
|
|
182
|
+
host: z.string().min(1).optional().describe("Seller to show changes for, as a bare domain such as api.example.com; omit for every seller"),
|
|
183
|
+
since: z.string().min(1).optional().describe("Show only changes after this time, in ISO 8601 (such as the generated_at of a verdict you already have); omit for the latest changes"),
|
|
106
184
|
},
|
|
185
|
+
outputSchema: CHANGES_OUTPUT,
|
|
107
186
|
}, async ({ host, since }) => {
|
|
108
187
|
opts.onToolCall?.("recent_changes");
|
|
109
188
|
const q = new URLSearchParams();
|
|
@@ -116,7 +195,7 @@ export function createServer(opts = {}) {
|
|
|
116
195
|
const head = r.count
|
|
117
196
|
? `${r.count} changes${host ? ` for ${host}` : ""}${since ? ` since ${since}` : ""}, newest first${r.count > rows.length ? ` (first ${rows.length} shown)` : ""}.`
|
|
118
197
|
: `No changes${host ? ` for ${host}` : ""}${since ? ` since ${since}` : ""}. The index is diffed hourly.`;
|
|
119
|
-
return { content: [{ type: "text", text: `${head}\n\n${JSON.stringify(rows, null, 2)}` }] };
|
|
198
|
+
return { content: [{ type: "text", text: `${head}\n\n${JSON.stringify(rows, null, 2)}` }], structuredContent: { count: r.count, events: rows } };
|
|
120
199
|
});
|
|
121
200
|
return server;
|
|
122
201
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paidfor/mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"mcpName": "io.github.sarmadahsan/paidfor",
|
|
5
5
|
"description": "MCP server for the paidfor.dev x402 seller index: check_x402_seller, list_reliable_sellers and recent_changes, plus pay_x402 (stdio, your own wallet)",
|
|
6
6
|
"license": "MIT",
|