@fdkey/mcp 0.2.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/CHANGELOG.md +47 -0
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/guard.d.ts +17 -0
- package/dist/guard.d.ts.map +1 -0
- package/dist/guard.js +49 -0
- package/dist/guard.js.map +1 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +409 -0
- package/dist/index.js.map +1 -0
- package/dist/router-static.d.ts +13 -0
- package/dist/router-static.d.ts.map +1 -0
- package/dist/router-static.js +16 -0
- package/dist/router-static.js.map +1 -0
- package/dist/session-store.d.ts +45 -0
- package/dist/session-store.d.ts.map +1 -0
- package/dist/session-store.js +71 -0
- package/dist/session-store.js.map +1 -0
- package/dist/types.d.ts +156 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/vps-client.d.ts +42 -0
- package/dist/vps-client.d.ts.map +1 -0
- package/dist/vps-client.js +90 -0
- package/dist/vps-client.js.map +1 -0
- package/dist/vps-router.d.ts +29 -0
- package/dist/vps-router.d.ts.map +1 -0
- package/dist/vps-router.js +146 -0
- package/dist/vps-router.js.map +1 -0
- package/dist/well-known.d.ts +14 -0
- package/dist/well-known.d.ts.map +1 -0
- package/dist/well-known.js +42 -0
- package/dist/well-known.js.map +1 -0
- package/package.json +72 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { jwtVerify, decodeProtectedHeader, decodeJwt } from 'jose';
|
|
3
|
+
import { InitializeRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { normalisePolicy } from './types.js';
|
|
5
|
+
import { canCall, markVerified, consumePolicy } from './guard.js';
|
|
6
|
+
import { VpsClient, VpsHttpError } from './vps-client.js';
|
|
7
|
+
import { StaticRouter } from './router-static.js';
|
|
8
|
+
import { createSessionStore } from './session-store.js';
|
|
9
|
+
import { WellKnownClient } from './well-known.js';
|
|
10
|
+
/** @internal Test-only export. Not part of the supported public API;
|
|
11
|
+
* may change or disappear without notice. Used by `index.test.ts` to
|
|
12
|
+
* verify the lazy-import contract directly. */
|
|
13
|
+
export { LazyVpsRouter as __LazyVpsRouterForTesting };
|
|
14
|
+
/** Hardcoded SDK version. Forwarded to the VPS as `integrator.sdk_version`
|
|
15
|
+
* on every challenge fetch so we can correlate failures with SDK releases.
|
|
16
|
+
* MUST be kept in sync with package.json version on every release — there's
|
|
17
|
+
* a smoke test that checks this match. */
|
|
18
|
+
const SDK_VERSION = '0.2.0';
|
|
19
|
+
/** Default VPS URL used when no `vpsUrl` and no `discoveryUrl` are provided. */
|
|
20
|
+
const DEFAULT_VPS_URL = 'https://api.fdkey.com';
|
|
21
|
+
/** Lazy wrapper around the multi-VPS `VpsRouter`. Defers `await import('./vps-router.js')`
|
|
22
|
+
* until the first `getTarget()` call so Workers/Bun/Deno builds — which never hit this
|
|
23
|
+
* path unless `discoveryUrl` is set — don't pull undici into the bundle.
|
|
24
|
+
*
|
|
25
|
+
* If `undici` is not installed (it's an `optionalDependency`), the dynamic
|
|
26
|
+
* import will fail. We catch that and rethrow with a clear, actionable
|
|
27
|
+
* error so the integrator doesn't have to debug a `Cannot find module
|
|
28
|
+
* 'undici'` stack trace. */
|
|
29
|
+
class LazyVpsRouter {
|
|
30
|
+
discoveryUrl;
|
|
31
|
+
inner = null;
|
|
32
|
+
constructor(discoveryUrl) {
|
|
33
|
+
this.discoveryUrl = discoveryUrl;
|
|
34
|
+
}
|
|
35
|
+
async getTarget() {
|
|
36
|
+
if (!this.inner) {
|
|
37
|
+
try {
|
|
38
|
+
const { VpsRouter } = await import('./vps-router.js');
|
|
39
|
+
this.inner = new VpsRouter(this.discoveryUrl);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
// We want to rethrow with a friendly message ONLY for the specific
|
|
43
|
+
// case "undici not installed" (the optionalDependency wasn't
|
|
44
|
+
// resolved). Match the Node module-resolution error code AND the
|
|
45
|
+
// module name so an unrelated bundler glitch on `vps-router.js`
|
|
46
|
+
// itself doesn't surface a misleading "install undici" error.
|
|
47
|
+
const code = err.code;
|
|
48
|
+
const isModuleNotFound = code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND';
|
|
49
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
50
|
+
if (isModuleNotFound && msg.includes('undici')) {
|
|
51
|
+
throw new Error("@fdkey/mcp: `discoveryUrl` is set but `undici` is not installed. " +
|
|
52
|
+
"Multi-VPS routing requires undici (Node-only). Run " +
|
|
53
|
+
"`npm install undici` to enable it, or remove `discoveryUrl` to " +
|
|
54
|
+
"use the single-VPS StaticRouter path which works on Workers/Bun/Deno.");
|
|
55
|
+
}
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return this.inner.getTarget();
|
|
60
|
+
}
|
|
61
|
+
recordFailure(ip) {
|
|
62
|
+
if (this.inner)
|
|
63
|
+
this.inner.recordFailure(ip);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Maps the wrapped (and original) server objects to their bounded
|
|
67
|
+
* SessionStore. Use getFdkeyContext() rather than touching this directly. */
|
|
68
|
+
const FDKEY_REGISTRY = new WeakMap();
|
|
69
|
+
/** Read the current FDKEY context for a given session. Pass either the `extra`
|
|
70
|
+
* argument from a tool handler or a session ID string. Returns null if the server
|
|
71
|
+
* was not wrapped with withFdkey() or the session doesn't exist yet.
|
|
72
|
+
*
|
|
73
|
+
* Reads use `store.peek()` which does NOT slide the LRU position — querying
|
|
74
|
+
* the context shouldn't extend a session's lifetime; only actual tool calls
|
|
75
|
+
* do that. */
|
|
76
|
+
export function getFdkeyContext(server, extraOrSessionId) {
|
|
77
|
+
const store = FDKEY_REGISTRY.get(server);
|
|
78
|
+
if (!store)
|
|
79
|
+
return null;
|
|
80
|
+
const sid = typeof extraOrSessionId === 'string'
|
|
81
|
+
? extraOrSessionId
|
|
82
|
+
: extraOrSessionId?.sessionId ?? 'stdio';
|
|
83
|
+
const s = store.peek(sid);
|
|
84
|
+
if (!s)
|
|
85
|
+
return { verified: false, verifiedAt: null, score: null, tier: null, claims: null };
|
|
86
|
+
return {
|
|
87
|
+
verified: s.verified,
|
|
88
|
+
verifiedAt: s.verifiedAt,
|
|
89
|
+
score: extractScore(s.lastClaims),
|
|
90
|
+
tier: extractTier(s.lastClaims),
|
|
91
|
+
claims: s.lastClaims,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function extractScore(claims) {
|
|
95
|
+
if (!claims)
|
|
96
|
+
return null;
|
|
97
|
+
const v = claims.score;
|
|
98
|
+
return typeof v === 'number' ? v : null;
|
|
99
|
+
}
|
|
100
|
+
function extractTier(claims) {
|
|
101
|
+
if (!claims)
|
|
102
|
+
return null;
|
|
103
|
+
const v = claims.tier;
|
|
104
|
+
return typeof v === 'string' ? v : null;
|
|
105
|
+
}
|
|
106
|
+
const GET_CHALLENGE_TOOL = 'fdkey_get_challenge';
|
|
107
|
+
const SUBMIT_CHALLENGE_TOOL = 'fdkey_submit_challenge';
|
|
108
|
+
const GET_CHALLENGE_DESC = 'Request an AI identity verification challenge. Call this when a tool returns fdkey_verification_required, when asked to verify, or to verify proactively. There is a time limit on your answers.';
|
|
109
|
+
const SUBMIT_CHALLENGE_DESC = 'Submit answers to the active FDKEY challenge. On verified:true, retry the tool that was blocked. On verified:false, call fdkey_get_challenge to try again.';
|
|
110
|
+
function mkError(text) {
|
|
111
|
+
return { content: [{ type: 'text', text }], isError: true };
|
|
112
|
+
}
|
|
113
|
+
function mkResult(obj) {
|
|
114
|
+
return { content: [{ type: 'text', text: JSON.stringify(obj) }] };
|
|
115
|
+
}
|
|
116
|
+
function expiresInSeconds(expiresAtIso) {
|
|
117
|
+
const ms = new Date(expiresAtIso).getTime() - Date.now();
|
|
118
|
+
return Math.max(0, Math.round(ms / 1000));
|
|
119
|
+
}
|
|
120
|
+
function challengePayload(c) {
|
|
121
|
+
return {
|
|
122
|
+
expires_in_seconds: c.expires_in_seconds ?? expiresInSeconds(c.expires_at),
|
|
123
|
+
difficulty: c.difficulty,
|
|
124
|
+
types_served: c.types_served,
|
|
125
|
+
header: c.header,
|
|
126
|
+
puzzles: c.puzzles,
|
|
127
|
+
footer: c.footer,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Wraps an MCP server with FDKEY verification middleware.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* const server = withFdkey(new McpServer({ name: 'my-server', version: '1.0.0' }), {
|
|
135
|
+
* apiKey: process.env.FDKEY_API_KEY!,
|
|
136
|
+
* protect: {
|
|
137
|
+
* login: { policy: 'each_call' },
|
|
138
|
+
* register: { policy: 'once_per_session' },
|
|
139
|
+
* },
|
|
140
|
+
* });
|
|
141
|
+
* server.registerTool('login', { inputSchema: { username: z.string() } }, async (args) => { ... });
|
|
142
|
+
*/
|
|
143
|
+
export function withFdkey(server, config) {
|
|
144
|
+
const protect = config.protect ?? {};
|
|
145
|
+
const onFail = config.onFail ?? 'block';
|
|
146
|
+
// Default is 'allow' (fail-open) so an FDKEY service outage doesn't
|
|
147
|
+
// brick integrator workflows — protected tools fall through to their
|
|
148
|
+
// original handler. Override with `onVpsError: 'block'` if your threat
|
|
149
|
+
// model prefers fail-closed.
|
|
150
|
+
const onVpsError = config.onVpsError ?? 'allow';
|
|
151
|
+
const inlineChallenge = config.inlineChallenge ?? false;
|
|
152
|
+
// Router selection:
|
|
153
|
+
// discoveryUrl set → multi-VPS routing via undici-backed VpsRouter
|
|
154
|
+
// (Node-only; lazy-imported to keep undici out
|
|
155
|
+
// of Workers/Bun/Deno bundles)
|
|
156
|
+
// vpsUrl set → StaticRouter pinned to that URL (default fetch)
|
|
157
|
+
// neither set → StaticRouter at https://api.fdkey.com
|
|
158
|
+
const router = config.discoveryUrl
|
|
159
|
+
? new LazyVpsRouter(config.discoveryUrl)
|
|
160
|
+
: new StaticRouter(config.vpsUrl ?? DEFAULT_VPS_URL);
|
|
161
|
+
const vpsClient = new VpsClient(router, config.apiKey, config.difficulty ?? 'medium');
|
|
162
|
+
const wellKnown = new WellKnownClient(router);
|
|
163
|
+
// Bounded per-server session store. See session-store.ts for the
|
|
164
|
+
// full TTL + LRU eviction contract; the wrapped server itself only
|
|
165
|
+
// sees `get()` and `peek()`.
|
|
166
|
+
const store = createSessionStore();
|
|
167
|
+
const getSession = (id) => store.get(id);
|
|
168
|
+
let latestClientInfo = null;
|
|
169
|
+
let latestProtocolVersion = null;
|
|
170
|
+
// Hook (1): wrap the existing initialize handler so we can read protocolVersion
|
|
171
|
+
// from the request params before delegating. The Server class registers its
|
|
172
|
+
// own _oninitialize via setRequestHandler in its constructor; calling
|
|
173
|
+
// setRequestHandler again replaces the entry. We retrieve the prior handler
|
|
174
|
+
// through the protocol's private _requestHandlers map and re-invoke it,
|
|
175
|
+
// preserving the official init behavior. Private-API access is bounded to
|
|
176
|
+
// these few lines and documented; if MCP SDK refactors this away we update.
|
|
177
|
+
const protocolPrivate = server.server;
|
|
178
|
+
const origInitialize = protocolPrivate._requestHandlers?.get('initialize');
|
|
179
|
+
if (origInitialize) {
|
|
180
|
+
server.server.setRequestHandler(InitializeRequestSchema, async (request, extra) => {
|
|
181
|
+
try {
|
|
182
|
+
// request.params.protocolVersion is what the client sent
|
|
183
|
+
const pv = request.params?.protocolVersion;
|
|
184
|
+
if (typeof pv === 'string')
|
|
185
|
+
latestProtocolVersion = pv;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// Reading the field must never break initialize; swallow errors.
|
|
189
|
+
}
|
|
190
|
+
return origInitialize(request, extra);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
// Hook (2): post-init capture for clientInfo + capabilities.
|
|
194
|
+
const previousOnInitialized = server.server.oninitialized;
|
|
195
|
+
server.server.oninitialized = () => {
|
|
196
|
+
const ci = server.server.getClientVersion();
|
|
197
|
+
const caps = server.server.getClientCapabilities();
|
|
198
|
+
if (ci) {
|
|
199
|
+
latestClientInfo = {
|
|
200
|
+
name: ci.name,
|
|
201
|
+
version: ci.version,
|
|
202
|
+
title: ci.title,
|
|
203
|
+
capabilities: caps,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (previousOnInitialized)
|
|
207
|
+
previousOnInitialized();
|
|
208
|
+
};
|
|
209
|
+
// Integrator-side block — same value every challenge call. Read once at
|
|
210
|
+
// wrap time. server_info comes from McpServer's ctor args; the underlying
|
|
211
|
+
// Server stores it as private `_serverInfo`. Accessing via type assertion
|
|
212
|
+
// because there's no public getter as of MCP TS SDK 1.x.
|
|
213
|
+
const serverPrivate = server.server;
|
|
214
|
+
const integratorMeta = {
|
|
215
|
+
server_name: serverPrivate._serverInfo?.name,
|
|
216
|
+
server_version: serverPrivate._serverInfo?.version,
|
|
217
|
+
sdk_version: SDK_VERSION,
|
|
218
|
+
};
|
|
219
|
+
// Tags: integrator-supplied free-form key/value dimensions. Forwarded as-is
|
|
220
|
+
// on every challenge fetch. VPS bounds them at 16 keys / 50 char keys / 200
|
|
221
|
+
// char values and rejects with HTTP 400 on overflow.
|
|
222
|
+
const configuredTags = config.tags;
|
|
223
|
+
/** Lazy-copy session-scoped agent metadata on first tool call. Idempotent —
|
|
224
|
+
* re-runs are no-ops because we use ??= guards. Returns a ChallengeMeta
|
|
225
|
+
* bundle ready to pass to vpsClient.fetchChallenge(). */
|
|
226
|
+
function captureChallengeMeta(session, extra) {
|
|
227
|
+
session.mcpSessionId ??= extra.sessionId ?? 'stdio';
|
|
228
|
+
if (session.transport === 'unknown') {
|
|
229
|
+
session.transport = extra.sessionId ? 'http' : 'stdio';
|
|
230
|
+
}
|
|
231
|
+
session.clientInfo ??= latestClientInfo;
|
|
232
|
+
session.protocolVersion ??= latestProtocolVersion;
|
|
233
|
+
const agent = {
|
|
234
|
+
transport: session.transport,
|
|
235
|
+
mcp_session_id: session.mcpSessionId ?? undefined,
|
|
236
|
+
};
|
|
237
|
+
if (session.clientInfo) {
|
|
238
|
+
agent.client_name = session.clientInfo.name;
|
|
239
|
+
agent.client_version = session.clientInfo.version;
|
|
240
|
+
if (session.clientInfo.title)
|
|
241
|
+
agent.client_title = session.clientInfo.title;
|
|
242
|
+
if (session.clientInfo.capabilities) {
|
|
243
|
+
agent.client_capabilities = session.clientInfo.capabilities;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (session.protocolVersion)
|
|
247
|
+
agent.protocol_version = session.protocolVersion;
|
|
248
|
+
return {
|
|
249
|
+
agent,
|
|
250
|
+
integrator: integratorMeta,
|
|
251
|
+
tags: configuredTags,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
// --- Injected tool: fdkey_get_challenge ---
|
|
255
|
+
server.registerTool(GET_CHALLENGE_TOOL, { description: GET_CHALLENGE_DESC }, async (extra) => {
|
|
256
|
+
const e = extra;
|
|
257
|
+
const session = getSession(e.sessionId ?? 'stdio');
|
|
258
|
+
const meta = captureChallengeMeta(session, e);
|
|
259
|
+
try {
|
|
260
|
+
const challenge = await vpsClient.fetchChallenge(meta);
|
|
261
|
+
session.pendingChallengeId = challenge.challenge_id;
|
|
262
|
+
return mkResult(challengePayload(challenge));
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
return mkError(`fdkey_service_unavailable: ${String(err)}`);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
// --- Injected tool: fdkey_submit_challenge ---
|
|
269
|
+
server.registerTool(SUBMIT_CHALLENGE_TOOL, {
|
|
270
|
+
description: SUBMIT_CHALLENGE_DESC,
|
|
271
|
+
inputSchema: { answers: z.record(z.string(), z.unknown()) },
|
|
272
|
+
}, async (args, extra) => {
|
|
273
|
+
const session = getSession(extra.sessionId ?? 'stdio');
|
|
274
|
+
if (!session.pendingChallengeId) {
|
|
275
|
+
return mkResult({
|
|
276
|
+
verified: false,
|
|
277
|
+
error: 'no_active_challenge',
|
|
278
|
+
message: `No active challenge. Call ${GET_CHALLENGE_TOOL} first.`,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
let result;
|
|
282
|
+
try {
|
|
283
|
+
result = await vpsClient.submitAnswers(session.pendingChallengeId, args.answers);
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
session.pendingChallengeId = null;
|
|
287
|
+
if (err instanceof VpsHttpError && err.body.error === 'challenge_expired') {
|
|
288
|
+
return mkResult({
|
|
289
|
+
verified: false,
|
|
290
|
+
error: 'challenge_expired',
|
|
291
|
+
message: `Challenge expired. Call ${GET_CHALLENGE_TOOL} to start a new one.`,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
if (err instanceof VpsHttpError && err.status >= 400 && err.status < 500) {
|
|
295
|
+
// 4xx (e.g. wrong_user, already_submitted) — treat as a failed verification
|
|
296
|
+
if (onFail === 'allow') {
|
|
297
|
+
markVerified(session);
|
|
298
|
+
return mkResult({ verified: true, message: 'Verification skipped per server configuration.' });
|
|
299
|
+
}
|
|
300
|
+
return mkResult({ verified: false, error: err.body.error ?? 'verification_failed' });
|
|
301
|
+
}
|
|
302
|
+
// 5xx or transport error
|
|
303
|
+
if (onVpsError === 'allow') {
|
|
304
|
+
markVerified(session);
|
|
305
|
+
return mkResult({ verified: true, message: 'VPS unreachable — access allowed per server configuration.' });
|
|
306
|
+
}
|
|
307
|
+
return mkError(`fdkey_service_unavailable: verification service is temporarily unreachable. Retry in a few seconds or contact the server operator. (${String(err)})`);
|
|
308
|
+
}
|
|
309
|
+
session.pendingChallengeId = null;
|
|
310
|
+
if (result.verified && result.jwt) {
|
|
311
|
+
try {
|
|
312
|
+
const header = decodeProtectedHeader(result.jwt);
|
|
313
|
+
const kid = header.kid;
|
|
314
|
+
if (!kid)
|
|
315
|
+
throw new Error('JWT missing kid header');
|
|
316
|
+
const pubKey = await wellKnown.getKey(kid);
|
|
317
|
+
if (!pubKey)
|
|
318
|
+
throw new Error(`Unknown key id: ${kid}`);
|
|
319
|
+
// 30s clock tolerance — covers NTP drift between the VPS and the SDK host.
|
|
320
|
+
// Without this, JWTs with `nbf` slightly in the future (issuer clock ahead)
|
|
321
|
+
// get rejected by recipients with slightly slower clocks.
|
|
322
|
+
await jwtVerify(result.jwt, pubKey, { clockTolerance: 30 });
|
|
323
|
+
// Cache decoded claims so getFdkeyContext() can surface them to integrator handlers (G3)
|
|
324
|
+
session.lastClaims = decodeJwt(result.jwt);
|
|
325
|
+
}
|
|
326
|
+
catch (jwtErr) {
|
|
327
|
+
if (onFail === 'allow') {
|
|
328
|
+
markVerified(session);
|
|
329
|
+
return mkResult({ verified: true, message: 'Verification passed (JWT validation skipped).' });
|
|
330
|
+
}
|
|
331
|
+
return mkResult({
|
|
332
|
+
verified: false,
|
|
333
|
+
message: `Verification failed: invalid JWT — ${String(jwtErr)}`,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
markVerified(session);
|
|
337
|
+
return mkResult({ verified: true, message: 'Verification passed. You can now access protected tools.' });
|
|
338
|
+
}
|
|
339
|
+
// VPS responded 200 with verified=false
|
|
340
|
+
if (onFail === 'allow') {
|
|
341
|
+
markVerified(session);
|
|
342
|
+
return mkResult({ verified: true, message: 'Verification failed but access allowed per server configuration.' });
|
|
343
|
+
}
|
|
344
|
+
return mkResult({ verified: false, message: 'Verification failed. Call fdkey_get_challenge to try again.' });
|
|
345
|
+
});
|
|
346
|
+
// --- Proxy: intercept registerTool() and deprecated tool() to gate protected tools ---
|
|
347
|
+
function makeInterceptor(targetFn) {
|
|
348
|
+
return function (...toolArgs) {
|
|
349
|
+
const name = toolArgs[0];
|
|
350
|
+
// Our injected tools are already registered — pass through unchanged
|
|
351
|
+
if (name === GET_CHALLENGE_TOOL || name === SUBMIT_CHALLENGE_TOOL) {
|
|
352
|
+
return Reflect.apply(targetFn, server, toolArgs);
|
|
353
|
+
}
|
|
354
|
+
const entry = protect[name];
|
|
355
|
+
if (!entry) {
|
|
356
|
+
return Reflect.apply(targetFn, server, toolArgs);
|
|
357
|
+
}
|
|
358
|
+
const idx = toolArgs.length - 1;
|
|
359
|
+
const original = toolArgs[idx];
|
|
360
|
+
const policy = normalisePolicy(entry.policy);
|
|
361
|
+
toolArgs[idx] = async (...cbArgs) => {
|
|
362
|
+
// extra is always the last cb arg: (args, extra) or (extra) for no-arg tools
|
|
363
|
+
const extra = cbArgs[cbArgs.length - 1];
|
|
364
|
+
const session = getSession(extra?.sessionId ?? 'stdio');
|
|
365
|
+
// Capture client info on every protected-tool call too — the agent
|
|
366
|
+
// may hit a gated tool before fdkey_get_challenge runs (the natural
|
|
367
|
+
// first-blocked-then-verify flow), and we still want metadata.
|
|
368
|
+
const meta = captureChallengeMeta(session, extra ?? {});
|
|
369
|
+
if (canCall(policy, name, session)) {
|
|
370
|
+
const result = await original(...cbArgs);
|
|
371
|
+
consumePolicy(policy, session);
|
|
372
|
+
return result;
|
|
373
|
+
}
|
|
374
|
+
if (inlineChallenge) {
|
|
375
|
+
try {
|
|
376
|
+
const challenge = await vpsClient.fetchChallenge(meta);
|
|
377
|
+
session.pendingChallengeId = challenge.challenge_id;
|
|
378
|
+
return mkError(`fdkey_verification_required. Solve the challenge below then call ${SUBMIT_CHALLENGE_TOOL} with your answers, then retry this tool.\n` +
|
|
379
|
+
JSON.stringify(challengePayload(challenge)));
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
if (onVpsError === 'allow')
|
|
383
|
+
return original(...cbArgs);
|
|
384
|
+
return mkError('fdkey_service_unavailable: verification service is temporarily unreachable. Retry in a few seconds or contact the server operator.');
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return mkError(`fdkey_verification_required. Call ${GET_CHALLENGE_TOOL} to start verification, then ${SUBMIT_CHALLENGE_TOOL} with your answers, then retry this tool.`);
|
|
388
|
+
};
|
|
389
|
+
return Reflect.apply(targetFn, server, toolArgs);
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
const wrapped = new Proxy(server, {
|
|
393
|
+
get(target, prop, receiver) {
|
|
394
|
+
if (prop === 'registerTool') {
|
|
395
|
+
return makeInterceptor(target.registerTool);
|
|
396
|
+
}
|
|
397
|
+
if (prop === 'tool') {
|
|
398
|
+
return makeInterceptor(target.tool);
|
|
399
|
+
}
|
|
400
|
+
return Reflect.get(target, prop, receiver);
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
// Register both the original and the wrapped reference so getFdkeyContext()
|
|
404
|
+
// works regardless of which the integrator passes.
|
|
405
|
+
FDKEY_REGISTRY.set(server, store);
|
|
406
|
+
FDKEY_REGISTRY.set(wrapped, store);
|
|
407
|
+
return wrapped;
|
|
408
|
+
}
|
|
409
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { IVpsRouter, RoutingTarget } from './types.js';
|
|
2
|
+
/** Used when the SDK consumer passes an explicit `vpsUrl` (or accepts the
|
|
3
|
+
* default `https://api.fdkey.com`). Bypasses all discovery and probing —
|
|
4
|
+
* fetch uses its default DNS resolution against whatever hostname (or IP)
|
|
5
|
+
* is in the URL. No `dispatcher` is set, so this works on every runtime
|
|
6
|
+
* that has a global `fetch` (Node 18+, Cloudflare Workers, Bun, Deno). */
|
|
7
|
+
export declare class StaticRouter implements IVpsRouter {
|
|
8
|
+
private readonly url;
|
|
9
|
+
constructor(url: string);
|
|
10
|
+
getTarget(): Promise<RoutingTarget>;
|
|
11
|
+
recordFailure(_ip: string | undefined): void;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=router-static.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router-static.d.ts","sourceRoot":"","sources":["../src/router-static.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE5D;;;;2EAI2E;AAC3E,qBAAa,YAAa,YAAW,UAAU;IACjC,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,MAAM;IAClC,SAAS,IAAI,OAAO,CAAC,aAAa,CAAC;IAGzC,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;CAC7C"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Used when the SDK consumer passes an explicit `vpsUrl` (or accepts the
|
|
2
|
+
* default `https://api.fdkey.com`). Bypasses all discovery and probing —
|
|
3
|
+
* fetch uses its default DNS resolution against whatever hostname (or IP)
|
|
4
|
+
* is in the URL. No `dispatcher` is set, so this works on every runtime
|
|
5
|
+
* that has a global `fetch` (Node 18+, Cloudflare Workers, Bun, Deno). */
|
|
6
|
+
export class StaticRouter {
|
|
7
|
+
url;
|
|
8
|
+
constructor(url) {
|
|
9
|
+
this.url = url;
|
|
10
|
+
}
|
|
11
|
+
async getTarget() {
|
|
12
|
+
return { url: this.url };
|
|
13
|
+
}
|
|
14
|
+
recordFailure(_ip) { }
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=router-static.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router-static.js","sourceRoot":"","sources":["../src/router-static.ts"],"names":[],"mappings":"AAEA;;;;2EAI2E;AAC3E,MAAM,OAAO,YAAY;IACM;IAA7B,YAA6B,GAAW;QAAX,QAAG,GAAH,GAAG,CAAQ;IAAG,CAAC;IAC5C,KAAK,CAAC,SAAS;QACb,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC;IAC3B,CAAC;IACD,aAAa,CAAC,GAAuB,IAAS,CAAC;CAChD"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { SessionState } from './types.js';
|
|
2
|
+
/** Drop sessions that have been idle this long. One hour is comfortably
|
|
3
|
+
* longer than the default JWT lifetime (~5 min) plus typical agent
|
|
4
|
+
* retry/backoff windows, so an active session is never evicted while
|
|
5
|
+
* it could legitimately make another call. */
|
|
6
|
+
export declare const SESSION_IDLE_TTL_MS: number;
|
|
7
|
+
/** Hard cap on the per-server session map. Hit this only on pathological
|
|
8
|
+
* burst traffic — even at this size the eviction is O(1) thanks to the
|
|
9
|
+
* JS Map's preserved-insertion-order semantics. */
|
|
10
|
+
export declare const MAX_SESSIONS = 10000;
|
|
11
|
+
export interface SessionStore {
|
|
12
|
+
/** Get-or-create the session for `id`, sliding the LRU position to the
|
|
13
|
+
* tail. Opportunistically evicts one stale (TTL-expired) entry per
|
|
14
|
+
* miss, and force-evicts the LRU when at the size cap. */
|
|
15
|
+
get(id: string): SessionState;
|
|
16
|
+
/** Number of currently-live sessions. Test-only convenience. */
|
|
17
|
+
size(): number;
|
|
18
|
+
/** Read-only access to a session without sliding LRU position. Returns
|
|
19
|
+
* undefined if the session doesn't exist. Used by `getFdkeyContext` so
|
|
20
|
+
* reading verification context doesn't extend a session's lifetime. */
|
|
21
|
+
peek(id: string): SessionState | undefined;
|
|
22
|
+
}
|
|
23
|
+
/** Bounded session store. Memory is bounded by `maxSize` (the hard guarantee).
|
|
24
|
+
* Two eviction policies cooperate:
|
|
25
|
+
*
|
|
26
|
+
* 1. **TTL sweep on miss.** When a brand-new session arrives, the head
|
|
27
|
+
* of the map (always the LRU by insertion-order) is checked: if it
|
|
28
|
+
* has been idle longer than `idleTtlMs`, it's dropped. Sweep is
|
|
29
|
+
* O(1) per miss and only fires on misses — a busy session with
|
|
30
|
+
* many hits doesn't trigger it. This means stale entries can sit
|
|
31
|
+
* in the map longer than `idleTtlMs` if the server only ever sees
|
|
32
|
+
* hits; they're dropped opportunistically as new sessions arrive.
|
|
33
|
+
*
|
|
34
|
+
* 2. **LRU cap on insert.** When `sessions.size === maxSize`, the head
|
|
35
|
+
* (LRU) entry is force-dropped to make room — regardless of age.
|
|
36
|
+
* O(1) thanks to the JS Map's preserved-insertion-order semantics.
|
|
37
|
+
* This is the actual memory bound: maps NEVER exceed `maxSize`.
|
|
38
|
+
*
|
|
39
|
+
* Net guarantee: long-lived MCP servers stay capped at `maxSize` entries
|
|
40
|
+
* (default 10k × ~200 bytes ≈ 2 MB) regardless of transport-level
|
|
41
|
+
* disconnect signals. Idle entries below the cap may linger past their
|
|
42
|
+
* TTL until pressure forces them out — that's acceptable because the
|
|
43
|
+
* cap itself is the safety property. */
|
|
44
|
+
export declare function createSessionStore(maxSize?: number, idleTtlMs?: number, now?: () => number): SessionStore;
|
|
45
|
+
//# sourceMappingURL=session-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-store.d.ts","sourceRoot":"","sources":["../src/session-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C;;;+CAG+C;AAC/C,eAAO,MAAM,mBAAmB,QAAiB,CAAC;AAClD;;oDAEoD;AACpD,eAAO,MAAM,YAAY,QAAS,CAAC;AAEnC,MAAM,WAAW,YAAY;IAC3B;;+DAE2D;IAC3D,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,CAAC;IAC9B,gEAAgE;IAChE,IAAI,IAAI,MAAM,CAAC;IACf;;4EAEwE;IACxE,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;CAC5C;AAED;;;;;;;;;;;;;;;;;;;;yCAoByC;AACzC,wBAAgB,kBAAkB,CAChC,OAAO,GAAE,MAAqB,EAC9B,SAAS,GAAE,MAA4B,EACvC,GAAG,GAAE,MAAM,MAAiB,GAC3B,YAAY,CAwCd"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { newSession } from './guard.js';
|
|
2
|
+
/** Drop sessions that have been idle this long. One hour is comfortably
|
|
3
|
+
* longer than the default JWT lifetime (~5 min) plus typical agent
|
|
4
|
+
* retry/backoff windows, so an active session is never evicted while
|
|
5
|
+
* it could legitimately make another call. */
|
|
6
|
+
export const SESSION_IDLE_TTL_MS = 60 * 60 * 1000;
|
|
7
|
+
/** Hard cap on the per-server session map. Hit this only on pathological
|
|
8
|
+
* burst traffic — even at this size the eviction is O(1) thanks to the
|
|
9
|
+
* JS Map's preserved-insertion-order semantics. */
|
|
10
|
+
export const MAX_SESSIONS = 10_000;
|
|
11
|
+
/** Bounded session store. Memory is bounded by `maxSize` (the hard guarantee).
|
|
12
|
+
* Two eviction policies cooperate:
|
|
13
|
+
*
|
|
14
|
+
* 1. **TTL sweep on miss.** When a brand-new session arrives, the head
|
|
15
|
+
* of the map (always the LRU by insertion-order) is checked: if it
|
|
16
|
+
* has been idle longer than `idleTtlMs`, it's dropped. Sweep is
|
|
17
|
+
* O(1) per miss and only fires on misses — a busy session with
|
|
18
|
+
* many hits doesn't trigger it. This means stale entries can sit
|
|
19
|
+
* in the map longer than `idleTtlMs` if the server only ever sees
|
|
20
|
+
* hits; they're dropped opportunistically as new sessions arrive.
|
|
21
|
+
*
|
|
22
|
+
* 2. **LRU cap on insert.** When `sessions.size === maxSize`, the head
|
|
23
|
+
* (LRU) entry is force-dropped to make room — regardless of age.
|
|
24
|
+
* O(1) thanks to the JS Map's preserved-insertion-order semantics.
|
|
25
|
+
* This is the actual memory bound: maps NEVER exceed `maxSize`.
|
|
26
|
+
*
|
|
27
|
+
* Net guarantee: long-lived MCP servers stay capped at `maxSize` entries
|
|
28
|
+
* (default 10k × ~200 bytes ≈ 2 MB) regardless of transport-level
|
|
29
|
+
* disconnect signals. Idle entries below the cap may linger past their
|
|
30
|
+
* TTL until pressure forces them out — that's acceptable because the
|
|
31
|
+
* cap itself is the safety property. */
|
|
32
|
+
export function createSessionStore(maxSize = MAX_SESSIONS, idleTtlMs = SESSION_IDLE_TTL_MS, now = Date.now) {
|
|
33
|
+
const sessions = new Map();
|
|
34
|
+
return {
|
|
35
|
+
get(id) {
|
|
36
|
+
const t = now();
|
|
37
|
+
const existing = sessions.get(id);
|
|
38
|
+
if (existing) {
|
|
39
|
+
// Slide to the tail of the LRU order: delete + re-insert is O(1).
|
|
40
|
+
sessions.delete(id);
|
|
41
|
+
existing.lastTouchedAt = t;
|
|
42
|
+
sessions.set(id, existing);
|
|
43
|
+
return existing;
|
|
44
|
+
}
|
|
45
|
+
// Miss → opportunistic TTL sweep of the head, then enforce the cap.
|
|
46
|
+
const head = sessions.keys().next().value;
|
|
47
|
+
if (head !== undefined) {
|
|
48
|
+
const headSession = sessions.get(head);
|
|
49
|
+
if (headSession && t - headSession.lastTouchedAt > idleTtlMs) {
|
|
50
|
+
sessions.delete(head);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (sessions.size >= maxSize) {
|
|
54
|
+
const lruKey = sessions.keys().next().value;
|
|
55
|
+
if (lruKey !== undefined)
|
|
56
|
+
sessions.delete(lruKey);
|
|
57
|
+
}
|
|
58
|
+
const fresh = newSession();
|
|
59
|
+
fresh.lastTouchedAt = t;
|
|
60
|
+
sessions.set(id, fresh);
|
|
61
|
+
return fresh;
|
|
62
|
+
},
|
|
63
|
+
peek(id) {
|
|
64
|
+
return sessions.get(id);
|
|
65
|
+
},
|
|
66
|
+
size() {
|
|
67
|
+
return sessions.size;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=session-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-store.js","sourceRoot":"","sources":["../src/session-store.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC;;;+CAG+C;AAC/C,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAClD;;oDAEoD;AACpD,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC;AAenC;;;;;;;;;;;;;;;;;;;;yCAoByC;AACzC,MAAM,UAAU,kBAAkB,CAChC,UAAkB,YAAY,EAC9B,YAAoB,mBAAmB,EACvC,MAAoB,IAAI,CAAC,GAAG;IAE5B,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAwB,CAAC;IAEjD,OAAO;QACL,GAAG,CAAC,EAAU;YACZ,MAAM,CAAC,GAAG,GAAG,EAAE,CAAC;YAChB,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAClC,IAAI,QAAQ,EAAE,CAAC;gBACb,kEAAkE;gBAClE,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gBACpB,QAAQ,CAAC,aAAa,GAAG,CAAC,CAAC;gBAC3B,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;gBAC3B,OAAO,QAAQ,CAAC;YAClB,CAAC;YACD,oEAAoE;YACpE,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;YAC1C,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;gBACvB,MAAM,WAAW,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBACvC,IAAI,WAAW,IAAI,CAAC,GAAG,WAAW,CAAC,aAAa,GAAG,SAAS,EAAE,CAAC;oBAC7D,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;gBACxB,CAAC;YACH,CAAC;YACD,IAAI,QAAQ,CAAC,IAAI,IAAI,OAAO,EAAE,CAAC;gBAC7B,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;gBAC5C,IAAI,MAAM,KAAK,SAAS;oBAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YACpD,CAAC;YACD,MAAM,KAAK,GAAG,UAAU,EAAE,CAAC;YAC3B,KAAK,CAAC,aAAa,GAAG,CAAC,CAAC;YACxB,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;YACxB,OAAO,KAAK,CAAC;QACf,CAAC;QAED,IAAI,CAAC,EAAU;YACb,OAAO,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC1B,CAAC;QAED,IAAI;YACF,OAAO,QAAQ,CAAC,IAAI,CAAC;QACvB,CAAC;KACF,CAAC;AACJ,CAAC"}
|