@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,139 +1,226 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { ENVIRONMENTS, SURFACES, VERIFY_IDENTITY_MODES, fetchContract, NO_CONTRACT_CACHE, systemClock, fetchTransport, describeFailure, loadDecisionSpaceFromContract, uuidFromKey, resolveEndpoint, createApiClient, createContractProvider, nodeContractCache, CLI_VERSION, withPrefix, ephemeralCredential, isJsonObject, error_explanation_v1_schema_default, verification_v1_schema_default, diagnosis_v1_schema_default, plan_v1_schema_default, IDENTITY_QUESTION, IDENTITY_TREE, CAPABILITY_QUESTION, CAPABILITIES_TREE, WEB_EMBEDDING_QUESTION, PLATFORM_TREE, loadDecisionSpace, nodeFileSystem, expandCapabilities, detectThemeFromText, planIntegration, parseOrigin, parseLoaderUrl, isEverfurLoaderHost, EVERFUR_LOADER_HOSTS, resolveCredential, PARTNER_KEY, runVerification,
|
|
2
|
+
import { ENVIRONMENTS, ENVELOPE_SHAPE, SURFACES, VERIFY_IDENTITY_MODES, refusal, API_BASE_URL_BY_ENVIRONMENT, fetchContract, NO_CONTRACT_CACHE, systemClock, fetchTransport, describeFailure, loadDecisionSpaceFromContract, uuidFromKey, resolveEndpoint, createApiClient, createContractProvider, nodeContractCache, CLI_VERSION, withPrefix, ephemeralCredential, isJsonObject, REFUSAL_POLICY, error_explanation_v1_schema_default, verification_v1_schema_default, diagnosis_v1_schema_default, plan_v1_schema_default, NEXT_ACTIONS, IDENTITY_QUESTION, IDENTITY_TREE, CAPABILITY_QUESTION, CAPABILITIES_TREE, WEB_EMBEDDING_QUESTION, PLATFORM_TREE, loadDecisionSpace, nodeFileSystem, expandCapabilities, detectThemeFromText, planIntegration, parseOrigin, parseLoaderUrl, isEverfurLoaderHost, EVERFUR_LOADER_HOSTS, resolveCredential, PARTNER_KEY, runVerification, themePropFromSource, normalizeSurfaces, failedResult, buildSupportBundle, chooseWidgetAuth, DIAGNOSTICS_PATH, diagnoseFailure, parseDiagnostics, buildDiagnosisDocument, toolCodeExplanation, resolveOffline, resolveWithoutRegistry, knownCodes, loadErrorRegistryFromContract, loadErrorRegistry, resolveAfterRegistryMiss, TOOL_CODES, registryAlsoIn, buildExplanationDocument, nextActionFor, digestOfProjection, detectProject, BASE_URL_ENV_VAR, checkTenantMode, isWorkspaceRootEvidence, plannableCapabilities, blockedCapabilities, offlineDocument, toolCodeMeaning, SESSION_TOKEN, SESSION_MINT_TREE, BACKEND_LANGUAGE_QUESTION, ALL_CAPABILITIES } from './chunk-EBVP6FAD.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
5
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
-
import { join } from 'path';
|
|
6
|
+
import { isAbsolute, join, dirname, basename } from 'path';
|
|
7
7
|
import { createHash } from 'crypto';
|
|
8
8
|
import { InvalidTokenError, TooManyRequestsError, TemporarilyUnavailableError } from '@modelcontextprotocol/sdk/server/auth/errors.js';
|
|
9
9
|
import { completable } from '@modelcontextprotocol/sdk/server/completable.js';
|
|
10
10
|
|
|
11
|
-
var
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
DIAGNOSTICS_FAILED: { retryable: "from_failure", nextAction: "from_failure", docsPath: "everfur://contract/errors" },
|
|
46
|
-
DIAGNOSTICS_MALFORMED: { retryable: false, nextAction: "fix_environment", docsPath: null },
|
|
47
|
-
// the contract, fetched from GET /partners/contract (everfur_plan_integration, everfur_explain_error)
|
|
48
|
-
CONTRACT_CREDENTIAL_MISSING: { retryable: false, nextAction: "fix_environment", docsPath: null },
|
|
49
|
-
CONTRACT_CREDENTIAL_UNUSABLE: { retryable: false, nextAction: "fix_environment", docsPath: null },
|
|
50
|
-
CONTRACT_UNAVAILABLE: { retryable: "from_failure", nextAction: "from_failure", docsPath: null },
|
|
51
|
-
UNKNOWN_OPTION: { retryable: false, nextAction: "fix_arguments", docsPath: "everfur://guide" },
|
|
52
|
-
// everfur_explain_error
|
|
53
|
-
CONTRACT_UNUSABLE: { retryable: false, nextAction: "fix_arguments", docsPath: null },
|
|
54
|
-
NO_ERROR_REGISTRY: { retryable: false, nextAction: "fix_arguments", docsPath: null },
|
|
55
|
-
UNKNOWN_ERROR_CODE: { retryable: false, nextAction: "fix_arguments", docsPath: "everfur://contract/errors" },
|
|
56
|
-
// everfur_send_test_message
|
|
57
|
-
ENTITLEMENTS_UNREADABLE: { retryable: "from_failure", nextAction: "from_failure", docsPath: "everfur://contract/errors" },
|
|
58
|
-
NOT_A_SANDBOX_TENANT: { retryable: false, nextAction: "use_sandbox_tenant", docsPath: null },
|
|
59
|
-
// from the mint call's own status: a 429 (VerifyMintRateLimited) or 503 (VerifyMintUnavailable) is transient
|
|
60
|
-
SESSION_NOT_MINTED: { retryable: "from_failure", nextAction: "from_failure", docsPath: null },
|
|
61
|
-
PET_NOT_REGISTERED: { retryable: "from_failure", nextAction: "from_failure", docsPath: "everfur://contract/errors" },
|
|
62
|
-
CHAT_NOT_GRANTED: { retryable: false, nextAction: "call_diagnose", docsPath: null },
|
|
63
|
-
CONVERSATIONS_UNREADABLE: { retryable: "from_failure", nextAction: "from_failure", docsPath: "everfur://contract/errors" },
|
|
64
|
-
PRIOR_TURN_UNANSWERED: { retryable: true, nextAction: "wait_then_retry", docsPath: null },
|
|
65
|
-
CONVERSATION_NOT_CREATED: { retryable: "from_failure", nextAction: "from_failure", docsPath: "everfur://contract/errors" },
|
|
66
|
-
SEND_FAILED: { retryable: "from_failure", nextAction: "from_failure", docsPath: "everfur://contract/errors" },
|
|
67
|
-
DEADLINE_EXCEEDED: { retryable: true, nextAction: "retry", docsPath: null },
|
|
68
|
-
STREAM_FAILED: { retryable: true, nextAction: "retry", docsPath: null },
|
|
69
|
-
EMPTY_STREAM: { retryable: true, nextAction: "retry", docsPath: null },
|
|
70
|
-
// the hosted tenant read tools (everfur_get_tenant_status and the rest), over GET /mcp/tenant/*
|
|
71
|
-
// (docs/HOSTED_TENANT_API.md): the read failed on the wire, answered a body outside the documented shape, or
|
|
72
|
-
// reached a surface that is not switched on for the connected tenant (a setting Everfur holds)
|
|
73
|
-
TENANT_READ_FAILED: { retryable: "from_failure", nextAction: "from_failure", docsPath: "everfur://contract/errors" },
|
|
74
|
-
TENANT_READ_MALFORMED: { retryable: false, nextAction: "report_to_developer", docsPath: null },
|
|
75
|
-
TENANT_SURFACE_NOT_ENABLED: { retryable: false, nextAction: "report_to_developer", docsPath: null },
|
|
76
|
-
// hosted only: the connected tenant has no publishable key, or one of a shape no deployment issues. Its keys are
|
|
77
|
-
// Everfur's to issue and nothing a developer exports reaches them, so the refusal is reported, never "fixed" locally
|
|
78
|
-
TENANT_PUBLISHABLE_KEY_UNUSABLE: { retryable: false, nextAction: "report_to_developer", docsPath: null },
|
|
79
|
-
// any tool: a body threw instead of returning a document
|
|
80
|
-
INTERNAL_ERROR: { retryable: false, nextAction: "report_to_developer", docsPath: null }
|
|
81
|
-
});
|
|
82
|
-
function fromFailure(details) {
|
|
83
|
-
const kind = details["kind"];
|
|
84
|
-
if (kind === "network" || kind === "timeout") return { retryable: true, nextAction: "retry" };
|
|
85
|
-
const status = details["status"];
|
|
86
|
-
if (typeof status === "number") {
|
|
87
|
-
if (status === 429) return { retryable: true, nextAction: "wait_then_retry" };
|
|
88
|
-
if (status >= 500) return { retryable: true, nextAction: "retry" };
|
|
89
|
-
if (status === 401 || status === 403) return { retryable: false, nextAction: "fix_environment" };
|
|
90
|
-
}
|
|
91
|
-
if (typeof details["code"] === "string") return { retryable: false, nextAction: "call_explain_error" };
|
|
92
|
-
return { retryable: false, nextAction: "report_to_developer" };
|
|
93
|
-
}
|
|
94
|
-
function errorBody(code, message, details = {}) {
|
|
95
|
-
const policy = REFUSAL_POLICY[code];
|
|
96
|
-
const derived = fromFailure(details);
|
|
97
|
-
const requestId = details["request_id"];
|
|
11
|
+
var PLAN_STEPS_BUDGET_BYTES = 24 * 1024;
|
|
12
|
+
var SUMMARY_MAX_CHARS = 120;
|
|
13
|
+
var INTENT_MAX_CHARS = 72;
|
|
14
|
+
var PLAN_SUMMARY_KIND = "everfur.plan.summary";
|
|
15
|
+
var PLAN_SUMMARY_VERSION = 2;
|
|
16
|
+
var PLAN_ID_PATTERN = /^[0-9a-f]{16}$/;
|
|
17
|
+
var MAX_PAGES = 999;
|
|
18
|
+
var PLAN_STEPS_INPUT_SHAPE = {
|
|
19
|
+
plan_id: z.string().regex(PLAN_ID_PATTERN).optional().describe("The plan_id of an earlier summary. When given, the call refuses with PLAN_CHANGED if the plan these arguments now produce has other steps, other arguments or another contract than the one summarised, so steps are never served from a plan other than the one summarised."),
|
|
20
|
+
page: z.number().int().min(1).max(MAX_PAGES).optional().describe("A page of an earlier summary, 1 to its pages, with the same arguments that produced it. The call then returns that page instead of the summary: its steps in full (detail, commands, files and the complete body of each file), and on page 1 the detection, the warnings and the checklist in full and the principles. Not with steps."),
|
|
21
|
+
steps: z.array(z.string().min(1).max(128)).min(1).max(64).optional().describe("Step ids from an earlier summary, with the same arguments that produced it. The call then returns those steps in full (detail, commands, files and the complete body of each file) instead of the summary, a page at a time: remaining_steps names the rest. Not with page.")
|
|
22
|
+
};
|
|
23
|
+
function byteLength(text) {
|
|
24
|
+
return new TextEncoder().encode(text).byteLength;
|
|
25
|
+
}
|
|
26
|
+
function planIdOf(plan) {
|
|
27
|
+
const identity = { steps: plan.steps, supplied: plan.supplied, contract: plan.decisionSpace?.digest ?? null };
|
|
28
|
+
return digestOfProjection(JSON.parse(JSON.stringify(identity))).slice(0, 16);
|
|
29
|
+
}
|
|
30
|
+
var SENTENCE_END = /[.!?](?=\s+[^a-z\s]|\s*$)/;
|
|
31
|
+
var NAMES_A_SECRET = /SECRET_KEY/;
|
|
32
|
+
function oneLine(text, max = SUMMARY_MAX_CHARS) {
|
|
33
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
34
|
+
const end = flat.search(SENTENCE_END);
|
|
35
|
+
const sentence = end === -1 ? flat : flat.slice(0, end + 1);
|
|
36
|
+
if (sentence.length <= max || NAMES_A_SECRET.test(sentence)) return sentence;
|
|
37
|
+
const cut = sentence.slice(0, max - 1);
|
|
38
|
+
const space = cut.lastIndexOf(" ");
|
|
39
|
+
return `${(space > 0 ? cut.slice(0, space) : cut).replace(/[,;:(]$/, "")}\u2026`;
|
|
40
|
+
}
|
|
41
|
+
function principleByTitle(principle) {
|
|
42
|
+
return { treeId: principle.treeId, id: principle.id, title: principle.title, guide: `everfur://guide/${principle.treeId}` };
|
|
43
|
+
}
|
|
44
|
+
function preambleOf(plan) {
|
|
98
45
|
return {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
request_id: typeof requestId === "string" ? requestId : null,
|
|
104
|
-
docs_path: policy.docsPath,
|
|
105
|
-
details
|
|
46
|
+
detected: plan.detected,
|
|
47
|
+
warnings: plan.warnings,
|
|
48
|
+
principles: plan.principles.map(principleByTitle),
|
|
49
|
+
...plan.checklist === void 0 ? {} : { checklist: plan.checklist }
|
|
106
50
|
};
|
|
107
51
|
}
|
|
108
|
-
|
|
109
|
-
|
|
52
|
+
var PAGE_ENVELOPE_BYTES = byteLength(JSON.stringify({
|
|
53
|
+
ok: true,
|
|
54
|
+
environment_source: "connected_tenant",
|
|
55
|
+
plan_id: "0".repeat(16),
|
|
56
|
+
page: MAX_PAGES,
|
|
57
|
+
pages: MAX_PAGES,
|
|
58
|
+
next_page: MAX_PAGES,
|
|
59
|
+
steps: []
|
|
60
|
+
}));
|
|
61
|
+
function pagesOf(plan) {
|
|
62
|
+
const pages = [];
|
|
63
|
+
let page = [];
|
|
64
|
+
let bytes = PAGE_ENVELOPE_BYTES + byteLength(JSON.stringify(preambleOf(plan)));
|
|
65
|
+
for (const step of plan.steps) {
|
|
66
|
+
const size = byteLength(JSON.stringify(step)) + 1;
|
|
67
|
+
const pageHasContent = page.length > 0 || pages.length === 0;
|
|
68
|
+
if (pageHasContent && bytes + size > PLAN_STEPS_BUDGET_BYTES) {
|
|
69
|
+
pages.push(page);
|
|
70
|
+
page = [];
|
|
71
|
+
bytes = PAGE_ENVELOPE_BYTES;
|
|
72
|
+
}
|
|
73
|
+
page.push(step);
|
|
74
|
+
bytes += size;
|
|
75
|
+
}
|
|
76
|
+
pages.push(page);
|
|
77
|
+
return pages;
|
|
78
|
+
}
|
|
79
|
+
function detectedFacts(detected) {
|
|
80
|
+
const { theme } = detected;
|
|
110
81
|
return {
|
|
111
|
-
|
|
112
|
-
|
|
82
|
+
root: detected.root,
|
|
83
|
+
projectType: detected.projectType,
|
|
84
|
+
packageManager: detected.packageManager,
|
|
85
|
+
dependencies: Object.fromEntries(detected.dependencies.map((dependency) => [dependency.name, dependency.installed ?? dependency.declared])),
|
|
86
|
+
...detected.webEmbedding === void 0 ? {} : { webEmbedding: detected.webEmbedding },
|
|
87
|
+
...detected.webFramework === void 0 ? {} : { webFramework: detected.webFramework },
|
|
88
|
+
...theme === void 0 ? {} : { theme: theme === null ? null : { source: theme.source, confidence: theme.confidence, theme: theme.theme } }
|
|
113
89
|
};
|
|
114
90
|
}
|
|
115
|
-
function
|
|
116
|
-
const
|
|
91
|
+
function goalOf(plan, pages) {
|
|
92
|
+
const capabilities = plan.supplied.capabilities ?? [];
|
|
93
|
+
const what = capabilities.length === 0 ? "Everfur" : `Everfur ${capabilities.join(", ")}`;
|
|
94
|
+
const where = `this ${plan.detected.projectType} project`;
|
|
95
|
+
const environment = plan.supplied.environment ? ` on ${plan.supplied.environment}` : "";
|
|
96
|
+
const identity = plan.supplied.identityMode ? `, identity ${plan.supplied.identityMode}` : "";
|
|
97
|
+
const steps = plan.steps.length === 1 ? "1 step" : `${plan.steps.length} steps`;
|
|
98
|
+
return `Add ${what} to ${where}${environment}${identity}: ${steps} on ${pages === 1 ? "1 page" : `${pages} pages`}.`;
|
|
99
|
+
}
|
|
100
|
+
function summarizePlan(plan) {
|
|
101
|
+
const { steps, checklist, warnings } = plan;
|
|
102
|
+
const pages = pagesOf(plan);
|
|
103
|
+
const pageOf = new Map(pages.flatMap((page, index) => page.map((step) => [step.id, index + 1])));
|
|
104
|
+
return {
|
|
105
|
+
schemaVersion: plan.schemaVersion,
|
|
106
|
+
kind: PLAN_SUMMARY_KIND,
|
|
107
|
+
generatedBy: plan.generatedBy,
|
|
108
|
+
status: plan.status,
|
|
109
|
+
detected: detectedFacts(plan.detected),
|
|
110
|
+
supplied: plan.supplied,
|
|
111
|
+
missingFacts: plan.missingFacts,
|
|
112
|
+
blocked: plan.blocked,
|
|
113
|
+
...plan.platformBlock === void 0 ? {} : { platformBlock: plan.platformBlock },
|
|
114
|
+
decisionSpace: plan.decisionSpace,
|
|
115
|
+
summaryVersion: PLAN_SUMMARY_VERSION,
|
|
116
|
+
goal: goalOf(plan, pages.length),
|
|
117
|
+
// A warning is what an agent must not have to fetch, so each is here by its first sentence, which states it; page 1
|
|
118
|
+
// carries it whole.
|
|
119
|
+
warnings: warnings.map(({ code, message }) => ({ code, message: oneLine(message) })),
|
|
120
|
+
pages: pages.length,
|
|
121
|
+
steps: steps.map((step) => ({
|
|
122
|
+
id: step.id,
|
|
123
|
+
title: step.title,
|
|
124
|
+
intent: oneLine(step.detail, INTENT_MAX_CHARS),
|
|
125
|
+
page: pageOf.get(step.id) ?? 1
|
|
126
|
+
})),
|
|
127
|
+
// The definition of done by its requirements: what an agent is held to is never a thing to fetch later. Each
|
|
128
|
+
// item's check and the steps it covers are on page 1.
|
|
129
|
+
...checklist === void 0 ? {} : { checklist: checklist.map(({ id, requirement }) => ({ id, requirement })) }
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
var FETCH_STEPS = Object.freeze({
|
|
133
|
+
tool: "everfur_plan_integration",
|
|
134
|
+
with: "the same arguments plus plan_id and page, for page 1 to pages",
|
|
135
|
+
returns: "the steps in full, file bodies included; page 1 also detection, warnings and checklist in full, and the principles",
|
|
136
|
+
when: "every page before any command runs or any file is written, since the plan's own steps change the project"
|
|
137
|
+
});
|
|
138
|
+
function isPlanDocument(value) {
|
|
139
|
+
return typeof value === "object" && value !== null && Array.isArray(value.steps);
|
|
140
|
+
}
|
|
141
|
+
function summarizeRefusal(result) {
|
|
142
|
+
const error = result.document["error"];
|
|
143
|
+
const plan = error?.details?.["plan"];
|
|
144
|
+
if (error === void 0 || !isPlanDocument(plan)) return result;
|
|
117
145
|
return {
|
|
118
146
|
isError: true,
|
|
119
|
-
document: { ...result,
|
|
147
|
+
document: { ...result.document, error: { ...error, details: { ...error.details, plan: summarizePlan(plan) } } }
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function stepPage(plan, requested, envelope) {
|
|
151
|
+
const wanted = new Set(requested);
|
|
152
|
+
const selected = plan.steps.filter((step) => wanted.has(step.id));
|
|
153
|
+
const page = [];
|
|
154
|
+
let bytes = byteLength(JSON.stringify({ ...envelope, steps: [], remaining_steps: selected.map((step) => step.id) }));
|
|
155
|
+
for (const step of selected) {
|
|
156
|
+
const size = byteLength(JSON.stringify(step)) + 1;
|
|
157
|
+
if (page.length > 0 && bytes + size > PLAN_STEPS_BUDGET_BYTES) break;
|
|
158
|
+
page.push(step);
|
|
159
|
+
bytes += size;
|
|
160
|
+
}
|
|
161
|
+
return { page, remaining: selected.slice(page.length).map((step) => step.id) };
|
|
162
|
+
}
|
|
163
|
+
function numberedPage(plan, number, envelope) {
|
|
164
|
+
const pages = pagesOf(plan);
|
|
165
|
+
const steps = pages[number - 1];
|
|
166
|
+
if (steps === void 0) {
|
|
167
|
+
return refusal(
|
|
168
|
+
"UNKNOWN_PLAN_PAGE",
|
|
169
|
+
`This plan has ${pages.length === 1 ? "1 page" : `${pages.length} pages`}, so there is no page ${number}. Pages run from 1 to the summary's pages.`,
|
|
170
|
+
{ page: number, pages: pages.length, plan_id: envelope["plan_id"] }
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
isError: false,
|
|
175
|
+
document: {
|
|
176
|
+
...envelope,
|
|
177
|
+
page: number,
|
|
178
|
+
pages: pages.length,
|
|
179
|
+
...number < pages.length ? { next_page: number + 1 } : {},
|
|
180
|
+
...number === 1 ? preambleOf(plan) : {},
|
|
181
|
+
steps
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function planOnTheWire(result, request) {
|
|
186
|
+
if (result.isError) return summarizeRefusal(result);
|
|
187
|
+
const { plan, ...rest } = result.document;
|
|
188
|
+
if (!isPlanDocument(plan)) return result;
|
|
189
|
+
const planId = planIdOf(plan);
|
|
190
|
+
if (request.page !== void 0 && request.steps !== void 0) {
|
|
191
|
+
return refusal(
|
|
192
|
+
"UNKNOWN_PLAN_PAGE",
|
|
193
|
+
"page and steps were both given. Ask for a page of the summary with page, or for steps by id with steps, not both.",
|
|
194
|
+
{ page: request.page, steps: request.steps, plan_id: planId }
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (request.plan_id !== void 0 && request.plan_id !== planId) {
|
|
198
|
+
return refusal(
|
|
199
|
+
"PLAN_CHANGED",
|
|
200
|
+
`These arguments now produce plan ${planId}, not plan ${request.plan_id}. A plan_id stands for the plan's steps, the arguments it was asked with and the contract it was built from, so one of three things changed since the call that returned it: the arguments differ from that call, the project the plan reads changed in a way that changes a step (as a command run or a file written from the plan can), or the contract changed. The same call without plan_id, page and steps returns the summary of the current plan.`,
|
|
201
|
+
{ plan_id: request.plan_id, current_plan_id: planId }
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
const envelope = { ...rest, plan_id: planId };
|
|
205
|
+
if (request.page !== void 0) return numberedPage(plan, request.page, envelope);
|
|
206
|
+
if (request.steps === void 0) {
|
|
207
|
+
return { isError: false, document: { ...envelope, plan: summarizePlan(plan), fetch_steps: FETCH_STEPS } };
|
|
208
|
+
}
|
|
209
|
+
const known = new Set(plan.steps.map((step) => step.id));
|
|
210
|
+
const unknown = [...new Set(request.steps)].filter((id) => !known.has(id));
|
|
211
|
+
if (unknown.length > 0) {
|
|
212
|
+
return refusal(
|
|
213
|
+
"UNKNOWN_PLAN_STEP",
|
|
214
|
+
"One or more step ids are not steps of this plan. The ids it has are listed in details.steps.",
|
|
215
|
+
{ unknown, plan_id: planId, steps: plan.steps.map((step) => step.id) }
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const { page, remaining } = stepPage(plan, request.steps, envelope);
|
|
219
|
+
return {
|
|
220
|
+
isError: false,
|
|
221
|
+
document: { ...envelope, steps: page, ...remaining.length === 0 ? {} : { remaining_steps: remaining } }
|
|
120
222
|
};
|
|
121
223
|
}
|
|
122
|
-
var ERROR_BODY_SCHEMA = z.object({
|
|
123
|
-
code: z.enum(Object.keys(REFUSAL_POLICY)).describe("Stable refusal code. The message beside it is prose for the developer and may change; the code does not."),
|
|
124
|
-
message: z.string().describe("What went wrong, for the developer."),
|
|
125
|
-
retryable: z.boolean().describe("Whether the same call, unchanged, can succeed later."),
|
|
126
|
-
next_action: z.enum(NEXT_ACTIONS).describe("What to do next, from a closed vocabulary."),
|
|
127
|
-
request_id: z.string().nullable().describe("The request id of the Everfur call that failed, when there was one."),
|
|
128
|
-
docs_path: z.string().nullable().describe("An MCP resource of this server that explains the refusal, or null."),
|
|
129
|
-
details: z.object({}).passthrough().describe("The facts specific to this refusal.")
|
|
130
|
-
}).passthrough();
|
|
131
|
-
var ENVELOPE_SHAPE = {
|
|
132
|
-
ok: z.boolean().describe("True when the tool did what was asked."),
|
|
133
|
-
error: ERROR_BODY_SCHEMA.optional().describe("Present exactly when the result is an error."),
|
|
134
|
-
failed_precondition: z.string().optional().describe("DEPRECATED: mirrors error.code for 0.3.x callers."),
|
|
135
|
-
message: z.string().optional().describe("DEPRECATED: mirrors error.message for 0.3.x callers.")
|
|
136
|
-
};
|
|
137
224
|
|
|
138
225
|
// src/mcp/tools.ts
|
|
139
226
|
var DEFAULT_VERIFY_USER_REF = "everfur-mcp-verify";
|
|
@@ -230,7 +317,18 @@ function clientFor(context, ports = {}) {
|
|
|
230
317
|
})
|
|
231
318
|
};
|
|
232
319
|
}
|
|
233
|
-
|
|
320
|
+
var IDENTITY_MODE_MEANING = Object.freeze({
|
|
321
|
+
anonymous: '"anonymous" (no backend, scoped to the device)',
|
|
322
|
+
"partner-user-ref": '"partner-user-ref" (your backend mints a session)',
|
|
323
|
+
"shopify-app-proxy": '"shopify-app-proxy" (a Shopify storefront; Shopify vouches for the signed-in customer and no partner backend mints)'
|
|
324
|
+
});
|
|
325
|
+
function identityModeList(options) {
|
|
326
|
+
const modes = options ?? ["anonymous", "partner-user-ref", "shopify-app-proxy"];
|
|
327
|
+
const described = modes.map((mode) => IDENTITY_MODE_MEANING[mode] ?? `"${mode}"`);
|
|
328
|
+
if (described.length === 1) return described[0];
|
|
329
|
+
return `${described.slice(0, -1).join(", ")}${described.length > 2 ? "," : ""} or ${described[described.length - 1]}`;
|
|
330
|
+
}
|
|
331
|
+
function supplyOverMcp(fact, space, options = null, detection = null) {
|
|
234
332
|
switch (fact) {
|
|
235
333
|
case "decisionTrees":
|
|
236
334
|
return "pass `contract_path` (a contract-public projection), or set EVERFUR_CONTRACT_PUBLIC in the server environment, or unset both so the contract is fetched with EVERFUR_DOCS_KEY";
|
|
@@ -241,21 +339,55 @@ function supplyOverMcp(fact, space) {
|
|
|
241
339
|
return `${choices}. Which surfaces to build is the developer's choice, and nothing in the project decides it. The plan holds no credential, so it cannot know which of these the tenant is entitled to: everfur_verify_integration is the fail-closed check of what the tenant actually has`;
|
|
242
340
|
}
|
|
243
341
|
case "identityMode":
|
|
244
|
-
return
|
|
342
|
+
return "pass `identity_mode`: " + identityModeList(options) + ". " + (options === null ? "It is the developer's choice, and it changes the integration shape" : "These are the only modes this refusal accepts; the mode it refused cannot be built here");
|
|
245
343
|
case "sessionMintLanguage":
|
|
246
344
|
return "pass `session_mint_language`, the language of the backend that will mint sessions and receive Everfur's webhooks (node, python, go or other-language as the contract names them; node gets generated files)";
|
|
247
345
|
case "embedding":
|
|
248
346
|
return 'pass `embedding`: "in-page" (the SDK\'s React components in the page) or "frame" (the loader and the Everfur-hosted iframe). Detected from react-dom when omitted';
|
|
249
347
|
case "environment":
|
|
250
|
-
return 'pass `environment`: "
|
|
348
|
+
return 'pass `environment`: "test" (your sandbox tenant on https://api.everfur.com/api/v1, a pk_test_ key, livemode false), "staging" (your sandbox tenant on https://api-staging.everfur.com/api/v1, for early access) or "production" (your live tenant on https://api.everfur.com/api/v1, a pk_live_ key). It is the developer\'s choice; production is never assumed';
|
|
251
349
|
case "projectType":
|
|
252
|
-
|
|
350
|
+
if (detection !== null && isWorkspaceRootEvidence(detection.projectTypeEvidence)) {
|
|
351
|
+
return "describe the workspace package that holds the app, not the workspace root: pass its `project_root` when this is a local MCP, or its own `project_details` (with `workspace_root` for the root's lockfile) when this is a remote MCP; this fact is detected, not asserted";
|
|
352
|
+
}
|
|
253
353
|
return "pass `project_root` when this is a local MCP, or the manifest and file-marker-only `project_details` when this is a remote MCP; this fact is detected, not asserted";
|
|
354
|
+
case "packageManager":
|
|
355
|
+
return "pass `project_root` when this is a local MCP, or the manifest and file-marker-only `project_details` when this is a remote MCP (for a workspace package, with `workspace_root` naming the workspace root's lockfile or packageManager field); this fact is detected, not asserted";
|
|
254
356
|
default:
|
|
255
357
|
return "supply this fact as a tool argument of the same name";
|
|
256
358
|
}
|
|
257
359
|
}
|
|
258
|
-
var
|
|
360
|
+
var FIXED_QUESTIONS = Object.freeze({
|
|
361
|
+
capabilities: "Which Everfur surfaces should this integration build?",
|
|
362
|
+
identityMode: "How does your app identify the person using it?",
|
|
363
|
+
sessionMintLanguage: "What language is the backend that will mint Everfur sessions?",
|
|
364
|
+
environment: "Which Everfur environment should the generated code target?",
|
|
365
|
+
embedding: "Should the Everfur surface render inside your page, or in an Everfur-hosted frame?"
|
|
366
|
+
});
|
|
367
|
+
var QUESTION_ROUND_INSTRUCTION = "One question round: every question in missing_facts goes to the developer in one message, in the order given, worded as given and with its options; a question with required_when belongs in the same message and is used only for the answers it names. The answers are the developer's alone. The next everfur_plan_integration call carries the same arguments plus every answer.";
|
|
368
|
+
function questionOptions(fact, space) {
|
|
369
|
+
switch (fact) {
|
|
370
|
+
case "capabilities":
|
|
371
|
+
return space === null ? null : [...plannableCapabilities(space), ALL_CAPABILITIES];
|
|
372
|
+
case "identityMode":
|
|
373
|
+
return space === null ? null : space.choices(IDENTITY_TREE, IDENTITY_QUESTION);
|
|
374
|
+
case "sessionMintLanguage":
|
|
375
|
+
return space === null ? null : space.choices(SESSION_MINT_TREE, BACKEND_LANGUAGE_QUESTION);
|
|
376
|
+
case "environment":
|
|
377
|
+
return [...ENVIRONMENTS];
|
|
378
|
+
case "embedding":
|
|
379
|
+
return space === null ? ["in-page", "frame"] : space.choices(PLATFORM_TREE, WEB_EMBEDDING_QUESTION);
|
|
380
|
+
default:
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function fixedQuestion(fact, space, admitted = null) {
|
|
385
|
+
const question = FIXED_QUESTIONS[fact];
|
|
386
|
+
if (question === void 0) return null;
|
|
387
|
+
const options = admitted ?? questionOptions(fact, space);
|
|
388
|
+
return options === null || options.length === 0 ? { question } : { question, options: [...options] };
|
|
389
|
+
}
|
|
390
|
+
var REMOTE_PROJECT_MARKERS = [
|
|
259
391
|
"app.json",
|
|
260
392
|
"app.config.js",
|
|
261
393
|
"app.config.ts",
|
|
@@ -270,13 +402,39 @@ var REMOTE_PROJECT_MARKER = z.enum([
|
|
|
270
402
|
"src/app/",
|
|
271
403
|
"pages/",
|
|
272
404
|
"src/pages/",
|
|
405
|
+
// A server directory, and a tsconfig in it the plan must never write over (hosted MCP web integration test, 2026-10-04).
|
|
406
|
+
"server/",
|
|
407
|
+
"server/tsconfig.json",
|
|
408
|
+
// An env template the app already has, which the plan adds to and never replaces (hosted MCP mobile test, 2026-10-04).
|
|
409
|
+
".env.example",
|
|
273
410
|
"package-lock.json",
|
|
274
411
|
"npm-shrinkwrap.json",
|
|
275
412
|
"pnpm-lock.yaml",
|
|
276
413
|
"yarn.lock",
|
|
277
414
|
"bun.lockb",
|
|
278
415
|
"bun.lock"
|
|
279
|
-
]
|
|
416
|
+
];
|
|
417
|
+
function normalizeRemoteMarker(value) {
|
|
418
|
+
if (typeof value !== "string") return value;
|
|
419
|
+
const bare = value.trim().replace(/\\/g, "/").replace(/\/{2,}/g, "/").replace(/^(?:\.\/)+/, "").replace(/\/+$/, "");
|
|
420
|
+
const listed = REMOTE_PROJECT_MARKERS;
|
|
421
|
+
if (listed.includes(`${bare}/`)) return `${bare}/`;
|
|
422
|
+
if (listed.includes(bare)) return bare;
|
|
423
|
+
return value;
|
|
424
|
+
}
|
|
425
|
+
var REMOTE_PROJECT_MARKER = z.preprocess(normalizeRemoteMarker, z.enum(REMOTE_PROJECT_MARKERS));
|
|
426
|
+
var REMOTE_WORKSPACE_LOCKFILES = [
|
|
427
|
+
"package-lock.json",
|
|
428
|
+
"npm-shrinkwrap.json",
|
|
429
|
+
"pnpm-lock.yaml",
|
|
430
|
+
"yarn.lock",
|
|
431
|
+
"bun.lockb",
|
|
432
|
+
"bun.lock"
|
|
433
|
+
];
|
|
434
|
+
var REMOTE_WORKSPACE_ROOT = z.object({
|
|
435
|
+
package_manager: z.string().min(1).max(128).optional(),
|
|
436
|
+
files: z.array(z.preprocess(normalizeRemoteMarker, z.enum(REMOTE_WORKSPACE_LOCKFILES))).max(6).optional()
|
|
437
|
+
}).strict();
|
|
280
438
|
var REMOTE_DEPENDENCY_MAP = z.record(z.string().min(1).max(128), z.string().min(1).max(256));
|
|
281
439
|
var REMOTE_PROJECT_DETAILS = z.object({
|
|
282
440
|
/** A display label only. It is never treated as a path by the remote MCP. */
|
|
@@ -292,8 +450,14 @@ var REMOTE_PROJECT_DETAILS = z.object({
|
|
|
292
450
|
flutter_sdk: z.string().min(1).max(128).optional(),
|
|
293
451
|
dependencies: REMOTE_DEPENDENCY_MAP.optional()
|
|
294
452
|
}).strict().optional(),
|
|
295
|
-
/**
|
|
453
|
+
/**
|
|
454
|
+
* Presence-only markers, from the fixed allowlist above. File contents are neither accepted nor read. `ios/` and
|
|
455
|
+
* `android/` are listed only when checked in: on an Expo app, directories .gitignore lists are continuous native
|
|
456
|
+
* generation's output, and the app is managed.
|
|
457
|
+
*/
|
|
296
458
|
files: z.array(REMOTE_PROJECT_MARKER).max(24).optional(),
|
|
459
|
+
/** For a package inside a workspace: the workspace root's packageManager field and lockfiles, presence only. */
|
|
460
|
+
workspace_root: REMOTE_WORKSPACE_ROOT.optional(),
|
|
297
461
|
/** Optional resolved versions for the small dependency report; never needed to select an integration path. */
|
|
298
462
|
installed_versions: REMOTE_DEPENDENCY_MAP.optional()
|
|
299
463
|
}).strict();
|
|
@@ -319,9 +483,18 @@ function detectRemoteProject(details) {
|
|
|
319
483
|
}));
|
|
320
484
|
}
|
|
321
485
|
for (const marker of details.files ?? []) {
|
|
322
|
-
const path = join(REMOTE_ROOT, marker);
|
|
486
|
+
const path = join(REMOTE_ROOT, marker.replace(/\/+$/, ""));
|
|
323
487
|
if (marker.endsWith("/")) directories.add(path);
|
|
324
488
|
else files.set(path, "");
|
|
489
|
+
for (let parent = dirname(path); parent.startsWith(`${REMOTE_ROOT}/`); parent = dirname(parent)) directories.add(parent);
|
|
490
|
+
}
|
|
491
|
+
if (details.workspace_root !== void 0) {
|
|
492
|
+
const workspaceRoot = dirname(REMOTE_ROOT);
|
|
493
|
+
files.set(join(workspaceRoot, "package.json"), JSON.stringify({
|
|
494
|
+
workspaces: [basename(REMOTE_ROOT)],
|
|
495
|
+
packageManager: details.workspace_root.package_manager
|
|
496
|
+
}));
|
|
497
|
+
for (const lockfile of details.workspace_root.files ?? []) files.set(join(workspaceRoot, lockfile), "");
|
|
325
498
|
}
|
|
326
499
|
for (const [name, version] of Object.entries(details.installed_versions ?? {})) {
|
|
327
500
|
files.set(join(REMOTE_ROOT, "node_modules", ...name.split("/"), "package.json"), JSON.stringify({ version }));
|
|
@@ -376,7 +549,20 @@ function planDetection(args) {
|
|
|
376
549
|
)
|
|
377
550
|
};
|
|
378
551
|
}
|
|
379
|
-
|
|
552
|
+
if (args.project_details !== void 0) return { ok: true, detection: detectRemoteProject(args.project_details) };
|
|
553
|
+
const root = args.project_root;
|
|
554
|
+
const problem = !isAbsolute(root) ? "it is not an absolute path" : !nodeFileSystem.isDirectory(root) ? "no readable directory exists at it" : null;
|
|
555
|
+
if (problem !== null) {
|
|
556
|
+
return {
|
|
557
|
+
ok: false,
|
|
558
|
+
result: refusal(
|
|
559
|
+
"LOCAL_PATH_NOT_READABLE",
|
|
560
|
+
`project_root cannot be read: ${problem}. Pass the absolute path of the application root on the machine this server runs on, or describe the project with \`project_details\` instead.`,
|
|
561
|
+
{ arguments: ["project_root"], project_root: root, next_action: "fix_arguments" }
|
|
562
|
+
)
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
return { ok: true, detection: detectProject(nodeFileSystem, root) };
|
|
380
566
|
}
|
|
381
567
|
var THEME_SOURCE_MAX_BYTES = 256 * 1024;
|
|
382
568
|
var PLAN_INPUT_SHAPE = {
|
|
@@ -386,13 +572,14 @@ var PLAN_INPUT_SHAPE = {
|
|
|
386
572
|
"For a remote MCP: the text of the project's tailwind.config.{js,ts}, the CSS that declares its theme (a Tailwind v4 @theme block or :root custom properties), or a React Native theme or colours file, so the plan can match the generated surface to the project's own theme. Read statically and never executed. With project_root the project is read from disk instead; this argument, when given, wins. The plan refuses to guess a theme it cannot read and names the missing facts in its match-theme step."
|
|
387
573
|
),
|
|
388
574
|
capabilities: z.array(CAPABILITY).optional().describe(
|
|
389
|
-
'Which Everfur capabilities this integration uses, as the contract names them. The ids the contract can plan today are "chat", "entitlements", "events", "notifications", "photo", "records", "records-clinic" and "televet"; "animations", "records-push", "records-transfer", "server-pets" and "video" are blocked today and a plan refuses them with the reason. Pass ["all"] for every plannable capability. The loaded contract is the authority: an UNKNOWN_OPTION refusal lists its exact ids. The plan holds no credential, so everfur_verify_integration (not this tool) is the fail-closed check of what the tenant actually has. Omit if unknown; the tool will refuse and list the ids.'
|
|
575
|
+
'Which Everfur capabilities this integration uses, as the contract names them. The ids the contract can plan today are "chat", "entitlements", "events", "notifications", "photo", "records", "records-clinic" and "televet"; "animations", "records-push", "records-transfer", "server-pets" and "video" are blocked today and a plan refuses them with the reason. Pass ["all"] for every plannable capability. The loaded contract is the authority: an UNKNOWN_OPTION refusal lists its exact ids. The plan holds no credential, so everfur_verify_integration (not this tool) is the fail-closed check of what the tenant actually has. The native setup an in-app vet call needs (the Daily modules, an Expo config plugin, a clean prebuild, camera and microphone declarations, and a warning naming the missing-native-module error Expo Go produces) is planned only when the generated React Native file mounts that call; a plan whose file mounts only the vet visit button installs none of it. Omit if unknown; the tool will refuse and list the ids.'
|
|
390
576
|
),
|
|
391
577
|
identity_mode: IDENTITY_MODE.optional().describe('anonymous = no backend, scoped to the device. partner-user-ref = your backend mints a session against your own user id. shopify-app-proxy = a Shopify storefront, where Shopify vouches for the signed-in customer and no partner backend mints. "personalized" is accepted as an alias for partner-user-ref.'),
|
|
392
578
|
session_mint_language: MINT_LANGUAGE.optional().describe("Backend language that will mint sessions. Only meaningful when identity_mode is partner-user-ref."),
|
|
393
579
|
embedding: EMBEDDING.optional().describe(`For a web project only: "in-page" mounts the SDK's React components in the page (detected when package.json declares react-dom), "frame" includes the loader and renders the surface in an iframe on Everfur's origin (detected otherwise). Omit to accept what the project says; pass it to override.`),
|
|
394
|
-
environment: z.enum(ENVIRONMENTS).optional().describe(
|
|
395
|
-
contract_path: z.string().optional().describe("Path to a contract-public projection. Defaults to EVERFUR_CONTRACT_PUBLIC, then to the contract fetched from the Everfur API with EVERFUR_DOCS_KEY.")
|
|
580
|
+
environment: z.enum(ENVIRONMENTS).optional().describe("The Everfur deployment the generated configuration targets: test (your sandbox tenant on the production API, a pk_test_ key, livemode false), staging (your sandbox tenant on the staging API, for early access) or production (your live tenant, a pk_live_ key). Required for a plan that writes code; never assumed. Ask the developer. On the Everfur-hosted server an omitted environment is the connected tenant's, one naming a different host is refused (ENVIRONMENT_MISMATCH), and one naming a tenant mode the connected tenant does not have is refused (ENVIRONMENT_MODE_MISMATCH)."),
|
|
581
|
+
contract_path: z.string().optional().describe("Path to a contract-public projection. Defaults to EVERFUR_CONTRACT_PUBLIC, then to the contract fetched from the Everfur API with EVERFUR_DOCS_KEY."),
|
|
582
|
+
...PLAN_STEPS_INPUT_SHAPE
|
|
396
583
|
};
|
|
397
584
|
var HOSTED_PLAN_INPUT_SHAPE = {
|
|
398
585
|
project_details: REMOTE_PROJECT_DETAILS.optional().describe("A safe projection of the project: package dependencies, package manager, and fixed file-presence markers. Never source files, env files, or credentials; this server reads no path."),
|
|
@@ -403,9 +590,11 @@ var HOSTED_PLAN_INPUT_SHAPE = {
|
|
|
403
590
|
identity_mode: PLAN_INPUT_SHAPE.identity_mode,
|
|
404
591
|
session_mint_language: PLAN_INPUT_SHAPE.session_mint_language,
|
|
405
592
|
embedding: PLAN_INPUT_SHAPE.embedding,
|
|
593
|
+
// Owner-approved 2026-10-01 (production sandbox plan section 10: the CLI environment text).
|
|
406
594
|
environment: PLAN_INPUT_SHAPE.environment.describe(
|
|
407
|
-
|
|
408
|
-
)
|
|
595
|
+
"The Everfur deployment the generated configuration targets: test (the sandbox tenant on the production API, a pk_test_ key, livemode false; for a live tenant, its paired sandbox), staging (the sandbox tenant on the staging API, for early access) or production (the live tenant, a pk_live_ key). Omitted, the plan targets the connected tenant's deployment and mode; one naming a different host than the connected tenant's is refused (ENVIRONMENT_MISMATCH), and one naming a mode it does not have is refused (ENVIRONMENT_MODE_MISMATCH)."
|
|
596
|
+
),
|
|
597
|
+
...PLAN_STEPS_INPUT_SHAPE
|
|
409
598
|
};
|
|
410
599
|
function apiHostOf(apiBaseUrl) {
|
|
411
600
|
try {
|
|
@@ -425,28 +614,81 @@ function environmentOfApi(apiBaseUrl) {
|
|
|
425
614
|
function hostedEnvironment(requested, context) {
|
|
426
615
|
if (context.credentialSource !== "hosted") return { ok: true, environment: requested, fromTenant: false };
|
|
427
616
|
const connectedApi = context.env[BASE_URL_ENV_VAR];
|
|
428
|
-
const
|
|
429
|
-
if (
|
|
617
|
+
const deployment = environmentOfApi(connectedApi);
|
|
618
|
+
if (deployment === null) return { ok: true, environment: requested, fromTenant: false };
|
|
619
|
+
const mode = context.tenantMode ?? null;
|
|
620
|
+
const connected = deployment === "production" && mode !== null && !mode.livemode ? "test" : deployment;
|
|
430
621
|
if (requested === null) return { ok: true, environment: connected, fromTenant: true };
|
|
431
622
|
const requestedApi = API_BASE_URL_BY_ENVIRONMENT[requested];
|
|
432
|
-
if (apiHostOf(requestedApi) === apiHostOf(API_BASE_URL_BY_ENVIRONMENT[connected])) {
|
|
433
|
-
return { ok: true, environment: requested, fromTenant: false };
|
|
434
|
-
}
|
|
435
623
|
const requestedHost = apiHostOf(requestedApi) ?? requestedApi;
|
|
436
624
|
const connectedHost = apiHostOf(connectedApi ?? "") ?? String(connectedApi);
|
|
625
|
+
if (apiHostOf(requestedApi) !== apiHostOf(API_BASE_URL_BY_ENVIRONMENT[deployment])) {
|
|
626
|
+
return {
|
|
627
|
+
ok: false,
|
|
628
|
+
result: refusal(
|
|
629
|
+
"ENVIRONMENT_MISMATCH",
|
|
630
|
+
`This server is connected to a tenant on ${connectedHost} (${connected}), but environment "${requested}" targets ${requestedHost}. A plan for ${requestedHost} would carry a key that host does not know, and every request would answer 401. Which of the two was meant is the developer's to say.`,
|
|
631
|
+
{
|
|
632
|
+
environment: requested,
|
|
633
|
+
requested_host: requestedHost,
|
|
634
|
+
connected_host: connectedHost,
|
|
635
|
+
connected_environment: connected,
|
|
636
|
+
how_to_fix: [
|
|
637
|
+
`omit environment to plan for the connected tenant on ${connectedHost}`,
|
|
638
|
+
`or connect the editor to the Everfur MCP server of the ${requested} environment and plan there`
|
|
639
|
+
]
|
|
640
|
+
}
|
|
641
|
+
)
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
const modeMismatch = mode !== null && (requested === "production" && !mode.livemode || requested === "test" && deployment === "production" && mode.livemode && !mode.paired);
|
|
645
|
+
if (!modeMismatch) return { ok: true, environment: requested, fromTenant: false };
|
|
646
|
+
const needs = requested === "production" ? "a live tenant, and the connected tenant is a sandbox" : "a sandbox tenant, or a live tenant with a paired sandbox, and the connected tenant is live with no paired sandbox";
|
|
437
647
|
return {
|
|
438
648
|
ok: false,
|
|
439
649
|
result: refusal(
|
|
440
|
-
"
|
|
441
|
-
`
|
|
650
|
+
"ENVIRONMENT_MODE_MISMATCH",
|
|
651
|
+
`Environment "${requested}" needs ${needs}. Test and production share ${connectedHost}, so the tenant's mode, not the host, decides which one a plan is for. Which was meant is the developer's to say.`,
|
|
442
652
|
{
|
|
443
653
|
environment: requested,
|
|
444
|
-
requested_host: requestedHost,
|
|
445
654
|
connected_host: connectedHost,
|
|
446
655
|
connected_environment: connected,
|
|
656
|
+
tenant_livemode: mode.livemode,
|
|
657
|
+
test_mode_paired: mode.paired,
|
|
658
|
+
how_to_fix: [
|
|
659
|
+
`omit environment to plan for the connected tenant (${connected})`,
|
|
660
|
+
requested === "production" ? "or connect the editor with the live tenant and plan there" : "or connect the editor with the sandbox tenant, or pair this live tenant with one, and plan there"
|
|
661
|
+
]
|
|
662
|
+
}
|
|
663
|
+
)
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
async function localTenantMode(environment, context, ports) {
|
|
667
|
+
if (context.credentialSource === "hosted") return { ok: true, warning: null };
|
|
668
|
+
const check = await checkTenantMode({
|
|
669
|
+
env: context.env,
|
|
670
|
+
environment,
|
|
671
|
+
transport: ports.transport ?? fetchTransport,
|
|
672
|
+
clock: ports.clock ?? systemClock
|
|
673
|
+
});
|
|
674
|
+
if (check.kind === "not_checked") return { ok: true, warning: check.warning };
|
|
675
|
+
if (check.kind !== "mismatch" || environment === null) return { ok: true, warning: null };
|
|
676
|
+
const host = apiHostOf(API_BASE_URL_BY_ENVIRONMENT[environment]) ?? API_BASE_URL_BY_ENVIRONMENT[environment];
|
|
677
|
+
const needs = environment === "production" ? "a live tenant" : "a sandbox tenant";
|
|
678
|
+
const has = check.livemode ? "a live tenant (livemode true)" : "a sandbox tenant (livemode false)";
|
|
679
|
+
const other = check.livemode ? "production" : "test";
|
|
680
|
+
return {
|
|
681
|
+
ok: false,
|
|
682
|
+
result: refusal(
|
|
683
|
+
"ENVIRONMENT_MODE_MISMATCH",
|
|
684
|
+
`Environment "${environment}" needs ${needs}, and the ${PARTNER_KEY.envVar} in this server's environment belongs to ${has}. Test and production share ${host}, so the tenant's mode, not the host, decides which one a plan is for. Which was meant is the developer's to say.`,
|
|
685
|
+
{
|
|
686
|
+
environment,
|
|
687
|
+
connected_host: host,
|
|
688
|
+
tenant_livemode: check.livemode,
|
|
447
689
|
how_to_fix: [
|
|
448
|
-
`
|
|
449
|
-
`or
|
|
690
|
+
`pass environment "${other}" to plan for the tenant of this server's ${PARTNER_KEY.envVar}`,
|
|
691
|
+
`or set ${PARTNER_KEY.envVar} in this server's environment to the publishable key of the tenant you meant`
|
|
450
692
|
]
|
|
451
693
|
}
|
|
452
694
|
)
|
|
@@ -457,6 +699,8 @@ async function planIntegrationTool(args, context, ports = {}) {
|
|
|
457
699
|
if (detected.ok === false) return detected.result;
|
|
458
700
|
const environment = hostedEnvironment(args.environment ?? null, context);
|
|
459
701
|
if (environment.ok === false) return environment.result;
|
|
702
|
+
const mode = await localTenantMode(environment.environment, context, ports);
|
|
703
|
+
if (mode.ok === false) return mode.result;
|
|
460
704
|
const contractPath = args.contract_path ?? context.env["EVERFUR_CONTRACT_PUBLIC"] ?? null;
|
|
461
705
|
let load;
|
|
462
706
|
if (contractPath === null) {
|
|
@@ -489,7 +733,9 @@ async function planIntegrationTool(args, context, ports = {}) {
|
|
|
489
733
|
environment: environment.environment,
|
|
490
734
|
space,
|
|
491
735
|
cliVersion: CLI_VERSION,
|
|
492
|
-
...theme === void 0 ? {} : { theme }
|
|
736
|
+
...theme === void 0 ? {} : { theme },
|
|
737
|
+
// The hosted server knows whether a live tenant has a paired sandbox, which decides how a live plan is verified.
|
|
738
|
+
...context.credentialSource === "hosted" && context.tenantMode?.livemode === true ? { tenantPairedSandbox: context.tenantMode.paired } : {}
|
|
493
739
|
};
|
|
494
740
|
const plan = planIntegration(input);
|
|
495
741
|
if (plan.platformBlock) {
|
|
@@ -514,13 +760,18 @@ async function planIntegrationTool(args, context, ports = {}) {
|
|
|
514
760
|
"UNDERSPECIFIED_INTEGRATION",
|
|
515
761
|
"This plan cannot be produced without the facts below. Each one is the developer's to answer, and none is guessed here.",
|
|
516
762
|
{
|
|
763
|
+
ask: QUESTION_ROUND_INSTRUCTION,
|
|
517
764
|
missing_facts: plan.missingFacts.map((fact) => ({
|
|
518
765
|
fact: fact.fact,
|
|
766
|
+
...fixedQuestion(fact.fact, space, fact.options ?? null) ?? {},
|
|
519
767
|
why: fact.reason,
|
|
520
|
-
how_to_supply: supplyOverMcp(fact.fact, space),
|
|
768
|
+
how_to_supply: supplyOverMcp(fact.fact, space, fact.options ?? null, detected.detection),
|
|
521
769
|
// The CLI's own phrasing, kept so the two surfaces are visibly the same refusal and a partner
|
|
522
770
|
// moving between them recognises it.
|
|
523
|
-
how_to_supply_via_cli: fact.supplyWith
|
|
771
|
+
how_to_supply_via_cli: fact.supplyWith,
|
|
772
|
+
// A fact needed only for some answers to another missing fact, asked in the same round (one question round,
|
|
773
|
+
// timed integration run 2026-09-26): ask it beside that fact, and it is unused for any other answer.
|
|
774
|
+
...fact.requiredWhen === void 0 ? {} : { required_when: { fact: fact.requiredWhen.fact, one_of: [...fact.requiredWhen.oneOf] } }
|
|
524
775
|
})),
|
|
525
776
|
// Everything detection already knows, so the agent does not learn it only after the round trip.
|
|
526
777
|
warnings: plan.warnings,
|
|
@@ -538,21 +789,35 @@ async function planIntegrationTool(args, context, ports = {}) {
|
|
|
538
789
|
{ blocked: plan.blocked, warnings: plan.warnings, plan, next_call: null }
|
|
539
790
|
);
|
|
540
791
|
}
|
|
792
|
+
const ready = mode.warning === null ? plan : { ...plan, warnings: [...plan.warnings, mode.warning] };
|
|
541
793
|
return {
|
|
542
794
|
isError: false,
|
|
543
|
-
document: { ok: true, plan, ...environment.fromTenant ? { environment_source: "connected_tenant" } : {} }
|
|
795
|
+
document: { ok: true, plan: ready, ...environment.fromTenant ? { environment_source: "connected_tenant" } : {} }
|
|
544
796
|
};
|
|
545
797
|
}
|
|
546
798
|
var PLAN_OUTPUT_SHAPE = {
|
|
547
799
|
...ENVELOPE_SHAPE,
|
|
548
|
-
|
|
800
|
+
plan_id: z.string().optional().describe("A digest of the plan's steps, the arguments it was asked with and the contract it was built from. Present when ok is true."),
|
|
801
|
+
plan: z.object({}).passthrough().optional().describe("The plan summary (kind everfur.plan.summary, summaryVersion 2): the goal, detection, what was supplied, each warning by its first sentence, every step by its id, title, one-line intent and page, the number of pages, and the checklist's requirements. Present when ok is true and neither page nor steps was given."),
|
|
802
|
+
fetch_steps: z.object({}).passthrough().optional().describe("With the summary: the call that returns its pages."),
|
|
803
|
+
page: z.number().int().optional().describe("With page: the page this response carries."),
|
|
804
|
+
pages: z.number().int().optional().describe("With page: how many pages the plan has."),
|
|
805
|
+
next_page: z.number().int().optional().describe("With page: the page to ask for next, absent on the last page."),
|
|
806
|
+
detected: z.object({}).passthrough().optional().describe("On page 1: what was detected in the project, in full, with the evidence for each fact."),
|
|
807
|
+
warnings: z.array(z.object({}).passthrough()).optional().describe("On page 1: every warning of the plan, in full."),
|
|
808
|
+
principles: z.array(z.object({}).passthrough()).optional().describe("On page 1: the principles of every decision tree the plan consulted, each by its title and the everfur://guide section that states it in full."),
|
|
809
|
+
checklist: z.array(z.object({}).passthrough()).optional().describe("On page 1: the definition of done in full, each requirement with the check that proves it and the steps it covers."),
|
|
810
|
+
steps: z.array(z.object({}).passthrough()).optional().describe("With page or steps: those steps in full, as everfur://schema/plan.v1 describes a step, each with the complete body of every file it writes."),
|
|
811
|
+
remaining_steps: z.array(z.string()).optional().describe("With steps: requested steps left out to keep this response small. The same call with steps set to these returns them."),
|
|
549
812
|
environment_source: z.literal("connected_tenant").optional().describe("Present when environment was omitted on the Everfur-hosted server and the plan targets the connected tenant's deployment.")
|
|
550
813
|
};
|
|
814
|
+
var VERIFY_EMBEDDING = z.enum(["in-page", "frame"]);
|
|
551
815
|
var VERIFY_INPUT_SHAPE = {
|
|
552
816
|
user_ref: z.string().optional().describe("End-user id to mint the test session for. Defaults to a CLI-owned value."),
|
|
553
817
|
identity_mode: z.enum(VERIFY_IDENTITY_MODES).optional().describe(`The identity to verify with. "partner-user-ref" (the default): a signed session. "anonymous": the publishable key and a device session id derived from user_ref, exactly what an anonymous integration sends; nothing is minted, the verdict and the conversation list are read on that identity, and the tenant's sessions_required policy is reported (it refuses only a publishable-key caller that asserts x-everfur-user-ref; pure anonymous chat keeps working).`),
|
|
554
818
|
attempts: z.number().int().min(1).max(20).optional().describe("How many times to read the verdict before concluding it is empty."),
|
|
555
|
-
origin: z.string().optional().describe("
|
|
819
|
+
origin: z.string().optional().describe("The web page origin, e.g. https://shop.example.com: the origin an in-page page calls the API from, or the one the frame renders on. Pass embedding beside it. Adds the web checks: the CORS preflight (of a widget request with the headers the SDK sends for in-page, of the fence request for a frame), CORS exposure of X-Request-ID and Retry-After, whether the tenant requires signed sessions, and, for a frame, the embed fence for that origin and the loader pairing with the configured API."),
|
|
820
|
+
embedding: VERIFY_EMBEDDING.optional().describe('With origin: how the page embeds the surface, the embedding the plan was built on (its supplied.embedding, else its detected.webEmbedding). "frame" runs every web check; "in-page" reports the embed fence and the loader N/A, since only the frame uses them, and still checks CORS and the session policy. Not named, every web check runs: only a page named in-page has the fence and the loader N/A, and a project read locally whose page includes the Everfur loader is checked as the frame it embeds.'),
|
|
556
821
|
loader_url: z.string().optional().describe("With origin: the loader to probe instead of the one derived from EVERFUR_API_BASE_URL (https, or http on localhost). Without it a local or unrecognised API reports the loader check as not checked."),
|
|
557
822
|
surfaces: z.array(z.enum(SURFACES)).optional().describe("Surfaces to exercise with the minted session. chat and records: the diagnostics reason, then a read on the session. televet: mints a real vet visit handoff, on a SANDBOX tenant only (never a live one); the one-time token is never returned. theme: the branding the verdict carries, judged with the SDK's own rules, merged with the theme prop everfur_plan_integration writes when project_root or theme_source is given. A surface Everfur has not switched on is reported GATED, not FAIL. With identity_mode anonymous, chat is read on the anonymous identity and records and televet are skipped: both take a signed session only."),
|
|
558
823
|
project_root: z.string().optional().describe("Absolute path to the application root. Adds offline project checks: no partner secret key in source or client build output, no secret behind a NEXT_PUBLIC_/VITE_/EXPO_PUBLIC_ prefix, the secret read only by server code, 'use client' on Next.js App Router files importing the SDK, and that the env files name the same API host this verify ran against. File contents are never returned."),
|
|
@@ -565,6 +830,7 @@ var HOSTED_VERIFY_INPUT_SHAPE = {
|
|
|
565
830
|
identity_mode: VERIFY_INPUT_SHAPE.identity_mode,
|
|
566
831
|
attempts: VERIFY_INPUT_SHAPE.attempts,
|
|
567
832
|
origin: VERIFY_INPUT_SHAPE.origin,
|
|
833
|
+
embedding: VERIFY_INPUT_SHAPE.embedding,
|
|
568
834
|
loader_url: VERIFY_INPUT_SHAPE.loader_url.describe(
|
|
569
835
|
"With origin: the loader to probe instead of the one that pairs with the connected tenant's API. On this server it is accepted only on an Everfur SDK host."
|
|
570
836
|
),
|
|
@@ -607,6 +873,8 @@ async function verifyIntegrationTool(args, context, ports = {}) {
|
|
|
607
873
|
attempts: args.attempts ?? 3,
|
|
608
874
|
pollDelayMs: 500,
|
|
609
875
|
origin: origin.origin,
|
|
876
|
+
// Named, or read from project_root when one is given; otherwise the frame, which is what origin has always checked.
|
|
877
|
+
embedding: args.embedding ?? null,
|
|
610
878
|
loaderUrl: loader.url,
|
|
611
879
|
// The MCP server's documented environment holds no secret key, so it verifies only with a session the partner
|
|
612
880
|
// backend minted (EVERFUR_SESSION_TOKEN) and never reads EVERFUR_PARTNER_SECRET_KEY, even when one is set.
|
|
@@ -624,7 +892,11 @@ async function verifyIntegrationTool(args, context, ports = {}) {
|
|
|
624
892
|
// Anonymous verifies the path an anonymous integration uses: the publishable key and a derived session id.
|
|
625
893
|
identityMode: args.identity_mode ?? "partner-user-ref",
|
|
626
894
|
// A pasted theme wins over the project on disk, as it does for the plan; absent, the project's own (if any) is read.
|
|
627
|
-
|
|
895
|
+
// A theme prop literal is read as the prop; a theme_source nothing could be read from fails the theme check.
|
|
896
|
+
...args.theme_source === void 0 ? {} : (() => {
|
|
897
|
+
const read = themePropFromSource(args.theme_source, "theme_source");
|
|
898
|
+
return { themeProp: read.prop, themeSourceUnread: read.unread };
|
|
899
|
+
})()
|
|
628
900
|
});
|
|
629
901
|
const result = {
|
|
630
902
|
ok: verification.status === "passed",
|
|
@@ -644,12 +916,12 @@ async function verifyIntegrationTool(args, context, ports = {}) {
|
|
|
644
916
|
}
|
|
645
917
|
var VERIFY_OUTPUT_SHAPE = {
|
|
646
918
|
...ENVELOPE_SHAPE,
|
|
647
|
-
status: z.enum(["passed", "failed", "incomplete"]).optional().describe("passed: every step PASS. incomplete: a step was skipped for a missing credential. failed: a step FAILED."),
|
|
919
|
+
status: z.enum(["passed", "failed", "incomplete"]).optional().describe("passed: every step PASS, GATED or N/A. incomplete: a step was skipped for a missing credential. failed: a step FAILED."),
|
|
648
920
|
base_url: z.string().optional().describe("The API base URL the checks ran against."),
|
|
649
921
|
steps: z.array(z.object({
|
|
650
922
|
id: z.string(),
|
|
651
923
|
title: z.string(),
|
|
652
|
-
result: z.enum(["PASS", "FAIL", "SKIP", "GATED"]),
|
|
924
|
+
result: z.enum(["PASS", "FAIL", "SKIP", "GATED", "N/A"]),
|
|
653
925
|
reason: z.string(),
|
|
654
926
|
requestId: z.string().nullable(),
|
|
655
927
|
details: z.array(z.string())
|
|
@@ -768,19 +1040,34 @@ var HOSTED_EXPLAIN_ERROR_INPUT_SHAPE = {
|
|
|
768
1040
|
};
|
|
769
1041
|
async function explainErrorTool(args, context, ports = {}) {
|
|
770
1042
|
const code = args.code.trim();
|
|
1043
|
+
const toolCode = toolCodeExplanation(code, CLI_VERSION);
|
|
1044
|
+
if (toolCode !== null) {
|
|
1045
|
+
const wire = await wireExplanationOf(code, args, context, ports);
|
|
1046
|
+
if (wire === null) return { isError: false, document: { ok: true, explanation: toolCode } };
|
|
1047
|
+
return { isError: false, document: { ok: true, explanation: withToolCodeBeside(code, wire) } };
|
|
1048
|
+
}
|
|
771
1049
|
const offline = resolveOffline(code);
|
|
772
|
-
if (offline?.kind === "sdk_member") return offlineExplanation(offline);
|
|
1050
|
+
if (offline?.kind === "sdk_member") return offlineExplanation(offline, "no_registry");
|
|
773
1051
|
const contractPath = args.contract_path ?? context.env["EVERFUR_CONTRACT_PUBLIC"] ?? null;
|
|
774
1052
|
let load;
|
|
775
1053
|
if (contractPath === null) {
|
|
776
1054
|
const fetched = await contractProviderFor(context, ports)();
|
|
777
1055
|
if (fetched.kind === "unavailable") {
|
|
778
|
-
|
|
779
|
-
return
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1056
|
+
const resolved = resolveWithoutRegistry(code);
|
|
1057
|
+
if (resolved.kind !== "unknown") return offlineExplanation(resolved, "no_registry");
|
|
1058
|
+
return refusal(
|
|
1059
|
+
"UNKNOWN_ERROR_CODE",
|
|
1060
|
+
`"${code}" is not a code this server knows, so its cause and whether a retry can help are unknown here. The shape of the string does not say, and the code the response carried may differ from this one. The live error registry was not read, and it can list a code the tables inside this server predate.`,
|
|
1061
|
+
{
|
|
1062
|
+
did_you_mean: resolved.suggestions,
|
|
1063
|
+
case_sensitive: true,
|
|
1064
|
+
searched: {
|
|
1065
|
+
source: "the SDK taxonomy, the SDK contract table, the MCP tools' codes, the diagnostics reason codes, the CLI's error codes and the SDK's sub-enums this server carries",
|
|
1066
|
+
codes: knownCodes(null).length
|
|
1067
|
+
},
|
|
1068
|
+
registry: { read: false, reason: fetched.message }
|
|
1069
|
+
}
|
|
1070
|
+
);
|
|
784
1071
|
}
|
|
785
1072
|
load = loadErrorRegistryFromContract(fetched.contract);
|
|
786
1073
|
} else {
|
|
@@ -805,7 +1092,7 @@ async function explainErrorTool(args, context, ports = {}) {
|
|
|
805
1092
|
const entry = load.registry.lookup(code);
|
|
806
1093
|
if (entry === null) {
|
|
807
1094
|
const fallback = resolveAfterRegistryMiss(code, load.registry);
|
|
808
|
-
if (fallback.kind !== "unknown") return offlineExplanation(fallback);
|
|
1095
|
+
if (fallback.kind !== "unknown") return offlineExplanation(fallback, "registry_miss");
|
|
809
1096
|
return refusal(
|
|
810
1097
|
"UNKNOWN_ERROR_CODE",
|
|
811
1098
|
`"${code}" is not in the error registry, so its cause and whether a retry can help are unknown here. The shape of the string does not say, and the code the response carried may differ from this one.`,
|
|
@@ -818,7 +1105,9 @@ async function explainErrorTool(args, context, ports = {}) {
|
|
|
818
1105
|
codes: load.registry.codes.length,
|
|
819
1106
|
// The SDK taxonomy and the SDK contract table were searched too, so a reader knows the refusal is not
|
|
820
1107
|
// just "this one file does not have it".
|
|
821
|
-
with_sdk_tables: knownCodes(load.registry).length
|
|
1108
|
+
with_sdk_tables: knownCodes(load.registry).length,
|
|
1109
|
+
// Of them, the codes the tools on this server return (tool-codes.ts).
|
|
1110
|
+
mcp_tool_codes: TOOL_CODES.length
|
|
822
1111
|
}
|
|
823
1112
|
}
|
|
824
1113
|
);
|
|
@@ -827,31 +1116,61 @@ async function explainErrorTool(args, context, ports = {}) {
|
|
|
827
1116
|
isError: false,
|
|
828
1117
|
document: {
|
|
829
1118
|
ok: true,
|
|
830
|
-
explanation:
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1119
|
+
explanation: {
|
|
1120
|
+
...buildExplanationDocument({
|
|
1121
|
+
entry,
|
|
1122
|
+
contractPath: load.path,
|
|
1123
|
+
digest: load.digest,
|
|
1124
|
+
cliVersion: CLI_VERSION
|
|
1125
|
+
}),
|
|
1126
|
+
...registryAlsoIn(code)
|
|
1127
|
+
}
|
|
836
1128
|
}
|
|
837
1129
|
};
|
|
838
1130
|
}
|
|
839
|
-
function
|
|
1131
|
+
async function wireExplanationOf(code, args, context, ports) {
|
|
1132
|
+
const offline = resolveOffline(code);
|
|
1133
|
+
if (offline !== null) return offlineDocument(offline, CLI_VERSION, "no_registry");
|
|
1134
|
+
const contractPath = args.contract_path ?? context.env["EVERFUR_CONTRACT_PUBLIC"] ?? null;
|
|
1135
|
+
let load;
|
|
1136
|
+
if (contractPath === null) {
|
|
1137
|
+
const fetched = await contractProviderFor(context, ports)();
|
|
1138
|
+
if (fetched.kind === "unavailable") return null;
|
|
1139
|
+
load = loadErrorRegistryFromContract(fetched.contract);
|
|
1140
|
+
} else {
|
|
1141
|
+
load = loadErrorRegistry(nodeFileSystem, contractPath);
|
|
1142
|
+
}
|
|
1143
|
+
if (load.kind !== "loaded") return null;
|
|
1144
|
+
const entry = load.registry.lookup(code);
|
|
1145
|
+
if (entry === null) return null;
|
|
1146
|
+
return buildExplanationDocument({ entry, contractPath: load.path, digest: load.digest, cliVersion: CLI_VERSION });
|
|
1147
|
+
}
|
|
1148
|
+
function withToolCodeBeside(code, wire) {
|
|
1149
|
+
const tool = toolCodeMeaning(code);
|
|
1150
|
+
if (tool === null) return wire;
|
|
840
1151
|
return {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1152
|
+
...wire,
|
|
1153
|
+
collision: `"${code}" is both a code in the error registry and a code an Everfur MCP tool returns (${tool.tools.join(", ")}). This document explains the registry's code, the one a response from the Everfur API carries; the error.code of an everfur_* tool result is the tool code in mcpToolCode.`,
|
|
1154
|
+
mcpToolCode: {
|
|
1155
|
+
tools: [...tool.tools],
|
|
1156
|
+
meaning: tool.meaning,
|
|
1157
|
+
nextAction: tool.nextAction,
|
|
1158
|
+
retryable: tool.retryable,
|
|
1159
|
+
policyNextAction: tool.policyNextAction,
|
|
1160
|
+
docsPath: tool.docsPath
|
|
845
1161
|
}
|
|
846
1162
|
};
|
|
847
1163
|
}
|
|
1164
|
+
function offlineExplanation(resolution, context) {
|
|
1165
|
+
return { isError: false, document: { ok: true, explanation: offlineDocument(resolution, CLI_VERSION, context) } };
|
|
1166
|
+
}
|
|
848
1167
|
var EXPLAIN_ERROR_OUTPUT_SHAPE = {
|
|
849
1168
|
...ENVELOPE_SHAPE,
|
|
850
1169
|
explanation: z.object({}).passthrough().optional().describe("The registry's explanation, shaped by everfur://schema/error-explanation.v1. Present when ok is true.")
|
|
851
1170
|
};
|
|
852
1171
|
var ACCESS_TOKEN_SHAPE = /^mcp_at_[0-9a-f]{64}$/;
|
|
853
1172
|
var CONTEXT_PATH = "/mcp/context";
|
|
854
|
-
var CONTEXT_CACHE_TTL_MS =
|
|
1173
|
+
var CONTEXT_CACHE_TTL_MS = 15e3;
|
|
855
1174
|
var REFUSAL_CACHE_TTL_MS = 3e4;
|
|
856
1175
|
var MAX_CACHE_ENTRIES = 1e4;
|
|
857
1176
|
function isAccessTokenShaped(raw) {
|
|
@@ -866,7 +1185,17 @@ function tokenBucketOf(presented) {
|
|
|
866
1185
|
}
|
|
867
1186
|
var MCP_SCOPE = "mcp:tools";
|
|
868
1187
|
var REVOKED_AUTHORIZATION_REMEDY = "the authorization was revoked or expired since it was verified; reconnect the editor to sign in again";
|
|
869
|
-
var ACCEPT_CONTEXT_WITHOUT_RESOURCE =
|
|
1188
|
+
var ACCEPT_CONTEXT_WITHOUT_RESOURCE = false;
|
|
1189
|
+
var CONTEXT_WIRE_FIELDS = Object.freeze([
|
|
1190
|
+
"tenant",
|
|
1191
|
+
"user_id",
|
|
1192
|
+
"publishable_key",
|
|
1193
|
+
"api_base_url",
|
|
1194
|
+
"scopes",
|
|
1195
|
+
"expires_at",
|
|
1196
|
+
"resource"
|
|
1197
|
+
]);
|
|
1198
|
+
Object.freeze([...CONTEXT_WIRE_FIELDS, "test"]);
|
|
870
1199
|
function canonicalResource(value) {
|
|
871
1200
|
let parsed;
|
|
872
1201
|
try {
|
|
@@ -878,6 +1207,23 @@ function canonicalResource(value) {
|
|
|
878
1207
|
while (path.endsWith("/")) path = path.slice(0, -1);
|
|
879
1208
|
return `${parsed.origin}${path}${parsed.search}${parsed.hash}`;
|
|
880
1209
|
}
|
|
1210
|
+
function parseTestMode(value) {
|
|
1211
|
+
if (value === void 0 || value === null) return { ok: true, test: null };
|
|
1212
|
+
if (!isJsonObject(value)) return { ok: false };
|
|
1213
|
+
const slug = value["slug"];
|
|
1214
|
+
const publishableKey = value["publishable_key"];
|
|
1215
|
+
if (typeof slug !== "string" || slug.length === 0) return { ok: false };
|
|
1216
|
+
if (publishableKey !== null && typeof publishableKey !== "string") return { ok: false };
|
|
1217
|
+
if (value["livemode"] !== false) return { ok: false };
|
|
1218
|
+
return {
|
|
1219
|
+
ok: true,
|
|
1220
|
+
test: Object.freeze({
|
|
1221
|
+
slug,
|
|
1222
|
+
publishableKey: publishableKey === null || publishableKey.length === 0 ? null : publishableKey,
|
|
1223
|
+
livemode: false
|
|
1224
|
+
})
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
881
1227
|
function parseTenantContext(body) {
|
|
882
1228
|
if (!isJsonObject(body)) return null;
|
|
883
1229
|
const tenant = body["tenant"];
|
|
@@ -900,6 +1246,8 @@ function parseTenantContext(body) {
|
|
|
900
1246
|
if (!Array.isArray(scopes) || !scopes.every((scope) => typeof scope === "string")) return null;
|
|
901
1247
|
if (typeof expiresAt !== "string" || Number.isNaN(Date.parse(expiresAt))) return null;
|
|
902
1248
|
if (resource !== void 0 && resource !== null && typeof resource !== "string") return null;
|
|
1249
|
+
const test = parseTestMode(body["test"]);
|
|
1250
|
+
if (!test.ok) return null;
|
|
903
1251
|
return Object.freeze({
|
|
904
1252
|
tenant: Object.freeze({ clientId, slug, livemode }),
|
|
905
1253
|
userId,
|
|
@@ -907,7 +1255,8 @@ function parseTenantContext(body) {
|
|
|
907
1255
|
apiBaseUrl,
|
|
908
1256
|
scopes: Object.freeze([...scopes]),
|
|
909
1257
|
expiresAt,
|
|
910
|
-
resource: typeof resource === "string" ? resource : null
|
|
1258
|
+
resource: typeof resource === "string" ? resource : null,
|
|
1259
|
+
test: test.test
|
|
911
1260
|
});
|
|
912
1261
|
}
|
|
913
1262
|
function createTtlCache(ttlMs, now, live) {
|
|
@@ -1345,6 +1694,13 @@ function registerResources(server, provider) {
|
|
|
1345
1694
|
var TIMESTAMP = z.string();
|
|
1346
1695
|
var COUNT = z.number().int().min(0);
|
|
1347
1696
|
var TENANT = z.object({ client_id: z.string(), slug: z.string(), livemode: z.boolean() });
|
|
1697
|
+
var CONNECTED_TENANT = z.object({
|
|
1698
|
+
slug: z.string(),
|
|
1699
|
+
livemode: z.boolean(),
|
|
1700
|
+
paired_sandbox_slug: z.string().nullable(),
|
|
1701
|
+
environment: z.string(),
|
|
1702
|
+
publishable_key_prefix: z.string().nullable()
|
|
1703
|
+
});
|
|
1348
1704
|
var TENANT_STATUS = z.object({
|
|
1349
1705
|
tenant: TENANT,
|
|
1350
1706
|
mode: z.object({
|
|
@@ -1494,10 +1850,35 @@ var TENANT_STATUS_TOOL = {
|
|
|
1494
1850
|
name: "everfur_get_tenant_status",
|
|
1495
1851
|
description: "Read the connected tenant's status: its plan and limits, which features are on and which it may switch, live or test mode and its paired tenant, its embed origins, the capabilities its account holds, its publishable key's id and prefix (never the key), and its Televet return destination keys and booking lead. Read-only.",
|
|
1496
1852
|
input: {},
|
|
1497
|
-
output: {
|
|
1853
|
+
output: {
|
|
1854
|
+
...ENVELOPE_SHAPE,
|
|
1855
|
+
tenant_status: TENANT_STATUS.optional().describe("The status, as GET /mcp/tenant/status serves it."),
|
|
1856
|
+
connected_tenant: CONNECTED_TENANT.optional().describe(
|
|
1857
|
+
"When the status read is switched off for the tenant: which tenant is connected, from the sign-in itself."
|
|
1858
|
+
),
|
|
1859
|
+
tenant_status_unavailable: z.object({ code: z.string(), message: z.string(), request_id: z.string().nullable() }).optional().describe("Why the status itself was not read, beside connected_tenant.")
|
|
1860
|
+
},
|
|
1498
1861
|
async run(_args, context) {
|
|
1499
1862
|
const read = await readTenant(context, "status", "status", TENANT_STATUS);
|
|
1500
|
-
|
|
1863
|
+
if (read.ok) return ok({ tenant_status: read.value });
|
|
1864
|
+
const error = read.result.document["error"];
|
|
1865
|
+
const connected = context.connectedTenant ?? null;
|
|
1866
|
+
if (connected === null || error?.code !== "TENANT_SURFACE_NOT_ENABLED") return read.result;
|
|
1867
|
+
const deployment = environmentOfApi(connected.apiBaseUrl);
|
|
1868
|
+
return ok({
|
|
1869
|
+
connected_tenant: {
|
|
1870
|
+
slug: connected.slug,
|
|
1871
|
+
livemode: connected.livemode,
|
|
1872
|
+
paired_sandbox_slug: connected.pairedSandboxSlug,
|
|
1873
|
+
environment: deployment === "production" && !connected.livemode ? "test" : deployment ?? "unknown",
|
|
1874
|
+
publishable_key_prefix: connected.publishableKeyPrefix
|
|
1875
|
+
},
|
|
1876
|
+
tenant_status_unavailable: {
|
|
1877
|
+
code: "TENANT_SURFACE_NOT_ENABLED",
|
|
1878
|
+
message: typeof error.message === "string" ? error.message : "",
|
|
1879
|
+
request_id: typeof error.details?.request_id === "string" ? error.details.request_id : null
|
|
1880
|
+
}
|
|
1881
|
+
});
|
|
1501
1882
|
}
|
|
1502
1883
|
};
|
|
1503
1884
|
var WEBHOOK_ENDPOINTS_TOOL = {
|
|
@@ -1746,9 +2127,9 @@ function sharedRules(options) {
|
|
|
1746
2127
|
const hosted = options.sessionSource === "hosted";
|
|
1747
2128
|
const project = hosted ? `- Describe the project with project_details (dependencies, package manager, file-presence markers; never source
|
|
1748
2129
|
files or credentials). This server runs on Everfur's hosts and cannot read a path on my machine. To match the
|
|
1749
|
-
surface to my site's theme, pass theme_source: the text of my tailwind config or the CSS that declares my
|
|
2130
|
+
surface to my site's theme, pass theme_source: the text of my tailwind config or the CSS that declares my colors.` : `- Describe the project with project_root when the MCP server runs beside the project, or with project_details
|
|
1750
2131
|
(dependencies, package manager, file-presence markers; never source files or credentials) for a remote MCP, plus
|
|
1751
|
-
theme_source (the text of my tailwind config or the CSS that declares my
|
|
2132
|
+
theme_source (the text of my tailwind config or the CSS that declares my colors) so the plan matches my theme.`;
|
|
1752
2133
|
const session = hosted ? `- Verification runs with a session this server obtains from the Everfur API for my sandbox tenant; on a live
|
|
1753
2134
|
tenant the session step is GATED. Never ask me for a session, a key or a variable to set.` : "- Verification reads EVERFUR_SESSION_TOKEN, a short-lived session my backend minted; never ask for the secret key.";
|
|
1754
2135
|
const keys = hosted ? "- Never put a key in a tool argument, a file you write, or a message. This server holds none of mine." : "- Never put a key in a tool argument, a file you write, or a message. Keys come from the environment.";
|
|
@@ -1761,39 +2142,73 @@ ${session}
|
|
|
1761
2142
|
says the code is unknown, say so; do not explain it yourself.
|
|
1762
2143
|
${keys}`;
|
|
1763
2144
|
}
|
|
2145
|
+
var PROMPT_DESCRIPTIONS = Object.freeze({
|
|
2146
|
+
integrateWeb: "Plan, write, build and verify an Everfur integration in a web project.",
|
|
2147
|
+
integrateReactNative: "Plan, write, typecheck and verify an Everfur integration in a React Native or Expo app.",
|
|
2148
|
+
diagnoseIntegration: "Explain an error code, read diagnostics and verify the wire, before changing any code."
|
|
2149
|
+
});
|
|
2150
|
+
function verifyCall(embedding, origin) {
|
|
2151
|
+
const given = origin === void 0 || origin.trim() === "" ? null : origin.trim();
|
|
2152
|
+
const pageOrigin = (where) => given === null ? `\`origin\` set to ${where} (ask me for it)` : `\`origin\` set to ${JSON.stringify(given)}`;
|
|
2153
|
+
switch (embedding?.trim()) {
|
|
2154
|
+
case "in-page":
|
|
2155
|
+
return `Call everfur_verify_integration with \`embedding\` "in-page" and ${pageOrigin("the page origin the SDK runs on")}: the page calls the API from that origin, so the CORS preflight, the exposure and the session policy apply, and the frame's own checks (its embed fence and its loader) are reported N/A.`;
|
|
2156
|
+
case "frame":
|
|
2157
|
+
return `Call everfur_verify_integration with \`embedding\` "frame" and ${pageOrigin("the page origin the frame renders on")}.`;
|
|
2158
|
+
default:
|
|
2159
|
+
return `Call everfur_verify_integration with \`embedding\` set to the embedding the plan was built on (its \`supplied.embedding\`, else its \`detected.webEmbedding\`: "in-page" or "frame") and ${pageOrigin("the page origin")}. With "in-page" the frame's own checks (the embed fence and the loader) are reported N/A; the CORS checks apply to both.`;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
1764
2162
|
function integrateWebPrompt(args, options = {}) {
|
|
1765
2163
|
const { origin, ...planArgs } = args;
|
|
1766
|
-
const verifyWith = origin === void 0 || origin.trim() === "" ? "with `origin` set to the page origin the surface is embedded on (ask me for it)" : `with \`origin\` set to ${JSON.stringify(origin.trim())}`;
|
|
1767
2164
|
return {
|
|
1768
|
-
description:
|
|
2165
|
+
description: PROMPT_DESCRIPTIONS.integrateWeb,
|
|
1769
2166
|
text: `Integrate Everfur into this web project. Follow these steps in order.
|
|
1770
2167
|
${knownArguments(planArgs)}
|
|
1771
2168
|
1. Read everfur://guide/platform and everfur://guide/identity-mode.
|
|
1772
|
-
2. Call everfur_plan_integration. If it refuses with UNDERSPECIFIED_INTEGRATION, ask me
|
|
1773
|
-
error.details.missing_facts,
|
|
2169
|
+
2. Call everfur_plan_integration. If it refuses with UNDERSPECIFIED_INTEGRATION, ask me the question of every fact in
|
|
2170
|
+
error.details.missing_facts in one message, worded as given and with its options (a fact with required_when is
|
|
2171
|
+
needed only for the answers it names: ask it beside them), then call it again with my answers. Do not guess them.
|
|
2172
|
+
If the plan's match-theme step says the theme could not be read, do what it says: find this app's brand colors
|
|
2173
|
+
in its own code (a Tailwind config, the CSS that declares its theme, or a React Native theme file) and call again
|
|
2174
|
+
with that file's text as theme_source. Never pick a color.
|
|
1774
2175
|
3. If it refuses with PLATFORM_NOT_SUPPORTED or CAPABILITIES_BLOCKED, tell me the reason and the owner from
|
|
1775
2176
|
error.details and stop. Do not build around a blocked surface.
|
|
1776
|
-
4.
|
|
2177
|
+
4. Call everfur_plan_integration again with the same arguments plus plan_id and page, for page 1 to the summary's
|
|
2178
|
+
pages, for every step's full detail, commands and file bodies (with steps instead of page, call again with steps
|
|
2179
|
+
set to remaining_steps until none remain). Then write exactly the files the plan's steps name and
|
|
2180
|
+
run the commands it gives, in order.
|
|
1777
2181
|
5. Build the project and fix any build error before going on.
|
|
1778
|
-
6.
|
|
2182
|
+
6. ${verifyCall(args.embedding, origin)} Report each step's result with its request id.
|
|
1779
2183
|
7. If the capability verdict is empty, call everfur_diagnose and report the reason it gives per capability.
|
|
2184
|
+
8. Report every item of the plan's checklist with the result of its verify. The integration is done only when every
|
|
2185
|
+
item holds.
|
|
1780
2186
|
${sharedRules(options)}`
|
|
1781
2187
|
};
|
|
1782
2188
|
}
|
|
1783
2189
|
function integrateReactNativePrompt(args, options = {}) {
|
|
1784
2190
|
return {
|
|
1785
|
-
description:
|
|
2191
|
+
description: PROMPT_DESCRIPTIONS.integrateReactNative,
|
|
1786
2192
|
text: `Integrate Everfur into this React Native app. Follow these steps in order.
|
|
1787
2193
|
${knownArguments(args)}
|
|
1788
2194
|
1. Read everfur://guide/platform and everfur://guide/identity-mode.
|
|
1789
|
-
2. Call everfur_plan_integration. If it refuses with UNDERSPECIFIED_INTEGRATION, ask me
|
|
1790
|
-
error.details.missing_facts,
|
|
2195
|
+
2. Call everfur_plan_integration. If it refuses with UNDERSPECIFIED_INTEGRATION, ask me the question of every fact in
|
|
2196
|
+
error.details.missing_facts in one message, worded as given and with its options (a fact with required_when is
|
|
2197
|
+
needed only for the answers it names: ask it beside them), then call it again with my answers. Do not guess them.
|
|
2198
|
+
If the plan's match-theme step says the theme could not be read, do what it says: find this app's brand colors
|
|
2199
|
+
in its own code (a Tailwind config, the CSS that declares its theme, or a React Native theme file) and call again
|
|
2200
|
+
with that file's text as theme_source. Never pick a color.
|
|
1791
2201
|
3. If it refuses with PLATFORM_NOT_SUPPORTED or CAPABILITIES_BLOCKED, tell me the reason and the owner from
|
|
1792
2202
|
error.details and stop. Do not build around a blocked surface.
|
|
1793
|
-
4.
|
|
2203
|
+
4. Call everfur_plan_integration again with the same arguments plus plan_id and page, for page 1 to the summary's
|
|
2204
|
+
pages, for every step's full detail, commands and file bodies (with steps instead of page, call again with steps
|
|
2205
|
+
set to remaining_steps until none remain). Then write exactly the files the plan's steps name and
|
|
2206
|
+
run the install commands it gives, in order.
|
|
1794
2207
|
5. Typecheck the app and fix any error before going on.
|
|
1795
2208
|
6. Call everfur_verify_integration. Report each step's result with its request id.
|
|
1796
2209
|
7. If the capability verdict is empty, call everfur_diagnose and report the reason it gives per capability.
|
|
2210
|
+
8. Report every item of the plan's checklist with the result of its verify. The integration is done only when every
|
|
2211
|
+
item holds.
|
|
1797
2212
|
${sharedRules(options)}`
|
|
1798
2213
|
};
|
|
1799
2214
|
}
|
|
@@ -1804,7 +2219,7 @@ What I see: ${args.symptom.trim()}
|
|
|
1804
2219
|
const code = args.error_code?.trim() ? `call everfur_explain_error with the code ${JSON.stringify(args.error_code.trim())}` : "if a response carried an error code, call everfur_explain_error with that exact code";
|
|
1805
2220
|
const requestId = args.request_id?.trim() ? ` The failing request id was ${JSON.stringify(args.request_id.trim())}; quote it in your report.` : "";
|
|
1806
2221
|
return {
|
|
1807
|
-
description:
|
|
2222
|
+
description: PROMPT_DESCRIPTIONS.diagnoseIntegration,
|
|
1808
2223
|
text: `Diagnose my Everfur integration. Do not change any code until the cause is named.
|
|
1809
2224
|
${symptom}
|
|
1810
2225
|
1. First, ${code}, and follow its retryable flag and next action.
|
|
@@ -1821,9 +2236,11 @@ function registerPrompts(server, provider, options = {}) {
|
|
|
1821
2236
|
const capabilities = contractChoices(provider, CAPABILITIES_TREE, CAPABILITY_QUESTION);
|
|
1822
2237
|
const identityModes = contractChoices(provider, IDENTITY_TREE, IDENTITY_QUESTION);
|
|
1823
2238
|
const embeddings = contractChoices(provider, PLATFORM_TREE, WEB_EMBEDDING_QUESTION);
|
|
1824
|
-
const projectRoot =
|
|
1825
|
-
|
|
1826
|
-
|
|
2239
|
+
const projectRoot = options.sessionSource === "hosted" ? {} : {
|
|
2240
|
+
project_root: z.string().optional().describe(
|
|
2241
|
+
"Absolute path to the project, for a local MCP. A remote MCP cannot read it; pass project_details to everfur_plan_integration instead."
|
|
2242
|
+
)
|
|
2243
|
+
};
|
|
1827
2244
|
const capabilityList = () => completable(
|
|
1828
2245
|
z.string().optional().describe("Comma-separated capabilities, as the contract names them."),
|
|
1829
2246
|
completeList(capabilities)
|
|
@@ -1840,9 +2257,9 @@ function registerPrompts(server, provider, options = {}) {
|
|
|
1840
2257
|
"integrate-web",
|
|
1841
2258
|
{
|
|
1842
2259
|
title: "Integrate Everfur into a web project",
|
|
1843
|
-
description:
|
|
2260
|
+
description: PROMPT_DESCRIPTIONS.integrateWeb,
|
|
1844
2261
|
argsSchema: {
|
|
1845
|
-
|
|
2262
|
+
...projectRoot,
|
|
1846
2263
|
capabilities: capabilityList(),
|
|
1847
2264
|
identity_mode: identityMode(),
|
|
1848
2265
|
embedding: completable(
|
|
@@ -1850,7 +2267,9 @@ function registerPrompts(server, provider, options = {}) {
|
|
|
1850
2267
|
startingWith(embeddings)
|
|
1851
2268
|
),
|
|
1852
2269
|
environment: environment(),
|
|
1853
|
-
origin: z.string().optional().describe(
|
|
2270
|
+
origin: z.string().optional().describe(
|
|
2271
|
+
"The page origin, e.g. https://shop.example.com: where the SDK runs (embedding in-page) or where the frame renders (embedding frame). Verification passes it with the embedding and runs the CORS checks against it; with in-page the frame's own checks (the embed fence and the loader) are reported N/A."
|
|
2272
|
+
)
|
|
1854
2273
|
}
|
|
1855
2274
|
},
|
|
1856
2275
|
(args) => userMessage(integrateWebPrompt(args, options))
|
|
@@ -1859,9 +2278,9 @@ function registerPrompts(server, provider, options = {}) {
|
|
|
1859
2278
|
"integrate-react-native",
|
|
1860
2279
|
{
|
|
1861
2280
|
title: "Integrate Everfur into a React Native app",
|
|
1862
|
-
description:
|
|
2281
|
+
description: PROMPT_DESCRIPTIONS.integrateReactNative,
|
|
1863
2282
|
argsSchema: {
|
|
1864
|
-
|
|
2283
|
+
...projectRoot,
|
|
1865
2284
|
capabilities: capabilityList(),
|
|
1866
2285
|
identity_mode: identityMode(),
|
|
1867
2286
|
environment: environment()
|
|
@@ -1873,7 +2292,7 @@ function registerPrompts(server, provider, options = {}) {
|
|
|
1873
2292
|
"diagnose-integration",
|
|
1874
2293
|
{
|
|
1875
2294
|
title: "Diagnose an Everfur integration",
|
|
1876
|
-
description:
|
|
2295
|
+
description: PROMPT_DESCRIPTIONS.diagnoseIntegration,
|
|
1877
2296
|
argsSchema: {
|
|
1878
2297
|
error_code: completable(
|
|
1879
2298
|
z.string().optional().describe("The error code a response carried, exactly as it appeared."),
|
|
@@ -1919,6 +2338,7 @@ function frameOf(block) {
|
|
|
1919
2338
|
if (typeof obj["status"] === "string") return { kind: `status:${obj["status"]}` };
|
|
1920
2339
|
if (obj["error"] !== void 0 || typeof obj["code"] === "string") return { kind: "error" };
|
|
1921
2340
|
if (obj["done"] === true) return { kind: "done" };
|
|
2341
|
+
if (obj["metadata"] === true) return { kind: "metadata" };
|
|
1922
2342
|
return { kind: "unknown" };
|
|
1923
2343
|
} catch {
|
|
1924
2344
|
return { kind: "malformed" };
|
|
@@ -2140,9 +2560,12 @@ async function sendTestMessage(args, env, ports = {}, identitySources = {}) {
|
|
|
2140
2560
|
"This tool sends a REAL message and only runs against a sandbox tenant. This key resolves to a tenant reporting livemode true, or to a deployment that does not report livemode at all. Refusing: a test message landing in a tenant that holds real veterinary records is not recoverable by deleting it afterwards.",
|
|
2141
2561
|
{
|
|
2142
2562
|
livemode: body.livemode ?? null,
|
|
2143
|
-
// A sandbox is a TENANT,
|
|
2144
|
-
//
|
|
2145
|
-
|
|
2563
|
+
// A sandbox is a TENANT, and the verdict is the authority: a sandbox tenant is issued a pk_test_ key (EFBackend
|
|
2564
|
+
// #1630), an older one may still carry pk_live_, and either way its verdict reports livemode false
|
|
2565
|
+
// (commands/plan/environment.ts).
|
|
2566
|
+
// `everfur sandbox create` is Everfur-internal (it needs an admin token), so it is no partner's remedy; the
|
|
2567
|
+
// documented path is the partner docs' testing page (tester run, 2026-10-01).
|
|
2568
|
+
how_to_fix: 'Use the publishable key of a sandbox tenant: its key starts pk_test_ (a sandbox created before pk_test_ keys may still carry pk_live_), and its entitlements report livemode false. How a sandbox is created, and how a live tenant reaches its paired sandbox, is in docs/partner/10-TESTING.md ("Your sandbox and your live tenant").'
|
|
2146
2569
|
}
|
|
2147
2570
|
);
|
|
2148
2571
|
}
|
|
@@ -2300,10 +2723,21 @@ machine as the project, or with project_details (package dependencies, package m
|
|
|
2300
2723
|
never source files or credentials) when this is a remote MCP that cannot read the project. The plan matches the
|
|
2301
2724
|
generated surface to the project's own theme (a Tailwind v4 @theme block, tailwind.config, :root custom properties,
|
|
2302
2725
|
a React Native theme file), read statically and never executed; a remote MCP passes that text as theme_source.
|
|
2303
|
-
When the theme cannot be read the plan says so in its match-theme step and never guesses one.
|
|
2726
|
+
When the theme cannot be read the plan says so in its match-theme step and never guesses one. A plan comes back as
|
|
2727
|
+
a summary with a plan_id and a number of pages (the goal, the detected facts, each step's id, title, one-line intent
|
|
2728
|
+
and page, and the checklist); call it again with the same arguments plus plan_id and page, for page 1 to pages, for
|
|
2729
|
+
the steps in full, with their commands and the complete body of each file, before writing them. Page 1 also carries
|
|
2730
|
+
the detection, the warnings and the checklist in full, and the principles. (The same arguments plus plan_id and steps,
|
|
2731
|
+
step ids, return those steps instead, a page at a time, and remaining_steps names the rest.) Fetch every page
|
|
2732
|
+
before any command runs or any file is written: each call recomputes the plan from the project, and the plan's own
|
|
2733
|
+
commands and files change it. everfur_plan_integration refuses
|
|
2304
2734
|
underspecified input by naming exactly which facts are missing. When it refuses, ask the developer those specific questions and call it again. Do not guess the
|
|
2305
2735
|
answers and do not proceed without them: an integration built on a guessed identity mode is wrong in a way
|
|
2306
|
-
that only shows up in production.
|
|
2736
|
+
that only shows up in production. Each missing fact carries a fixed question and its options: ask them all in one
|
|
2737
|
+
message, worded as given. When the plan's match-theme step says the theme could not be read, find the host app's
|
|
2738
|
+
brand colours in its own code as that step lists, pass the file's text as theme_source and plan again; never pick a
|
|
2739
|
+
colour. The tenant's console branding (partner console) is the other side of the theme. A ready plan carries a
|
|
2740
|
+
checklist: the integration is finished only when every item holds, each proved by its verify.
|
|
2307
2741
|
|
|
2308
2742
|
Then everfur_verify_integration to exercise the real wire, and everfur_diagnose when a working integration
|
|
2309
2743
|
stops returning capabilities. Verification uses EVERFUR_SESSION_TOKEN, a short-lived session the partner's backend
|
|
@@ -2313,7 +2747,7 @@ integration is verified with identity_mode anonymous instead: the publishable ke
|
|
|
2313
2747
|
nothing minted.
|
|
2314
2748
|
|
|
2315
2749
|
Whenever an Everfur response carries an error code, call everfur_explain_error with that code rather than
|
|
2316
|
-
explaining it yourself. Three naming families are live at once and
|
|
2750
|
+
explaining it yourself; that includes the error.code these tools return. Three naming families are live at once and
|
|
2317
2751
|
one code is the default for four different HTTP statuses, so a code explained from the shape of its string
|
|
2318
2752
|
produces a confident, wrong retry policy. If the tool says the code is unknown, say so; do not fill it in.
|
|
2319
2753
|
|
|
@@ -2330,7 +2764,7 @@ The documented environment is EVERFUR_DOCS_KEY, EVERFUR_API_BASE_URL, EVERFUR_PA
|
|
|
2330
2764
|
EVERFUR_SESSION_TOKEN.`;
|
|
2331
2765
|
function toContent(result) {
|
|
2332
2766
|
return {
|
|
2333
|
-
content: [{ type: "text", text: JSON.stringify(result.document
|
|
2767
|
+
content: [{ type: "text", text: JSON.stringify(result.document) }],
|
|
2334
2768
|
structuredContent: result.document,
|
|
2335
2769
|
isError: result.isError
|
|
2336
2770
|
};
|
|
@@ -2355,36 +2789,36 @@ var TOOL_DESCRIPTIONS = Object.freeze({
|
|
|
2355
2789
|
})
|
|
2356
2790
|
});
|
|
2357
2791
|
var HOSTED_TOOL_DESCRIPTIONS = Object.freeze({
|
|
2358
|
-
verify: "Exercise the real wire as the connected tenant: a short-lived session the Everfur API mints for a synthetic test user of a sandbox tenant, the entitlement verdict read with it, and the assertion that the verdict is not empty; optionally the chat, records, televet and theme surfaces and, with origin, the web checks. On a live tenant the signed session step is reported GATED. With identity_mode anonymous it verifies the anonymous path instead (the tenant's publishable key and a device session id derived for a synthetic test user), which is the read-only check a live tenant takes: GET requests only, with nothing minted and nothing written. Returns PASS/SKIP/FAIL/GATED per step with the request id of the call that produced it. Mints a session, and on a sandbox tenant only a real Televet handoff, so it is not read-only.",
|
|
2792
|
+
verify: "Exercise the real wire as the connected tenant: a short-lived session the Everfur API mints for a synthetic test user of a sandbox tenant, the entitlement verdict read with it, and the assertion that the verdict is not empty; optionally the chat, records, televet and theme surfaces and, with origin and embedding, the web checks of the page (the frame's embed fence and loader are N/A for embedding in-page). On a live tenant with a paired sandbox the session is minted on that sandbox and the call uses its publishable key; on a live tenant with no pair the signed session step is reported GATED. With identity_mode anonymous it verifies the anonymous path instead (the tenant's publishable key and a device session id derived for a synthetic test user), which is the read-only check a live tenant takes: GET requests only, with nothing minted and nothing written. Returns PASS/SKIP/FAIL/GATED/N/A per step with the request id of the call that produced it. Mints a session, and on a sandbox tenant only a real Televet handoff, so it is not read-only.",
|
|
2359
2793
|
diagnose: "Separate the four byte-identical causes of an empty entitlement verdict for the connected tenant, with its publishable key. Read-only. Reports honestly when the deployment does not serve the diagnostics route rather than relaying a bare 404. With bundle: true, returns one support bundle (SDK and CLI versions, contract digest, credential presence without values, and the diagnosis request id) whatever state the integration is in.",
|
|
2360
|
-
sendTestMessage: "Send a REAL message and stream a real veterinary answer as a synthetic test user of the connected tenant, a SANDBOX tenant only: the session is one the Everfur API mints for everfur-verify- and a stable id derived from idempotency_key, and a live tenant is refused before anything is sent. Returns the answer plus a frame histogram, so a stream that died mid-answer is distinguishable from a short one. With pet_profile it registers a sandbox pet first so the answer is pet-aware. Calling again with the same idempotency_key reuses that conversation and returns the existing answer instead of sending a second message."
|
|
2794
|
+
sendTestMessage: "Send a REAL message and stream a real veterinary answer as a synthetic test user of the connected tenant, a SANDBOX tenant only: the session is one the Everfur API mints for everfur-verify- and a stable id derived from idempotency_key, and a live tenant with no paired sandbox is refused before anything is sent (a paired one runs on its sandbox). Returns the answer plus a frame histogram, so a stream that died mid-answer is distinguishable from a short one. With pet_profile it registers a sandbox pet first so the answer is pet-aware. Calling again with the same idempotency_key reuses that conversation and returns the existing answer instead of sending a second message."
|
|
2361
2795
|
});
|
|
2362
2796
|
var EFFECT_HINTS = Object.freeze({
|
|
2363
2797
|
reads: Object.freeze({ readOnlyHint: true, destructiveHint: false, idempotentHint: true }),
|
|
2364
|
-
writes_sandbox: Object.freeze({ readOnlyHint: false, destructiveHint:
|
|
2365
|
-
writes_live: Object.freeze({ readOnlyHint: false, destructiveHint:
|
|
2798
|
+
writes_sandbox: Object.freeze({ readOnlyHint: false, destructiveHint: true, idempotentHint: false }),
|
|
2799
|
+
writes_live: Object.freeze({ readOnlyHint: false, destructiveHint: true, idempotentHint: false }),
|
|
2366
2800
|
destroys_live: Object.freeze({ readOnlyHint: false, destructiveHint: true, idempotentHint: false })
|
|
2367
2801
|
});
|
|
2368
2802
|
var TOOL_DECLARATIONS = Object.freeze({
|
|
2369
|
-
everfur_plan_integration: { title: "Plan an Everfur integration", effect: "reads" },
|
|
2370
|
-
everfur_verify_integration: { title: "Verify the wire end to end", effect: "writes_sandbox" },
|
|
2371
|
-
everfur_diagnose: { title: "Explain why a capability is unavailable", effect: "reads" },
|
|
2372
|
-
everfur_explain_error: { title: "Explain an Everfur API error code", effect: "reads" },
|
|
2373
|
-
everfur_send_test_message: { title: "Send a real message against a sandbox tenant", effect: "writes_sandbox" },
|
|
2374
|
-
everfur_get_tenant_status: { title: "Read the tenant status", effect: "reads" },
|
|
2375
|
-
everfur_list_webhook_endpoints: { title: "List webhook endpoints", effect: "reads" },
|
|
2376
|
-
everfur_list_webhook_deliveries: { title: "List webhook deliveries", effect: "reads" },
|
|
2377
|
-
everfur_get_webhook_delivery: { title: "Read one webhook delivery", effect: "reads" },
|
|
2378
|
-
everfur_list_events: { title: "List emitted events", effect: "reads" },
|
|
2379
|
-
everfur_get_usage_summary: { title: "Read the usage summary", effect: "reads" },
|
|
2380
|
-
everfur_get_records_status_summary: { title: "Count records requests by status", effect: "reads" },
|
|
2381
|
-
everfur_search_docs: { title: "Search the Everfur docs", effect: "reads" }
|
|
2803
|
+
everfur_plan_integration: { title: "Plan an Everfur integration", effect: "reads", openWorld: false },
|
|
2804
|
+
everfur_verify_integration: { title: "Verify the wire end to end", effect: "writes_sandbox", openWorld: true },
|
|
2805
|
+
everfur_diagnose: { title: "Explain why a capability is unavailable", effect: "reads", openWorld: false },
|
|
2806
|
+
everfur_explain_error: { title: "Explain an Everfur API error code", effect: "reads", openWorld: false },
|
|
2807
|
+
everfur_send_test_message: { title: "Send a real message against a sandbox tenant", effect: "writes_sandbox", openWorld: false },
|
|
2808
|
+
everfur_get_tenant_status: { title: "Read the tenant status", effect: "reads", openWorld: false },
|
|
2809
|
+
everfur_list_webhook_endpoints: { title: "List webhook endpoints", effect: "reads", openWorld: false },
|
|
2810
|
+
everfur_list_webhook_deliveries: { title: "List webhook deliveries", effect: "reads", openWorld: false },
|
|
2811
|
+
everfur_get_webhook_delivery: { title: "Read one webhook delivery", effect: "reads", openWorld: false },
|
|
2812
|
+
everfur_list_events: { title: "List emitted events", effect: "reads", openWorld: false },
|
|
2813
|
+
everfur_get_usage_summary: { title: "Read the usage summary", effect: "reads", openWorld: false },
|
|
2814
|
+
everfur_get_records_status_summary: { title: "Count records requests by status", effect: "reads", openWorld: false },
|
|
2815
|
+
everfur_search_docs: { title: "Search the Everfur docs", effect: "reads", openWorld: false }
|
|
2382
2816
|
});
|
|
2383
2817
|
var TOOL_ANNOTATIONS = Object.freeze(
|
|
2384
2818
|
Object.fromEntries(
|
|
2385
2819
|
Object.entries(TOOL_DECLARATIONS).map(([name, declared]) => [
|
|
2386
2820
|
name,
|
|
2387
|
-
Object.freeze({ title: declared.title, ...EFFECT_HINTS[declared.effect], openWorldHint:
|
|
2821
|
+
Object.freeze({ title: declared.title, ...EFFECT_HINTS[declared.effect], openWorldHint: declared.openWorld })
|
|
2388
2822
|
])
|
|
2389
2823
|
)
|
|
2390
2824
|
);
|
|
@@ -2431,18 +2865,20 @@ function buildServer(context, ports = {}, options = {}) {
|
|
|
2431
2865
|
"everfur_plan_integration",
|
|
2432
2866
|
{
|
|
2433
2867
|
title: TOOL_ANNOTATIONS.everfur_plan_integration.title,
|
|
2434
|
-
description: `Produce an integration plan for a project. REFUSES underspecified input by naming the missing facts rather than guessing them, and REFUSES a project shape the SDK ships no entry point for (PLATFORM_NOT_SUPPORTED) or a capability set the contract blocks outright (CAPABILITIES_BLOCKED). Read-only. ${describe.planContract}`,
|
|
2868
|
+
description: `Produce an integration plan for a project. REFUSES underspecified input by naming the missing facts rather than guessing them, and REFUSES a project shape the SDK ships no entry point for (PLATFORM_NOT_SUPPORTED) or a capability set the contract blocks outright (CAPABILITIES_BLOCKED). Returns a small summary with a plan_id and a number of pages: the goal, the detected facts, each step's id, title, one-line intent and page, and the checklist. The same arguments plus plan_id and page (1 to pages) return that page: its steps in full, with their commands and the complete body of each file they write, and on page 1 the detection, warnings and checklist in full. With steps (step ids) instead of page, those steps come a page at a time and remaining_steps names the rest. Each call recomputes the plan from the project, which the plan's own commands and files change, so every page is fetched before any command runs or any file is written. Read-only. ${describe.planContract}`,
|
|
2435
2869
|
inputSchema: argumentsSchema(PLAN_INPUT_SHAPE, HOSTED_PLAN_INPUT_SHAPE, readsLocalFiles),
|
|
2436
2870
|
outputSchema: PLAN_OUTPUT_SHAPE,
|
|
2437
2871
|
annotations: { ...TOOL_ANNOTATIONS.everfur_plan_integration }
|
|
2438
2872
|
},
|
|
2439
|
-
|
|
2873
|
+
// The planner computes the full plan, as the CLI prints it; what the wire carries is its summary or the steps asked
|
|
2874
|
+
// for (plan-wire.ts).
|
|
2875
|
+
(args, extra) => run("everfur_plan_integration", args, extra.authInfo, async (resolved) => planOnTheWire(await planIntegrationTool(args, resolved, shared), args))
|
|
2440
2876
|
);
|
|
2441
2877
|
server.registerTool(
|
|
2442
2878
|
"everfur_verify_integration",
|
|
2443
2879
|
{
|
|
2444
2880
|
title: TOOL_ANNOTATIONS.everfur_verify_integration.title,
|
|
2445
|
-
description: hosted ? HOSTED_TOOL_DESCRIPTIONS.verify : "Mint a session, poll entitlements, and hard-assert the verdict is not empty; optionally exercise the chat, records, televet and theme surfaces and check the project on disk. With identity_mode anonymous it verifies the anonymous path instead (the publishable key and a derived session id, the conversation list, and whether the tenant requires signed sessions). Returns PASS/SKIP/FAIL/GATED per step with the request id of the call that produced it. Mints a real session, and on a sandbox tenant only a real Televet handoff, so it is not read-only; it sends no write to a live tenant.",
|
|
2881
|
+
description: hosted ? HOSTED_TOOL_DESCRIPTIONS.verify : "Mint a session, poll entitlements, and hard-assert the verdict is not empty; optionally exercise the chat, records, televet and theme surfaces and check the project on disk. With identity_mode anonymous it verifies the anonymous path instead (the publishable key and a derived session id, the conversation list, and whether the tenant requires signed sessions). Returns PASS/SKIP/FAIL/GATED/N/A per step with the request id of the call that produced it. Mints a real session, and on a sandbox tenant only a real Televet handoff, so it is not read-only; it sends no write to a live tenant.",
|
|
2446
2882
|
inputSchema: argumentsSchema(VERIFY_INPUT_SHAPE, HOSTED_VERIFY_INPUT_SHAPE, readsLocalFiles),
|
|
2447
2883
|
outputSchema: VERIFY_OUTPUT_SHAPE,
|
|
2448
2884
|
annotations: { ...TOOL_ANNOTATIONS.everfur_verify_integration }
|
|
@@ -2464,7 +2900,7 @@ function buildServer(context, ports = {}, options = {}) {
|
|
|
2464
2900
|
"everfur_explain_error",
|
|
2465
2901
|
{
|
|
2466
2902
|
title: TOOL_ANNOTATIONS.everfur_explain_error.title,
|
|
2467
|
-
description: `Explain
|
|
2903
|
+
description: `Explain an error code: cause, whether it is retryable, and the next action. A wire code is read from the registry in the contract. ${describe.explainContract} The SDK's own codes (EverfurError and its closed sub-enums such as AttachmentErrorCode and VetVisitErrorReason), the diagnostics reason codes, the CLI's error codes and the error.code these tools return (VERIFICATION_FAILED, PLAN_CHANGED and the rest) are answered offline. Needs no project. REFUSES a code the registry does not list, with near misses, rather than inventing an explanation from the shape of the string.`,
|
|
2468
2904
|
inputSchema: argumentsSchema(EXPLAIN_ERROR_INPUT_SHAPE, HOSTED_EXPLAIN_ERROR_INPUT_SHAPE, readsLocalFiles),
|
|
2469
2905
|
outputSchema: EXPLAIN_ERROR_OUTPUT_SHAPE,
|
|
2470
2906
|
annotations: { ...TOOL_ANNOTATIONS.everfur_explain_error }
|