@opendatalabs/personal-server-ts-core 1.3.5 → 1.5.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/dist/api/index.d.ts +47 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +2 -1
- package/dist/api/index.js.map +1 -1
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/read-client.d.ts +23 -0
- package/dist/mcp/read-client.d.ts.map +1 -1
- package/dist/mcp/read-client.js +16 -4
- package/dist/mcp/read-client.js.map +1 -1
- package/dist/mcp/session.d.ts +160 -0
- package/dist/mcp/session.d.ts.map +1 -0
- package/dist/mcp/session.js +234 -0
- package/dist/mcp/session.js.map +1 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +135 -15
- package/dist/mcp/tools.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-signing MCP session (Option A / chatbot-first).
|
|
3
|
+
*
|
|
4
|
+
* A third-party app that holds its OWN key and a DCR grant proves control of
|
|
5
|
+
* that key ONCE — a Web3Signed handshake to `POST /mcp/session` — and the PS
|
|
6
|
+
* mints a short-lived bearer session token bound to `{ builderAddress, grantId }`.
|
|
7
|
+
* MCP tool calls then present that token as `Authorization: Bearer …`; each read
|
|
8
|
+
* authorizes as the builder via `verifyDataReadPolicy` (signer == grantee) with
|
|
9
|
+
* NO per-read signature and NO PS-held key.
|
|
10
|
+
*
|
|
11
|
+
* Contrast with the owner's-Claude flow, where the PS generates and holds a
|
|
12
|
+
* per-connection grantee key and signs reads itself. Here the app is
|
|
13
|
+
* self-custody; the PS only ever recovered its address (at handshake).
|
|
14
|
+
*/
|
|
15
|
+
import { generateMcpGrantee } from "./grantee.js";
|
|
16
|
+
import { hashConnectionToken } from "./connection-api.js";
|
|
17
|
+
import { verifyDataReadPolicy } from "../policy/data-read.js";
|
|
18
|
+
import { handleX402Cycle } from "../api/index.js";
|
|
19
|
+
import { GrantOwnerMismatchError, GrantRequiredError, GrantRevokedError, InvalidSignatureError, ProtocolError, UnregisteredBuilderError, } from "../errors/catalog.js";
|
|
20
|
+
/**
|
|
21
|
+
* KNOWN LIMITATION: in-memory only. A process restart (or a second instance)
|
|
22
|
+
* starts with an empty store, so live session tokens die with the process and
|
|
23
|
+
* the client must re-handshake. Acceptable for today's single-instance
|
|
24
|
+
* Personal Server; persist via the host's state store before multi-instance.
|
|
25
|
+
*/
|
|
26
|
+
export function createInMemoryMcpSessionStore() {
|
|
27
|
+
const byHash = new Map();
|
|
28
|
+
return {
|
|
29
|
+
async create(record) {
|
|
30
|
+
byHash.set(record.tokenHash, record);
|
|
31
|
+
},
|
|
32
|
+
async getByTokenHash(tokenHash) {
|
|
33
|
+
const record = byHash.get(tokenHash);
|
|
34
|
+
if (!record)
|
|
35
|
+
return null;
|
|
36
|
+
if (record.expiresAtMs <= Date.now()) {
|
|
37
|
+
byHash.delete(tokenHash);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return record;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* KNOWN LIMITATION: in-memory only. A process restart (or a second instance)
|
|
46
|
+
* forgets consumed proof ids, so a still-valid handshake proof could be
|
|
47
|
+
* replayed once against the fresh process. The blast radius is bounded by the
|
|
48
|
+
* proof's own expiry, and today's Personal Server runs single-instance, so we
|
|
49
|
+
* document rather than persist. Persist via the host's state store (do not
|
|
50
|
+
* invent a new storage layer) before running multiple instances.
|
|
51
|
+
*/
|
|
52
|
+
export function createInMemoryMcpProofReplayStore() {
|
|
53
|
+
const seen = new Map();
|
|
54
|
+
return {
|
|
55
|
+
async consume(proofId, expiresAtMs) {
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
for (const [id, exp] of seen) {
|
|
58
|
+
if (exp <= now)
|
|
59
|
+
seen.delete(id);
|
|
60
|
+
}
|
|
61
|
+
const existing = seen.get(proofId);
|
|
62
|
+
if (existing !== undefined && existing > now)
|
|
63
|
+
return true;
|
|
64
|
+
seen.set(proofId, expiresAtMs);
|
|
65
|
+
return false;
|
|
66
|
+
},
|
|
67
|
+
async release(proofId) {
|
|
68
|
+
seen.delete(proofId);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const DEFAULT_SESSION_TTL_MS = 60 * 60 * 1000;
|
|
73
|
+
/**
|
|
74
|
+
* Validate the handshake identity and mint a session token. Per-scope / expiry
|
|
75
|
+
* enforcement is authoritative at read time (`verifyDataReadPolicy`), so this
|
|
76
|
+
* does the minimal checks that give a clear handshake error: builder registered,
|
|
77
|
+
* grant exists + not revoked, and grantee == builder.
|
|
78
|
+
*/
|
|
79
|
+
export async function createMcpSession(input, options) {
|
|
80
|
+
const builder = await options.authSessionVerifier.getBuilder(input.builderAddress);
|
|
81
|
+
if (!builder)
|
|
82
|
+
throw new UnregisteredBuilderError();
|
|
83
|
+
const grant = await options.grantVerifier.getGrant(input.grantId);
|
|
84
|
+
if (!grant) {
|
|
85
|
+
throw new GrantRequiredError({
|
|
86
|
+
reason: "Grant not found",
|
|
87
|
+
grantId: input.grantId,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (grant.revokedAt !== null) {
|
|
91
|
+
throw new GrantRevokedError({ grantId: grant.id });
|
|
92
|
+
}
|
|
93
|
+
if (builder.id.toLowerCase() !== grant.granteeId.toLowerCase()) {
|
|
94
|
+
throw new InvalidSignatureError({
|
|
95
|
+
reason: "Handshake signer is not the grant builder",
|
|
96
|
+
expected: grant.granteeId,
|
|
97
|
+
actual: input.builderAddress,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
// Ownership binding — the grant MUST have been issued by THIS server's
|
|
101
|
+
// owner (same check as `verifyDataReadPolicy`, which stays authoritative at
|
|
102
|
+
// read time). A wrong-owner grant can never become valid for this server,
|
|
103
|
+
// so reject at handshake with a clear error instead of minting a token
|
|
104
|
+
// whose every read would 403. Fail closed on a grantor-less grant: gateway
|
|
105
|
+
// responses are untrusted runtime data, despite their type.
|
|
106
|
+
if (!grant.grantorAddress ||
|
|
107
|
+
grant.grantorAddress.toLowerCase() !== options.serverOwner.toLowerCase()) {
|
|
108
|
+
throw new GrantOwnerMismatchError({
|
|
109
|
+
grantId: grant.id,
|
|
110
|
+
expected: options.serverOwner,
|
|
111
|
+
actual: grant.grantorAddress ?? null,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// Prepare the token first (deterministic, can't meaningfully fail) so the
|
|
115
|
+
// only fallible step after consuming the proof is persistence.
|
|
116
|
+
const token = options.randomToken();
|
|
117
|
+
const tokenHash = await hashConnectionToken(token);
|
|
118
|
+
const ttlMs = options.ttlMs ?? DEFAULT_SESSION_TTL_MS;
|
|
119
|
+
const nowMs = options.now?.() ?? Date.now();
|
|
120
|
+
// Replay guard: a still-valid handshake proof must mint at most one token.
|
|
121
|
+
// Consume ONLY after identity validation, and roll the reservation back if
|
|
122
|
+
// persistence fails — so a transient failure never burns a valid proof.
|
|
123
|
+
// consume() is an atomic check-and-set, so concurrent duplicates still
|
|
124
|
+
// resolve to exactly one winner.
|
|
125
|
+
const usingReplayGuard = Boolean(input.proof && options.replayStore);
|
|
126
|
+
if (input.proof && options.replayStore) {
|
|
127
|
+
const replayed = await options.replayStore.consume(input.proof.id, input.proof.expiresAtMs);
|
|
128
|
+
if (replayed) {
|
|
129
|
+
throw new ProtocolError(401, "MCP_SESSION_PROOF_REPLAY", "Handshake proof already used; sign a fresh proof");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
await options.store.create({
|
|
134
|
+
tokenHash,
|
|
135
|
+
builderAddress: input.builderAddress,
|
|
136
|
+
grantId: grant.id,
|
|
137
|
+
scopes: grant.scopes ?? [],
|
|
138
|
+
createdAt: new Date(nowMs).toISOString(),
|
|
139
|
+
expiresAtMs: nowMs + ttlMs,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
// Persistence failed — release the reservation so a legitimate retry with
|
|
144
|
+
// the same still-valid proof isn't rejected as a replay.
|
|
145
|
+
if (usingReplayGuard && input.proof && options.replayStore?.release) {
|
|
146
|
+
await options.replayStore.release(input.proof.id);
|
|
147
|
+
}
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
accessToken: token,
|
|
152
|
+
expiresInSeconds: Math.floor(ttlMs / 1000),
|
|
153
|
+
grantId: grant.id,
|
|
154
|
+
scopes: grant.scopes ?? [],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
export function createMcpSessionAuthPort(params) {
|
|
158
|
+
return {
|
|
159
|
+
async authorizeOwner() {
|
|
160
|
+
throw new InvalidSignatureError({
|
|
161
|
+
reason: "MCP session cannot perform owner operations",
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
async authorizeBuilderList() {
|
|
165
|
+
// Allowed: the session builder may list its own granted scopes.
|
|
166
|
+
},
|
|
167
|
+
async authorizeBuilderRead(input) {
|
|
168
|
+
const grant = await verifyDataReadPolicy({
|
|
169
|
+
signer: params.builderAddress,
|
|
170
|
+
grantId: params.grantId,
|
|
171
|
+
requestedScope: input.scope,
|
|
172
|
+
fileId: input.fileId,
|
|
173
|
+
serverOwner: params.serverOwner,
|
|
174
|
+
}, {
|
|
175
|
+
authSessionVerifier: params.authSessionVerifier,
|
|
176
|
+
grantVerifier: params.grantVerifier,
|
|
177
|
+
runtimeAvailability: params.runtimeAvailability,
|
|
178
|
+
});
|
|
179
|
+
// Paid session: enforce x402 per read (reuses the exact HTTP cycle). The
|
|
180
|
+
// read client forwards the app's `X-PAYMENT` proof on `input.request`.
|
|
181
|
+
if (params.payment) {
|
|
182
|
+
const cycle = await handleX402Cycle({
|
|
183
|
+
deps: params.payment.dataApiDeps,
|
|
184
|
+
request: input.request,
|
|
185
|
+
scope: input.scope,
|
|
186
|
+
fileIdParam: input.fileId,
|
|
187
|
+
// Bind the payment to the exact data version being served (a
|
|
188
|
+
// cursor-pinned or `at=`-pinned read may serve an older version
|
|
189
|
+
// than the latest entry) — otherwise the challenge/accessRecord
|
|
190
|
+
// would settle against different bytes than were returned.
|
|
191
|
+
atParam: input.at,
|
|
192
|
+
grantId: params.grantId,
|
|
193
|
+
builder: params.builderAddress,
|
|
194
|
+
gateway: params.payment.gateway,
|
|
195
|
+
gatewayConfig: params.payment.gatewayConfig,
|
|
196
|
+
gatewayUrl: params.payment.gatewayUrl,
|
|
197
|
+
});
|
|
198
|
+
if (cycle.kind === "challenge") {
|
|
199
|
+
throw new ProtocolError(402, "PAYMENT_REQUIRED", "Payment required for this read", { challenge: cycle.body });
|
|
200
|
+
}
|
|
201
|
+
if (cycle.kind === "gateway-error") {
|
|
202
|
+
throw new ProtocolError(cycle.status, "PAYMENT_GATEWAY_ERROR", "Payment gateway rejected the payment", { body: cycle.body });
|
|
203
|
+
}
|
|
204
|
+
// cycle.kind === "ok" — payment settled; proceed to read.
|
|
205
|
+
}
|
|
206
|
+
return { builder: params.builderAddress, grantId: grant.id };
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Build a synthetic connection record + a throwaway signing account for the
|
|
212
|
+
* streamable `/mcp` handler. The MCP read client still signs its in-process
|
|
213
|
+
* request (the read path requires an account), but the session auth port
|
|
214
|
+
* ignores that signature and authorizes as the real builder — so this key is
|
|
215
|
+
* disposable and never leaves the process.
|
|
216
|
+
*/
|
|
217
|
+
export function buildMcpSessionConnection(session, options = {}) {
|
|
218
|
+
const ephemeral = generateMcpGrantee();
|
|
219
|
+
const nowIso = new Date(options.now?.() ?? Date.now()).toISOString();
|
|
220
|
+
const connection = {
|
|
221
|
+
id: options.id ?? `mcp-session-${session.grantId}`,
|
|
222
|
+
displayName: "MCP session",
|
|
223
|
+
granteeAddress: session.builderAddress,
|
|
224
|
+
granteePublicKey: ephemeral.key.publicKey,
|
|
225
|
+
encryptedGranteePrivateKey: ephemeral.key.encryptedPrivateKey,
|
|
226
|
+
tokenHash: "",
|
|
227
|
+
status: "approved",
|
|
228
|
+
grants: [{ grantId: session.grantId, scopes: session.scopes }],
|
|
229
|
+
createdAt: nowIso,
|
|
230
|
+
approvedAt: nowIso,
|
|
231
|
+
};
|
|
232
|
+
return { connection, account: ephemeral.account };
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/mcp/session.ts"],"names":[],"mappings":"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"}
|
package/dist/mcp/tools.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/mcp/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AASzD,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,mBAAmB,CAAC;IAChC,UAAU,EAAE,iBAAiB,CAAC;IAC9B,gBAAgB,CAAC,EAAE,mBAAmB,CAAC;CACxC;AAED,MAAM,MAAM,oBAAoB,GAC5B;IACE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,GACD;IACE,IAAI,EAAE,eAAe,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GACD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE;QACR,GAAG,EAAE,MAAM,CAAC;QACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACjC,CAAC;CACH,CAAC;AAEN,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,oBAAoB,EAAE,CAAC;IAChC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAG5C,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC;AAE7D,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,iBAAiB,CAAC;IAC/B,OAAO,CACL,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,aAAa,CAAC,CAAC;CAC3B;AAiGD;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,QAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/mcp/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AASzD,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,mBAAmB,CAAC;IAChC,UAAU,EAAE,iBAAiB,CAAC;IAC9B,gBAAgB,CAAC,EAAE,mBAAmB,CAAC;CACxC;AAED,MAAM,MAAM,oBAAoB,GAC5B;IACE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,GACD;IACE,IAAI,EAAE,eAAe,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GACD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE;QACR,GAAG,EAAE,MAAM,CAAC;QACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACjC,CAAC;CACH,CAAC;AAEN,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,oBAAoB,EAAE,CAAC;IAChC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAG5C,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC;AAE7D,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,iBAAiB,CAAC;IAC/B,OAAO,CACL,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,aAAa,CAAC,CAAC;CAC3B;AAiGD;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,QAAS,CAAC;AA4iD9C,eAAO,MAAM,SAAS,EAAE,SAAS,iBAAiB,EAQxC,CAAC"}
|
package/dist/mcp/tools.js
CHANGED
|
@@ -582,6 +582,10 @@ const readScope = {
|
|
|
582
582
|
.min(1000)
|
|
583
583
|
.max(MAX_READ_SCOPE_TIMEOUT_MS)
|
|
584
584
|
.optional(),
|
|
585
|
+
payment: z
|
|
586
|
+
.string()
|
|
587
|
+
.optional()
|
|
588
|
+
.describe("Base64 X-PAYMENT proof (x402). Provide it after a prior call returned payment_required."),
|
|
585
589
|
},
|
|
586
590
|
async handler(args, { connection, readClient }) {
|
|
587
591
|
const scope = typeof args.scope === "string" ? args.scope : null;
|
|
@@ -630,12 +634,14 @@ const readScope = {
|
|
|
630
634
|
// Advisory only — serve the read without guidance.
|
|
631
635
|
});
|
|
632
636
|
}
|
|
637
|
+
const payment = typeof args.payment === "string" ? args.payment : undefined;
|
|
633
638
|
try {
|
|
634
639
|
const result = await withTimeout(readClient.readScopeBlocks({
|
|
635
640
|
scope,
|
|
636
641
|
grantId: grant.grantId,
|
|
637
642
|
cursor,
|
|
638
643
|
maxBytes,
|
|
644
|
+
payment,
|
|
639
645
|
...(blockIds.length > 0 ? { blockIds } : {}),
|
|
640
646
|
}), timeoutMs, `read blocks for ${scope}`);
|
|
641
647
|
const nextCursor = result.nextCursor ?? null;
|
|
@@ -668,6 +674,30 @@ const readScope = {
|
|
|
668
674
|
}, true);
|
|
669
675
|
}
|
|
670
676
|
if (err instanceof McpDataReadError) {
|
|
677
|
+
if (err.status === 402) {
|
|
678
|
+
const body = err.body;
|
|
679
|
+
const challenge = body?.error?.details?.challenge;
|
|
680
|
+
// Only a genuine PAYMENT_REQUIRED with a challenge is signable. A
|
|
681
|
+
// gateway failure that also surfaces as 402 (e.g. PAYMENT_GATEWAY_ERROR
|
|
682
|
+
// — insufficient escrow balance) is NOT signable; presenting it as a
|
|
683
|
+
// challenge would make the client sign an error envelope in a loop.
|
|
684
|
+
if (body?.error?.errorCode === "PAYMENT_REQUIRED" && challenge) {
|
|
685
|
+
return textResult({
|
|
686
|
+
payment_required: true,
|
|
687
|
+
status: 402,
|
|
688
|
+
challenge,
|
|
689
|
+
message: "This read requires payment. Sign the x402 challenge and call read_scope again with the `payment` argument.",
|
|
690
|
+
}, true);
|
|
691
|
+
}
|
|
692
|
+
// Gateway-side payment failure — surface the real error + remediation.
|
|
693
|
+
return textResult({
|
|
694
|
+
error: "payment_failed",
|
|
695
|
+
status: 402,
|
|
696
|
+
errorCode: body?.error?.errorCode ?? "PAYMENT_ERROR",
|
|
697
|
+
message: body?.error?.message ??
|
|
698
|
+
"Payment could not be completed (e.g. insufficient escrow balance). This is not a signable challenge; resolve the gateway error and retry.",
|
|
699
|
+
}, true);
|
|
700
|
+
}
|
|
671
701
|
if (err.status === 503) {
|
|
672
702
|
return textResult({
|
|
673
703
|
error: "bounded_data_unavailable",
|
|
@@ -817,7 +847,7 @@ function decodeSearchCursor(raw) {
|
|
|
817
847
|
const searchPersonalContext = {
|
|
818
848
|
name: "search_personal_context",
|
|
819
849
|
title: "Search personal context",
|
|
820
|
-
description: "Search approved scopes. Omit scopes for default sweep; name scopes for targeted search. Continue with nextSearchCursor.",
|
|
850
|
+
description: "Search approved scopes. Omit scopes for default sweep; name scopes for targeted search. Continue with nextSearchCursor. Free discovery/preview — it does not settle payments; chargeable scopes are returned in paymentRequiredScopes, fetch those via read_scope with a `payment` proof.",
|
|
821
851
|
inputSchema: {
|
|
822
852
|
query: z.string().min(1).max(SEARCH_QUERY_MAX_CHARS),
|
|
823
853
|
scopes: z
|
|
@@ -865,6 +895,11 @@ const searchPersonalContext = {
|
|
|
865
895
|
const matches = [];
|
|
866
896
|
const searchedScopes = [];
|
|
867
897
|
const truncatedScopes = [];
|
|
898
|
+
// Chargeable scopes hit on a paid (self-signing) session. Search is a free
|
|
899
|
+
// discovery/preview surface and does not settle x402 payments — it surfaces
|
|
900
|
+
// these explicitly so the client retrieves them via read_scope with a
|
|
901
|
+
// `payment` proof, instead of burying the 402 as an opaque scope error.
|
|
902
|
+
const paymentRequiredScopes = [];
|
|
868
903
|
const errors = [...resolved.errors];
|
|
869
904
|
// When resuming, start from the scope index encoded in the cursor
|
|
870
905
|
const startScopeIndex = resumeCursor
|
|
@@ -884,7 +919,12 @@ const searchPersonalContext = {
|
|
|
884
919
|
const grant = resolveGrantForScope(connection, scope);
|
|
885
920
|
if (!grant)
|
|
886
921
|
continue;
|
|
887
|
-
|
|
922
|
+
// The index preview path bypasses the data API auth port (and thus
|
|
923
|
+
// x402). Skip it when reads are payment-enforced so paid content is never
|
|
924
|
+
// previewed for free — search falls back to the paid bounded block read.
|
|
925
|
+
if (!resumeCursor &&
|
|
926
|
+
!readClient.enforcesPayment &&
|
|
927
|
+
typeof readClient.searchScopeIndex === "function") {
|
|
888
928
|
const remainingMs = deadline - Date.now();
|
|
889
929
|
if (remainingMs <= 0) {
|
|
890
930
|
nextSearchCursor = encodeSearchCursor({ scopeIndex });
|
|
@@ -1021,18 +1061,32 @@ const searchPersonalContext = {
|
|
|
1021
1061
|
}
|
|
1022
1062
|
}
|
|
1023
1063
|
catch (err) {
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
error
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
}
|
|
1064
|
+
const paymentChallenge = err instanceof McpDataReadError &&
|
|
1065
|
+
err.status === 402 &&
|
|
1066
|
+
err.body?.error?.errorCode ===
|
|
1067
|
+
"PAYMENT_REQUIRED";
|
|
1068
|
+
if (paymentChallenge) {
|
|
1069
|
+
// Paid session hit a chargeable scope. Surface it as payment-required
|
|
1070
|
+
// (retrieve via read_scope with `payment`) rather than a scope error.
|
|
1071
|
+
// A gateway 402 (PAYMENT_GATEWAY_ERROR) falls through to `errors`.
|
|
1072
|
+
if (!paymentRequiredScopes.includes(scope)) {
|
|
1073
|
+
paymentRequiredScopes.push(scope);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
else {
|
|
1077
|
+
errors.push({
|
|
1078
|
+
scope,
|
|
1079
|
+
error: err instanceof OperationTimeoutError
|
|
1080
|
+
? "scope_search_timeout"
|
|
1081
|
+
: "scope_read_failed",
|
|
1082
|
+
status: err instanceof McpDataReadError ? err.status : undefined,
|
|
1083
|
+
bodyPreview: stringifyPreview(err instanceof McpDataReadError
|
|
1084
|
+
? err.body
|
|
1085
|
+
: err instanceof OperationTimeoutError
|
|
1086
|
+
? err.message
|
|
1087
|
+
: "unexpected scope read failure"),
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1036
1090
|
}
|
|
1037
1091
|
if (matches.length >= limit) {
|
|
1038
1092
|
// Result limit hit — if more scopes remain, offer cursor for next scope.
|
|
@@ -1067,6 +1121,13 @@ const searchPersonalContext = {
|
|
|
1067
1121
|
],
|
|
1068
1122
|
truncatedScopes,
|
|
1069
1123
|
errors,
|
|
1124
|
+
...(paymentRequiredScopes.length > 0
|
|
1125
|
+
? {
|
|
1126
|
+
payment_required: true,
|
|
1127
|
+
paymentRequiredScopes,
|
|
1128
|
+
paymentHint: "These scopes are chargeable; search does not settle x402 payments. Retrieve each with read_scope (it returns a payment_required challenge; sign it and re-call read_scope with the `payment` argument).",
|
|
1129
|
+
}
|
|
1130
|
+
: {}),
|
|
1070
1131
|
...(nextSearchCursor ? { nextSearchCursor } : {}),
|
|
1071
1132
|
elapsedMs,
|
|
1072
1133
|
limits: {
|
|
@@ -1091,6 +1152,8 @@ const getScopeFile = {
|
|
|
1091
1152
|
description: "Fetch an approved file/PDF scope. Defaults to metadata plus a resource link; set includeContent=true only when the client supports inline resource blobs. Use search_personal_context for extracted text.",
|
|
1092
1153
|
inputSchema: {
|
|
1093
1154
|
...rawScopeFileInputSchema,
|
|
1155
|
+
// x402 proof (see read_scope's `payment`); provide after a payment_required.
|
|
1156
|
+
payment: z.string().optional(),
|
|
1094
1157
|
timeoutMs: z
|
|
1095
1158
|
.number()
|
|
1096
1159
|
.int()
|
|
@@ -1117,12 +1180,14 @@ const getScopeFile = {
|
|
|
1117
1180
|
const maxBytes = clampInteger(args.maxBytes, DEFAULT_READ_FILE_MAX_BYTES, 1, MAX_READ_FILE_MAX_BYTES);
|
|
1118
1181
|
const timeoutMs = clampInteger(args.timeoutMs, DEFAULT_READ_FILE_TIMEOUT_MS, 1000, MAX_READ_FILE_TIMEOUT_MS);
|
|
1119
1182
|
const resourceUri = rawScopeResourceUri({ scope, at, fileId });
|
|
1183
|
+
const payment = typeof args.payment === "string" ? args.payment : undefined;
|
|
1120
1184
|
try {
|
|
1121
1185
|
const raw = await withTimeout(readClient.readRawScopeFile({
|
|
1122
1186
|
scope,
|
|
1123
1187
|
grantId: grant.grantId,
|
|
1124
1188
|
at,
|
|
1125
1189
|
fileId,
|
|
1190
|
+
payment,
|
|
1126
1191
|
}), timeoutMs, `read raw file for ${scope}`);
|
|
1127
1192
|
const metadata = rawScopeMetadata(raw);
|
|
1128
1193
|
const base = {
|
|
@@ -1133,7 +1198,24 @@ const getScopeFile = {
|
|
|
1133
1198
|
maxBytes,
|
|
1134
1199
|
timeoutMs,
|
|
1135
1200
|
};
|
|
1136
|
-
|
|
1201
|
+
// A paid read settles a SINGLE-USE x402 payment for this fetch. A resource
|
|
1202
|
+
// link would force a second, unpaid resources/read → another 402 that
|
|
1203
|
+
// cannot be satisfied, so a paid read returns the bytes inline (regardless
|
|
1204
|
+
// of includeContent) — BUT the hard size cap is still enforced to protect
|
|
1205
|
+
// server/client memory and the MCP transport. An oversized paid file
|
|
1206
|
+
// returns a structured error, never an inline dump; clients size maxBytes
|
|
1207
|
+
// from the payment challenge's `sizeBytes` before paying.
|
|
1208
|
+
const paidRead = Boolean(payment);
|
|
1209
|
+
if (paidRead && raw.sizeBytes > maxBytes) {
|
|
1210
|
+
return textResult({
|
|
1211
|
+
error: "paid_file_exceeds_max_bytes",
|
|
1212
|
+
status: 413,
|
|
1213
|
+
sizeBytes: raw.sizeBytes,
|
|
1214
|
+
maxBytes,
|
|
1215
|
+
message: "Payment settled, but the file exceeds maxBytes and is not returned inline. Retry with a larger maxBytes (and a fresh payment); files above the inline ceiling cannot be delivered over MCP.",
|
|
1216
|
+
}, true);
|
|
1217
|
+
}
|
|
1218
|
+
if (!paidRead && (!includeContent || raw.sizeBytes > maxBytes)) {
|
|
1137
1219
|
return {
|
|
1138
1220
|
content: [
|
|
1139
1221
|
{
|
|
@@ -1199,6 +1281,44 @@ const getScopeFile = {
|
|
|
1199
1281
|
}, true);
|
|
1200
1282
|
}
|
|
1201
1283
|
if (err instanceof McpDataReadError) {
|
|
1284
|
+
if (err.status === 402) {
|
|
1285
|
+
const body = err.body;
|
|
1286
|
+
const challenge = body?.error?.details?.challenge;
|
|
1287
|
+
// Only a genuine PAYMENT_REQUIRED with a challenge is signable; a
|
|
1288
|
+
// gateway 402 (e.g. insufficient escrow) is surfaced as an error.
|
|
1289
|
+
if (body?.error?.errorCode === "PAYMENT_REQUIRED" && challenge) {
|
|
1290
|
+
// Surface the file size so the client can set maxBytes >= sizeBytes
|
|
1291
|
+
// BEFORE paying — a paid read must fit the inline size cap (the
|
|
1292
|
+
// single-use payment can't fund a second resources/read).
|
|
1293
|
+
let meta = null;
|
|
1294
|
+
try {
|
|
1295
|
+
meta = await readClient.getScopeMetadata(scope);
|
|
1296
|
+
}
|
|
1297
|
+
catch {
|
|
1298
|
+
meta = null;
|
|
1299
|
+
}
|
|
1300
|
+
const sizeBytes = meta?.sizeBytes;
|
|
1301
|
+
return textResult({
|
|
1302
|
+
payment_required: true,
|
|
1303
|
+
status: 402,
|
|
1304
|
+
challenge,
|
|
1305
|
+
...(typeof sizeBytes === "number"
|
|
1306
|
+
? { sizeBytes, recommendedMaxBytes: sizeBytes }
|
|
1307
|
+
: {}),
|
|
1308
|
+
message: "This file requires payment. Sign the x402 challenge and call get_scope_file again with the `payment` argument" +
|
|
1309
|
+
(typeof sizeBytes === "number"
|
|
1310
|
+
? ` and maxBytes >= ${sizeBytes}.`
|
|
1311
|
+
: "."),
|
|
1312
|
+
}, true);
|
|
1313
|
+
}
|
|
1314
|
+
return textResult({
|
|
1315
|
+
error: "payment_failed",
|
|
1316
|
+
status: 402,
|
|
1317
|
+
errorCode: body?.error?.errorCode ?? "PAYMENT_ERROR",
|
|
1318
|
+
message: body?.error?.message ??
|
|
1319
|
+
"Payment could not be completed. This is not a signable challenge; resolve the gateway error and retry.",
|
|
1320
|
+
}, true);
|
|
1321
|
+
}
|
|
1202
1322
|
return textResult({
|
|
1203
1323
|
error: err.status === 404
|
|
1204
1324
|
? "scope_file_not_found"
|