@paidfor/mcp 0.3.1 → 0.4.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/README.md +8 -3
- package/dist/index.js +44 -5
- package/dist/pay.js +3 -3
- package/dist/rules.d.ts +16 -0
- package/dist/rules.js +17 -0
- package/dist/server.d.ts +109 -10
- package/dist/server.js +215 -26
- package/dist/setup.d.ts +48 -0
- package/dist/setup.js +118 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
# @paidfor/mcp
|
|
2
2
|
|
|
3
|
+
**[Listed in the Claude connector directory](https://claude.ai/directory/paidfor).** In claude.ai or Claude Desktop: Settings, Connectors, browse the directory, paidfor, Connect; no key, no configuration.
|
|
4
|
+
|
|
3
5
|
MCP server for [paidfor.dev](https://paidfor.dev), the x402 seller failure index on Base mainnet.
|
|
4
6
|
|
|
5
7
|
**No account, no key.** Everything that reads the index or contributes data (the site, the API, the MCP tools, the client, telemetry and the changes feed) needs no account and no key, now and later. An email address is asked for only to deliver alerts or to join a waitlist, as a confirmation link, never a profile.
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
[](https://cursor.com/install-mcp?name=paidfor&config=eyJ1cmwiOiJodHRwczovL21jcC5wYWlkZm9yLmRldi9tY3AifQ%3D%3D)
|
|
10
|
+
|
|
11
|
+
Three read tools and one report tool on every transport, plus two paying tools on stdio only when a wallet is configured:
|
|
8
12
|
|
|
9
13
|
- `check_x402_seller({ url })`: verdict (`avoid`, `caution`, `ok`, `unknown`) and one sentence for an x402 resource URL or seller domain, with the seller's paid-call count, failure rate and 95% interval, the resource's listed price, modal quote and max overcharge ratio when known, and links to the seller page and raw evidence.
|
|
10
14
|
- `list_reliable_sellers({ category?, limit? })`: zero-failure sellers with at least 30 paid calls.
|
|
11
15
|
- `recent_changes({ host?, since? })`: what changed, newest first (verdicts, new failures, moved quotes, pay-to addresses, replies, the operator series maturing or disagreeing); called when the user asks what changed or before relying on a verdict older than a day. Reads `GET https://paidfor.dev/v1/changes`.
|
|
16
|
+
- `report_x402_payment({ url, tx_hash, outcome, quoted_usdc?, http_status? })`: after your agent pays an x402 seller with any wallet, reports what came back (`ok`, `non_delivery`, `wrong_goods`, `overcharge`, `duplicate`) with the settlement transaction hash. No key, no wallet: the index reads the payer and the amount from the transaction on Base and answers in one sentence (accepted and verified, pending, already reported, or rejected with the reason). It moves no money; the same hash never makes a second row. Posts the tx_only row to `POST https://paidfor.dev/v1/telemetry`.
|
|
12
17
|
- `pay_x402({ url, method?, headers?, body?, intent_id?, max_usd? })` (stdio, wallet configured): pays through [`@paidfor/x402-safe-fetch`](https://www.npmjs.com/package/@paidfor/x402-safe-fetch) with your own wallet. See "Let your agent pay, safely" below.
|
|
13
18
|
- `wallet_status()` (stdio, wallet configured): the paying address, its USDC balance on Base, the limits and today's spend. Never the key.
|
|
14
19
|
|
|
@@ -18,7 +23,7 @@ The read tools use the public JSON API (`GET https://paidfor.dev/v1/check`, `/v1
|
|
|
18
23
|
|
|
19
24
|
## Hosted (Streamable HTTP)
|
|
20
25
|
|
|
21
|
-
`https://mcp.paidfor.dev/mcp`; a plain `GET` on it (or on `/status`) returns the service status with the data `generated_at` it last saw. `/health` on that host is answered by Manufact's gateway. Cursor, Claude Code and any Streamable HTTP client take the URL directly; Claude Desktop takes it under Settings, Connectors, or through `mcp-remote`:
|
|
26
|
+
`https://mcp.paidfor.dev/mcp`; a plain `GET` on it (or on `/status`) returns the service status with the data `generated_at` it last saw. A browser (`Accept` preferring `text/html`) gets a setup page from `src/setup.ts` on `/mcp` and `/` instead; `?format=json` brings the status back. `/health` on that host is answered by Manufact's gateway. Cursor, Claude Code and any Streamable HTTP client take the URL directly; Claude Desktop takes it under Settings, Connectors, or through `mcp-remote`:
|
|
22
27
|
|
|
23
28
|
```json
|
|
24
29
|
{ "mcpServers": { "paidfor": { "url": "https://mcp.paidfor.dev/mcp" } } }
|
|
@@ -105,4 +110,4 @@ Usage is counted by Manufact's gateway (every `tools/call` with client and sessi
|
|
|
105
110
|
|
|
106
111
|
## Registry
|
|
107
112
|
|
|
108
|
-
`server.json` is the Official MCP Registry entry (`
|
|
113
|
+
`server.json` is the Official MCP Registry entry (`dev.paidfor/paidfor`, matching `mcpName` in `package.json` from 0.3.3) and the manifest the site serves at https://paidfor.dev/.well-known/mcp.json and `/mcp.json`. Its `_meta` is `MANIFEST_META` from `src/setup.ts`; the site test fails when they differ. Publish npm first, then `mcp-publisher login dns --domain paidfor.dev` and `mcp-publisher publish` (checklist in `docs/listings.md` at the repo root).
|
package/dist/index.js
CHANGED
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
* The hosted Streamable HTTP service: `node dist/index.js`. Plain @modelcontextprotocol/sdk,
|
|
3
3
|
* no platform SDK, so the same folder runs on Manufact Cloud, Fly, Railway or a bare VM.
|
|
4
4
|
* Stateless: one transport per request. GET /mcp (without Accept: text/event-stream) and
|
|
5
|
-
* GET /status describe the service,
|
|
5
|
+
* GET /status describe the service (a browser, Accept preferring text/html, gets the setup page from setup.ts instead,
|
|
6
|
+
* and ?format=json brings the JSON back), including the site it reads and the export's
|
|
6
7
|
* generated_at it last saw; the warm-up ping uses GET /mcp. GET /health is the plain
|
|
7
8
|
* platform check (Manufact's gateway answers /health itself, Fly and Railway forward it).
|
|
8
9
|
*
|
|
9
10
|
* Env: PORT (default 3000), HOST (default 0.0.0.0), PAIDFOR_API_BASE (default https://paidfor.dev).
|
|
10
11
|
*/
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
11
13
|
import { createServer as createHttpServer } from "node:http";
|
|
12
14
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
13
|
-
import {
|
|
15
|
+
import { clientTag, CLIENT_TAGS, createServer, DEFAULT_BASE, HOSTED_CLIENT_HEADER, HOSTED_INSTRUCTIONS, TOOL_NAMES, VERSION, taggedClientHeader } from "./server.js";
|
|
16
|
+
import { HOSTED_URL, prefersHtml, setupPageHtml, wantsJsonFormat } from "./setup.js";
|
|
14
17
|
const base = (process.env.PAIDFOR_API_BASE ?? process.env.PAIDFOR_URL ?? DEFAULT_BASE).replace(/\/$/, "");
|
|
15
18
|
const port = Number(process.env.PORT ?? 3000);
|
|
16
19
|
const host = process.env.HOST ?? "0.0.0.0";
|
|
@@ -21,10 +24,25 @@ const noteGeneratedAt = (g) => {
|
|
|
21
24
|
if (g)
|
|
22
25
|
dataGeneratedAt = g;
|
|
23
26
|
};
|
|
24
|
-
fetch(`${base}/v1/summary`, { headers: { Accept: "application/json", "User-Agent": `paidfor-mcp/${VERSION}`, "X-Paidfor-Client":
|
|
27
|
+
fetch(`${base}/v1/summary`, { headers: { Accept: "application/json", "User-Agent": `paidfor-mcp/${VERSION}`, "X-Paidfor-Client": HOSTED_CLIENT_HEADER } })
|
|
25
28
|
.then((r) => (r.ok ? r.json() : null))
|
|
26
29
|
.then((j) => noteGeneratedAt(j?.generated_at))
|
|
27
30
|
.catch(() => undefined);
|
|
31
|
+
/**
|
|
32
|
+
* The server is stateless, so the client tag read on initialize rides in the Mcp-Session-Id it answers with
|
|
33
|
+
* (`<tag>.<uuid>`), which clients send back on every later request; the stateless transport ignores the header itself.
|
|
34
|
+
*/
|
|
35
|
+
const SESSION_TAG = new RegExp(`^(${CLIENT_TAGS.join("|")})\\.`);
|
|
36
|
+
const sessionTag = (req) => {
|
|
37
|
+
const id = req.headers["mcp-session-id"];
|
|
38
|
+
return (typeof id === "string" && SESSION_TAG.exec(id)?.[1]) || null;
|
|
39
|
+
};
|
|
40
|
+
const readBody = (req) => new Promise((resolve, reject) => {
|
|
41
|
+
const chunks = [];
|
|
42
|
+
req.on("data", (c) => chunks.push(c));
|
|
43
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
44
|
+
req.on("error", reject);
|
|
45
|
+
});
|
|
28
46
|
const CORS = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS", "Access-Control-Allow-Headers": "*" };
|
|
29
47
|
const sendJson = (res, status, body) => {
|
|
30
48
|
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", ...CORS });
|
|
@@ -57,19 +75,40 @@ createHttpServer(async (req, res) => {
|
|
|
57
75
|
}
|
|
58
76
|
if (path === "/" || path === "/mcp") {
|
|
59
77
|
if (req.method === "GET" && !(req.headers.accept ?? "").includes("text/event-stream")) {
|
|
78
|
+
if (prefersHtml(req.headers.accept) && !wantsJsonFormat(req.url)) {
|
|
79
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "private, max-age=600", Vary: "Accept", ...CORS });
|
|
80
|
+
res.end(setupPageHtml({ endpoint: HOSTED_URL, statusUrl: `${HOSTED_URL}?format=json` }));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
60
83
|
sendJson(res, 200, status());
|
|
61
84
|
return;
|
|
62
85
|
}
|
|
63
86
|
for (const [k, v] of Object.entries(CORS))
|
|
64
87
|
res.setHeader(k, v);
|
|
65
|
-
|
|
88
|
+
let body;
|
|
89
|
+
let tag = sessionTag(req);
|
|
90
|
+
if (req.method === "POST") {
|
|
91
|
+
try {
|
|
92
|
+
body = JSON.parse(await readBody(req));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
sendJson(res, 400, { jsonrpc: "2.0", error: { code: -32700, message: "Parse error: Invalid JSON" }, id: null });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const init = (Array.isArray(body) ? body : [body]).find((m) => m?.method === "initialize");
|
|
99
|
+
if (init) {
|
|
100
|
+
tag = clientTag(init.params?.clientInfo?.name);
|
|
101
|
+
res.setHeader("Mcp-Session-Id", `${tag}.${randomUUID()}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const server = createServer({ baseUrl: base, onResponse: noteGeneratedAt, clientHeader: taggedClientHeader(tag), instructions: HOSTED_INSTRUCTIONS });
|
|
66
105
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
67
106
|
res.on("close", () => {
|
|
68
107
|
void transport.close();
|
|
69
108
|
void server.close();
|
|
70
109
|
});
|
|
71
110
|
await server.connect(transport);
|
|
72
|
-
await transport.handleRequest(req, res);
|
|
111
|
+
await transport.handleRequest(req, res, body);
|
|
73
112
|
return;
|
|
74
113
|
}
|
|
75
114
|
sendJson(res, 404, { error: "not found", endpoints: ["/mcp", "/status", "/health"] });
|
package/dist/pay.js
CHANGED
|
@@ -17,7 +17,7 @@ const USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
|
17
17
|
/** The client's smallest positive cap: with the daily budget spent, any quote is refused before signing. */
|
|
18
18
|
const MIN_CAP_USD = 0.000001;
|
|
19
19
|
export const PAY_INSTRUCTIONS = "If pay_x402 is available, use it for every x402 purchase instead of any other way of paying; it checks the seller first, refuses avoid sellers, caps the amount, " +
|
|
20
|
-
"never pays twice for the same intent, and reports the verified outcome to the index. Reuse intent_id when retrying the same purchase. " +
|
|
20
|
+
"never pays twice for the same intent, and reports the verified outcome to the index by itself (report_x402_payment is not needed after it). Reuse intent_id when retrying the same purchase. " +
|
|
21
21
|
"Tell the user the amount and the seller verdict before the first purchase from a new seller. " +
|
|
22
22
|
"pay_x402 signs with the user's own wallet on this machine; the key never leaves it.";
|
|
23
23
|
/** The stdio server's instructions with a wallet configured: the pay paragraph goes in before the no-account promise, which stays last. */
|
|
@@ -74,7 +74,7 @@ export async function registerPayTools(server, o) {
|
|
|
74
74
|
server.registerTool("pay_x402", {
|
|
75
75
|
title: "Pay an x402 seller safely",
|
|
76
76
|
description: PAY_DESCRIPTION,
|
|
77
|
-
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
77
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true, title: "Pay an x402 seller safely" },
|
|
78
78
|
inputSchema: {
|
|
79
79
|
url: z.string().url().describe("The x402 resource URL"),
|
|
80
80
|
method: z.string().min(1).max(10).optional().describe("HTTP method, default GET"),
|
|
@@ -138,7 +138,7 @@ export async function registerPayTools(server, o) {
|
|
|
138
138
|
server.registerTool("wallet_status", {
|
|
139
139
|
title: "Show the paying wallet",
|
|
140
140
|
description: WALLET_STATUS_DESCRIPTION,
|
|
141
|
-
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
141
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, title: "Show the paying wallet" },
|
|
142
142
|
inputSchema: {},
|
|
143
143
|
}, async () => {
|
|
144
144
|
const spent = await ledger.spentMicro();
|
package/dist/rules.d.ts
CHANGED
|
@@ -24,6 +24,22 @@ export declare const NEVER_PAY_TWICE = "Never pay the same seller twice for one
|
|
|
24
24
|
export declare const SENDS_ONLY_URL = "These tools read a public index, need no key, and send nothing but the URL being checked.";
|
|
25
25
|
/** sha256 of the JSON of every constant above (lib/skill.test.ts recomputes it). */
|
|
26
26
|
export declare const RULES_SHA = "7258b901f6577a02dfe997416d3de61e122f5da21476eb686dd1c1a9379bffc6";
|
|
27
|
+
/**
|
|
28
|
+
* report_x402_payment (POST /v1/telemetry, tx_only proof). Kept out of RULES_SHA: the skill moved to its `.2` revision
|
|
29
|
+
* when these were added (2026-10-29), not RULES_REVISION. Changing them is a skill-only change: bump that suffix.
|
|
30
|
+
*/
|
|
31
|
+
export declare const REPORT_TRIGGER = "after your agent pays an x402 seller with any wallet";
|
|
32
|
+
export declare const REPORT_DESCRIPTION: string;
|
|
33
|
+
/** The outcomes a reporter can name, in the index's words. */
|
|
34
|
+
export declare const REPORT_OUTCOMES: {
|
|
35
|
+
readonly ok: "the response was usable for its purpose";
|
|
36
|
+
readonly non_delivery: "an error, an empty body or nothing usable after paying";
|
|
37
|
+
readonly wrong_goods: "a usable response, but not what was asked for";
|
|
38
|
+
readonly overcharge: "charged more than the quoted or listed price";
|
|
39
|
+
readonly duplicate: "two settlements for one purchase";
|
|
40
|
+
};
|
|
41
|
+
/** What the report sends, for the instructions and the skill. */
|
|
42
|
+
export declare const REPORT_SENDS = "the resource URL, the settlement transaction hash, the outcome and the quote if given";
|
|
27
43
|
/** "a, b, and c" */
|
|
28
44
|
export declare function listAnd(xs: readonly string[]): string;
|
|
29
45
|
/** "a, b, or c" */
|
package/dist/rules.js
CHANGED
|
@@ -28,6 +28,23 @@ export const NEVER_PAY_TWICE = "Never pay the same seller twice for one task bec
|
|
|
28
28
|
export const SENDS_ONLY_URL = "These tools read a public index, need no key, and send nothing but the URL being checked.";
|
|
29
29
|
/** sha256 of the JSON of every constant above (lib/skill.test.ts recomputes it). */
|
|
30
30
|
export const RULES_SHA = "7258b901f6577a02dfe997416d3de61e122f5da21476eb686dd1c1a9379bffc6";
|
|
31
|
+
/**
|
|
32
|
+
* report_x402_payment (POST /v1/telemetry, tx_only proof). Kept out of RULES_SHA: the skill moved to its `.2` revision
|
|
33
|
+
* when these were added (2026-10-29), not RULES_REVISION. Changing them is a skill-only change: bump that suffix.
|
|
34
|
+
*/
|
|
35
|
+
export const REPORT_TRIGGER = "after your agent pays an x402 seller with any wallet";
|
|
36
|
+
export const REPORT_DESCRIPTION = "After your agent pays an x402 seller with any wallet, report what came back. Needs the resource URL, the settlement transaction hash and the outcome. " +
|
|
37
|
+
"Free, no account, no key; the report counts only with on-chain proof.";
|
|
38
|
+
/** The outcomes a reporter can name, in the index's words. */
|
|
39
|
+
export const REPORT_OUTCOMES = {
|
|
40
|
+
ok: "the response was usable for its purpose",
|
|
41
|
+
non_delivery: "an error, an empty body or nothing usable after paying",
|
|
42
|
+
wrong_goods: "a usable response, but not what was asked for",
|
|
43
|
+
overcharge: "charged more than the quoted or listed price",
|
|
44
|
+
duplicate: "two settlements for one purchase",
|
|
45
|
+
};
|
|
46
|
+
/** What the report sends, for the instructions and the skill. */
|
|
47
|
+
export const REPORT_SENDS = "the resource URL, the settlement transaction hash, the outcome and the quote if given";
|
|
31
48
|
/** "a, b, and c" */
|
|
32
49
|
export function listAnd(xs) {
|
|
33
50
|
return xs.length < 2 ? (xs[0] ?? "") : `${xs.slice(0, -1).join(", ")}, and ${xs.at(-1)}`;
|
package/dist/server.d.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The paidfor MCP server. One factory, three transports: stdio (npx @paidfor/mcp),
|
|
3
|
-
* the hosted Streamable HTTP service (index.ts) and the Next.js route at /mcp on the site.
|
|
4
|
-
* The read tools are thin readers of the public JSON API; the verdict logic lives on the site.
|
|
5
|
-
* The paying tools (pay.ts) are added by stdio.ts alone, and only when a wallet is configured.
|
|
6
|
-
*/
|
|
7
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { REPORT_OUTCOMES } from "./rules.js";
|
|
8
3
|
export * from "./rules.js";
|
|
9
|
-
export declare const VERSION = "0.
|
|
4
|
+
export declare const VERSION = "0.4.0";
|
|
10
5
|
/** 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.
|
|
6
|
+
export declare const CLIENT_HEADER = "mcp/0.4.0";
|
|
7
|
+
/** What the hosted service (index.ts) sends instead, so the index can count hosted and stdio use apart. */
|
|
8
|
+
export declare const HOSTED_CLIENT_HEADER = "mcp-hosted/0.4.0";
|
|
9
|
+
/**
|
|
10
|
+
* Which client product a hosted session comes from, read from `clientInfo.name` on initialize and nothing else from the
|
|
11
|
+
* handshake: a product name, never a user. The hosted service appends it to its header (`mcp-hosted/<version>+claude`),
|
|
12
|
+
* so the index's channel counts split directory traffic by client with no change on the index side.
|
|
13
|
+
*/
|
|
14
|
+
export declare const CLIENT_TAGS: readonly ["claude", "chatgpt", "cursor", "other"];
|
|
15
|
+
export type ClientTag = (typeof CLIENT_TAGS)[number];
|
|
16
|
+
export declare function clientTag(name: unknown): ClientTag;
|
|
17
|
+
export declare const taggedClientHeader: (tag: ClientTag | null) => string;
|
|
12
18
|
export declare const DEFAULT_BASE = "https://paidfor.dev";
|
|
13
|
-
export declare const TOOL_NAMES: readonly ["check_x402_seller", "list_reliable_sellers", "recent_changes"];
|
|
19
|
+
export declare const TOOL_NAMES: readonly ["check_x402_seller", "list_reliable_sellers", "recent_changes", "report_x402_payment"];
|
|
14
20
|
/**
|
|
15
21
|
* The no-account promise, word for word wherever it appears: the server instructions (last line),
|
|
16
22
|
* this package's README, /methodology, /install.md and /privacy on the site.
|
|
@@ -19,6 +25,14 @@ export declare const NO_ACCOUNT_PROMISE: string;
|
|
|
19
25
|
/** Sent on initialize by every transport (stdio, the hosted service, the site's /mcp route), so clients call the tools unprompted.
|
|
20
26
|
* Generated from rules.ts, like the tool descriptions below and the skill at paidfor.dev/skill.md. */
|
|
21
27
|
export declare const INSTRUCTIONS: string;
|
|
28
|
+
/**
|
|
29
|
+
* What the hosted transports (index.ts, the site's /mcp route) send instead of INSTRUCTIONS: the same facts, described
|
|
30
|
+
* rather than directed, because the Connectors Directory review rejects text that tells Claude how to behave or to call
|
|
31
|
+
* tools unprompted. stdio and the skill keep INSTRUCTIONS; the tool descriptions are shared by both.
|
|
32
|
+
*/
|
|
33
|
+
export declare const HOSTED_INSTRUCTIONS: string;
|
|
34
|
+
/** The first sentence of each read tool description: when to call it. The manifest and the setup page list these as they are. */
|
|
35
|
+
export declare const TOOL_TRIGGERS: Record<(typeof TOOL_NAMES)[number], string>;
|
|
22
36
|
export declare const CHECK_DESCRIPTION: string;
|
|
23
37
|
export declare const LIST_DESCRIPTION: string;
|
|
24
38
|
export declare const CHANGES_DESCRIPTION: string;
|
|
@@ -29,7 +43,92 @@ export type ServerOptions = {
|
|
|
29
43
|
onToolCall?: (tool: string) => void;
|
|
30
44
|
/** Called with the export's generated_at from every successful response (the hosted service shows it on /health). */
|
|
31
45
|
onResponse?: (generated_at: string) => void;
|
|
32
|
-
/**
|
|
46
|
+
/** The X-Paidfor-Client header on every index call; defaults to CLIENT_HEADER (stdio). */
|
|
47
|
+
clientHeader?: string;
|
|
48
|
+
/** Replaces INSTRUCTIONS; stdio.ts passes INSTRUCTIONS_WITH_PAY when it registers pay_x402, the hosted transports HOSTED_INSTRUCTIONS. */
|
|
33
49
|
instructions?: string;
|
|
34
50
|
};
|
|
51
|
+
/** Advertised on initialize (Implementation in the MCP schema) for clients and directories. */
|
|
52
|
+
export declare const SERVER_INFO: {
|
|
53
|
+
name: string;
|
|
54
|
+
title: string;
|
|
55
|
+
version: string;
|
|
56
|
+
description: string;
|
|
57
|
+
websiteUrl: string;
|
|
58
|
+
icons: {
|
|
59
|
+
src: string;
|
|
60
|
+
mimeType: string;
|
|
61
|
+
sizes: string[];
|
|
62
|
+
}[];
|
|
63
|
+
};
|
|
64
|
+
type TelemetryAnswer = {
|
|
65
|
+
status?: string;
|
|
66
|
+
error?: string;
|
|
67
|
+
reason?: string;
|
|
68
|
+
payer?: string;
|
|
69
|
+
pay_to?: string;
|
|
70
|
+
amount_usdc?: string;
|
|
71
|
+
block?: number;
|
|
72
|
+
own?: boolean;
|
|
73
|
+
unattributed_seller?: boolean;
|
|
74
|
+
verification_url?: string;
|
|
75
|
+
};
|
|
76
|
+
/** The tx_only telemetry row the skill also sends (paidfor-client docs/telemetry.md, "tx_only"); the index reads the rest from Base. */
|
|
77
|
+
export declare function reportPayload(i: {
|
|
78
|
+
url: string;
|
|
79
|
+
tx_hash: string;
|
|
80
|
+
outcome: keyof typeof REPORT_OUTCOMES;
|
|
81
|
+
quoted_usdc?: string | number;
|
|
82
|
+
http_status?: number;
|
|
83
|
+
client: string;
|
|
84
|
+
}): {
|
|
85
|
+
latency_ms: number;
|
|
86
|
+
validator_failures: string[];
|
|
87
|
+
client: string;
|
|
88
|
+
proof: {
|
|
89
|
+
network: string;
|
|
90
|
+
tx_hash: string;
|
|
91
|
+
};
|
|
92
|
+
amount_usdc?: string | undefined;
|
|
93
|
+
path_hash: string;
|
|
94
|
+
outcome: "ok" | "duplicate" | "non_delivery" | "wrong_goods" | "overcharge";
|
|
95
|
+
path?: string | undefined;
|
|
96
|
+
v: number;
|
|
97
|
+
proof_level: string;
|
|
98
|
+
host: string;
|
|
99
|
+
};
|
|
100
|
+
/** The index's answer as one sentence and the structured result. */
|
|
101
|
+
export declare function reportResult(httpStatus: number, a: TelemetryAnswer): {
|
|
102
|
+
sentence: string;
|
|
103
|
+
reason: null;
|
|
104
|
+
payer: string | null;
|
|
105
|
+
pay_to: string | null;
|
|
106
|
+
amount_usdc: string | null;
|
|
107
|
+
verification_url: string | null;
|
|
108
|
+
status: "verified";
|
|
109
|
+
} | {
|
|
110
|
+
sentence: string;
|
|
111
|
+
reason: null;
|
|
112
|
+
payer: string | null;
|
|
113
|
+
pay_to: string | null;
|
|
114
|
+
amount_usdc: string | null;
|
|
115
|
+
verification_url: string | null;
|
|
116
|
+
status: "pending";
|
|
117
|
+
} | {
|
|
118
|
+
sentence: string;
|
|
119
|
+
reason: null;
|
|
120
|
+
payer: string | null;
|
|
121
|
+
pay_to: string | null;
|
|
122
|
+
amount_usdc: string | null;
|
|
123
|
+
verification_url: string | null;
|
|
124
|
+
status: "duplicate";
|
|
125
|
+
} | {
|
|
126
|
+
reason: string;
|
|
127
|
+
sentence: string;
|
|
128
|
+
payer: string | null;
|
|
129
|
+
pay_to: string | null;
|
|
130
|
+
amount_usdc: string | null;
|
|
131
|
+
verification_url: string | null;
|
|
132
|
+
status: "rejected";
|
|
133
|
+
};
|
|
35
134
|
export declare function createServer(opts?: ServerOptions): McpServer;
|
package/dist/server.js
CHANGED
|
@@ -4,15 +4,36 @@
|
|
|
4
4
|
* The read tools are thin readers of the public JSON API; the verdict logic lives on the site.
|
|
5
5
|
* The paying tools (pay.ts) are added by stdio.ts alone, and only when a wallet is configured.
|
|
6
6
|
*/
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
7
8
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
import { ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
8
10
|
import { z } from "zod";
|
|
9
|
-
import { CHANGES_TRIGGER, CHECK_TRIGGERS, CHOOSE_TRIGGER, NEVER_PAY_TWICE, SENDS_ONLY_URL, VERDICT_SENTENCE, cap, listAnd, listOr } from "./rules.js";
|
|
11
|
+
import { CHANGES_TRIGGER, CHECK_TRIGGERS, CHOOSE_TRIGGER, NEVER_PAY_TWICE, REPORT_DESCRIPTION, REPORT_OUTCOMES, REPORT_SENDS, SENDS_ONLY_URL, VERDICT_SENTENCE, cap, listAnd, listOr } from "./rules.js";
|
|
10
12
|
export * from "./rules.js";
|
|
11
|
-
export const VERSION = "0.
|
|
13
|
+
export const VERSION = "0.4.0";
|
|
12
14
|
/** Sent on every request to the index so usage can be counted by channel (never anything else). */
|
|
13
15
|
export const CLIENT_HEADER = `mcp/${VERSION}`;
|
|
16
|
+
/** What the hosted service (index.ts) sends instead, so the index can count hosted and stdio use apart. */
|
|
17
|
+
export const HOSTED_CLIENT_HEADER = `mcp-hosted/${VERSION}`;
|
|
18
|
+
/**
|
|
19
|
+
* Which client product a hosted session comes from, read from `clientInfo.name` on initialize and nothing else from the
|
|
20
|
+
* handshake: a product name, never a user. The hosted service appends it to its header (`mcp-hosted/<version>+claude`),
|
|
21
|
+
* so the index's channel counts split directory traffic by client with no change on the index side.
|
|
22
|
+
*/
|
|
23
|
+
export const CLIENT_TAGS = ["claude", "chatgpt", "cursor", "other"];
|
|
24
|
+
export function clientTag(name) {
|
|
25
|
+
const n = typeof name === "string" ? name.toLowerCase() : "";
|
|
26
|
+
if (n.includes("claude"))
|
|
27
|
+
return "claude";
|
|
28
|
+
if (n.includes("chatgpt") || n.includes("openai"))
|
|
29
|
+
return "chatgpt";
|
|
30
|
+
if (n.includes("cursor"))
|
|
31
|
+
return "cursor";
|
|
32
|
+
return "other";
|
|
33
|
+
}
|
|
34
|
+
export const taggedClientHeader = (tag) => (tag ? `${HOSTED_CLIENT_HEADER}+${tag}` : HOSTED_CLIENT_HEADER);
|
|
14
35
|
export const DEFAULT_BASE = "https://paidfor.dev";
|
|
15
|
-
export const TOOL_NAMES = ["check_x402_seller", "list_reliable_sellers", "recent_changes"];
|
|
36
|
+
export const TOOL_NAMES = ["check_x402_seller", "list_reliable_sellers", "recent_changes", "report_x402_payment"];
|
|
16
37
|
const CATEGORIES = ["inference", "data", "scraping", "compute", "other"];
|
|
17
38
|
/**
|
|
18
39
|
* The no-account promise, word for word wherever it appears: the server instructions (last line),
|
|
@@ -28,33 +49,168 @@ export const INSTRUCTIONS = "paidfor rates x402 sellers by paying them and verif
|
|
|
28
49
|
`${cap(CHOOSE_TRIGGER)}, prefer results from list_reliable_sellers. ` +
|
|
29
50
|
`${cap(CHANGES_TRIGGER)}, call recent_changes. ` +
|
|
30
51
|
`${NEVER_PAY_TWICE} ` +
|
|
31
|
-
SENDS_ONLY_URL +
|
|
52
|
+
`${SENDS_ONLY_URL} ` +
|
|
53
|
+
`After any x402 payment made with any wallet outside this server, call report_x402_payment with the resource URL, the settlement transaction hash and the outcome; it moves no money and sends only ${REPORT_SENDS}.` +
|
|
32
54
|
"\n" +
|
|
33
55
|
NO_ACCOUNT_PROMISE;
|
|
34
|
-
|
|
56
|
+
/**
|
|
57
|
+
* What the hosted transports (index.ts, the site's /mcp route) send instead of INSTRUCTIONS: the same facts, described
|
|
58
|
+
* rather than directed, because the Connectors Directory review rejects text that tells Claude how to behave or to call
|
|
59
|
+
* tools unprompted. stdio and the skill keep INSTRUCTIONS; the tool descriptions are shared by both.
|
|
60
|
+
*/
|
|
61
|
+
export const HOSTED_INSTRUCTIONS = "paidfor rates x402 sellers by paying them and verifying delivery. " +
|
|
62
|
+
"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: " +
|
|
63
|
+
'"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. ' +
|
|
64
|
+
"list_reliable_sellers returns sellers with zero failures across at least 30 paid calls, for choosing among sellers. " +
|
|
65
|
+
"recent_changes returns what changed in the index, which is diffed hourly. " +
|
|
66
|
+
`${SENDS_ONLY_URL} ` +
|
|
67
|
+
`report_x402_payment records what came back after a payment the user's own wallet already made, from the resource URL, the settlement transaction hash and the outcome; it moves no money, sends only ${REPORT_SENDS}, and the report counts only once the transaction is verified as a USDC transfer on Base.\n` +
|
|
68
|
+
NO_ACCOUNT_PROMISE;
|
|
69
|
+
/** The first sentence of each read tool description: when to call it. The manifest and the setup page list these as they are. */
|
|
70
|
+
export const TOOL_TRIGGERS = {
|
|
71
|
+
check_x402_seller: `Call this ${listOr(CHECK_TRIGGERS)}.`,
|
|
72
|
+
list_reliable_sellers: `Call this ${CHOOSE_TRIGGER}, or when the user asks for a safe or reliable API.`,
|
|
73
|
+
recent_changes: `Call this ${CHANGES_TRIGGER}.`,
|
|
74
|
+
report_x402_payment: REPORT_DESCRIPTION.split(". ")[0] + ".",
|
|
75
|
+
};
|
|
76
|
+
export const CHECK_DESCRIPTION = `${TOOL_TRIGGERS.check_x402_seller} ` +
|
|
35
77
|
"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
78
|
"listed price, modal quote and max overcharge ratio when known, and links to the seller page and raw evidence.";
|
|
37
|
-
export const LIST_DESCRIPTION =
|
|
38
|
-
"Returns sellers with zero failures across at least 30 paid calls, most-called first
|
|
39
|
-
|
|
79
|
+
export const LIST_DESCRIPTION = `${TOOL_TRIGGERS.list_reliable_sellers} ` +
|
|
80
|
+
"Returns sellers with zero failures across at least 30 paid calls, most-called first. For each: domain, category, paid-call count, " +
|
|
81
|
+
"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.";
|
|
82
|
+
export const CHANGES_DESCRIPTION = `${TOOL_TRIGGERS.recent_changes} ` +
|
|
40
83
|
"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
|
|
42
|
-
/**
|
|
84
|
+
"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).";
|
|
85
|
+
/** Advertised on initialize (Implementation in the MCP schema) for clients and directories. */
|
|
86
|
+
export const SERVER_INFO = {
|
|
87
|
+
name: "paidfor",
|
|
88
|
+
title: "paidfor: x402 seller reliability",
|
|
89
|
+
version: VERSION,
|
|
90
|
+
description: "Helps an agent decide whether to pay an x402 API seller on Base: verdicts, failure rates and evidence from real paid calls, " +
|
|
91
|
+
"a list of sellers that have never failed, and a feed of recent changes. No account or API key needed.",
|
|
92
|
+
websiteUrl: DEFAULT_BASE,
|
|
93
|
+
icons: [{ src: `${DEFAULT_BASE}/icon.png`, mimeType: "image/png", sizes: ["512x512"] }],
|
|
94
|
+
};
|
|
95
|
+
const BY_TYPE = z.record(z.string(), z.number()).describe("Failure counts by type (non_delivery, overcharge, duplicate, wrong_goods)");
|
|
96
|
+
/** The structured result of check_x402_seller: the same object the text content carries after its lead sentence. */
|
|
97
|
+
const CHECK_OUTPUT = {
|
|
98
|
+
verdict: z.enum(["avoid", "caution", "ok", "unknown"]),
|
|
99
|
+
verdict_text: z.string().describe("One sentence to act on, with the evidence link"),
|
|
100
|
+
host: z.string().nullable(),
|
|
101
|
+
seller: z
|
|
102
|
+
.object({
|
|
103
|
+
domain: z.string(), calls: z.number(), failures: z.number(), failure_rate: z.number(),
|
|
104
|
+
wilson_low: z.number().describe("95% interval lower bound on the failure rate"),
|
|
105
|
+
wilson_high: z.number().describe("95% interval upper bound on the failure rate"),
|
|
106
|
+
by_type: BY_TYPE, low_sample: z.boolean(), last_seen: z.string(),
|
|
107
|
+
})
|
|
108
|
+
.nullable()
|
|
109
|
+
.describe("Measured paid calls to this seller; null when the study never paid it"),
|
|
110
|
+
resource: z
|
|
111
|
+
.object({
|
|
112
|
+
url: z.string(), method: z.string(), listed_price_usdc: z.string().nullable(), modal_quote_usdc: z.string().nullable(),
|
|
113
|
+
max_quote_usdc: z.string().nullable(), max_overcharge_ratio: z.number().nullable(), calls: z.number(), failure_rate: z.number(),
|
|
114
|
+
})
|
|
115
|
+
.nullable()
|
|
116
|
+
.describe("Measured paid calls to this exact URL; null when not measured"),
|
|
117
|
+
probe: z.looseObject({}).nullable().describe("Hourly unpaid probes of the seller's 402 quote; null when not probed"),
|
|
118
|
+
candidate: z.looseObject({}).nullable().describe("Queue state for a seller not yet measured; null otherwise"),
|
|
119
|
+
seller_url: z.string().nullable(),
|
|
120
|
+
evidence_url: z.string().nullable(),
|
|
121
|
+
methodology_url: z.string(),
|
|
122
|
+
generated_at: z.string().describe("When the index was generated"),
|
|
123
|
+
};
|
|
124
|
+
const LIST_OUTPUT = {
|
|
125
|
+
generated_at: z.string().describe("When the index was generated"),
|
|
126
|
+
sellers: z.array(z.object({
|
|
127
|
+
domain: z.string(), category: z.string(), calls: z.number(),
|
|
128
|
+
failure_rate_upper_bound: z.number().describe("95% interval upper bound on the failure rate"),
|
|
129
|
+
median_quote_usdc: z.string().nullable(), inflow_30d_usdc: z.string().nullable(), last_seen: z.string(),
|
|
130
|
+
url: z.string().describe("The seller's paidfor page"),
|
|
131
|
+
})),
|
|
132
|
+
};
|
|
133
|
+
const CHANGES_OUTPUT = {
|
|
134
|
+
count: z.number().describe("Matching changes the index returned (it caps the feed at the newest 500); events holds at most the newest 100"),
|
|
135
|
+
events: z.array(z.object({
|
|
136
|
+
id: z.string(), at: z.string(), type: z.string(), host: z.string(), url: z.string().describe("The seller's paidfor page"),
|
|
137
|
+
data: z.record(z.string(), z.unknown()).describe("Type-specific details"),
|
|
138
|
+
})),
|
|
139
|
+
};
|
|
140
|
+
const REPORT_OUTPUT = {
|
|
141
|
+
status: z.enum(["verified", "pending", "duplicate", "rejected"]).describe("verified: counted once folded in (minutes); pending: checked on Base again within minutes; duplicate: this transaction was already reported; rejected: not counted, see reason"),
|
|
142
|
+
sentence: z.string().describe("The result in one sentence"),
|
|
143
|
+
reason: z.string().nullable().describe("Why a report was rejected; null otherwise"),
|
|
144
|
+
payer: z.string().nullable().describe("The paying wallet, read from the transaction (never from the report)"),
|
|
145
|
+
pay_to: z.string().nullable(),
|
|
146
|
+
amount_usdc: z.string().nullable().describe("USDC moved, read from the transaction"),
|
|
147
|
+
verification_url: z.string().nullable(),
|
|
148
|
+
};
|
|
149
|
+
/** The longest plain path sent; a longer one goes as its hash only. */
|
|
150
|
+
const MAX_REPORT_PATH = 512;
|
|
151
|
+
/** The tx_only telemetry row the skill also sends (paidfor-client docs/telemetry.md, "tx_only"); the index reads the rest from Base. */
|
|
152
|
+
export function reportPayload(i) {
|
|
153
|
+
const u = new URL(i.url);
|
|
154
|
+
const path = u.pathname || "/";
|
|
155
|
+
const quoted = i.quoted_usdc === undefined ? undefined : Number(i.quoted_usdc);
|
|
156
|
+
return {
|
|
157
|
+
v: 2,
|
|
158
|
+
proof_level: "tx_only",
|
|
159
|
+
host: u.hostname.toLowerCase(),
|
|
160
|
+
...(path.length <= MAX_REPORT_PATH ? { path } : {}),
|
|
161
|
+
path_hash: createHash("sha256").update(path.replace(/\/$/, "") || "/").digest("hex"),
|
|
162
|
+
outcome: i.outcome,
|
|
163
|
+
...(quoted !== undefined && Number.isFinite(quoted) && quoted > 0 ? { amount_usdc: quoted.toFixed(6) } : {}),
|
|
164
|
+
latency_ms: 0,
|
|
165
|
+
validator_failures: i.http_status !== undefined && (i.http_status < 200 || i.http_status > 299) ? [`http_status:${i.http_status}`] : [],
|
|
166
|
+
client: i.client,
|
|
167
|
+
proof: { network: "eip155:8453", tx_hash: i.tx_hash.toLowerCase() },
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/** The index's answer as one sentence and the structured result. */
|
|
171
|
+
export function reportResult(httpStatus, a) {
|
|
172
|
+
const base = { reason: null, payer: a.payer ?? null, pay_to: a.pay_to ?? null, amount_usdc: a.amount_usdc ?? null, verification_url: a.verification_url ?? null };
|
|
173
|
+
if (httpStatus === 202 && a.status === "verified") {
|
|
174
|
+
const extra = [
|
|
175
|
+
a.own ? "it is one of paidfor's own wallets, so it is kept apart from the operator series" : null,
|
|
176
|
+
a.unattributed_seller ? "the seller is not in the index yet, so the payment is kept unattributed and the seller is queued for measurement" : null,
|
|
177
|
+
].filter(Boolean);
|
|
178
|
+
return { status: "verified", ...base, sentence: `Accepted and verified: ${a.amount_usdc} USDC from ${a.payer} to ${a.pay_to} in Base block ${a.block}${extra.length ? `; ${extra.join("; ")}` : ""}.` };
|
|
179
|
+
}
|
|
180
|
+
if (httpStatus === 202)
|
|
181
|
+
return { status: "pending", ...base, sentence: `Accepted pending verification: the transaction is not readable on Base yet and is checked again within minutes (${a.verification_url}).` };
|
|
182
|
+
if (httpStatus === 200 && a.status === "duplicate")
|
|
183
|
+
return { status: "duplicate", ...base, sentence: `Already reported: this transaction is in the index once and was not counted again (${a.verification_url}).` };
|
|
184
|
+
const why = (a.error ?? `HTTP ${httpStatus}`).replace(/^not accepted: /, "");
|
|
185
|
+
return { status: "rejected", ...base, reason: a.reason ?? why, sentence: `Rejected: ${why}.` };
|
|
186
|
+
}
|
|
187
|
+
/** The read tools only read the public index; the directories ask for these hints, and for the title repeated as annotations.title. */
|
|
43
188
|
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
|
-
|
|
189
|
+
async function getJsonAs(url, client = CLIENT_HEADER) {
|
|
190
|
+
const res = await fetch(url, { headers: { Accept: "application/json", "User-Agent": `paidfor-mcp/${VERSION}`, "X-Paidfor-Client": client } });
|
|
191
|
+
if (!res.ok) {
|
|
192
|
+
// Pass the API's own reason through (e.g. "since must be an ISO 8601 time"), so the caller can fix the input.
|
|
193
|
+
const reason = await res.json().then((j) => j.error, () => undefined);
|
|
194
|
+
throw new Error(`${url} returned HTTP ${res.status}${reason ? `: ${reason}` : ""}`);
|
|
195
|
+
}
|
|
48
196
|
return (await res.json());
|
|
49
197
|
}
|
|
50
198
|
export function createServer(opts = {}) {
|
|
51
199
|
const base = (opts.baseUrl ?? process.env.PAIDFOR_API_BASE ?? process.env.PAIDFOR_URL ?? DEFAULT_BASE).replace(/\/$/, "");
|
|
52
|
-
const
|
|
200
|
+
const getJson = (url) => getJsonAs(url, opts.clientHeader);
|
|
201
|
+
const server = new McpServer(SERVER_INFO, { instructions: opts.instructions ?? INSTRUCTIONS });
|
|
202
|
+
// No resources, templates or prompts, but answer the list calls with empty lists instead of
|
|
203
|
+
// "Method not found": directory scanners (ChatGPT's looks for widget resources) fail the scan otherwise.
|
|
204
|
+
server.server.registerCapabilities({ resources: {}, prompts: {} });
|
|
205
|
+
server.server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [] }));
|
|
206
|
+
server.server.setRequestHandler(ListResourceTemplatesRequestSchema, () => ({ resourceTemplates: [] }));
|
|
207
|
+
server.server.setRequestHandler(ListPromptsRequestSchema, () => ({ prompts: [] }));
|
|
53
208
|
server.registerTool("check_x402_seller", {
|
|
54
209
|
title: "Check an x402 seller before paying",
|
|
55
210
|
description: CHECK_DESCRIPTION,
|
|
56
|
-
annotations: READ_ONLY,
|
|
211
|
+
annotations: { ...READ_ONLY, title: "Check an x402 seller before paying" },
|
|
57
212
|
inputSchema: { url: z.string().min(1).describe("x402 resource URL, or a bare seller domain such as api.example.com") },
|
|
213
|
+
outputSchema: CHECK_OUTPUT,
|
|
58
214
|
}, async ({ url }) => {
|
|
59
215
|
opts.onToolCall?.("check_x402_seller");
|
|
60
216
|
const r = await getJson(`${base}/v1/check?url=${encodeURIComponent(url)}`);
|
|
@@ -70,21 +226,24 @@ export function createServer(opts = {}) {
|
|
|
70
226
|
? { 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
227
|
: null,
|
|
72
228
|
probe: r.probe,
|
|
229
|
+
candidate: r.candidate ?? null,
|
|
73
230
|
seller_url: r.seller_url,
|
|
74
231
|
evidence_url: r.evidence_url,
|
|
75
232
|
methodology_url: r.methodology_url,
|
|
76
233
|
generated_at: r.generated_at,
|
|
77
234
|
};
|
|
78
|
-
|
|
235
|
+
const lead = r.candidate ? `${r.verdict_text} ${r.candidate.note}` : r.verdict_text;
|
|
236
|
+
return { content: [{ type: "text", text: `${lead}\n\n${JSON.stringify(short, null, 2)}` }], structuredContent: short };
|
|
79
237
|
});
|
|
80
238
|
server.registerTool("list_reliable_sellers", {
|
|
81
|
-
title: "List reliable
|
|
239
|
+
title: "List reliable pay-per-call API sellers",
|
|
82
240
|
description: LIST_DESCRIPTION,
|
|
83
|
-
annotations: READ_ONLY,
|
|
241
|
+
annotations: { ...READ_ONLY, title: "List reliable pay-per-call API sellers" },
|
|
84
242
|
inputSchema: {
|
|
85
|
-
category: z.enum(CATEGORIES).optional().describe("
|
|
86
|
-
limit: z.number().int().min(1).max(500).optional().describe("
|
|
243
|
+
category: z.enum(CATEGORIES).optional().describe("Kind of service to list sellers for; omit to list every kind"),
|
|
244
|
+
limit: z.number().int().min(1).max(500).optional().describe("How many sellers to return, most-called first; 50 when omitted"),
|
|
87
245
|
},
|
|
246
|
+
outputSchema: LIST_OUTPUT,
|
|
88
247
|
}, async ({ category, limit }) => {
|
|
89
248
|
opts.onToolCall?.("list_reliable_sellers");
|
|
90
249
|
const q = new URLSearchParams({ min_calls: "30", zero_failures: "1", sort: "calls", limit: String(limit ?? 50) });
|
|
@@ -94,16 +253,17 @@ export function createServer(opts = {}) {
|
|
|
94
253
|
opts.onResponse?.(r.generated_at);
|
|
95
254
|
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
255
|
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)}` }] };
|
|
256
|
+
return { content: [{ type: "text", text: `${head}\n\n${JSON.stringify(rows, null, 2)}` }], structuredContent: { generated_at: r.generated_at, sellers: rows } };
|
|
98
257
|
});
|
|
99
258
|
server.registerTool("recent_changes", {
|
|
100
259
|
title: "Recent changes to x402 sellers",
|
|
101
260
|
description: CHANGES_DESCRIPTION,
|
|
102
|
-
annotations: READ_ONLY,
|
|
261
|
+
annotations: { ...READ_ONLY, title: "Recent changes to x402 sellers" },
|
|
103
262
|
inputSchema: {
|
|
104
|
-
host: z.string().min(1).optional().describe("
|
|
105
|
-
since: z.string().min(1).optional().describe("
|
|
263
|
+
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"),
|
|
264
|
+
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
265
|
},
|
|
266
|
+
outputSchema: CHANGES_OUTPUT,
|
|
107
267
|
}, async ({ host, since }) => {
|
|
108
268
|
opts.onToolCall?.("recent_changes");
|
|
109
269
|
const q = new URLSearchParams();
|
|
@@ -116,7 +276,36 @@ export function createServer(opts = {}) {
|
|
|
116
276
|
const head = r.count
|
|
117
277
|
? `${r.count} changes${host ? ` for ${host}` : ""}${since ? ` since ${since}` : ""}, newest first${r.count > rows.length ? ` (first ${rows.length} shown)` : ""}.`
|
|
118
278
|
: `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)}` }] };
|
|
279
|
+
return { content: [{ type: "text", text: `${head}\n\n${JSON.stringify(rows, null, 2)}` }], structuredContent: { count: r.count, events: rows } };
|
|
280
|
+
});
|
|
281
|
+
server.registerTool("report_x402_payment", {
|
|
282
|
+
title: "Report an x402 payment outcome",
|
|
283
|
+
description: REPORT_DESCRIPTION,
|
|
284
|
+
// It writes one row to the index (not read-only), changes nothing else (not destructive), and the same tx_hash never makes a second row.
|
|
285
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true, title: "Report an x402 payment outcome" },
|
|
286
|
+
inputSchema: {
|
|
287
|
+
url: z.string().url().describe("The x402 resource URL that was paid for"),
|
|
288
|
+
tx_hash: z.string().regex(/^0x[0-9a-fA-F]{64}$/).describe("The settlement transaction hash on Base: the `transaction` field of the seller's PAYMENT-RESPONSE or X-PAYMENT-RESPONSE header (base64 JSON)"),
|
|
289
|
+
outcome: z.enum(Object.keys(REPORT_OUTCOMES)).describe(`What came back: ${Object.entries(REPORT_OUTCOMES).map(([k, v]) => `${k} (${v})`).join(", ")}`),
|
|
290
|
+
quoted_usdc: z.union([z.number().positive(), z.string().regex(/^\d{1,12}(\.\d{1,6})?$/)]).optional().describe("The price the 402 quoted, in USDC, if known"),
|
|
291
|
+
http_status: z.number().int().min(100).max(599).optional().describe("The HTTP status of the paid response, if known"),
|
|
292
|
+
},
|
|
293
|
+
outputSchema: REPORT_OUTPUT,
|
|
294
|
+
}, async ({ url, tx_hash, outcome, quoted_usdc, http_status }) => {
|
|
295
|
+
opts.onToolCall?.("report_x402_payment");
|
|
296
|
+
const client = opts.clientHeader ?? CLIENT_HEADER;
|
|
297
|
+
const res = await fetch(`${base}/v1/telemetry`, {
|
|
298
|
+
method: "POST",
|
|
299
|
+
headers: { "Content-Type": "application/json", Accept: "application/json", "User-Agent": `paidfor-mcp/${VERSION}`, "X-Paidfor-Client": client },
|
|
300
|
+
body: JSON.stringify(reportPayload({ url, tx_hash, outcome, quoted_usdc, http_status, client })),
|
|
301
|
+
redirect: "error",
|
|
302
|
+
signal: AbortSignal.timeout(30_000),
|
|
303
|
+
});
|
|
304
|
+
const a = (await res.json().catch(() => ({})));
|
|
305
|
+
if (res.status >= 500)
|
|
306
|
+
throw new Error(`${base}/v1/telemetry returned HTTP ${res.status}${a.error ? `: ${a.error}` : ""}; nothing was recorded, try again later`);
|
|
307
|
+
const r = reportResult(res.status, a);
|
|
308
|
+
return { content: [{ type: "text", text: r.sentence }], structuredContent: r };
|
|
120
309
|
});
|
|
121
310
|
return server;
|
|
122
311
|
}
|
package/dist/setup.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export declare const HOSTED_URL = "https://mcp.paidfor.dev/mcp";
|
|
2
|
+
export declare const SETUP_URL = "https://paidfor.dev/install";
|
|
3
|
+
export declare const SKILL_URL = "https://paidfor.dev/skill.md";
|
|
4
|
+
export declare const PRIVACY_URL = "https://paidfor.dev/privacy";
|
|
5
|
+
export declare const MANIFEST_URL = "https://paidfor.dev/.well-known/mcp.json";
|
|
6
|
+
export declare const AGENT_SETUP = "Tell your agent to read https://paidfor.dev/skill.md";
|
|
7
|
+
/** What the hosted transports put in X-Paidfor-Client on every call to the index (taggedClientHeader: mcp-hosted/<version>+<client tag>). */
|
|
8
|
+
export declare const CLIENT_HEADER_NOTE = "Every call the server makes to the index carries X-Paidfor-Client: mcp-hosted/<version>+<client>, so use can be counted by channel; <client> is your client product (claude, chatgpt, cursor or other, from its name on initialize), never you.";
|
|
9
|
+
export declare const claudeCodeCommand: (url: string) => string;
|
|
10
|
+
export declare const cursorServer: (url: string) => {
|
|
11
|
+
url: string;
|
|
12
|
+
};
|
|
13
|
+
export declare const cursorDeepLink: (url: string) => string;
|
|
14
|
+
export declare const cursorConfig: (url: string) => string;
|
|
15
|
+
/**
|
|
16
|
+
* True when the Accept header ranks text/html above JSON, which is a browser opening the URL. Anything else
|
|
17
|
+
* (no header, *\/*, application/json, text/event-stream, the MCP clients' "application/json, text/event-stream")
|
|
18
|
+
* keeps the JSON status body or the MCP transport.
|
|
19
|
+
*/
|
|
20
|
+
export declare function prefersHtml(accept: string | null | undefined): boolean;
|
|
21
|
+
export type SetupPageOptions = {
|
|
22
|
+
/** The URL this page answers on. The snippets always name HOSTED_URL, as /install does. */
|
|
23
|
+
endpoint: string;
|
|
24
|
+
/** Where the JSON status body is for a browser: the same URL with ?format=json. */
|
|
25
|
+
statusUrl: string;
|
|
26
|
+
};
|
|
27
|
+
/** The page a browser gets from GET on the MCP URL. Plain HTML, no script, the same copy as /install. */
|
|
28
|
+
export declare function setupPageHtml({ endpoint, statusUrl }: SetupPageOptions): string;
|
|
29
|
+
/** Status JSON for a browser that asks: GET <mcp url>?format=json. */
|
|
30
|
+
export declare const wantsJsonFormat: (url: string | undefined) => boolean;
|
|
31
|
+
/**
|
|
32
|
+
* The manifest's `_meta`, under the registry's publisher-provided key (the one `_meta` key the server.json schema
|
|
33
|
+
* names). mcp/server.json carries a copy; lib/manifest.test.ts on the site fails when the two differ.
|
|
34
|
+
*/
|
|
35
|
+
export declare const MANIFEST_META: {
|
|
36
|
+
"io.modelcontextprotocol.registry/publisher-provided": {
|
|
37
|
+
tools: {
|
|
38
|
+
name: "check_x402_seller" | "list_reliable_sellers" | "recent_changes" | "report_x402_payment";
|
|
39
|
+
trigger: string;
|
|
40
|
+
}[];
|
|
41
|
+
setup_url: string;
|
|
42
|
+
setup_page: string;
|
|
43
|
+
skill_url: string;
|
|
44
|
+
privacy_url: string;
|
|
45
|
+
auth: string;
|
|
46
|
+
no_account: string;
|
|
47
|
+
};
|
|
48
|
+
};
|
package/dist/setup.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a person or a directory sees of the server without speaking MCP: the setup snippets (shared with the site's
|
|
3
|
+
* /install), the HTML page a browser gets from GET on the MCP URL, and the `_meta` block of the manifest at
|
|
4
|
+
* paidfor.dev/.well-known/mcp.json. All of it is generated from server.ts and rules.ts, like the tool descriptions.
|
|
5
|
+
*/
|
|
6
|
+
import { DEFAULT_BASE, HOSTED_INSTRUCTIONS, NO_ACCOUNT_PROMISE, TOOL_NAMES, TOOL_TRIGGERS, VERSION } from "./server.js";
|
|
7
|
+
export const HOSTED_URL = "https://mcp.paidfor.dev/mcp";
|
|
8
|
+
export const SETUP_URL = `${DEFAULT_BASE}/install`;
|
|
9
|
+
export const SKILL_URL = `${DEFAULT_BASE}/skill.md`;
|
|
10
|
+
export const PRIVACY_URL = `${DEFAULT_BASE}/privacy`;
|
|
11
|
+
export const MANIFEST_URL = `${DEFAULT_BASE}/.well-known/mcp.json`;
|
|
12
|
+
export const AGENT_SETUP = `Tell your agent to read ${SKILL_URL}`;
|
|
13
|
+
/** What the hosted transports put in X-Paidfor-Client on every call to the index (taggedClientHeader: mcp-hosted/<version>+<client tag>). */
|
|
14
|
+
export const CLIENT_HEADER_NOTE = "Every call the server makes to the index carries X-Paidfor-Client: mcp-hosted/<version>+<client>, so use can be counted by channel; <client> is your client product (claude, chatgpt, cursor or other, from its name on initialize), never you.";
|
|
15
|
+
export const claudeCodeCommand = (url) => `claude mcp add --transport http --scope user paidfor ${url}`;
|
|
16
|
+
export const cursorServer = (url) => ({ url });
|
|
17
|
+
export const cursorDeepLink = (url) => `cursor://anysphere.cursor-deeplink/mcp/install?name=paidfor&config=${encodeURIComponent(Buffer.from(JSON.stringify(cursorServer(url)), "utf8").toString("base64"))}`;
|
|
18
|
+
export const cursorConfig = (url) => JSON.stringify({ mcpServers: { paidfor: cursorServer(url) } }, null, 2);
|
|
19
|
+
/**
|
|
20
|
+
* True when the Accept header ranks text/html above JSON, which is a browser opening the URL. Anything else
|
|
21
|
+
* (no header, *\/*, application/json, text/event-stream, the MCP clients' "application/json, text/event-stream")
|
|
22
|
+
* keeps the JSON status body or the MCP transport.
|
|
23
|
+
*/
|
|
24
|
+
export function prefersHtml(accept) {
|
|
25
|
+
if (!accept)
|
|
26
|
+
return false;
|
|
27
|
+
const q = (type) => {
|
|
28
|
+
let best = 0;
|
|
29
|
+
for (const part of accept.split(",")) {
|
|
30
|
+
const [t, ...params] = part.trim().toLowerCase().split(";");
|
|
31
|
+
if (t.trim() !== type)
|
|
32
|
+
continue;
|
|
33
|
+
const qp = params.map((p) => p.trim()).find((p) => p.startsWith("q="));
|
|
34
|
+
best = Math.max(best, qp ? Number(qp.slice(2)) || 0 : 1);
|
|
35
|
+
}
|
|
36
|
+
return best;
|
|
37
|
+
};
|
|
38
|
+
const html = q("text/html");
|
|
39
|
+
return html > 0 && html > q("application/json") && q("text/event-stream") === 0;
|
|
40
|
+
}
|
|
41
|
+
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
42
|
+
/** Tailwind v4's neutral scale, the site's tokens (app/globals.css), mirrored for dark the same way. */
|
|
43
|
+
const CSS = `:root{color-scheme:light dark;--bg:#fff;--fg:oklch(20.5% 0 0);--muted:oklch(43.9% 0 0);--line:oklch(92.2% 0 0);--code:oklch(97% 0 0);--link:oklch(87% 0 0)}
|
|
44
|
+
@media (prefers-color-scheme:dark){:root{--bg:#000;--fg:oklch(97% 0 0);--muted:oklch(70.8% 0 0);--line:oklch(26.9% 0 0);--code:oklch(20.5% 0 0);--link:oklch(37.1% 0 0)}}
|
|
45
|
+
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.6 ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji";-webkit-font-smoothing:antialiased}
|
|
46
|
+
main{max-width:44rem;margin:0 auto;padding:24px 16px 48px}h1{font-size:1.5rem;font-weight:600;margin:8px 0 4px}h2{font-size:1.05rem;font-weight:600;margin:28px 0 6px}
|
|
47
|
+
p{margin:6px 0}.muted{color:var(--muted);font-size:.9rem}a{color:inherit;text-decoration:underline;text-decoration-color:var(--link);text-underline-offset:2px}a:hover{text-decoration-color:var(--fg)}
|
|
48
|
+
code,pre{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.85em}code{background:var(--code);padding:0 4px;border-radius:4px}
|
|
49
|
+
pre{background:var(--code);border:1px solid var(--line);border-radius:6px;padding:10px 12px;overflow-x:auto;white-space:pre-wrap;overflow-wrap:anywhere}pre code{background:none;padding:0}
|
|
50
|
+
.btn{display:inline-block;border:1px solid var(--fg);border-radius:6px;padding:4px 12px;text-decoration:none;font-size:.9rem;margin:4px 0}
|
|
51
|
+
ul{padding-left:20px;margin:6px 0}li{margin:4px 0}details{border:1px solid var(--line);border-radius:6px;padding:8px 12px;margin-top:8px}summary{cursor:pointer;font-weight:500}
|
|
52
|
+
details p{white-space:pre-wrap;font-size:.9rem}footer{border-top:1px solid var(--line);margin-top:32px;padding-top:12px}`;
|
|
53
|
+
/** The page a browser gets from GET on the MCP URL. Plain HTML, no script, the same copy as /install. */
|
|
54
|
+
export function setupPageHtml({ endpoint, statusUrl }) {
|
|
55
|
+
const pre = (s) => `<pre><code>${esc(s)}</code></pre>`;
|
|
56
|
+
const tools = TOOL_NAMES.map((t) => `<li><code>${t}</code>: ${esc(TOOL_TRIGGERS[t].replace(/^Call this /, "call it "))}</li>`).join("");
|
|
57
|
+
return `<!doctype html>
|
|
58
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
59
|
+
<title>paidfor MCP server</title>
|
|
60
|
+
<meta name="description" content="Set up the paidfor MCP server: check an x402 seller before your agent pays. No account, no key.">
|
|
61
|
+
<link rel="icon" href="${DEFAULT_BASE}/icon.png"><link rel="alternate" type="application/json" href="${esc(MANIFEST_URL)}">
|
|
62
|
+
<style>${CSS}</style></head>
|
|
63
|
+
<body><main>
|
|
64
|
+
<p class="muted"><a href="${DEFAULT_BASE}">paidfor.dev</a> · MCP server ${VERSION}</p>
|
|
65
|
+
<h1>paidfor MCP server</h1>
|
|
66
|
+
<p>It rates x402 sellers by paying them and verifying delivery, so your agent can check a seller before it pays one.</p>
|
|
67
|
+
<p class="muted">${endpoint === HOSTED_URL
|
|
68
|
+
? `This is a Streamable HTTP MCP endpoint: <code>${esc(endpoint)}</code>. Add it to your MCP client; opening it in a browser shows this page.`
|
|
69
|
+
: `<code>${esc(endpoint)}</code> is a Streamable HTTP MCP endpoint serving the same tools as <code>${HOSTED_URL}</code>, the URL the setup below uses.`}</p>
|
|
70
|
+
|
|
71
|
+
<h2>One line for any agent</h2>
|
|
72
|
+
${pre(AGENT_SETUP)}
|
|
73
|
+
<p class="muted">The skill installs the server where it can and keeps the rules where it cannot.</p>
|
|
74
|
+
|
|
75
|
+
<h2>Claude Code</h2>
|
|
76
|
+
${pre(claudeCodeCommand(HOSTED_URL))}
|
|
77
|
+
|
|
78
|
+
<h2>Claude (claude.ai, desktop, mobile)</h2>
|
|
79
|
+
<p>Customize, Connectors, + Add, Add custom connector. Name <code>paidfor</code>, URL:</p>
|
|
80
|
+
${pre(HOSTED_URL)}
|
|
81
|
+
<p class="muted">Choose No sign in. On Team and Enterprise an owner adds it under Organization settings, Connectors.</p>
|
|
82
|
+
|
|
83
|
+
<h2>Cursor</h2>
|
|
84
|
+
<p><a class="btn" href="${esc(cursorDeepLink(HOSTED_URL))}">Add to Cursor</a></p>
|
|
85
|
+
<p class="muted">Or merge into <code>~/.cursor/mcp.json</code>:</p>
|
|
86
|
+
${pre(cursorConfig(HOSTED_URL))}
|
|
87
|
+
|
|
88
|
+
<p>VS Code, Windsurf, Codex, ChatGPT and stdio: <a href="${SETUP_URL}">${SETUP_URL.replace("https://", "")}</a>.</p>
|
|
89
|
+
|
|
90
|
+
<h2>Tools</h2>
|
|
91
|
+
<ul>${tools}</ul>
|
|
92
|
+
<details><summary>Server instructions, as sent on initialize</summary><p>${esc(HOSTED_INSTRUCTIONS)}</p></details>
|
|
93
|
+
|
|
94
|
+
<h2>No account, no key</h2>
|
|
95
|
+
<p>${esc(NO_ACCOUNT_PROMISE)}</p>
|
|
96
|
+
<p class="muted">${esc(CLIENT_HEADER_NOTE)}</p>
|
|
97
|
+
|
|
98
|
+
<footer class="muted"><a href="${MANIFEST_URL}">Manifest</a> · <a href="${DEFAULT_BASE}/api">API</a> · <a href="${PRIVACY_URL}">Privacy</a> · <a href="${esc(statusUrl)}">Status JSON</a></footer>
|
|
99
|
+
</main></body></html>
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
102
|
+
/** Status JSON for a browser that asks: GET <mcp url>?format=json. */
|
|
103
|
+
export const wantsJsonFormat = (url) => /[?&]format=json(&|$)/.test(url ?? "");
|
|
104
|
+
/**
|
|
105
|
+
* The manifest's `_meta`, under the registry's publisher-provided key (the one `_meta` key the server.json schema
|
|
106
|
+
* names). mcp/server.json carries a copy; lib/manifest.test.ts on the site fails when the two differ.
|
|
107
|
+
*/
|
|
108
|
+
export const MANIFEST_META = {
|
|
109
|
+
"io.modelcontextprotocol.registry/publisher-provided": {
|
|
110
|
+
tools: TOOL_NAMES.map((name) => ({ name, trigger: TOOL_TRIGGERS[name] })),
|
|
111
|
+
setup_url: SETUP_URL,
|
|
112
|
+
setup_page: HOSTED_URL,
|
|
113
|
+
skill_url: SKILL_URL,
|
|
114
|
+
privacy_url: PRIVACY_URL,
|
|
115
|
+
auth: "none",
|
|
116
|
+
no_account: NO_ACCOUNT_PROMISE,
|
|
117
|
+
},
|
|
118
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paidfor/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"mcpName": "
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"mcpName": "dev.paidfor/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",
|
|
7
7
|
"type": "module",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
},
|
|
14
14
|
"main": "./dist/server.js",
|
|
15
15
|
"exports": {
|
|
16
|
-
".": "./dist/server.js"
|
|
16
|
+
".": "./dist/server.js",
|
|
17
|
+
"./setup": "./dist/setup.js"
|
|
17
18
|
},
|
|
18
19
|
"files": [
|
|
19
20
|
"dist",
|