@forum-labs/payfetch 1.0.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/LICENSE +21 -0
- package/README.md +467 -0
- package/dist/bin/payfetch-mcp.d.ts +2 -0
- package/dist/bin/payfetch-mcp.js +9 -0
- package/dist/bin/payfetch.d.ts +2 -0
- package/dist/bin/payfetch.js +126 -0
- package/dist/src/config.d.ts +35 -0
- package/dist/src/config.js +170 -0
- package/dist/src/core/budget.d.ts +62 -0
- package/dist/src/core/budget.js +236 -0
- package/dist/src/core/constants.d.ts +66 -0
- package/dist/src/core/constants.js +115 -0
- package/dist/src/core/fs.d.ts +14 -0
- package/dist/src/core/fs.js +104 -0
- package/dist/src/core/integrity.d.ts +23 -0
- package/dist/src/core/integrity.js +150 -0
- package/dist/src/core/ledger.d.ts +149 -0
- package/dist/src/core/ledger.js +357 -0
- package/dist/src/core/notes.d.ts +22 -0
- package/dist/src/core/notes.js +24 -0
- package/dist/src/core/pipeline.d.ts +143 -0
- package/dist/src/core/pipeline.js +795 -0
- package/dist/src/core/policy.d.ts +57 -0
- package/dist/src/core/policy.js +224 -0
- package/dist/src/core/transport.d.ts +93 -0
- package/dist/src/core/transport.js +364 -0
- package/dist/src/guards/index.d.ts +4 -0
- package/dist/src/guards/index.js +3 -0
- package/dist/src/guards/internal.d.ts +17 -0
- package/dist/src/guards/internal.js +74 -0
- package/dist/src/guards/safety.d.ts +2 -0
- package/dist/src/guards/safety.js +94 -0
- package/dist/src/guards/trust.d.ts +2 -0
- package/dist/src/guards/trust.js +79 -0
- package/dist/src/guards/types.d.ts +59 -0
- package/dist/src/guards/types.js +20 -0
- package/dist/src/index.d.ts +58 -0
- package/dist/src/index.js +151 -0
- package/dist/src/mcp/server.d.ts +6 -0
- package/dist/src/mcp/server.js +125 -0
- package/dist/src/mcp/tools.d.ts +46 -0
- package/dist/src/mcp/tools.js +321 -0
- package/dist/src/payer/mpp.d.ts +7 -0
- package/dist/src/payer/mpp.js +13 -0
- package/dist/src/payer/parse402.d.ts +6 -0
- package/dist/src/payer/parse402.js +169 -0
- package/dist/src/payer/signer_cdp.d.ts +14 -0
- package/dist/src/payer/signer_cdp.js +64 -0
- package/dist/src/payer/signer_local.d.ts +8 -0
- package/dist/src/payer/signer_local.js +14 -0
- package/dist/src/payer/types.d.ts +99 -0
- package/dist/src/payer/types.js +10 -0
- package/dist/src/payer/x402.d.ts +23 -0
- package/dist/src/payer/x402.js +165 -0
- package/dist/src/report/report.d.ts +162 -0
- package/dist/src/report/report.js +220 -0
- package/mcpb/manifest.json +104 -0
- package/package.json +72 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { PayfetchDeps, PaymentQuote } from "../payer/types.js";
|
|
2
|
+
export type GuardId = "trust" | "safety";
|
|
3
|
+
export interface PrePayGuard {
|
|
4
|
+
id: GuardId;
|
|
5
|
+
applies(req: GuardInput): boolean;
|
|
6
|
+
check(req: GuardInput, deps: PayfetchDeps): Promise<GuardResult>;
|
|
7
|
+
}
|
|
8
|
+
export type GuardInput = {
|
|
9
|
+
url: string;
|
|
10
|
+
host: string;
|
|
11
|
+
quote: PaymentQuote;
|
|
12
|
+
context: {
|
|
13
|
+
tokenAddress?: string;
|
|
14
|
+
chain?: "solana" | "base" | "ethereum";
|
|
15
|
+
};
|
|
16
|
+
dryRun?: boolean;
|
|
17
|
+
config?: TrustGuardConfig | SafetyGuardConfig;
|
|
18
|
+
};
|
|
19
|
+
export type GuardResult = {
|
|
20
|
+
id: string;
|
|
21
|
+
verdict: "pass" | "warn" | "block" | "unavailable";
|
|
22
|
+
detail: Record<string, unknown>;
|
|
23
|
+
latencyMs: number;
|
|
24
|
+
costUsd: number;
|
|
25
|
+
};
|
|
26
|
+
export type TrustGuardConfig = {
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
mode: "advisory" | "enforce";
|
|
29
|
+
minScore: number | null;
|
|
30
|
+
blockVerdicts: string[];
|
|
31
|
+
blockUnrated: boolean;
|
|
32
|
+
onUnavailable: "proceed" | "block";
|
|
33
|
+
dailyBudgetUsd: number;
|
|
34
|
+
};
|
|
35
|
+
export type SafetyGuardConfig = {
|
|
36
|
+
enabled: boolean;
|
|
37
|
+
mode: "advisory" | "enforce";
|
|
38
|
+
depth: "basic" | "deep";
|
|
39
|
+
blockVerdicts: string[];
|
|
40
|
+
blockDeployerVerdicts: string[];
|
|
41
|
+
onUnavailable: "proceed" | "block";
|
|
42
|
+
onDegraded: "block" | "warn" | "proceed";
|
|
43
|
+
dailyBudgetUsd: number;
|
|
44
|
+
};
|
|
45
|
+
export declare const DEFAULT_TRUST_GUARD_CONFIG: TrustGuardConfig;
|
|
46
|
+
export declare const DEFAULT_SAFETY_GUARD_CONFIG: SafetyGuardConfig;
|
|
47
|
+
export type GuardRuntime = {
|
|
48
|
+
guardFetch: (url: string, init: RequestInit, opts?: {
|
|
49
|
+
dryRun?: boolean;
|
|
50
|
+
}) => Promise<Response>;
|
|
51
|
+
now: () => number;
|
|
52
|
+
log: PayfetchDeps["log"];
|
|
53
|
+
installId8: string;
|
|
54
|
+
via: string | null;
|
|
55
|
+
baseUrls: {
|
|
56
|
+
trust: string | null;
|
|
57
|
+
safety: string | null;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { GUARD_DAILY_BUDGET_DEFAULT_USD, SAFETY_BLOCK_DEPLOYER_VERDICTS_DEFAULT, SAFETY_BLOCK_VERDICTS_DEFAULT, SAFETY_GUARD_DEPTH_DEFAULT, SAFETY_ON_DEGRADED_DEFAULT, TRUST_BLOCK_VERDICTS_DEFAULT, } from "../core/constants.js";
|
|
2
|
+
export const DEFAULT_TRUST_GUARD_CONFIG = Object.freeze({
|
|
3
|
+
enabled: true,
|
|
4
|
+
mode: "advisory",
|
|
5
|
+
minScore: null,
|
|
6
|
+
blockVerdicts: [...TRUST_BLOCK_VERDICTS_DEFAULT],
|
|
7
|
+
blockUnrated: false,
|
|
8
|
+
onUnavailable: "block",
|
|
9
|
+
dailyBudgetUsd: GUARD_DAILY_BUDGET_DEFAULT_USD,
|
|
10
|
+
});
|
|
11
|
+
export const DEFAULT_SAFETY_GUARD_CONFIG = Object.freeze({
|
|
12
|
+
enabled: false,
|
|
13
|
+
mode: "enforce",
|
|
14
|
+
depth: SAFETY_GUARD_DEPTH_DEFAULT,
|
|
15
|
+
blockVerdicts: [...SAFETY_BLOCK_VERDICTS_DEFAULT],
|
|
16
|
+
blockDeployerVerdicts: [...SAFETY_BLOCK_DEPLOYER_VERDICTS_DEFAULT],
|
|
17
|
+
onUnavailable: "block",
|
|
18
|
+
onDegraded: SAFETY_ON_DEGRADED_DEFAULT,
|
|
19
|
+
dailyBudgetUsd: GUARD_DAILY_BUDGET_DEFAULT_USD,
|
|
20
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type PayfetchFs } from "./core/fs.js";
|
|
2
|
+
import type { Receipt } from "./core/ledger.js";
|
|
3
|
+
import { type Policy } from "./core/policy.js";
|
|
4
|
+
import { PayfetchEngine, type Decision, type FetchOpts, type SpendStatus } from "./core/pipeline.js";
|
|
5
|
+
import { adaptFetch, parseIpv4, type HttpClient } from "./core/transport.js";
|
|
6
|
+
import type { PayfetchDeps, PaymentPayer } from "./payer/types.js";
|
|
7
|
+
import type { PrePayGuard } from "./guards/types.js";
|
|
8
|
+
type DeepPartial<T> = {
|
|
9
|
+
[K in keyof T]?: T[K] extends readonly (infer U)[] ? U[] : T[K] extends object ? DeepPartial<T[K]> : T[K];
|
|
10
|
+
};
|
|
11
|
+
export interface Payfetch {
|
|
12
|
+
fetch(url: string, init?: RequestInit, opts?: FetchOpts): Promise<{
|
|
13
|
+
response: Response | null;
|
|
14
|
+
receipt: Receipt;
|
|
15
|
+
}>;
|
|
16
|
+
quote(url: string, init?: RequestInit): Promise<{
|
|
17
|
+
decision: Decision;
|
|
18
|
+
receipt: Receipt;
|
|
19
|
+
}>;
|
|
20
|
+
status(): Promise<SpendStatus>;
|
|
21
|
+
receipts(q: {
|
|
22
|
+
sinceTs?: number;
|
|
23
|
+
host?: string;
|
|
24
|
+
outcome?: Receipt["outcome"];
|
|
25
|
+
limit?: number;
|
|
26
|
+
}): Promise<Receipt[]>;
|
|
27
|
+
close(): void;
|
|
28
|
+
engine: PayfetchEngine;
|
|
29
|
+
}
|
|
30
|
+
export type CreatePayfetchOpts = {
|
|
31
|
+
deps: PayfetchDeps;
|
|
32
|
+
policy?: DeepPartial<Policy>;
|
|
33
|
+
guards?: PrePayGuard[];
|
|
34
|
+
payers?: PaymentPayer[];
|
|
35
|
+
fs?: PayfetchFs;
|
|
36
|
+
httpClient?: HttpClient;
|
|
37
|
+
resolve?: (host: string) => Promise<string[]>;
|
|
38
|
+
testMode?: boolean;
|
|
39
|
+
approver?: boolean;
|
|
40
|
+
via?: string | null;
|
|
41
|
+
delay?: (ms: number) => Promise<void>;
|
|
42
|
+
setTimer?: (fn: () => void, ms: number) => {
|
|
43
|
+
clear: () => void;
|
|
44
|
+
};
|
|
45
|
+
guardBaseUrls?: {
|
|
46
|
+
trust: string | null;
|
|
47
|
+
safety: string | null;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
export declare function createPayfetch(opts: CreatePayfetchOpts): Payfetch;
|
|
51
|
+
export declare function clearAutoDeny(dataDir: string, host: string, io?: {
|
|
52
|
+
fs?: PayfetchFs;
|
|
53
|
+
now?: () => number;
|
|
54
|
+
}): boolean;
|
|
55
|
+
export { adaptFetch, parseIpv4 };
|
|
56
|
+
export type { Decision, SpendStatus, FetchOpts };
|
|
57
|
+
export type { Policy } from "./core/policy.js";
|
|
58
|
+
export type { Receipt } from "./core/ledger.js";
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { lookup as dnsLookup } from "node:dns/promises";
|
|
2
|
+
import { Readable } from "node:stream";
|
|
3
|
+
import { Agent, request as undiciRequest } from "undici";
|
|
4
|
+
import { P1_SAFETY_BASE_URL, P2_TRUST_BASE_URL, } from "./core/constants.js";
|
|
5
|
+
import { Budget } from "./core/budget.js";
|
|
6
|
+
import { realFs } from "./core/fs.js";
|
|
7
|
+
import { Ledger } from "./core/ledger.js";
|
|
8
|
+
import { configPath, defaultPolicy, loadPolicy, mergePolicy, } from "./core/policy.js";
|
|
9
|
+
import { PayfetchEngine, } from "./core/pipeline.js";
|
|
10
|
+
import { adaptFetch, createPinnedLookup, parseIpv4, } from "./core/transport.js";
|
|
11
|
+
import { X402Payer } from "./payer/x402.js";
|
|
12
|
+
import { createSafetyGuard, createTrustGuard } from "./guards/index.js";
|
|
13
|
+
function hex32(deps) {
|
|
14
|
+
const b = deps.random();
|
|
15
|
+
let s = "";
|
|
16
|
+
for (let i = 0; i < 16; i++)
|
|
17
|
+
s += b[i].toString(16).padStart(2, "0");
|
|
18
|
+
return s;
|
|
19
|
+
}
|
|
20
|
+
export function createPayfetch(opts) {
|
|
21
|
+
if (!opts || !opts.deps)
|
|
22
|
+
throw new TypeError("createPayfetch: opts.deps is required");
|
|
23
|
+
const deps = opts.deps;
|
|
24
|
+
if (!deps.signer)
|
|
25
|
+
throw new TypeError("createPayfetch: deps.signer is required");
|
|
26
|
+
if (typeof deps.dataDir !== "string" || deps.dataDir.length === 0) {
|
|
27
|
+
throw new TypeError("createPayfetch: deps.dataDir is required");
|
|
28
|
+
}
|
|
29
|
+
const fs = opts.fs ?? realFs;
|
|
30
|
+
const dataDir = deps.dataDir;
|
|
31
|
+
const testMode = opts.testMode ?? process.env.PAYFETCH_TEST_MODE != null;
|
|
32
|
+
const approver = opts.approver ?? process.env.PAYFETCH_APPROVER === "1";
|
|
33
|
+
const via = opts.via ?? null;
|
|
34
|
+
const guardBaseUrls = opts.guardBaseUrls ?? { trust: P2_TRUST_BASE_URL, safety: P1_SAFETY_BASE_URL };
|
|
35
|
+
const ledger = new Ledger(fs, dataDir, deps.now);
|
|
36
|
+
ledger.acquireLock();
|
|
37
|
+
let state = ledger.loadStateRaw();
|
|
38
|
+
if (!state) {
|
|
39
|
+
state = ledger.rebuildState(hex32(deps));
|
|
40
|
+
ledger.saveState(state);
|
|
41
|
+
}
|
|
42
|
+
const budget = new Budget(state, ledger, deps.now);
|
|
43
|
+
const basePolicy = mergePolicy(defaultPolicy(), opts.policy);
|
|
44
|
+
let cached = {
|
|
45
|
+
ok: true,
|
|
46
|
+
policy: basePolicy,
|
|
47
|
+
};
|
|
48
|
+
let lastMtime = null;
|
|
49
|
+
const reload = () => {
|
|
50
|
+
const load = loadPolicy(dataDir, { fs, log: deps.log }, basePolicy);
|
|
51
|
+
if (load.ok) {
|
|
52
|
+
cached = { ok: true, policy: load.policy };
|
|
53
|
+
lastMtime = load.mtimeMs;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
cached = { ok: false, error: load.error };
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
reload();
|
|
60
|
+
const policyProvider = () => {
|
|
61
|
+
const cur = fs.statMtimeMs(configPath(dataDir));
|
|
62
|
+
if (cur !== lastMtime)
|
|
63
|
+
reload();
|
|
64
|
+
return cached;
|
|
65
|
+
};
|
|
66
|
+
const resolve = opts.resolve ?? defaultResolve;
|
|
67
|
+
const initialPolicy = cached.ok ? cached.policy : basePolicy;
|
|
68
|
+
const httpClient = opts.httpClient ??
|
|
69
|
+
createPinnedHttpClient(resolve, { allowPrivateTargets: initialPolicy.allowPrivateTargets });
|
|
70
|
+
const engine = new PayfetchEngine({
|
|
71
|
+
deps,
|
|
72
|
+
fs,
|
|
73
|
+
ledger,
|
|
74
|
+
budget,
|
|
75
|
+
payers: opts.payers ?? [new X402Payer()],
|
|
76
|
+
policyProvider,
|
|
77
|
+
transportIo: { request: httpClient, resolve, setTimer: opts.setTimer },
|
|
78
|
+
testMode,
|
|
79
|
+
approverEnabled: approver,
|
|
80
|
+
delay: opts.delay,
|
|
81
|
+
guardBaseUrls,
|
|
82
|
+
via,
|
|
83
|
+
});
|
|
84
|
+
engine.guards =
|
|
85
|
+
opts.guards ?? buildDefaultGuards(engine, initialPolicy, state.installId, via, guardBaseUrls, deps);
|
|
86
|
+
return {
|
|
87
|
+
fetch: (url, init, o) => engine.fetch(url, init, o),
|
|
88
|
+
quote: (url, init) => engine.quote(url, init),
|
|
89
|
+
status: () => engine.status(),
|
|
90
|
+
receipts: async (q) => engine.receipts(q),
|
|
91
|
+
close: () => ledger.releaseLock(),
|
|
92
|
+
engine,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function buildDefaultGuards(engine, policy, installId, via, baseUrls, deps) {
|
|
96
|
+
const rtFor = (id, baseUrl) => ({
|
|
97
|
+
guardFetch: engine.makeGuardFetch(id, () => engine.liveGuardBudgetUsd(id), baseUrl),
|
|
98
|
+
now: deps.now,
|
|
99
|
+
log: deps.log,
|
|
100
|
+
installId8: installId.slice(0, 8),
|
|
101
|
+
via,
|
|
102
|
+
baseUrls,
|
|
103
|
+
});
|
|
104
|
+
const trust = createTrustGuard(policy.guards.trust, rtFor("trust", baseUrls.trust));
|
|
105
|
+
const safety = createSafetyGuard(policy.guards.safety, rtFor("safety", baseUrls.safety));
|
|
106
|
+
return [trust, safety];
|
|
107
|
+
}
|
|
108
|
+
export function clearAutoDeny(dataDir, host, io = {}) {
|
|
109
|
+
const fs = io.fs ?? realFs;
|
|
110
|
+
const now = io.now ?? (() => Date.now());
|
|
111
|
+
const ledger = new Ledger(fs, dataDir, now);
|
|
112
|
+
const state = ledger.loadStateRaw() ?? ledger.rebuildState("0".repeat(32));
|
|
113
|
+
const budget = new Budget(state, ledger, now);
|
|
114
|
+
return budget.clearAutoDeny(host);
|
|
115
|
+
}
|
|
116
|
+
async function defaultResolve(host) {
|
|
117
|
+
try {
|
|
118
|
+
const res = await dnsLookup(host, { all: true });
|
|
119
|
+
return res.map((r) => r.address);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function createPinnedHttpClient(resolve, policy) {
|
|
126
|
+
const agent = new Agent({
|
|
127
|
+
connect: { lookup: createPinnedLookup(resolve, policy) },
|
|
128
|
+
});
|
|
129
|
+
const client = async (url, init) => {
|
|
130
|
+
const res = await undiciRequest(url, {
|
|
131
|
+
method: init.method,
|
|
132
|
+
headers: init.headers,
|
|
133
|
+
body: init.body ?? undefined,
|
|
134
|
+
dispatcher: agent,
|
|
135
|
+
signal: init.signal,
|
|
136
|
+
});
|
|
137
|
+
const headers = new Headers();
|
|
138
|
+
for (const [k, v] of Object.entries(res.headers)) {
|
|
139
|
+
if (Array.isArray(v))
|
|
140
|
+
headers.set(k, v.join(", "));
|
|
141
|
+
else if (v != null)
|
|
142
|
+
headers.set(k, String(v));
|
|
143
|
+
}
|
|
144
|
+
const body = res.body
|
|
145
|
+
? Readable.toWeb(res.body)
|
|
146
|
+
: null;
|
|
147
|
+
return { status: res.statusCode, headers, body };
|
|
148
|
+
};
|
|
149
|
+
return client;
|
|
150
|
+
}
|
|
151
|
+
export { adaptFetch, parseIpv4 };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import type { ElicitFn } from "../payer/types.js";
|
|
3
|
+
import { type ConfigIo, type EnvRecord } from "../config.js";
|
|
4
|
+
export declare const MCP_SERVER_NAME = "payfetch";
|
|
5
|
+
export declare function makeElicitBridge(server: Server): ElicitFn;
|
|
6
|
+
export declare function runStdioServer(env?: EnvRecord, io?: ConfigIo): Promise<void>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { CLIENT_VERSION, USDC_DECIMALS } from "../core/constants.js";
|
|
5
|
+
import { createPayfetch } from "../index.js";
|
|
6
|
+
import { ConfigError, buildFromEnv, realConfigIo, scrubSecrets, } from "../config.js";
|
|
7
|
+
import { PAYFETCH_TOOLS, UnknownToolError, dispatchTool, } from "./tools.js";
|
|
8
|
+
export const MCP_SERVER_NAME = "payfetch";
|
|
9
|
+
export function makeElicitBridge(server) {
|
|
10
|
+
return async (req) => {
|
|
11
|
+
const result = await server.elicitInput({
|
|
12
|
+
message: buildElicitMessage(req),
|
|
13
|
+
requestedSchema: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
approve: {
|
|
17
|
+
type: "boolean",
|
|
18
|
+
title: "Approve this payment",
|
|
19
|
+
description: "Approve this SINGLE payment (false = deny). This authorizes one payment only; it does not change any spending limit or list.",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
required: ["approve"],
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
if (result.action === "accept") {
|
|
26
|
+
return { approved: result.content?.approve === true, cancelled: false };
|
|
27
|
+
}
|
|
28
|
+
if (result.action === "decline") {
|
|
29
|
+
return { approved: false, cancelled: false };
|
|
30
|
+
}
|
|
31
|
+
return { approved: false, cancelled: true };
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function fmtUsd(n) {
|
|
35
|
+
return `$${n.toFixed(USDC_DECIMALS)}`;
|
|
36
|
+
}
|
|
37
|
+
function buildElicitMessage(req) {
|
|
38
|
+
const lines = [];
|
|
39
|
+
lines.push(`Approve a payment of ${fmtUsd(req.amountUsd)} to ${req.host}?`);
|
|
40
|
+
lines.push(`Resource: ${req.resource ?? "(unspecified)"}`);
|
|
41
|
+
lines.push(`Network / asset: ${req.networkLabel} / ${req.assetLabel}`);
|
|
42
|
+
const guards = req.guards;
|
|
43
|
+
if (guards.length > 0) {
|
|
44
|
+
lines.push("Trust / safety checks:");
|
|
45
|
+
for (const g of guards) {
|
|
46
|
+
const score = typeof g.detail?.score === "number" ? ` (score ${g.detail.score})` : "";
|
|
47
|
+
lines.push(` - ${g.id}: ${g.verdict}${score}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const b = req.remainingBudgets;
|
|
51
|
+
const totalStr = b.totalRemainingUsd == null ? "unset" : fmtUsd(b.totalRemainingUsd);
|
|
52
|
+
lines.push(`Remaining today — day: ${fmtUsd(b.dayRemainingUsd)}, host: ${fmtUsd(b.hostRemainingUsd)}, total: ${totalStr}`);
|
|
53
|
+
lines.push("Approving authorizes THIS payment only. Spending limits are set by the operator's config and cannot be changed here.");
|
|
54
|
+
return lines.join("\n");
|
|
55
|
+
}
|
|
56
|
+
function registerHandlers(server, ctx) {
|
|
57
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
58
|
+
tools: PAYFETCH_TOOLS.map((t) => ({
|
|
59
|
+
name: t.name,
|
|
60
|
+
description: t.description,
|
|
61
|
+
inputSchema: t.inputSchema,
|
|
62
|
+
})),
|
|
63
|
+
}));
|
|
64
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
65
|
+
const { name } = request.params;
|
|
66
|
+
const rawArgs = (request.params.arguments ?? {});
|
|
67
|
+
try {
|
|
68
|
+
const output = await dispatchTool(ctx, name, rawArgs);
|
|
69
|
+
return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
if (err instanceof UnknownToolError) {
|
|
73
|
+
throw new McpError(ErrorCode.MethodNotFound, err.message);
|
|
74
|
+
}
|
|
75
|
+
const message = scrubSecrets(String(err?.message ?? err), []);
|
|
76
|
+
return {
|
|
77
|
+
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
78
|
+
isError: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export async function runStdioServer(env = process.env, io = realConfigIo()) {
|
|
84
|
+
const opts = buildFromEnv(env, io);
|
|
85
|
+
const server = new Server({ name: MCP_SERVER_NAME, version: CLIENT_VERSION }, { capabilities: { tools: {} } });
|
|
86
|
+
opts.deps.elicit = makeElicitBridge(server);
|
|
87
|
+
const pf = createPayfetch(opts);
|
|
88
|
+
if (opts.approver === true && pf.engine.queueCapableNow()) {
|
|
89
|
+
pf.close();
|
|
90
|
+
throw new ConfigError("payfetch: refusing to start — PAYFETCH_APPROVER=1 with a queue-capable " +
|
|
91
|
+
"approval mode (approval.mode 'queue', or 'elicit' with elicitFallback " +
|
|
92
|
+
"'queue') lets the tool-driven agent approve its own payments (no " +
|
|
93
|
+
"requester/approver separation). Use approval.mode 'elicit' with " +
|
|
94
|
+
"elicitFallback 'deny' (human-in-the-loop via MCP elicitation), or start " +
|
|
95
|
+
"without PAYFETCH_APPROVER=1. (security fix M6)");
|
|
96
|
+
}
|
|
97
|
+
const ctx = { pf, dataDir: opts.deps.dataDir };
|
|
98
|
+
registerHandlers(server, ctx);
|
|
99
|
+
let closed = false;
|
|
100
|
+
const shutdown = () => {
|
|
101
|
+
if (closed)
|
|
102
|
+
return;
|
|
103
|
+
closed = true;
|
|
104
|
+
try {
|
|
105
|
+
pf.close();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
server.onclose = shutdown;
|
|
111
|
+
process.once("SIGINT", () => {
|
|
112
|
+
shutdown();
|
|
113
|
+
process.exit(0);
|
|
114
|
+
});
|
|
115
|
+
process.once("SIGTERM", () => {
|
|
116
|
+
shutdown();
|
|
117
|
+
process.exit(0);
|
|
118
|
+
});
|
|
119
|
+
const transport = new StdioServerTransport();
|
|
120
|
+
await server.connect(transport);
|
|
121
|
+
const clientCaps = server.getClientCapabilities();
|
|
122
|
+
if (!clientCaps || !clientCaps.elicitation) {
|
|
123
|
+
opts.deps.elicit = null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Payfetch } from "../index.js";
|
|
2
|
+
export declare const TOOL_NAMES: {
|
|
3
|
+
readonly PAID_FETCH: "paid_fetch";
|
|
4
|
+
readonly PAYMENT_QUOTE: "payment_quote";
|
|
5
|
+
readonly SPEND_STATUS: "spend_status";
|
|
6
|
+
readonly LIST_RECEIPTS: "list_receipts";
|
|
7
|
+
readonly APPROVE_PENDING: "approve_pending";
|
|
8
|
+
};
|
|
9
|
+
export declare const TOOL_DESCRIPTIONS: {
|
|
10
|
+
readonly paid_fetch: "Fetch a URL, automatically paying if it requires payment (HTTP 402, x402 protocol) — within the operator's spending policy. Free URLs are fetched normally at no cost. Use payment_quote first if you only want to know the price. Payments above the operator's approval threshold will ask the human for confirmation.";
|
|
11
|
+
readonly payment_quote: "Check what a paid URL costs and whether the current spending policy would allow paying it, WITHOUT paying. Returns the price, payment terms, trust-check results, and the policy decision.";
|
|
12
|
+
readonly spend_status: "Show today's agent spending: totals, remaining budgets overall and per host, active holds, and recent payments.";
|
|
13
|
+
readonly list_receipts: "Query the local payment receipt ledger (audit trail). Filter by time, host, or outcome.";
|
|
14
|
+
readonly approve_pending: "List or resolve payments waiting for human approval (queue mode). Approving grants a one-time re-run permission for that exact payment.";
|
|
15
|
+
};
|
|
16
|
+
export type JsonSchema = {
|
|
17
|
+
type: "object";
|
|
18
|
+
properties?: Record<string, unknown>;
|
|
19
|
+
required?: string[];
|
|
20
|
+
additionalProperties?: boolean;
|
|
21
|
+
};
|
|
22
|
+
export declare const PAID_FETCH_INPUT_SCHEMA: JsonSchema;
|
|
23
|
+
export declare const PAYMENT_QUOTE_INPUT_SCHEMA: JsonSchema;
|
|
24
|
+
export declare const SPEND_STATUS_INPUT_SCHEMA: JsonSchema;
|
|
25
|
+
export declare const LIST_RECEIPTS_INPUT_SCHEMA: JsonSchema;
|
|
26
|
+
export declare const APPROVE_PENDING_INPUT_SCHEMA: JsonSchema;
|
|
27
|
+
export type ToolDefinition = {
|
|
28
|
+
name: string;
|
|
29
|
+
description: string;
|
|
30
|
+
inputSchema: JsonSchema;
|
|
31
|
+
};
|
|
32
|
+
export declare const PAYFETCH_TOOLS: readonly ToolDefinition[];
|
|
33
|
+
export declare function policyLockNotice(dataDir: string): string;
|
|
34
|
+
export declare class UnknownToolError extends Error {
|
|
35
|
+
readonly toolName: string;
|
|
36
|
+
constructor(toolName: string);
|
|
37
|
+
}
|
|
38
|
+
export declare class ToolInputError extends Error {
|
|
39
|
+
constructor(message: string);
|
|
40
|
+
}
|
|
41
|
+
export type ToolContext = {
|
|
42
|
+
pf: Payfetch;
|
|
43
|
+
dataDir: string;
|
|
44
|
+
};
|
|
45
|
+
export declare function dispatchTool(ctx: ToolContext, name: string, args: Record<string, unknown>): Promise<unknown>;
|
|
46
|
+
export declare const USD_DISPLAY_DECIMALS = 6;
|