@everfur/cli 0.1.0 → 0.2.1
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 +295 -19
- package/dist/{chunk-67H43MI6.js → chunk-4UB26D67.js} +295 -510
- package/dist/chunk-FC23PLWG.js +161 -0
- package/dist/chunk-KQICUK54.js +12852 -0
- package/dist/chunk-SJUEHHU6.js +893 -0
- package/dist/chunk-XMIERMOW.js +2512 -0
- package/dist/everfur-mcp-container.d.ts +2 -0
- package/dist/everfur-mcp-container.js +60 -0
- package/dist/everfur-mcp-hosted.d.ts +2 -0
- package/dist/everfur-mcp-hosted.js +28 -0
- package/dist/everfur-mcp-http.d.ts +2 -0
- package/dist/everfur-mcp-http.js +21 -0
- package/dist/everfur-mcp.js +11 -365
- package/dist/everfur.js +4 -3
- package/dist/index.d.ts +875 -41
- package/dist/index.js +2 -21
- package/package.json +5 -4
- package/schema/diagnosis.v1.schema.json +41 -0
- package/schema/plan.v1.schema.json +90 -4
- package/schema/verification.v1.schema.json +7 -3
- package/contract/contract-public.json +0 -1335
- package/dist/chunk-KQZ6ZXL7.js +0 -3994
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { hostnameOf, listen, close, writeJson, serveStatelessMcp } from './chunk-FC23PLWG.js';
|
|
3
|
+
import { createHostedTokenVerifier, tokenBucketOf, isAccessTokenShaped, buildServer, MCP_SCOPE, tenantContextOf, sendTestUserRef, hostedVerifyUserRef, REVOKED_AUTHORIZATION_REMEDY, SYNTHETIC_USER_REF_PREFIX } from './chunk-XMIERMOW.js';
|
|
4
|
+
import { BASE_URL_ENV_VAR, resolveEndpoint, fetchTransport, systemClock, createApiClient, withPrefix, ephemeralCredential, validateContractText, isJsonObject, describeFailure } from './chunk-KQICUK54.js';
|
|
5
|
+
import { createServer } from 'http';
|
|
6
|
+
import { InvalidTokenError, TooManyRequestsError, TemporarilyUnavailableError } from '@modelcontextprotocol/sdk/server/auth/errors.js';
|
|
7
|
+
import { isIP } from 'net';
|
|
8
|
+
|
|
9
|
+
// src/mcp/hosted/config.ts
|
|
10
|
+
var PUBLIC_URL_ENV_VAR = "MCP_PUBLIC_URL";
|
|
11
|
+
var PORT_ENV_VAR = "EVERFUR_MCP_HTTP_PORT";
|
|
12
|
+
var LEGACY_PORT_ENV_VAR = "MCP_PORT";
|
|
13
|
+
var ALLOWED_HOSTS_ENV_VAR = "MCP_ALLOWED_HOSTS";
|
|
14
|
+
var ALLOWED_ORIGINS_ENV_VAR = "MCP_ALLOWED_ORIGINS";
|
|
15
|
+
var DEFAULT_ALLOWED_ORIGINS = Object.freeze(["https://claude.ai", "https://claude.com"]);
|
|
16
|
+
var TRUSTED_PROXY_HOPS_ENV_VAR = "MCP_TRUSTED_PROXY_HOPS";
|
|
17
|
+
var MAX_TRUSTED_PROXY_HOPS = 8;
|
|
18
|
+
var DEFAULT_HOSTED_PORT = 3100;
|
|
19
|
+
var DEFAULT_MCP_PATH = "/mcp";
|
|
20
|
+
var LOCAL_HTTP_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
21
|
+
function stripTrailingSlashes(value) {
|
|
22
|
+
let end = value.length;
|
|
23
|
+
while (end > 0 && value[end - 1] === "/") end -= 1;
|
|
24
|
+
return value.slice(0, end);
|
|
25
|
+
}
|
|
26
|
+
function parsePublicUrl(raw) {
|
|
27
|
+
const value = (raw ?? "").trim();
|
|
28
|
+
if (value.length === 0) {
|
|
29
|
+
throw new Error(`${PUBLIC_URL_ENV_VAR} is required: the full MCP endpoint URL editors reach this server at (https://mcp.everfur.com${DEFAULT_MCP_PATH}).`);
|
|
30
|
+
}
|
|
31
|
+
let parsed;
|
|
32
|
+
try {
|
|
33
|
+
parsed = new URL(value);
|
|
34
|
+
} catch {
|
|
35
|
+
throw new Error(`${PUBLIC_URL_ENV_VAR} is not a URL.`);
|
|
36
|
+
}
|
|
37
|
+
if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && LOCAL_HTTP_HOSTS.has(parsed.hostname))) {
|
|
38
|
+
throw new Error(`${PUBLIC_URL_ENV_VAR} must be https (http only on localhost, 127.0.0.1 or [::1]).`);
|
|
39
|
+
}
|
|
40
|
+
if (parsed.search !== "" || parsed.hash !== "" || value.endsWith("?") || value.endsWith("#")) {
|
|
41
|
+
throw new Error(`${PUBLIC_URL_ENV_VAR} must carry no query or fragment.`);
|
|
42
|
+
}
|
|
43
|
+
const path = stripTrailingSlashes(parsed.pathname);
|
|
44
|
+
if (path === "") {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`${PUBLIC_URL_ENV_VAR} must be the full MCP endpoint URL, path included (${parsed.origin}${DEFAULT_MCP_PATH}), not the origin: it is the resource every token names and the URL a user enters.`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
publicUrl: `${parsed.origin}${path}`,
|
|
51
|
+
publicOrigin: parsed.origin,
|
|
52
|
+
mcpPath: path
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function parsePort(env) {
|
|
56
|
+
const [name, raw] = (env[PORT_ENV_VAR] ?? "").trim() !== "" ? [PORT_ENV_VAR, env[PORT_ENV_VAR]] : [LEGACY_PORT_ENV_VAR, env[LEGACY_PORT_ENV_VAR]];
|
|
57
|
+
if (raw === void 0 || raw.trim() === "") return DEFAULT_HOSTED_PORT;
|
|
58
|
+
if (!/^\d+$/.test(raw.trim())) throw new Error(`${name} must be an integer between 0 and 65535.`);
|
|
59
|
+
const value = Number(raw.trim());
|
|
60
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 65535) {
|
|
61
|
+
throw new Error(`${name} must be an integer between 0 and 65535.`);
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
function parseTrustedProxyHops(raw) {
|
|
66
|
+
if (raw === void 0 || raw.trim() === "") return 0;
|
|
67
|
+
const value = Number(raw.trim());
|
|
68
|
+
if (!/^\d+$/.test(raw.trim()) || !Number.isSafeInteger(value) || value > MAX_TRUSTED_PROXY_HOPS) {
|
|
69
|
+
throw new Error(`${TRUSTED_PROXY_HOPS_ENV_VAR} must be an integer between 0 and ${MAX_TRUSTED_PROXY_HOPS}.`);
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
function parseAllowedHosts(raw) {
|
|
74
|
+
if (raw === void 0 || raw.trim() === "") return [];
|
|
75
|
+
const values = raw.split(",").map((value) => value.trim()).filter((value) => value.length > 0);
|
|
76
|
+
if (values.length === 0) throw new Error(`${ALLOWED_HOSTS_ENV_VAR} must contain at least one hostname when set.`);
|
|
77
|
+
return values.map((value) => {
|
|
78
|
+
const hostname = hostnameOf(value);
|
|
79
|
+
if (hostname === null) throw new Error(`${ALLOWED_HOSTS_ENV_VAR} carries a value that is not a hostname.`);
|
|
80
|
+
return hostname;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function normalizeOrigin(value) {
|
|
84
|
+
let parsed;
|
|
85
|
+
try {
|
|
86
|
+
parsed = new URL(value.trim());
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
|
|
91
|
+
if (parsed.username !== "" || parsed.password !== "" || parsed.search !== "" || parsed.hash !== "") return null;
|
|
92
|
+
if (parsed.pathname !== "/" && parsed.pathname !== "") return null;
|
|
93
|
+
return parsed.origin;
|
|
94
|
+
}
|
|
95
|
+
function parseAllowedOrigins(raw) {
|
|
96
|
+
if (raw === void 0 || raw.trim() === "") return [];
|
|
97
|
+
const values = raw.split(",").map((value) => value.trim()).filter((value) => value.length > 0);
|
|
98
|
+
if (values.length === 0) throw new Error(`${ALLOWED_ORIGINS_ENV_VAR} must contain at least one origin when set.`);
|
|
99
|
+
return values.map((value) => {
|
|
100
|
+
const origin = normalizeOrigin(value);
|
|
101
|
+
if (origin === null) throw new Error(`${ALLOWED_ORIGINS_ENV_VAR} carries a value that is not an origin (scheme and host only).`);
|
|
102
|
+
const hostname = new URL(origin).hostname;
|
|
103
|
+
if (origin.startsWith("http:") && !LOCAL_HTTP_HOSTS.has(hostname)) {
|
|
104
|
+
throw new Error(`${ALLOWED_ORIGINS_ENV_VAR} must list https origins (http only on localhost, 127.0.0.1 or [::1]).`);
|
|
105
|
+
}
|
|
106
|
+
return origin;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function loadHostedMcpConfig(env) {
|
|
110
|
+
if ((env[BASE_URL_ENV_VAR] ?? "").trim().length === 0) {
|
|
111
|
+
throw new Error(`${BASE_URL_ENV_VAR} is required: the Everfur API this server verifies bearers against.`);
|
|
112
|
+
}
|
|
113
|
+
const endpoint = resolveEndpoint(null, env);
|
|
114
|
+
if (endpoint.ok === false) {
|
|
115
|
+
throw new Error(`${BASE_URL_ENV_VAR} is not usable: ${endpoint.errors.join("; ").replace(/--base-url/g, BASE_URL_ENV_VAR)}`);
|
|
116
|
+
}
|
|
117
|
+
const publicUrl = parsePublicUrl(env[PUBLIC_URL_ENV_VAR]);
|
|
118
|
+
const publicHostname = hostnameOf(new URL(publicUrl.publicOrigin).host);
|
|
119
|
+
if (publicHostname === null) throw new Error(`${PUBLIC_URL_ENV_VAR} names no host.`);
|
|
120
|
+
return Object.freeze({
|
|
121
|
+
apiBaseUrl: endpoint.endpoint.baseUrl,
|
|
122
|
+
publicUrl: publicUrl.publicUrl,
|
|
123
|
+
publicOrigin: publicUrl.publicOrigin,
|
|
124
|
+
mcpPath: publicUrl.mcpPath,
|
|
125
|
+
host: "0.0.0.0",
|
|
126
|
+
port: parsePort(env),
|
|
127
|
+
allowedHosts: /* @__PURE__ */ new Set([publicHostname, ...parseAllowedHosts(env[ALLOWED_HOSTS_ENV_VAR])]),
|
|
128
|
+
trustedProxyHops: parseTrustedProxyHops(env[TRUSTED_PROXY_HOPS_ENV_VAR]),
|
|
129
|
+
allowedOrigins: /* @__PURE__ */ new Set([...DEFAULT_ALLOWED_ORIGINS, ...parseAllowedOrigins(env[ALLOWED_ORIGINS_ENV_VAR])])
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function resourceMetadataUrl(config) {
|
|
133
|
+
return `${config.publicUrl}/.well-known/oauth-protected-resource`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/mcp/hosted/contract.ts
|
|
137
|
+
var HOSTED_CONTRACT_PATH = "/mcp/contract";
|
|
138
|
+
var MAX_CACHED_DOCUMENTS = 3;
|
|
139
|
+
function createHostedContractCache() {
|
|
140
|
+
return { latest: null, documents: /* @__PURE__ */ new Map() };
|
|
141
|
+
}
|
|
142
|
+
function snapshotOf(outcome, source) {
|
|
143
|
+
return Object.freeze({
|
|
144
|
+
source,
|
|
145
|
+
digest: outcome.digest.actual,
|
|
146
|
+
generatedAt: outcome.generatedAt,
|
|
147
|
+
validation: { ...outcome, path: source }
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function unavailable(reason, cliCode, message, details, status = null, requestId = null) {
|
|
151
|
+
return Object.freeze({ kind: "unavailable", reason, cliCode, message, details: Object.freeze([...details]), status, requestId });
|
|
152
|
+
}
|
|
153
|
+
function remember(cache, outcome, etag) {
|
|
154
|
+
cache.documents.set(outcome.digest.actual, outcome);
|
|
155
|
+
cache.latest = { digest: outcome.digest.actual, etag };
|
|
156
|
+
while (cache.documents.size > MAX_CACHED_DOCUMENTS) {
|
|
157
|
+
const oldest = cache.documents.keys().next().value;
|
|
158
|
+
if (oldest === void 0 || oldest === outcome.digest.actual) break;
|
|
159
|
+
cache.documents.delete(oldest);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function readOnce(options) {
|
|
163
|
+
const endpoint = resolveEndpoint(null, { [BASE_URL_ENV_VAR]: options.apiBaseUrl });
|
|
164
|
+
if (endpoint.ok === false) {
|
|
165
|
+
return unavailable("endpoint_missing", "usage_error", "the hosted server has no usable Everfur API base URL to fetch the contract from", endpoint.errors);
|
|
166
|
+
}
|
|
167
|
+
const baseUrl = endpoint.endpoint.baseUrl;
|
|
168
|
+
const source = `GET ${HOSTED_CONTRACT_PATH} on ${baseUrl}`;
|
|
169
|
+
const cache = options.cache;
|
|
170
|
+
const cached = cache.latest === null ? null : cache.documents.get(cache.latest.digest) ?? null;
|
|
171
|
+
const client = createApiClient({
|
|
172
|
+
transport: options.transport ?? fetchTransport,
|
|
173
|
+
clock: options.clock ?? systemClock,
|
|
174
|
+
endpoint: endpoint.endpoint,
|
|
175
|
+
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
|
|
176
|
+
});
|
|
177
|
+
const result = await client.request({
|
|
178
|
+
method: "GET",
|
|
179
|
+
path: HOSTED_CONTRACT_PATH,
|
|
180
|
+
credentials: { authorization: withPrefix(ephemeralCredential("editor access token", options.bearer), "Bearer ") },
|
|
181
|
+
...cached !== null && cache.latest !== null ? { headers: { "if-none-match": cache.latest.etag } } : {},
|
|
182
|
+
acceptStatuses: [304],
|
|
183
|
+
responseType: "text"
|
|
184
|
+
});
|
|
185
|
+
if (result.ok === true) {
|
|
186
|
+
if (result.status === 304) {
|
|
187
|
+
if (cached === null) {
|
|
188
|
+
return unavailable("remote_error", "malformed_response", `${source} answered 304 to a request that held no cached contract`, [], 304, result.requestId);
|
|
189
|
+
}
|
|
190
|
+
return { kind: "loaded", contract: snapshotOf(cached, source) };
|
|
191
|
+
}
|
|
192
|
+
const text = typeof result.value === "string" ? result.value : "";
|
|
193
|
+
const outcome = validateContractText(source, text);
|
|
194
|
+
if (outcome.kind !== "valid") {
|
|
195
|
+
return unavailable(
|
|
196
|
+
"invalid_contract",
|
|
197
|
+
outcome.kind === "digest_mismatch" ? "digest_mismatch" : "invalid_contract",
|
|
198
|
+
`${source} did not return a usable contract`,
|
|
199
|
+
[
|
|
200
|
+
outcome.kind === "digest_mismatch" ? "the served document declares a digest that does not match its projection" : "the response body is not a contract-public envelope"
|
|
201
|
+
],
|
|
202
|
+
result.status,
|
|
203
|
+
result.requestId
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
remember(cache, outcome, result.headers["etag"] ?? `"${outcome.digest.actual}"`);
|
|
207
|
+
return { kind: "loaded", contract: snapshotOf(outcome, source) };
|
|
208
|
+
}
|
|
209
|
+
if (result.kind === "network" || result.kind === "timeout") {
|
|
210
|
+
if (cached !== null) {
|
|
211
|
+
return { kind: "loaded", contract: snapshotOf(cached, `(cached from ${source}; the API was unreachable, not revalidated)`) };
|
|
212
|
+
}
|
|
213
|
+
return unavailable("unreachable", "remote_unreachable", `the Everfur API at ${baseUrl} could not be reached to fetch the contract`, [
|
|
214
|
+
result.kind === "timeout" ? `the request timed out after ${result.timeoutMs}ms` : "the connection failed",
|
|
215
|
+
"no cached copy of the contract is available on this server"
|
|
216
|
+
]);
|
|
217
|
+
}
|
|
218
|
+
if (result.kind === "http" && (result.status === 401 || result.status === 403)) {
|
|
219
|
+
if (result.status === 401) options.onUnauthorized?.();
|
|
220
|
+
return unavailable("credential_rejected", "remote_error", `the Everfur API refused this sign-in for the contract (${result.status})`, [
|
|
221
|
+
REVOKED_AUTHORIZATION_REMEDY
|
|
222
|
+
], result.status, result.requestId);
|
|
223
|
+
}
|
|
224
|
+
if (result.kind === "http" && result.status === 404) {
|
|
225
|
+
return unavailable("remote_error", "remote_error", `${baseUrl} does not serve GET ${HOSTED_CONTRACT_PATH}`, [
|
|
226
|
+
"the hosted MCP surface is dark on this deployment of the Everfur API"
|
|
227
|
+
], 404, result.requestId);
|
|
228
|
+
}
|
|
229
|
+
return unavailable(
|
|
230
|
+
"remote_error",
|
|
231
|
+
result.kind === "malformed" ? "malformed_response" : "remote_error",
|
|
232
|
+
`${source} did not succeed`,
|
|
233
|
+
[result.kind === "http" ? `the API answered ${result.status}` : "the API answered a body this server does not recognise"],
|
|
234
|
+
result.status,
|
|
235
|
+
result.requestId
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
function createHostedContractProvider(options) {
|
|
239
|
+
let pending = null;
|
|
240
|
+
return () => {
|
|
241
|
+
if (pending === null) pending = readOnce(options);
|
|
242
|
+
return pending;
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/mcp/hosted/instructions.ts
|
|
247
|
+
var HOSTED_MCP_INSTRUCTIONS = `Everfur partner integration tools, hosted by Everfur.
|
|
248
|
+
|
|
249
|
+
Everfur is a pet-health API. Partners embed chat, medical-records and Televet (vet visit) surfaces in their own apps and
|
|
250
|
+
pages via @everfur/sdk (React Native and Expo; the web through @everfur/sdk/web and a hosted frame; Flutter through
|
|
251
|
+
everfur_sdk). On the web the plan follows the contract: a React tree embeds the SDK in the page, any other page includes
|
|
252
|
+
the loader and gets the frame, and a Shopify storefront gets the Everfur app's theme block with Shopify vouching for the
|
|
253
|
+
signed-in customer (identity_mode shopify-app-proxy). A capability the web cannot render (photo in the page, anything
|
|
254
|
+
but chat in the frame) is reported as blocked by the platform; the React Native entry points are not a substitute for
|
|
255
|
+
it, and the HTTP routes are not an integration of their own.
|
|
256
|
+
|
|
257
|
+
This server runs on Everfur's hosts as the partner who connected it. The partner console sign-in made when the
|
|
258
|
+
connector was added decides the tenant, and every tool runs as that tenant: its publishable key, API base URL and
|
|
259
|
+
contract are resolved from the sign-in on each call. The server holds no key, token or variable of the partner's and
|
|
260
|
+
reads no file or path on the developer's machine. A project is described with project_details (package dependencies,
|
|
261
|
+
package manager and file-presence markers, never source files or credentials), and a theme with theme_source (the text
|
|
262
|
+
of a tailwind config or of the CSS that declares the theme, read statically and never executed).
|
|
263
|
+
|
|
264
|
+
The integration tools:
|
|
265
|
+
- everfur_plan_integration produces an integration plan, or a refusal that names each missing fact (identity mode,
|
|
266
|
+
capabilities, backend language, environment) rather than a plan built on a guess. An integration built on a guessed
|
|
267
|
+
identity mode is wrong in a way that only shows up in production. Omitted, the environment is the connected tenant's.
|
|
268
|
+
- everfur_verify_integration exercises the real wire with a short-lived session the Everfur API mints for a synthetic
|
|
269
|
+
test user of a sandbox tenant (its entitlements report livemode false). On a live tenant the signed session step is
|
|
270
|
+
reported GATED; no session can be supplied by hand. identity_mode anonymous verifies the anonymous path instead: the
|
|
271
|
+
tenant's publishable key and a device session id derived for a synthetic test user, with nothing minted. It is the
|
|
272
|
+
read-only check a live tenant takes, GET requests only.
|
|
273
|
+
- everfur_diagnose separates the four byte-identical causes of an empty entitlement verdict.
|
|
274
|
+
- everfur_explain_error looks an error code up in the contract's registry, case sensitive, and refuses a code the
|
|
275
|
+
registry does not list, with near misses. Three naming families are live at once and one code is the default for
|
|
276
|
+
four different HTTP statuses, so the registry, not the shape of a code's string, says whether a retry can help.
|
|
277
|
+
- everfur_send_test_message sends a real message to a sandbox tenant only, and streams the real answer.
|
|
278
|
+
|
|
279
|
+
The tenant tools read the connected tenant's own state and change nothing:
|
|
280
|
+
- everfur_get_tenant_status: the plan and its limits, features, live or test mode, embed origins, the capabilities the
|
|
281
|
+
account holds, the publishable key's id and prefix, and the Televet return destination keys and booking lead.
|
|
282
|
+
- everfur_list_webhook_endpoints, everfur_list_webhook_deliveries and everfur_get_webhook_delivery: the webhook
|
|
283
|
+
endpoints and the delivery log with each delivery's attempts.
|
|
284
|
+
- everfur_list_events: the events Everfur emitted to the tenant.
|
|
285
|
+
- everfur_get_usage_summary and everfur_get_records_status_summary: aggregate chat usage and records request counts.
|
|
286
|
+
None of them returns a signing secret, a key value, or a member's conversation, message or record content.
|
|
287
|
+
everfur_search_docs searches the contract's error registry, capability packs and integration guide.
|
|
288
|
+
|
|
289
|
+
Every tool returns structured content. A failed call carries error.code, error.retryable and error.next_action, which
|
|
290
|
+
are stable, while the message is prose that may change. TENANT_SURFACE_NOT_ENABLED means that surface is not switched
|
|
291
|
+
on for the connected tenant, a setting Everfur holds rather than a fault in the call. CONTRACT_UNAVAILABLE means the Everfur API did not serve the contract, and error.retryable says
|
|
292
|
+
whether a later call can succeed. CONTRACT_CREDENTIAL_UNUSABLE means the Everfur API refused the sign-in the call
|
|
293
|
+
carried, which reconnecting the connector renews. Reference material is served as resources (everfur://guide,
|
|
294
|
+
everfur://contract/errors, everfur://mcp/refusal-codes), and the prompts integrate-web, integrate-react-native and
|
|
295
|
+
diagnose-integration lay out an order to call the tools in.`;
|
|
296
|
+
|
|
297
|
+
// src/mcp/hosted/tenant-env.ts
|
|
298
|
+
var VERIFY_SESSIONS_PATH = "/mcp/verify-sessions";
|
|
299
|
+
function toolApiBaseUrl(context, fallback) {
|
|
300
|
+
const resolved = resolveEndpoint(null, { [BASE_URL_ENV_VAR]: context.apiBaseUrl });
|
|
301
|
+
return resolved.ok ? resolved.endpoint.baseUrl : fallback;
|
|
302
|
+
}
|
|
303
|
+
async function mintVerifySession(options) {
|
|
304
|
+
const userRef = options.userRef;
|
|
305
|
+
if (!userRef.startsWith(SYNTHETIC_USER_REF_PREFIX)) {
|
|
306
|
+
return {
|
|
307
|
+
sessionToken: null,
|
|
308
|
+
outcome: { kind: "failed", reason: "the hosted server mints a session for a synthetic everfur-verify- test user only", requestId: null, details: [] }
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const endpoint = resolveEndpoint(null, { [BASE_URL_ENV_VAR]: options.apiBaseUrl });
|
|
312
|
+
if (endpoint.ok === false) {
|
|
313
|
+
return {
|
|
314
|
+
sessionToken: null,
|
|
315
|
+
outcome: { kind: "failed", reason: "the hosted server has no usable Everfur API base URL to mint a verify session with", requestId: null, details: [...endpoint.errors] }
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const client = createApiClient({
|
|
319
|
+
transport: options.transport ?? fetchTransport,
|
|
320
|
+
clock: options.clock ?? systemClock,
|
|
321
|
+
endpoint: endpoint.endpoint,
|
|
322
|
+
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
|
|
323
|
+
});
|
|
324
|
+
const result = await client.request({
|
|
325
|
+
method: "POST",
|
|
326
|
+
path: VERIFY_SESSIONS_PATH,
|
|
327
|
+
credentials: { authorization: withPrefix(ephemeralCredential("editor access token", options.bearer), "Bearer ") },
|
|
328
|
+
body: { user_ref: userRef }
|
|
329
|
+
});
|
|
330
|
+
if (result.ok === false) {
|
|
331
|
+
const failure = result.kind === "http" ? { kind: "http", status: result.status, code: result.code } : { kind: result.kind, status: null, code: null };
|
|
332
|
+
if (result.kind === "http" && result.status === 403 && result.code === "sandbox_only") {
|
|
333
|
+
return {
|
|
334
|
+
sessionToken: null,
|
|
335
|
+
outcome: {
|
|
336
|
+
kind: "gated",
|
|
337
|
+
reason: "this tenant is live: the Everfur API mints verify sessions for sandbox tenants only",
|
|
338
|
+
requestId: result.requestId
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (result.kind === "http" && result.status === 401) {
|
|
343
|
+
options.onUnauthorized?.();
|
|
344
|
+
return {
|
|
345
|
+
sessionToken: null,
|
|
346
|
+
outcome: {
|
|
347
|
+
kind: "failed",
|
|
348
|
+
reason: "the Everfur API refused the sign-in this call carried when asked to mint a verify session",
|
|
349
|
+
requestId: result.requestId,
|
|
350
|
+
details: [REVOKED_AUTHORIZATION_REMEDY],
|
|
351
|
+
failure
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
sessionToken: null,
|
|
357
|
+
outcome: {
|
|
358
|
+
kind: "failed",
|
|
359
|
+
// Operator-owned facts only: the method, the path, the status and the wire CODE, never the wire text.
|
|
360
|
+
reason: describeFailure(result),
|
|
361
|
+
requestId: result.requestId,
|
|
362
|
+
details: result.kind === "http" && result.code !== null ? [`call everfur_explain_error with the code ${result.code} for the cause and the next action`] : [],
|
|
363
|
+
failure
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
const body = result.value;
|
|
368
|
+
const token = isJsonObject(body) ? body["session_token"] : void 0;
|
|
369
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
370
|
+
return {
|
|
371
|
+
sessionToken: null,
|
|
372
|
+
outcome: {
|
|
373
|
+
kind: "failed",
|
|
374
|
+
reason: `POST ${VERIFY_SESSIONS_PATH} answered ${result.status} without a session_token`,
|
|
375
|
+
requestId: result.requestId,
|
|
376
|
+
details: []
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const expiresAtSeconds = isJsonObject(body) && typeof body["expires_at"] === "number" ? body["expires_at"] : null;
|
|
381
|
+
const capabilities = isJsonObject(body) && Array.isArray(body["capabilities"]) ? body["capabilities"].filter((entry) => typeof entry === "string") : [];
|
|
382
|
+
return {
|
|
383
|
+
sessionToken: token,
|
|
384
|
+
outcome: {
|
|
385
|
+
kind: "minted",
|
|
386
|
+
userRef,
|
|
387
|
+
expiresAt: expiresAtSeconds === null ? "unreported" : new Date(expiresAtSeconds * 1e3).toISOString(),
|
|
388
|
+
capabilities,
|
|
389
|
+
requestId: result.requestId
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function synthesizeTenantEnv(options) {
|
|
394
|
+
const publishableKey = options.context.publishableKey;
|
|
395
|
+
return Object.freeze({
|
|
396
|
+
[BASE_URL_ENV_VAR]: toolApiBaseUrl(options.context, options.apiBaseUrl),
|
|
397
|
+
...publishableKey === null ? {} : { EVERFUR_PARTNER_KEY: publishableKey }
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
function createHostedMint(options) {
|
|
401
|
+
return () => mintVerifySession(options);
|
|
402
|
+
}
|
|
403
|
+
var TENANT_READS_PATH = "/mcp/tenant";
|
|
404
|
+
var ROUTE_SEGMENT = /^[A-Za-z0-9_-]{1,64}$/;
|
|
405
|
+
function createHostedTenantReads(options) {
|
|
406
|
+
const endpoint = resolveEndpoint(null, { [BASE_URL_ENV_VAR]: options.apiBaseUrl });
|
|
407
|
+
if (endpoint.ok === false) {
|
|
408
|
+
throw new Error(`the hosted tenant reads need a usable API base URL: ${endpoint.errors.join("; ")}`);
|
|
409
|
+
}
|
|
410
|
+
const client = createApiClient({
|
|
411
|
+
transport: options.transport ?? fetchTransport,
|
|
412
|
+
clock: options.clock ?? systemClock,
|
|
413
|
+
endpoint: endpoint.endpoint,
|
|
414
|
+
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
|
|
415
|
+
});
|
|
416
|
+
return (route, query = {}) => {
|
|
417
|
+
const segments = route.split("/");
|
|
418
|
+
if (!segments.every((segment) => ROUTE_SEGMENT.test(segment))) {
|
|
419
|
+
return Promise.reject(new Error("a tenant read named a route outside /mcp/tenant"));
|
|
420
|
+
}
|
|
421
|
+
const params = new URLSearchParams();
|
|
422
|
+
for (const [name, value] of Object.entries(query)) {
|
|
423
|
+
if (value !== void 0) params.set(name, String(value));
|
|
424
|
+
}
|
|
425
|
+
const search = params.toString();
|
|
426
|
+
const path = `${TENANT_READS_PATH}/${segments.join("/")}${search === "" ? "" : `?${search}`}`;
|
|
427
|
+
return client.request({
|
|
428
|
+
method: "GET",
|
|
429
|
+
path,
|
|
430
|
+
credentials: { authorization: withPrefix(ephemeralCredential("editor access token", options.bearer), "Bearer ") }
|
|
431
|
+
}).then((result) => {
|
|
432
|
+
if (result.ok === false && result.kind === "http" && result.status === 401) options.onUnauthorized?.();
|
|
433
|
+
return result;
|
|
434
|
+
});
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
var DEFAULT_MAX_CLIENTS = 1e4;
|
|
438
|
+
function bucketKey(client, bucket) {
|
|
439
|
+
return `${client}\0${bucket}`;
|
|
440
|
+
}
|
|
441
|
+
function createFailureBudget(options) {
|
|
442
|
+
const now = options.now ?? Date.now;
|
|
443
|
+
const maxClients = options.maxClients ?? DEFAULT_MAX_CLIENTS;
|
|
444
|
+
const clientLimit = options.clientLimit ?? options.limit;
|
|
445
|
+
const windows = /* @__PURE__ */ new Map();
|
|
446
|
+
function current(client, at) {
|
|
447
|
+
const entry = windows.get(client);
|
|
448
|
+
if (entry === void 0) return null;
|
|
449
|
+
if (at - entry.startedAt >= options.windowMs) {
|
|
450
|
+
windows.delete(client);
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
return entry;
|
|
454
|
+
}
|
|
455
|
+
function trim(at) {
|
|
456
|
+
if (windows.size < maxClients) return;
|
|
457
|
+
for (const [client, entry] of windows) {
|
|
458
|
+
if (at - entry.startedAt >= options.windowMs) windows.delete(client);
|
|
459
|
+
}
|
|
460
|
+
while (windows.size >= maxClients) {
|
|
461
|
+
const oldest = windows.keys().next().value;
|
|
462
|
+
if (oldest === void 0) break;
|
|
463
|
+
windows.delete(oldest);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function count(key, at) {
|
|
467
|
+
const entry = current(key, at);
|
|
468
|
+
if (entry !== null) {
|
|
469
|
+
entry.count += 1;
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
trim(at);
|
|
473
|
+
windows.set(key, { startedAt: at, count: 1 });
|
|
474
|
+
}
|
|
475
|
+
function scopes(client, bucket) {
|
|
476
|
+
return bucket === void 0 ? [[client, clientLimit]] : [[bucketKey(client, bucket), options.limit], [client, clientLimit]];
|
|
477
|
+
}
|
|
478
|
+
return Object.freeze({
|
|
479
|
+
exhausted(client, bucket) {
|
|
480
|
+
const at = now();
|
|
481
|
+
return scopes(client, bucket).some(([key, limit]) => {
|
|
482
|
+
const entry = current(key, at);
|
|
483
|
+
return entry !== null && entry.count >= limit;
|
|
484
|
+
});
|
|
485
|
+
},
|
|
486
|
+
record(client, bucket) {
|
|
487
|
+
const at = now();
|
|
488
|
+
if (bucket === void 0) {
|
|
489
|
+
count(client, at);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const key = bucketKey(client, bucket);
|
|
493
|
+
const spent = current(key, at);
|
|
494
|
+
const underLimit = spent === null || spent.count < options.limit;
|
|
495
|
+
count(key, at);
|
|
496
|
+
if (underLimit) count(client, at);
|
|
497
|
+
},
|
|
498
|
+
retryAfterSeconds(client, bucket) {
|
|
499
|
+
const at = now();
|
|
500
|
+
let seconds = 1;
|
|
501
|
+
for (const [key, limit] of scopes(client, bucket)) {
|
|
502
|
+
const entry = current(key, at);
|
|
503
|
+
if (entry === null || entry.count < limit) continue;
|
|
504
|
+
seconds = Math.max(seconds, Math.ceil((entry.startedAt + options.windowMs - at) / 1e3));
|
|
505
|
+
}
|
|
506
|
+
return seconds;
|
|
507
|
+
},
|
|
508
|
+
get size() {
|
|
509
|
+
return windows.size;
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
function clientIpOf(req, trustedHops) {
|
|
514
|
+
const socketAddress = req.socket.remoteAddress ?? "unknown";
|
|
515
|
+
if (trustedHops <= 0) return socketAddress;
|
|
516
|
+
const raw = req.headers["x-forwarded-for"];
|
|
517
|
+
const header = Array.isArray(raw) ? raw.join(",") : raw ?? "";
|
|
518
|
+
const entries = header.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
519
|
+
const candidate = entries[entries.length - trustedHops];
|
|
520
|
+
return candidate !== void 0 && isIP(candidate) !== 0 ? candidate : socketAddress;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/mcp/hosted/well-known.ts
|
|
524
|
+
var WELL_KNOWN_DOCUMENTS = Object.freeze(["oauth-authorization-server", "oauth-protected-resource"]);
|
|
525
|
+
var WELL_KNOWN_MAX_AGE_SECONDS = 300;
|
|
526
|
+
function stripTrailingSlashes2(value) {
|
|
527
|
+
let end = value.length;
|
|
528
|
+
while (end > 0 && value[end - 1] === "/") end -= 1;
|
|
529
|
+
return value.slice(0, end);
|
|
530
|
+
}
|
|
531
|
+
function wellKnownPaths(config) {
|
|
532
|
+
const paths = /* @__PURE__ */ new Map();
|
|
533
|
+
const publicPath = stripTrailingSlashes2(config.publicUrl.slice(config.publicOrigin.length));
|
|
534
|
+
for (const document of WELL_KNOWN_DOCUMENTS) {
|
|
535
|
+
paths.set(`/.well-known/${document}`, document);
|
|
536
|
+
paths.set(`/.well-known/${document}${config.mcpPath}`, document);
|
|
537
|
+
if (publicPath.length > 0) paths.set(`${publicPath}/.well-known/${document}`, document);
|
|
538
|
+
}
|
|
539
|
+
return paths;
|
|
540
|
+
}
|
|
541
|
+
function describes(document, body, options) {
|
|
542
|
+
if (!isJsonObject(body)) return "the document is not a JSON object";
|
|
543
|
+
if (document === "oauth-protected-resource") {
|
|
544
|
+
const resource = body["resource"];
|
|
545
|
+
if (typeof resource !== "string" || stripTrailingSlashes2(resource) !== options.publicUrl) {
|
|
546
|
+
return "the document names a resource other than this server";
|
|
547
|
+
}
|
|
548
|
+
const servers = body["authorization_servers"];
|
|
549
|
+
if (!Array.isArray(servers) || servers.length === 0 || !servers.every((entry) => typeof entry === "string")) {
|
|
550
|
+
return "the document names no authorization server";
|
|
551
|
+
}
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
const issuer = body["issuer"];
|
|
555
|
+
if (typeof issuer !== "string" || stripTrailingSlashes2(issuer) !== options.publicOrigin) {
|
|
556
|
+
return "the document names an issuer other than this origin";
|
|
557
|
+
}
|
|
558
|
+
for (const field of ["authorization_endpoint", "token_endpoint"]) {
|
|
559
|
+
if (typeof body[field] !== "string") return `the document carries no ${field}`;
|
|
560
|
+
}
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
function createWellKnownMirror(options) {
|
|
564
|
+
const endpoint = resolveEndpoint(null, { [BASE_URL_ENV_VAR]: options.apiBaseUrl });
|
|
565
|
+
if (endpoint.ok === false) throw new Error(`the metadata mirror needs a usable API base URL: ${endpoint.errors.join("; ")}`);
|
|
566
|
+
const client = createApiClient({
|
|
567
|
+
transport: options.transport ?? fetchTransport,
|
|
568
|
+
clock: options.clock ?? systemClock,
|
|
569
|
+
endpoint: endpoint.endpoint,
|
|
570
|
+
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
|
|
571
|
+
});
|
|
572
|
+
const now = options.now ?? Date.now;
|
|
573
|
+
const maxAgeMs = options.maxAgeMs ?? WELL_KNOWN_MAX_AGE_SECONDS * 1e3;
|
|
574
|
+
const cache = /* @__PURE__ */ new Map();
|
|
575
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
576
|
+
async function fetchDocument(document) {
|
|
577
|
+
const result = await client.request({ method: "GET", path: `/mcp/.well-known/${document}`, responseType: "text" });
|
|
578
|
+
if (result.ok === false) {
|
|
579
|
+
return { ok: false, status: 503, reason: `the Everfur API did not serve ${document}` };
|
|
580
|
+
}
|
|
581
|
+
const text = typeof result.value === "string" ? result.value : "";
|
|
582
|
+
let parsed;
|
|
583
|
+
try {
|
|
584
|
+
parsed = JSON.parse(text);
|
|
585
|
+
} catch {
|
|
586
|
+
return { ok: false, status: 503, reason: `the Everfur API answered ${document} with a body that is not JSON` };
|
|
587
|
+
}
|
|
588
|
+
const problem = describes(document, parsed, options);
|
|
589
|
+
if (problem !== null) return { ok: false, status: 503, reason: `${document}: ${problem}` };
|
|
590
|
+
cache.set(document, { body: text, fetchedAt: now() });
|
|
591
|
+
return { ok: true, body: text };
|
|
592
|
+
}
|
|
593
|
+
return Object.freeze({
|
|
594
|
+
async read(document) {
|
|
595
|
+
const cached = cache.get(document);
|
|
596
|
+
if (cached !== void 0 && now() - cached.fetchedAt < maxAgeMs) return { ok: true, body: cached.body };
|
|
597
|
+
const pending = inflight.get(document);
|
|
598
|
+
if (pending !== void 0) return pending;
|
|
599
|
+
const refresh = fetchDocument(document).then((read) => {
|
|
600
|
+
if (read.ok === false && cached !== void 0) return { ok: true, body: cached.body };
|
|
601
|
+
return read;
|
|
602
|
+
}).finally(() => {
|
|
603
|
+
inflight.delete(document);
|
|
604
|
+
});
|
|
605
|
+
inflight.set(document, refresh);
|
|
606
|
+
return refresh;
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// src/mcp/hosted/http.ts
|
|
612
|
+
var HEALTH_PATH = "/healthz";
|
|
613
|
+
var MAX_BODY_BYTES = 1024 * 1024;
|
|
614
|
+
var DRAIN_CEILING_BYTES = 16 * 1024 * 1024;
|
|
615
|
+
var RETRY_AFTER_SECONDS = 5;
|
|
616
|
+
var CORS_REQUEST_HEADERS = "authorization, content-type, accept, mcp-protocol-version";
|
|
617
|
+
var CORS_EXPOSED_HEADERS = "www-authenticate, retry-after";
|
|
618
|
+
var PREFLIGHT_MAX_AGE_SECONDS = 300;
|
|
619
|
+
var UNAUTHENTICATED_FAILURE_LIMIT = 20;
|
|
620
|
+
var UNAUTHENTICATED_FAILURE_ADDRESS_LIMIT = 200;
|
|
621
|
+
var UNAUTHENTICATED_FAILURE_WINDOW_MS = 6e4;
|
|
622
|
+
function createHostedFailureBudget(now) {
|
|
623
|
+
return createFailureBudget({
|
|
624
|
+
limit: UNAUTHENTICATED_FAILURE_LIMIT,
|
|
625
|
+
clientLimit: UNAUTHENTICATED_FAILURE_ADDRESS_LIMIT,
|
|
626
|
+
windowMs: UNAUTHENTICATED_FAILURE_WINDOW_MS,
|
|
627
|
+
...now === void 0 ? {} : { now }
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
function refusalFor(req, config, methods) {
|
|
631
|
+
if (!methods.includes(req.method ?? "")) return { status: 405, message: "Method not allowed.", headers: { allow: methods.join(", ") } };
|
|
632
|
+
const host = hostnameOf(req.headers.host);
|
|
633
|
+
if (host === null || !config.allowedHosts.has(host)) return { status: 421, message: "Host is not allowed.", headers: {} };
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
function allowedOriginOf(origin, config) {
|
|
637
|
+
if (origin === void 0) return null;
|
|
638
|
+
const normalized = normalizeOrigin(origin);
|
|
639
|
+
return normalized !== null && config.allowedOrigins.has(normalized) ? normalized : null;
|
|
640
|
+
}
|
|
641
|
+
function originAllowed(origin, config) {
|
|
642
|
+
return origin === void 0 || allowedOriginOf(origin, config) !== null;
|
|
643
|
+
}
|
|
644
|
+
function bearerOf(req) {
|
|
645
|
+
const header = req.headers.authorization;
|
|
646
|
+
if (header === void 0 || header.trim() === "") return { present: false };
|
|
647
|
+
const match = header.match(/^Bearer\s+(\S+)\s*$/i);
|
|
648
|
+
return { present: true, token: match?.[1] ?? null };
|
|
649
|
+
}
|
|
650
|
+
function readBody(req, res, limit) {
|
|
651
|
+
return new Promise((resolve) => {
|
|
652
|
+
const chunks = [];
|
|
653
|
+
let received = 0;
|
|
654
|
+
let settled = false;
|
|
655
|
+
const settle = (value) => {
|
|
656
|
+
if (settled) return;
|
|
657
|
+
settled = true;
|
|
658
|
+
resolve(value);
|
|
659
|
+
};
|
|
660
|
+
req.on("data", (chunk) => {
|
|
661
|
+
if (settled) return;
|
|
662
|
+
received += chunk.length;
|
|
663
|
+
if (received > limit) {
|
|
664
|
+
writeJson(res, 413, { error: "Request body is too large." }, { connection: "close" });
|
|
665
|
+
req.destroy();
|
|
666
|
+
settle(null);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
chunks.push(chunk);
|
|
670
|
+
});
|
|
671
|
+
req.on("end", () => settle(Buffer.concat(chunks).toString("utf8")));
|
|
672
|
+
req.on("error", () => settle(null));
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
function jsonRpcError(res, status, code, message) {
|
|
676
|
+
writeJson(res, status, { jsonrpc: "2.0", error: { code, message }, id: null });
|
|
677
|
+
}
|
|
678
|
+
function createHostedMcpRequestListener(options) {
|
|
679
|
+
const { config } = options;
|
|
680
|
+
const transport = options.transport ?? fetchTransport;
|
|
681
|
+
const clock = options.clock ?? systemClock;
|
|
682
|
+
const failureBudget = options.failureBudget ?? createHostedFailureBudget(options.now);
|
|
683
|
+
const verifier = options.verifier ?? createHostedTokenVerifier({
|
|
684
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
685
|
+
resource: config.publicUrl,
|
|
686
|
+
transport,
|
|
687
|
+
clock,
|
|
688
|
+
failureBudget,
|
|
689
|
+
...options.now === void 0 ? {} : { now: options.now }
|
|
690
|
+
});
|
|
691
|
+
const wellKnown = options.wellKnown ?? createWellKnownMirror({
|
|
692
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
693
|
+
publicUrl: config.publicUrl,
|
|
694
|
+
publicOrigin: config.publicOrigin,
|
|
695
|
+
transport,
|
|
696
|
+
clock,
|
|
697
|
+
...options.now === void 0 ? {} : { now: options.now }
|
|
698
|
+
});
|
|
699
|
+
const contractCache = options.contractCache ?? createHostedContractCache();
|
|
700
|
+
const metadataPaths = wellKnownPaths(config);
|
|
701
|
+
const metadataUrl = resourceMetadataUrl(config);
|
|
702
|
+
const unauthorized = (res, error) => {
|
|
703
|
+
const challenge = error === null ? `Bearer resource_metadata="${metadataUrl}", scope="${MCP_SCOPE}"` : `Bearer error="${error.errorCode}", error_description="${error.message}", resource_metadata="${metadataUrl}", scope="${MCP_SCOPE}"`;
|
|
704
|
+
const body = error === null ? { error: "invalid_token", error_description: "Authentication required." } : error.toResponseObject();
|
|
705
|
+
writeJson(res, 401, body, { "www-authenticate": challenge });
|
|
706
|
+
};
|
|
707
|
+
const unavailable2 = (res, error) => {
|
|
708
|
+
writeJson(res, 503, error.toResponseObject(), { "retry-after": String(RETRY_AFTER_SECONDS) });
|
|
709
|
+
};
|
|
710
|
+
const throttled = (res, error, clientIp, bucket) => {
|
|
711
|
+
writeJson(res, 429, error.toResponseObject(), { "retry-after": String(failureBudget.retryAfterSeconds(clientIp, bucket)) });
|
|
712
|
+
};
|
|
713
|
+
const contextFor = async (call) => {
|
|
714
|
+
const tenant = tenantContextOf(call.authInfo);
|
|
715
|
+
if (tenant === null || call.authInfo === void 0) {
|
|
716
|
+
throw new Error("a hosted tool call reached the tools without a verified tenant context");
|
|
717
|
+
}
|
|
718
|
+
const idempotencyKey = call.args["idempotency_key"];
|
|
719
|
+
const label = call.args["user_ref"];
|
|
720
|
+
const userRef = call.tool === "everfur_send_test_message" && typeof idempotencyKey === "string" && idempotencyKey.length > 0 ? sendTestUserRef(idempotencyKey) : hostedVerifyUserRef(typeof label === "string" ? label : void 0);
|
|
721
|
+
const bearer = call.authInfo.token;
|
|
722
|
+
const onUnauthorized = () => verifier.forget(bearer);
|
|
723
|
+
return {
|
|
724
|
+
env: synthesizeTenantEnv({ context: tenant, apiBaseUrl: config.apiBaseUrl }),
|
|
725
|
+
credentialSource: "hosted",
|
|
726
|
+
hostedMint: createHostedMint({ bearer, apiBaseUrl: config.apiBaseUrl, userRef, transport, clock, onUnauthorized }),
|
|
727
|
+
syntheticUserRef: userRef,
|
|
728
|
+
tenantReads: createHostedTenantReads({ bearer, apiBaseUrl: config.apiBaseUrl, transport, clock, onUnauthorized })
|
|
729
|
+
};
|
|
730
|
+
};
|
|
731
|
+
return async (req, res) => {
|
|
732
|
+
const url = new URL(req.url ?? "/", "http://mcp.invalid");
|
|
733
|
+
if (url.pathname === HEALTH_PATH) {
|
|
734
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
735
|
+
writeJson(res, 405, { error: "Method not allowed." }, { allow: "GET, HEAD" });
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
writeJson(res, 200, { status: "ok" });
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const document = metadataPaths.get(url.pathname);
|
|
742
|
+
if (document !== void 0) {
|
|
743
|
+
if (req.method === "OPTIONS") {
|
|
744
|
+
res.writeHead(204, {
|
|
745
|
+
"access-control-allow-origin": "*",
|
|
746
|
+
"access-control-allow-methods": "GET, OPTIONS",
|
|
747
|
+
"access-control-allow-headers": "mcp-protocol-version",
|
|
748
|
+
"access-control-max-age": String(WELL_KNOWN_MAX_AGE_SECONDS)
|
|
749
|
+
});
|
|
750
|
+
res.end();
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const refused2 = refusalFor(req, config, ["GET", "HEAD"]);
|
|
754
|
+
if (refused2 !== null) {
|
|
755
|
+
writeJson(res, refused2.status, { error: refused2.message }, refused2.headers);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
const read = await wellKnown.read(document);
|
|
759
|
+
if (read.ok === false) {
|
|
760
|
+
writeJson(res, read.status, { error: "temporarily_unavailable", error_description: read.reason }, { "retry-after": String(RETRY_AFTER_SECONDS) });
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
res.writeHead(200, {
|
|
764
|
+
"content-type": "application/json; charset=utf-8",
|
|
765
|
+
"cache-control": `public, max-age=${WELL_KNOWN_MAX_AGE_SECONDS}`,
|
|
766
|
+
"access-control-allow-origin": "*"
|
|
767
|
+
});
|
|
768
|
+
res.end(req.method === "HEAD" ? void 0 : read.body);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
if (url.pathname !== config.mcpPath) {
|
|
772
|
+
writeJson(res, 404, { error: "Not found." });
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const corsOrigin = allowedOriginOf(req.headers.origin, config);
|
|
776
|
+
if (req.method === "OPTIONS" && corsOrigin !== null) {
|
|
777
|
+
const host = hostnameOf(req.headers.host);
|
|
778
|
+
if (host === null || !config.allowedHosts.has(host)) {
|
|
779
|
+
writeJson(res, 421, { error: "Host is not allowed." });
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
res.writeHead(204, {
|
|
783
|
+
"access-control-allow-origin": corsOrigin,
|
|
784
|
+
"access-control-allow-methods": "POST",
|
|
785
|
+
"access-control-allow-headers": CORS_REQUEST_HEADERS,
|
|
786
|
+
"access-control-max-age": String(PREFLIGHT_MAX_AGE_SECONDS),
|
|
787
|
+
vary: "Origin"
|
|
788
|
+
});
|
|
789
|
+
res.end();
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const refused = refusalFor(req, config, ["POST"]);
|
|
793
|
+
if (refused !== null) {
|
|
794
|
+
writeJson(res, refused.status, { error: refused.message }, refused.headers);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
if (!originAllowed(req.headers.origin, config)) {
|
|
798
|
+
writeJson(res, 403, { error: "Origin is not allowed." });
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (corsOrigin !== null) {
|
|
802
|
+
res.setHeader("access-control-allow-origin", corsOrigin);
|
|
803
|
+
res.setHeader("access-control-expose-headers", CORS_EXPOSED_HEADERS);
|
|
804
|
+
res.setHeader("vary", "Origin");
|
|
805
|
+
}
|
|
806
|
+
const declared = Number(req.headers["content-length"] ?? "0");
|
|
807
|
+
if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
|
|
808
|
+
const drain = declared <= DRAIN_CEILING_BYTES;
|
|
809
|
+
writeJson(res, 413, { error: "Request body is too large." }, drain ? {} : { connection: "close" });
|
|
810
|
+
if (drain) req.resume();
|
|
811
|
+
else req.destroy();
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
const clientIp = clientIpOf(req, config.trustedProxyHops);
|
|
815
|
+
const bearer = bearerOf(req);
|
|
816
|
+
if (bearer.present === false) {
|
|
817
|
+
unauthorized(res, null);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const bucket = bearer.token === null ? void 0 : tokenBucketOf(bearer.token);
|
|
821
|
+
if (bearer.token === null || !isAccessTokenShaped(bearer.token)) {
|
|
822
|
+
failureBudget.record(clientIp, bucket);
|
|
823
|
+
unauthorized(res, new InvalidTokenError("The bearer is not an Everfur MCP access token."));
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
let authInfo;
|
|
827
|
+
try {
|
|
828
|
+
authInfo = await verifier.verifyAccessToken(bearer.token, { clientIp, ...bucket === void 0 ? {} : { bucket } });
|
|
829
|
+
} catch (error) {
|
|
830
|
+
if (error instanceof InvalidTokenError) {
|
|
831
|
+
failureBudget.record(clientIp, bucket);
|
|
832
|
+
unauthorized(res, error);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (error instanceof TooManyRequestsError) {
|
|
836
|
+
throttled(res, error, clientIp, bucket);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
unavailable2(res, error instanceof TemporarilyUnavailableError ? error : new TemporarilyUnavailableError("The Everfur API could not verify this access token."));
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
if (typeof authInfo.expiresAt !== "number" || authInfo.expiresAt * 1e3 <= (options.now ?? Date.now)()) {
|
|
843
|
+
failureBudget.record(clientIp, bucket);
|
|
844
|
+
unauthorized(res, new InvalidTokenError("The access token has expired."));
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
const text = await readBody(req, res, MAX_BODY_BYTES);
|
|
848
|
+
if (text === null) return;
|
|
849
|
+
let parsedBody;
|
|
850
|
+
try {
|
|
851
|
+
parsedBody = JSON.parse(text);
|
|
852
|
+
} catch {
|
|
853
|
+
jsonRpcError(res, 400, -32700, "Parse error: Invalid JSON");
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
const mcp = buildServer(
|
|
857
|
+
{ env: {}, credentialSource: "hosted" },
|
|
858
|
+
{
|
|
859
|
+
transport,
|
|
860
|
+
clock,
|
|
861
|
+
contract: createHostedContractProvider({
|
|
862
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
863
|
+
bearer: authInfo.token,
|
|
864
|
+
cache: contractCache,
|
|
865
|
+
transport,
|
|
866
|
+
clock,
|
|
867
|
+
onUnauthorized: () => verifier.forget(authInfo.token)
|
|
868
|
+
})
|
|
869
|
+
},
|
|
870
|
+
{ instructions: HOSTED_MCP_INSTRUCTIONS, contextFor, readsLocalFiles: false }
|
|
871
|
+
);
|
|
872
|
+
await serveStatelessMcp(Object.assign(req, { auth: authInfo }), res, mcp, parsedBody);
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
async function startHostedMcpServer(options) {
|
|
876
|
+
const server = createServer(createHostedMcpRequestListener(options));
|
|
877
|
+
await listen(server, options.config.port, options.config.host);
|
|
878
|
+
const address = server.address();
|
|
879
|
+
if (address === null || typeof address === "string") {
|
|
880
|
+
await close(server);
|
|
881
|
+
throw new Error("The hosted MCP server did not bind a TCP address.");
|
|
882
|
+
}
|
|
883
|
+
const hostname = address.family === "IPv6" ? `[${address.address}]` : address.address;
|
|
884
|
+
return {
|
|
885
|
+
url: new URL(`http://${hostname}:${address.port}${options.config.mcpPath}`),
|
|
886
|
+
close: () => {
|
|
887
|
+
server.closeAllConnections();
|
|
888
|
+
return close(server);
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
export { loadHostedMcpConfig, startHostedMcpServer };
|