@everfur/cli 0.2.1 → 0.4.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 +104 -50
- package/dist/{chunk-4UB26D67.js → chunk-2MTX6YWO.js} +74 -30
- package/dist/{chunk-FC23PLWG.js → chunk-A2QEBO5U.js} +1 -1
- package/dist/{chunk-KQICUK54.js → chunk-EBVP6FAD.js} +5625 -1103
- package/dist/{chunk-XMIERMOW.js → chunk-QCXZJW5C.js} +663 -227
- package/dist/{chunk-SJUEHHU6.js → chunk-YRGSY4R2.js} +81 -14
- package/dist/everfur-mcp-hosted.js +4 -4
- package/dist/everfur-mcp-http.js +3 -3
- package/dist/everfur-mcp.js +2 -2
- package/dist/everfur.js +2 -2
- package/dist/index.d.ts +420 -113
- package/dist/index.js +2 -2
- package/package.json +4 -3
- package/schema/error-explanation.v1.schema.json +26 -4
- package/schema/plan.v1.schema.json +49 -4
- package/schema/verification.v1.schema.json +5 -4
- package/dist/everfur-mcp-container.d.ts +0 -2
- package/dist/everfur-mcp-container.js +0 -60
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { hostnameOf, listen, close, writeJson, serveStatelessMcp } from './chunk-
|
|
3
|
-
import { createHostedTokenVerifier, tokenBucketOf, isAccessTokenShaped, buildServer, MCP_SCOPE, tenantContextOf, sendTestUserRef, hostedVerifyUserRef, REVOKED_AUTHORIZATION_REMEDY, SYNTHETIC_USER_REF_PREFIX } from './chunk-
|
|
4
|
-
import { BASE_URL_ENV_VAR, resolveEndpoint, fetchTransport, systemClock, createApiClient, withPrefix, ephemeralCredential, validateContractText, isJsonObject, describeFailure } from './chunk-
|
|
2
|
+
import { hostnameOf, listen, close, writeJson, serveStatelessMcp } from './chunk-A2QEBO5U.js';
|
|
3
|
+
import { createHostedTokenVerifier, tokenBucketOf, isAccessTokenShaped, buildServer, MCP_SCOPE, tenantContextOf, sendTestUserRef, hostedVerifyUserRef, REVOKED_AUTHORIZATION_REMEDY, SYNTHETIC_USER_REF_PREFIX } from './chunk-QCXZJW5C.js';
|
|
4
|
+
import { BASE_URL_ENV_VAR, resolveEndpoint, fetchTransport, systemClock, createApiClient, withPrefix, ephemeralCredential, validateContractText, isJsonObject, describeFailure } from './chunk-EBVP6FAD.js';
|
|
5
5
|
import { createServer } from 'http';
|
|
6
6
|
import { InvalidTokenError, TooManyRequestsError, TemporarilyUnavailableError } from '@modelcontextprotocol/sdk/server/auth/errors.js';
|
|
7
7
|
import { isIP } from 'net';
|
|
@@ -12,8 +12,9 @@ var PORT_ENV_VAR = "EVERFUR_MCP_HTTP_PORT";
|
|
|
12
12
|
var LEGACY_PORT_ENV_VAR = "MCP_PORT";
|
|
13
13
|
var ALLOWED_HOSTS_ENV_VAR = "MCP_ALLOWED_HOSTS";
|
|
14
14
|
var ALLOWED_ORIGINS_ENV_VAR = "MCP_ALLOWED_ORIGINS";
|
|
15
|
-
var DEFAULT_ALLOWED_ORIGINS = Object.freeze(["https://claude.ai", "https://claude.com"]);
|
|
15
|
+
var DEFAULT_ALLOWED_ORIGINS = Object.freeze(["https://claude.ai", "https://claude.com", "https://chatgpt.com"]);
|
|
16
16
|
var TRUSTED_PROXY_HOPS_ENV_VAR = "MCP_TRUSTED_PROXY_HOPS";
|
|
17
|
+
var OPENAI_APPS_CHALLENGE_TOKEN_ENV_VAR = "OPENAI_APPS_CHALLENGE_TOKEN";
|
|
17
18
|
var MAX_TRUSTED_PROXY_HOPS = 8;
|
|
18
19
|
var DEFAULT_HOSTED_PORT = 3100;
|
|
19
20
|
var DEFAULT_MCP_PATH = "/mcp";
|
|
@@ -106,6 +107,14 @@ function parseAllowedOrigins(raw) {
|
|
|
106
107
|
return origin;
|
|
107
108
|
});
|
|
108
109
|
}
|
|
110
|
+
function parseOpenAiAppsChallengeToken(raw) {
|
|
111
|
+
const value = (raw ?? "").trim();
|
|
112
|
+
if (value.length === 0) return null;
|
|
113
|
+
if (!/^[\x21-\x7e]+$/.test(value)) {
|
|
114
|
+
throw new Error(`${OPENAI_APPS_CHALLENGE_TOKEN_ENV_VAR} must be one token of printable ASCII, with no space or line break inside it.`);
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
109
118
|
function loadHostedMcpConfig(env) {
|
|
110
119
|
if ((env[BASE_URL_ENV_VAR] ?? "").trim().length === 0) {
|
|
111
120
|
throw new Error(`${BASE_URL_ENV_VAR} is required: the Everfur API this server verifies bearers against.`);
|
|
@@ -126,7 +135,8 @@ function loadHostedMcpConfig(env) {
|
|
|
126
135
|
port: parsePort(env),
|
|
127
136
|
allowedHosts: /* @__PURE__ */ new Set([publicHostname, ...parseAllowedHosts(env[ALLOWED_HOSTS_ENV_VAR])]),
|
|
128
137
|
trustedProxyHops: parseTrustedProxyHops(env[TRUSTED_PROXY_HOPS_ENV_VAR]),
|
|
129
|
-
allowedOrigins: /* @__PURE__ */ new Set([...DEFAULT_ALLOWED_ORIGINS, ...parseAllowedOrigins(env[ALLOWED_ORIGINS_ENV_VAR])])
|
|
138
|
+
allowedOrigins: /* @__PURE__ */ new Set([...DEFAULT_ALLOWED_ORIGINS, ...parseAllowedOrigins(env[ALLOWED_ORIGINS_ENV_VAR])]),
|
|
139
|
+
openaiAppsChallengeToken: parseOpenAiAppsChallengeToken(env[OPENAI_APPS_CHALLENGE_TOKEN_ENV_VAR])
|
|
130
140
|
});
|
|
131
141
|
}
|
|
132
142
|
function resourceMetadataUrl(config) {
|
|
@@ -265,15 +275,35 @@ The integration tools:
|
|
|
265
275
|
- everfur_plan_integration produces an integration plan, or a refusal that names each missing fact (identity mode,
|
|
266
276
|
capabilities, backend language, environment) rather than a plan built on a guess. An integration built on a guessed
|
|
267
277
|
identity mode is wrong in a way that only shows up in production. Omitted, the environment is the connected tenant's.
|
|
278
|
+
A plan comes back as a small summary with a plan_id and a number of pages: the goal, the detected facts, each step's
|
|
279
|
+
id, title, one-line intent and page, and the checklist. The steps in full, with their commands and the complete file
|
|
280
|
+
bodies, come from further everfur_plan_integration calls with the same arguments plus plan_id and page, for page 1
|
|
281
|
+
to pages; page 1 also carries the detection, the warnings and the checklist in full, and the principles. (With
|
|
282
|
+
steps, step ids, instead of page, those steps come a page at a time and remaining_steps names the rest.) Each call
|
|
283
|
+
recomputes the plan from the project_details it is given, and the plan's own commands and files change the project,
|
|
284
|
+
so every page is fetched before any command runs or any file is written. The same arguments
|
|
285
|
+
and contract always produce the same plan, byte for byte. A refusal for missing facts gives each fact the developer
|
|
286
|
+
answers a fixed question and its closed list of options, so the same refusal always asks the same thing. A ready
|
|
287
|
+
plan carries a checklist: the finished integration as a fixed list of requirements, each with the check that proves
|
|
288
|
+
it. The theme is matched to the host app's own brand colours, read from its code (a Tailwind config, the CSS that
|
|
289
|
+
declares its theme, or a React Native theme file) and passed as theme_source; when none is given, the plan's
|
|
290
|
+
match-theme step names where those colours are looked for, and no colour is chosen for the host. The tenant's console
|
|
291
|
+
branding is the other side of the theme: it fills every field the theme leaves unset and wins for a field the tenant
|
|
292
|
+
locked in the partner console.
|
|
268
293
|
- 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
|
|
270
|
-
|
|
294
|
+
test user of a sandbox tenant (its entitlements report livemode false). On a live tenant with a paired sandbox the
|
|
295
|
+
session is minted on that sandbox and the call uses its publishable key; on a live tenant with no pair the signed
|
|
296
|
+
session step is reported GATED; no session can be supplied by hand. identity_mode anonymous verifies the anonymous path instead: the
|
|
271
297
|
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.
|
|
298
|
+
read-only check a live tenant takes, GET requests only. For a web page pass origin with embedding beside it: an
|
|
299
|
+
in-page page calls the API from its own origin, so the CORS checks apply to it, and with embedding in-page the
|
|
300
|
+
frame's own checks (the embed fence and the loader) are reported N/A.
|
|
273
301
|
- everfur_diagnose separates the four byte-identical causes of an empty entitlement verdict.
|
|
274
302
|
- everfur_explain_error looks an error code up in the contract's registry, case sensitive, and refuses a code the
|
|
275
303
|
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.
|
|
304
|
+
four different HTTP statuses, so the registry, not the shape of a code's string, says whether a retry can help. It
|
|
305
|
+
also answers, offline, the error.code the tools on this server return (VERIFICATION_FAILED, PLAN_CHANGED and the
|
|
306
|
+
rest).
|
|
277
307
|
- everfur_send_test_message sends a real message to a sandbox tenant only, and streams the real answer.
|
|
278
308
|
|
|
279
309
|
The tenant tools read the connected tenant's own state and change nothing:
|
|
@@ -296,6 +326,13 @@ diagnose-integration lay out an order to call the tools in.`;
|
|
|
296
326
|
|
|
297
327
|
// src/mcp/hosted/tenant-env.ts
|
|
298
328
|
var VERIFY_SESSIONS_PATH = "/mcp/verify-sessions";
|
|
329
|
+
var MINTING_TOOLS = /* @__PURE__ */ new Set(["everfur_verify_integration", "everfur_send_test_message"]);
|
|
330
|
+
function hostedCallMode(context, tool, args) {
|
|
331
|
+
if (!context.tenant.livemode || context.test === null) return "connected";
|
|
332
|
+
if (!MINTING_TOOLS.has(tool)) return "connected";
|
|
333
|
+
if (tool === "everfur_verify_integration" && args["identity_mode"] === "anonymous") return "connected";
|
|
334
|
+
return "test";
|
|
335
|
+
}
|
|
299
336
|
function toolApiBaseUrl(context, fallback) {
|
|
300
337
|
const resolved = resolveEndpoint(null, { [BASE_URL_ENV_VAR]: context.apiBaseUrl });
|
|
301
338
|
return resolved.ok ? resolved.endpoint.baseUrl : fallback;
|
|
@@ -334,7 +371,9 @@ async function mintVerifySession(options) {
|
|
|
334
371
|
sessionToken: null,
|
|
335
372
|
outcome: {
|
|
336
373
|
kind: "gated",
|
|
337
|
-
|
|
374
|
+
// The live-tenant wording applies to an UNPAIRED live tenant only: a paired one is minted on its sandbox, and is
|
|
375
|
+
// refused only when that sandbox is no longer an active partner sandbox (EFBackend #1629).
|
|
376
|
+
reason: options.paired === true ? "this tenant is live and its paired sandbox is no longer an active sandbox: the Everfur API mints verify sessions on a sandbox tenant only" : "this tenant is live: the Everfur API mints verify sessions for sandbox tenants only",
|
|
338
377
|
requestId: result.requestId
|
|
339
378
|
}
|
|
340
379
|
};
|
|
@@ -391,7 +430,8 @@ async function mintVerifySession(options) {
|
|
|
391
430
|
};
|
|
392
431
|
}
|
|
393
432
|
function synthesizeTenantEnv(options) {
|
|
394
|
-
const
|
|
433
|
+
const test = options.mode === "test" ? options.context.test : null;
|
|
434
|
+
const publishableKey = test === null ? options.context.publishableKey : test.publishableKey;
|
|
395
435
|
return Object.freeze({
|
|
396
436
|
[BASE_URL_ENV_VAR]: toolApiBaseUrl(options.context, options.apiBaseUrl),
|
|
397
437
|
...publishableKey === null ? {} : { EVERFUR_PARTNER_KEY: publishableKey }
|
|
@@ -610,6 +650,7 @@ function createWellKnownMirror(options) {
|
|
|
610
650
|
|
|
611
651
|
// src/mcp/hosted/http.ts
|
|
612
652
|
var HEALTH_PATH = "/healthz";
|
|
653
|
+
var OPENAI_APPS_CHALLENGE_PATH = "/.well-known/openai-apps-challenge";
|
|
613
654
|
var MAX_BODY_BYTES = 1024 * 1024;
|
|
614
655
|
var DRAIN_CEILING_BYTES = 16 * 1024 * 1024;
|
|
615
656
|
var RETRY_AFTER_SECONDS = 5;
|
|
@@ -720,12 +761,23 @@ function createHostedMcpRequestListener(options) {
|
|
|
720
761
|
const userRef = call.tool === "everfur_send_test_message" && typeof idempotencyKey === "string" && idempotencyKey.length > 0 ? sendTestUserRef(idempotencyKey) : hostedVerifyUserRef(typeof label === "string" ? label : void 0);
|
|
721
762
|
const bearer = call.authInfo.token;
|
|
722
763
|
const onUnauthorized = () => verifier.forget(bearer);
|
|
764
|
+
const mode = hostedCallMode(tenant, call.tool, call.args);
|
|
765
|
+
const paired = tenant.tenant.livemode && tenant.test !== null;
|
|
723
766
|
return {
|
|
724
|
-
env: synthesizeTenantEnv({ context: tenant, apiBaseUrl: config.apiBaseUrl }),
|
|
767
|
+
env: synthesizeTenantEnv({ context: tenant, apiBaseUrl: config.apiBaseUrl, mode }),
|
|
725
768
|
credentialSource: "hosted",
|
|
726
|
-
hostedMint: createHostedMint({ bearer, apiBaseUrl: config.apiBaseUrl, userRef, transport, clock, onUnauthorized }),
|
|
769
|
+
hostedMint: createHostedMint({ bearer, apiBaseUrl: config.apiBaseUrl, userRef, transport, clock, onUnauthorized, paired }),
|
|
727
770
|
syntheticUserRef: userRef,
|
|
728
|
-
tenantReads: createHostedTenantReads({ bearer, apiBaseUrl: config.apiBaseUrl, transport, clock, onUnauthorized })
|
|
771
|
+
tenantReads: createHostedTenantReads({ bearer, apiBaseUrl: config.apiBaseUrl, transport, clock, onUnauthorized }),
|
|
772
|
+
tenantMode: { livemode: tenant.tenant.livemode, paired },
|
|
773
|
+
connectedTenant: {
|
|
774
|
+
slug: tenant.tenant.slug,
|
|
775
|
+
livemode: tenant.tenant.livemode,
|
|
776
|
+
pairedSandboxSlug: tenant.test === null ? null : tenant.test.slug,
|
|
777
|
+
// The prefix alone, which names the tenant mode; the key itself never leaves the context.
|
|
778
|
+
publishableKeyPrefix: ["pk_test_", "pk_live_"].find((prefix) => tenant.publishableKey?.startsWith(prefix) === true) ?? null,
|
|
779
|
+
apiBaseUrl: tenant.apiBaseUrl
|
|
780
|
+
}
|
|
729
781
|
};
|
|
730
782
|
};
|
|
731
783
|
return async (req, res) => {
|
|
@@ -738,6 +790,21 @@ function createHostedMcpRequestListener(options) {
|
|
|
738
790
|
writeJson(res, 200, { status: "ok" });
|
|
739
791
|
return;
|
|
740
792
|
}
|
|
793
|
+
const challengeToken = config.openaiAppsChallengeToken;
|
|
794
|
+
if (challengeToken !== null && url.pathname === OPENAI_APPS_CHALLENGE_PATH) {
|
|
795
|
+
const refused2 = refusalFor(req, config, ["GET", "HEAD"]);
|
|
796
|
+
if (refused2 !== null) {
|
|
797
|
+
writeJson(res, refused2.status, { error: refused2.message }, refused2.headers);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
res.writeHead(200, {
|
|
801
|
+
"content-type": "text/plain; charset=utf-8",
|
|
802
|
+
"content-length": String(Buffer.byteLength(challengeToken)),
|
|
803
|
+
"cache-control": "no-store"
|
|
804
|
+
});
|
|
805
|
+
res.end(req.method === "HEAD" ? void 0 : challengeToken);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
741
808
|
const document = metadataPaths.get(url.pathname);
|
|
742
809
|
if (document !== void 0) {
|
|
743
810
|
if (req.method === "OPTIONS") {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { loadHostedMcpConfig, startHostedMcpServer } from './chunk-
|
|
3
|
-
import './chunk-
|
|
4
|
-
import './chunk-
|
|
5
|
-
import { CLI_VERSION } from './chunk-
|
|
2
|
+
import { loadHostedMcpConfig, startHostedMcpServer } from './chunk-YRGSY4R2.js';
|
|
3
|
+
import './chunk-A2QEBO5U.js';
|
|
4
|
+
import './chunk-QCXZJW5C.js';
|
|
5
|
+
import { CLI_VERSION } from './chunk-EBVP6FAD.js';
|
|
6
6
|
|
|
7
7
|
// src/bin/everfur-mcp-hosted.ts
|
|
8
8
|
async function main() {
|
package/dist/everfur-mcp-http.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { startRemoteMcpServer, loadRemoteMcpConfig } from './chunk-
|
|
3
|
-
import './chunk-
|
|
4
|
-
import { CLI_VERSION } from './chunk-
|
|
2
|
+
import { startRemoteMcpServer, loadRemoteMcpConfig } from './chunk-A2QEBO5U.js';
|
|
3
|
+
import './chunk-QCXZJW5C.js';
|
|
4
|
+
import { CLI_VERSION } from './chunk-EBVP6FAD.js';
|
|
5
5
|
|
|
6
6
|
// src/bin/everfur-mcp-http.ts
|
|
7
7
|
async function main() {
|
package/dist/everfur-mcp.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { runStdioServer } from './chunk-
|
|
3
|
-
import { CLI_VERSION } from './chunk-
|
|
2
|
+
import { runStdioServer } from './chunk-QCXZJW5C.js';
|
|
3
|
+
import { CLI_VERSION } from './chunk-EBVP6FAD.js';
|
|
4
4
|
|
|
5
5
|
// src/bin/everfur-mcp.ts
|
|
6
6
|
if (process.argv.slice(2).includes("--version")) {
|
package/dist/everfur.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { runCli } from './chunk-
|
|
3
|
-
import { ExitCode, systemClock, fetchTransport, CLI_VERSION, nodeFileSystem, nodeContractCache } from './chunk-
|
|
2
|
+
import { runCli } from './chunk-2MTX6YWO.js';
|
|
3
|
+
import { ExitCode, systemClock, fetchTransport, CLI_VERSION, nodeFileSystem, nodeContractCache } from './chunk-EBVP6FAD.js';
|
|
4
4
|
|
|
5
5
|
// src/bin/everfur.ts
|
|
6
6
|
async function main() {
|