@everfur/cli 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +1 -0
- package/README.md +58 -11
- package/SDK-LICENSE.txt +129 -0
- package/THIRD-PARTY-NOTICES.md +21 -0
- package/dist/chunk-J7SL4FA7.js +10368 -0
- package/dist/{chunk-AEY522A6.js → chunk-JI6USLIZ.js} +3 -2
- package/dist/{chunk-WPEB2AVZ.js → chunk-OHA3JVME.js} +359 -38
- package/dist/{chunk-U4Q4OAVK.js → chunk-UROEKBHB.js} +2 -1
- package/dist/{chunk-4Q2SPVPA.js → chunk-UZZJUV5A.js} +23 -3
- package/dist/everfur-mcp-hosted.d.ts +1 -2
- package/dist/everfur-mcp-hosted.js +5 -4
- package/dist/everfur-mcp-http.d.ts +1 -2
- package/dist/everfur-mcp-http.js +4 -3
- package/dist/everfur-mcp.d.ts +1 -2
- package/dist/everfur-mcp.js +10 -3
- package/dist/everfur.d.ts +1 -2
- package/dist/everfur.js +3 -2
- package/dist/index.d.ts +8 -4
- package/dist/index.js +3 -2
- package/package.json +9 -5
- package/LICENSE +0 -21
- package/dist/chunk-EDURPYNG.js +0 -2976
package/dist/chunk-EDURPYNG.js
DELETED
|
@@ -1,2976 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
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-WPEB2AVZ.js';
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
-
import { isAbsolute, join, dirname, basename } from 'path';
|
|
7
|
-
import { createHash } from 'crypto';
|
|
8
|
-
import { InvalidTokenError, TooManyRequestsError, TemporarilyUnavailableError } from '@modelcontextprotocol/sdk/server/auth/errors.js';
|
|
9
|
-
import { completable } from '@modelcontextprotocol/sdk/server/completable.js';
|
|
10
|
-
|
|
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) {
|
|
45
|
-
return {
|
|
46
|
-
detected: plan.detected,
|
|
47
|
-
warnings: plan.warnings,
|
|
48
|
-
principles: plan.principles.map(principleByTitle),
|
|
49
|
-
...plan.checklist === void 0 ? {} : { checklist: plan.checklist }
|
|
50
|
-
};
|
|
51
|
-
}
|
|
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;
|
|
81
|
-
return {
|
|
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 } }
|
|
89
|
-
};
|
|
90
|
-
}
|
|
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;
|
|
145
|
-
return {
|
|
146
|
-
isError: true,
|
|
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 } }
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// src/mcp/tools.ts
|
|
226
|
-
var DEFAULT_VERIFY_USER_REF = "everfur-mcp-verify";
|
|
227
|
-
var HOSTED_NO_PUBLISHABLE_KEY = Object.freeze({
|
|
228
|
-
message: "The connected tenant has no active publishable key, so this call was not made.",
|
|
229
|
-
howToFix: Object.freeze([
|
|
230
|
-
"No publishable key yet. Contact support@everfur.com to finish onboarding.",
|
|
231
|
-
"The API keys tab of the partner console lists this tenant's publishable keys."
|
|
232
|
-
])
|
|
233
|
-
});
|
|
234
|
-
function publishableKeyShape() {
|
|
235
|
-
const prefixes = [PARTNER_KEY.prefix, ...PARTNER_KEY.alsoAccepts ?? []].filter((prefix) => prefix !== null);
|
|
236
|
-
return `${prefixes.join(" or ")}, ${PARTNER_KEY.length ?? "a fixed number of"} characters in all`;
|
|
237
|
-
}
|
|
238
|
-
function hostedPublishableKeyRefusal(env) {
|
|
239
|
-
if (resolveCredential(env, PARTNER_KEY).status === "malformed") {
|
|
240
|
-
return refusal(
|
|
241
|
-
"TENANT_PUBLISHABLE_KEY_UNUSABLE",
|
|
242
|
-
`The connected tenant's publishable key does not have the shape this server sends (${publishableKeyShape()}), so this call was not made.`,
|
|
243
|
-
{
|
|
244
|
-
reason: "malformed",
|
|
245
|
-
how_to_fix: ["Everfur issues this tenant's publishable keys; the partner console names support@everfur.com for them."]
|
|
246
|
-
}
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
return refusal("TENANT_PUBLISHABLE_KEY_UNUSABLE", HOSTED_NO_PUBLISHABLE_KEY.message, {
|
|
250
|
-
reason: "missing",
|
|
251
|
-
how_to_fix: [...HOSTED_NO_PUBLISHABLE_KEY.howToFix]
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
var SYNTHETIC_USER_REF_PREFIX = "everfur-verify-";
|
|
255
|
-
function hostedVerifyUserRef(requested) {
|
|
256
|
-
const seed = requested === void 0 || requested.trim() === "" ? DEFAULT_VERIFY_USER_REF : requested;
|
|
257
|
-
return `${SYNTHETIC_USER_REF_PREFIX}${uuidFromKey(`everfur_verify_integration:${seed}`)}`;
|
|
258
|
-
}
|
|
259
|
-
function contractProviderFor(context, ports = {}) {
|
|
260
|
-
return ports.contract ?? (() => fetchContract({
|
|
261
|
-
env: context.env,
|
|
262
|
-
transport: ports.transport ?? fetchTransport,
|
|
263
|
-
clock: ports.clock ?? systemClock,
|
|
264
|
-
cache: ports.contractCache ?? NO_CONTRACT_CACHE
|
|
265
|
-
}));
|
|
266
|
-
}
|
|
267
|
-
function contractRefusal(load, extra = {}) {
|
|
268
|
-
const details = { how_to_fix: [...load.details], request_id: load.requestId, ...extra };
|
|
269
|
-
switch (load.reason) {
|
|
270
|
-
case "credential_missing":
|
|
271
|
-
return refusal("CONTRACT_CREDENTIAL_MISSING", `${load.message}.`, details);
|
|
272
|
-
case "credential_malformed":
|
|
273
|
-
case "credential_rejected":
|
|
274
|
-
return refusal("CONTRACT_CREDENTIAL_UNUSABLE", `${load.message}.`, { ...details, status: load.status });
|
|
275
|
-
case "endpoint_missing":
|
|
276
|
-
return refusal("WIRE_NOT_CONFIGURED", `${load.message}.`, { errors: [...load.details], ...extra });
|
|
277
|
-
case "unreachable":
|
|
278
|
-
return refusal("CONTRACT_UNAVAILABLE", `${load.message}.`, { ...details, kind: "network" });
|
|
279
|
-
case "remote_error":
|
|
280
|
-
case "invalid_contract":
|
|
281
|
-
return refusal("CONTRACT_UNAVAILABLE", `${load.message}.`, { ...details, status: load.status });
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
var CAPABILITY = z.string().min(1);
|
|
285
|
-
var IDENTITY_MODE_ALIAS = "personalized";
|
|
286
|
-
var IDENTITY_MODE = z.string().min(1);
|
|
287
|
-
function canonicalIdentityMode(mode) {
|
|
288
|
-
return mode === IDENTITY_MODE_ALIAS ? "partner-user-ref" : mode;
|
|
289
|
-
}
|
|
290
|
-
var MINT_LANGUAGE = z.string().min(1);
|
|
291
|
-
var EMBEDDING = z.string().min(1);
|
|
292
|
-
function unknownOptions(space, args) {
|
|
293
|
-
const problems = [];
|
|
294
|
-
const check = (argument, values, treeId, questionId, extra = []) => {
|
|
295
|
-
const allowed = [...space.choices(treeId, questionId), ...extra];
|
|
296
|
-
if (allowed.length === extra.length) return;
|
|
297
|
-
for (const value of values) {
|
|
298
|
-
if (!allowed.includes(value)) problems.push(`${argument}: "${value}" (supported: ${allowed.join(", ")})`);
|
|
299
|
-
}
|
|
300
|
-
};
|
|
301
|
-
check("capabilities", args.capabilities ?? [], CAPABILITIES_TREE, CAPABILITY_QUESTION, [ALL_CAPABILITIES]);
|
|
302
|
-
const identity = canonicalIdentityMode(args.identity_mode ?? null);
|
|
303
|
-
check("identity_mode", identity === null ? [] : [identity], IDENTITY_TREE, IDENTITY_QUESTION);
|
|
304
|
-
check("session_mint_language", args.session_mint_language === void 0 ? [] : [args.session_mint_language], SESSION_MINT_TREE, BACKEND_LANGUAGE_QUESTION);
|
|
305
|
-
check("embedding", args.embedding === void 0 ? [] : [args.embedding], PLATFORM_TREE, WEB_EMBEDDING_QUESTION);
|
|
306
|
-
return problems;
|
|
307
|
-
}
|
|
308
|
-
function clientFor(context, ports = {}) {
|
|
309
|
-
const endpoint = resolveEndpoint(null, context.env);
|
|
310
|
-
if (endpoint.ok === false) return { ok: false, errors: endpoint.errors };
|
|
311
|
-
return {
|
|
312
|
-
ok: true,
|
|
313
|
-
client: createApiClient({
|
|
314
|
-
transport: ports.transport ?? fetchTransport,
|
|
315
|
-
clock: ports.clock ?? systemClock,
|
|
316
|
-
endpoint: endpoint.endpoint
|
|
317
|
-
})
|
|
318
|
-
};
|
|
319
|
-
}
|
|
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) {
|
|
332
|
-
switch (fact) {
|
|
333
|
-
case "decisionTrees":
|
|
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";
|
|
335
|
-
case "capabilities": {
|
|
336
|
-
const plannable = space === null ? [] : plannableCapabilities(space);
|
|
337
|
-
const blocked = space === null ? [] : blockedCapabilities(space);
|
|
338
|
-
const choices = plannable.length === 0 ? 'pass `capabilities`, e.g. ["chat"] or ["chat","records"]' : `pass \`capabilities\`: one or more of ${plannable.join(", ")} (the ids the contract can plan today${blocked.length === 0 ? "" : `; blocked today: ${blocked.join(", ")}, which a plan refuses with the reason and where to ask`}), or ["all"] for every plannable one`;
|
|
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`;
|
|
340
|
-
}
|
|
341
|
-
case "identityMode":
|
|
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");
|
|
343
|
-
case "sessionMintLanguage":
|
|
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)";
|
|
345
|
-
case "embedding":
|
|
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';
|
|
347
|
-
case "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';
|
|
349
|
-
case "projectType":
|
|
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
|
-
}
|
|
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";
|
|
356
|
-
default:
|
|
357
|
-
return "supply this fact as a tool argument of the same name";
|
|
358
|
-
}
|
|
359
|
-
}
|
|
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 = [
|
|
391
|
-
"app.json",
|
|
392
|
-
"app.config.js",
|
|
393
|
-
"app.config.ts",
|
|
394
|
-
"ios/",
|
|
395
|
-
"android/",
|
|
396
|
-
"index.html",
|
|
397
|
-
"public/index.html",
|
|
398
|
-
"layout/theme.liquid",
|
|
399
|
-
"shopify.theme.toml",
|
|
400
|
-
"shopify.app.toml",
|
|
401
|
-
"app/",
|
|
402
|
-
"src/app/",
|
|
403
|
-
"pages/",
|
|
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",
|
|
410
|
-
"package-lock.json",
|
|
411
|
-
"npm-shrinkwrap.json",
|
|
412
|
-
"pnpm-lock.yaml",
|
|
413
|
-
"yarn.lock",
|
|
414
|
-
"bun.lockb",
|
|
415
|
-
"bun.lock"
|
|
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();
|
|
438
|
-
var REMOTE_DEPENDENCY_MAP = z.record(z.string().min(1).max(128), z.string().min(1).max(256));
|
|
439
|
-
var REMOTE_PROJECT_DETAILS = z.object({
|
|
440
|
-
/** A display label only. It is never treated as a path by the remote MCP. */
|
|
441
|
-
label: z.string().min(1).max(120).optional(),
|
|
442
|
-
package_json: z.object({
|
|
443
|
-
dependencies: REMOTE_DEPENDENCY_MAP.optional(),
|
|
444
|
-
dev_dependencies: REMOTE_DEPENDENCY_MAP.optional(),
|
|
445
|
-
peer_dependencies: REMOTE_DEPENDENCY_MAP.optional(),
|
|
446
|
-
package_manager: z.string().min(1).max(128).optional()
|
|
447
|
-
}).strict().optional(),
|
|
448
|
-
pubspec: z.object({
|
|
449
|
-
dart_sdk: z.string().min(1).max(128).optional(),
|
|
450
|
-
flutter_sdk: z.string().min(1).max(128).optional(),
|
|
451
|
-
dependencies: REMOTE_DEPENDENCY_MAP.optional()
|
|
452
|
-
}).strict().optional(),
|
|
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
|
-
*/
|
|
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(),
|
|
461
|
-
/** Optional resolved versions for the small dependency report; never needed to select an integration path. */
|
|
462
|
-
installed_versions: REMOTE_DEPENDENCY_MAP.optional()
|
|
463
|
-
}).strict();
|
|
464
|
-
var REMOTE_ROOT = "/everfur-remote-project";
|
|
465
|
-
function detectRemoteProject(details) {
|
|
466
|
-
const files = /* @__PURE__ */ new Map();
|
|
467
|
-
const directories = /* @__PURE__ */ new Set([REMOTE_ROOT]);
|
|
468
|
-
if (details.package_json !== void 0) {
|
|
469
|
-
files.set(join(REMOTE_ROOT, "package.json"), JSON.stringify({
|
|
470
|
-
dependencies: details.package_json.dependencies ?? {},
|
|
471
|
-
devDependencies: details.package_json.dev_dependencies ?? {},
|
|
472
|
-
peerDependencies: details.package_json.peer_dependencies ?? {},
|
|
473
|
-
packageManager: details.package_json.package_manager
|
|
474
|
-
}));
|
|
475
|
-
}
|
|
476
|
-
if (details.pubspec !== void 0) {
|
|
477
|
-
files.set(join(REMOTE_ROOT, "pubspec.yaml"), JSON.stringify({
|
|
478
|
-
environment: {
|
|
479
|
-
sdk: details.pubspec.dart_sdk,
|
|
480
|
-
flutter: details.pubspec.flutter_sdk
|
|
481
|
-
},
|
|
482
|
-
dependencies: details.pubspec.dependencies ?? {}
|
|
483
|
-
}));
|
|
484
|
-
}
|
|
485
|
-
for (const marker of details.files ?? []) {
|
|
486
|
-
const path = join(REMOTE_ROOT, marker.replace(/\/+$/, ""));
|
|
487
|
-
if (marker.endsWith("/")) directories.add(path);
|
|
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), "");
|
|
498
|
-
}
|
|
499
|
-
for (const [name, version] of Object.entries(details.installed_versions ?? {})) {
|
|
500
|
-
files.set(join(REMOTE_ROOT, "node_modules", ...name.split("/"), "package.json"), JSON.stringify({ version }));
|
|
501
|
-
}
|
|
502
|
-
const fs = Object.freeze({
|
|
503
|
-
readFile(path) {
|
|
504
|
-
return files.get(path) ?? null;
|
|
505
|
-
},
|
|
506
|
-
exists(path) {
|
|
507
|
-
return files.has(path) || directories.has(path);
|
|
508
|
-
},
|
|
509
|
-
isDirectory(path) {
|
|
510
|
-
return directories.has(path);
|
|
511
|
-
},
|
|
512
|
-
listDirectory() {
|
|
513
|
-
return Object.freeze([]);
|
|
514
|
-
}
|
|
515
|
-
});
|
|
516
|
-
const detection = detectProject(fs, REMOTE_ROOT);
|
|
517
|
-
const label = details.label === void 0 ? "remote project metadata" : `remote project metadata (${details.label})`;
|
|
518
|
-
return Object.freeze({
|
|
519
|
-
...detection,
|
|
520
|
-
root: `[${label}]`,
|
|
521
|
-
projectTypeEvidence: Object.freeze([
|
|
522
|
-
"detected from the remote manifest-and-marker projection; Everfur did not read source files or a local path",
|
|
523
|
-
...detection.projectTypeEvidence
|
|
524
|
-
]),
|
|
525
|
-
packageManagerEvidence: Object.freeze([
|
|
526
|
-
"detected from the remote manifest-and-marker projection",
|
|
527
|
-
...detection.packageManagerEvidence
|
|
528
|
-
])
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
function planDetection(args) {
|
|
532
|
-
if (args.project_root !== void 0 && args.project_details !== void 0) {
|
|
533
|
-
return {
|
|
534
|
-
ok: false,
|
|
535
|
-
result: refusal(
|
|
536
|
-
"PROJECT_SOURCE_AMBIGUOUS",
|
|
537
|
-
"Pass exactly one project source. `project_root` is for a local MCP; `project_details` is for a remote MCP.",
|
|
538
|
-
{ next_action: "fix_arguments" }
|
|
539
|
-
)
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
if (args.project_root === void 0 && args.project_details === void 0) {
|
|
543
|
-
return {
|
|
544
|
-
ok: false,
|
|
545
|
-
result: refusal(
|
|
546
|
-
"PROJECT_SOURCE_REQUIRED",
|
|
547
|
-
"Pass `project_root` to a local MCP or `project_details` to a remote MCP. project_details carries no source file and no credential.",
|
|
548
|
-
{ next_action: "fix_arguments" }
|
|
549
|
-
)
|
|
550
|
-
};
|
|
551
|
-
}
|
|
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) };
|
|
566
|
-
}
|
|
567
|
-
var THEME_SOURCE_MAX_BYTES = 256 * 1024;
|
|
568
|
-
var PLAN_INPUT_SHAPE = {
|
|
569
|
-
project_root: z.string().optional().describe("For a local MCP only: absolute path to the application root. Pass exactly one of this or project_details."),
|
|
570
|
-
project_details: REMOTE_PROJECT_DETAILS.optional().describe("For a remote MCP only: a safe projection of package dependencies, package manager, and fixed file-presence markers. Pass exactly one of this or project_root; never send source files, env files, or credentials."),
|
|
571
|
-
theme_source: z.string().min(1).max(THEME_SOURCE_MAX_BYTES).optional().describe(
|
|
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."
|
|
573
|
-
),
|
|
574
|
-
capabilities: z.array(CAPABILITY).optional().describe(
|
|
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.'
|
|
576
|
-
),
|
|
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.'),
|
|
578
|
-
session_mint_language: MINT_LANGUAGE.optional().describe("Backend language that will mint sessions. Only meaningful when identity_mode is partner-user-ref."),
|
|
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.`),
|
|
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
|
|
583
|
-
};
|
|
584
|
-
var HOSTED_PLAN_INPUT_SHAPE = {
|
|
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."),
|
|
586
|
-
theme_source: PLAN_INPUT_SHAPE.theme_source.describe(
|
|
587
|
-
"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. A theme the plan cannot read is named in its match-theme step as missing facts, never guessed."
|
|
588
|
-
),
|
|
589
|
-
capabilities: PLAN_INPUT_SHAPE.capabilities,
|
|
590
|
-
identity_mode: PLAN_INPUT_SHAPE.identity_mode,
|
|
591
|
-
session_mint_language: PLAN_INPUT_SHAPE.session_mint_language,
|
|
592
|
-
embedding: PLAN_INPUT_SHAPE.embedding,
|
|
593
|
-
// Owner-approved 2026-10-01 (production sandbox plan section 10: the CLI environment text).
|
|
594
|
-
environment: PLAN_INPUT_SHAPE.environment.describe(
|
|
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
|
|
598
|
-
};
|
|
599
|
-
function apiHostOf(apiBaseUrl) {
|
|
600
|
-
try {
|
|
601
|
-
return new URL(apiBaseUrl).hostname;
|
|
602
|
-
} catch {
|
|
603
|
-
return null;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
function environmentOfApi(apiBaseUrl) {
|
|
607
|
-
if (apiBaseUrl === void 0) return null;
|
|
608
|
-
const host = apiHostOf(apiBaseUrl);
|
|
609
|
-
if (host === null) return null;
|
|
610
|
-
if (host === apiHostOf(API_BASE_URL_BY_ENVIRONMENT.staging)) return "staging";
|
|
611
|
-
if (host === apiHostOf(API_BASE_URL_BY_ENVIRONMENT.production)) return "production";
|
|
612
|
-
return null;
|
|
613
|
-
}
|
|
614
|
-
function hostedEnvironment(requested, context) {
|
|
615
|
-
if (context.credentialSource !== "hosted") return { ok: true, environment: requested, fromTenant: false };
|
|
616
|
-
const connectedApi = context.env[BASE_URL_ENV_VAR];
|
|
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;
|
|
621
|
-
if (requested === null) return { ok: true, environment: connected, fromTenant: true };
|
|
622
|
-
const requestedApi = API_BASE_URL_BY_ENVIRONMENT[requested];
|
|
623
|
-
const requestedHost = apiHostOf(requestedApi) ?? requestedApi;
|
|
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";
|
|
647
|
-
return {
|
|
648
|
-
ok: false,
|
|
649
|
-
result: refusal(
|
|
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.`,
|
|
652
|
-
{
|
|
653
|
-
environment: requested,
|
|
654
|
-
connected_host: connectedHost,
|
|
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,
|
|
689
|
-
how_to_fix: [
|
|
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`
|
|
692
|
-
]
|
|
693
|
-
}
|
|
694
|
-
)
|
|
695
|
-
};
|
|
696
|
-
}
|
|
697
|
-
async function planIntegrationTool(args, context, ports = {}) {
|
|
698
|
-
const detected = planDetection(args);
|
|
699
|
-
if (detected.ok === false) return detected.result;
|
|
700
|
-
const environment = hostedEnvironment(args.environment ?? null, context);
|
|
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;
|
|
704
|
-
const contractPath = args.contract_path ?? context.env["EVERFUR_CONTRACT_PUBLIC"] ?? null;
|
|
705
|
-
let load;
|
|
706
|
-
if (contractPath === null) {
|
|
707
|
-
const fetched = await contractProviderFor(context, ports)();
|
|
708
|
-
if (fetched.kind === "unavailable") return contractRefusal(fetched);
|
|
709
|
-
load = loadDecisionSpaceFromContract(fetched.contract);
|
|
710
|
-
} else {
|
|
711
|
-
load = loadDecisionSpace(nodeFileSystem, contractPath);
|
|
712
|
-
}
|
|
713
|
-
if (load.kind === "loaded") {
|
|
714
|
-
const problems = unknownOptions(load.space, args);
|
|
715
|
-
if (problems.length > 0) {
|
|
716
|
-
return refusal(
|
|
717
|
-
"UNKNOWN_OPTION",
|
|
718
|
-
"One or more arguments name a value the contract does not publish; the values it supports are listed with each.",
|
|
719
|
-
{ unknown: problems }
|
|
720
|
-
);
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
const space = load !== null && load.kind === "loaded" ? load.space : null;
|
|
724
|
-
const expanded = args.capabilities ? expandCapabilities([...args.capabilities], space) : null;
|
|
725
|
-
const theme = args.theme_source === void 0 ? void 0 : detectThemeFromText(args.theme_source, "theme_source");
|
|
726
|
-
const input = {
|
|
727
|
-
detection: detected.detection,
|
|
728
|
-
capabilities: expanded === null ? null : [...expanded.capabilities],
|
|
729
|
-
allCapabilities: expanded?.expanded ?? false,
|
|
730
|
-
identityMode: canonicalIdentityMode(args.identity_mode ?? null),
|
|
731
|
-
sessionMintLanguage: args.session_mint_language ?? null,
|
|
732
|
-
embedding: args.embedding ?? null,
|
|
733
|
-
environment: environment.environment,
|
|
734
|
-
space,
|
|
735
|
-
cliVersion: CLI_VERSION,
|
|
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 } : {}
|
|
739
|
-
};
|
|
740
|
-
const plan = planIntegration(input);
|
|
741
|
-
if (plan.platformBlock) {
|
|
742
|
-
return refusal(
|
|
743
|
-
"PLATFORM_NOT_SUPPORTED",
|
|
744
|
-
"No plan can be produced for this project shape, whatever facts or capabilities are supplied. The React Native package is not a substitute for it and no other integration exists for it; the reason, who can unblock it and where to ask are below.",
|
|
745
|
-
{
|
|
746
|
-
project_type: plan.detected.projectType,
|
|
747
|
-
project_type_evidence: plan.detected.projectTypeEvidence,
|
|
748
|
-
reason: plan.platformBlock.reason,
|
|
749
|
-
unblock_owner: plan.platformBlock.unblockOwner,
|
|
750
|
-
request_path: plan.platformBlock.requestPath,
|
|
751
|
-
blocked: plan.blocked,
|
|
752
|
-
warnings: plan.warnings,
|
|
753
|
-
plan,
|
|
754
|
-
next_call: null
|
|
755
|
-
}
|
|
756
|
-
);
|
|
757
|
-
}
|
|
758
|
-
if (plan.status === "underspecified") {
|
|
759
|
-
return refusal(
|
|
760
|
-
"UNDERSPECIFIED_INTEGRATION",
|
|
761
|
-
"This plan cannot be produced without the facts below. Each one is the developer's to answer, and none is guessed here.",
|
|
762
|
-
{
|
|
763
|
-
ask: QUESTION_ROUND_INSTRUCTION,
|
|
764
|
-
missing_facts: plan.missingFacts.map((fact) => ({
|
|
765
|
-
fact: fact.fact,
|
|
766
|
-
...fixedQuestion(fact.fact, space, fact.options ?? null) ?? {},
|
|
767
|
-
why: fact.reason,
|
|
768
|
-
how_to_supply: supplyOverMcp(fact.fact, space, fact.options ?? null, detected.detection),
|
|
769
|
-
// The CLI's own phrasing, kept so the two surfaces are visibly the same refusal and a partner
|
|
770
|
-
// moving between them recognises it.
|
|
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] } }
|
|
775
|
-
})),
|
|
776
|
-
// Everything detection already knows, so the agent does not learn it only after the round trip.
|
|
777
|
-
warnings: plan.warnings,
|
|
778
|
-
next_call: {
|
|
779
|
-
tool: "everfur_plan_integration",
|
|
780
|
-
with: "the same arguments plus every missing fact above"
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
);
|
|
784
|
-
}
|
|
785
|
-
if (plan.status === "blocked") {
|
|
786
|
-
return refusal(
|
|
787
|
-
"CAPABILITIES_BLOCKED",
|
|
788
|
-
"Everything this integration asked for is blocked by the contract, so nothing can be built today. Each reason and who owns it are below; no plan works around a blocked surface.",
|
|
789
|
-
{ blocked: plan.blocked, warnings: plan.warnings, plan, next_call: null }
|
|
790
|
-
);
|
|
791
|
-
}
|
|
792
|
-
const ready = mode.warning === null ? plan : { ...plan, warnings: [...plan.warnings, mode.warning] };
|
|
793
|
-
return {
|
|
794
|
-
isError: false,
|
|
795
|
-
document: { ok: true, plan: ready, ...environment.fromTenant ? { environment_source: "connected_tenant" } : {} }
|
|
796
|
-
};
|
|
797
|
-
}
|
|
798
|
-
var PLAN_OUTPUT_SHAPE = {
|
|
799
|
-
...ENVELOPE_SHAPE,
|
|
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."),
|
|
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.")
|
|
813
|
-
};
|
|
814
|
-
var VERIFY_EMBEDDING = z.enum(["in-page", "frame"]);
|
|
815
|
-
var VERIFY_INPUT_SHAPE = {
|
|
816
|
-
user_ref: z.string().optional().describe("End-user id to mint the test session for. Defaults to a CLI-owned value."),
|
|
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).`),
|
|
818
|
-
attempts: z.number().int().min(1).max(20).optional().describe("How many times to read the verdict before concluding it is empty."),
|
|
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.'),
|
|
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."),
|
|
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."),
|
|
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."),
|
|
824
|
-
theme_source: z.string().min(1).max(THEME_SOURCE_MAX_BYTES).optional().describe("With the theme surface: the same text everfur_plan_integration takes as theme_source (the project's tailwind config, the CSS that declares its theme, or a React Native theme or colours file). The branding is judged merged with the theme prop the plan writes from it, as the SDK merges them. Read statically and never executed; when given, it wins over project_root.")
|
|
825
|
-
};
|
|
826
|
-
var HOSTED_VERIFY_INPUT_SHAPE = {
|
|
827
|
-
user_ref: VERIFY_INPUT_SHAPE.user_ref.describe(
|
|
828
|
-
"A label for the synthetic test user this verification runs as: the session is minted for everfur-verify- and a stable id derived from the label, never for a real member. Defaults to a fixed label."
|
|
829
|
-
),
|
|
830
|
-
identity_mode: VERIFY_INPUT_SHAPE.identity_mode,
|
|
831
|
-
attempts: VERIFY_INPUT_SHAPE.attempts,
|
|
832
|
-
origin: VERIFY_INPUT_SHAPE.origin,
|
|
833
|
-
embedding: VERIFY_INPUT_SHAPE.embedding,
|
|
834
|
-
loader_url: VERIFY_INPUT_SHAPE.loader_url.describe(
|
|
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."
|
|
836
|
-
),
|
|
837
|
-
surfaces: VERIFY_INPUT_SHAPE.surfaces.describe(
|
|
838
|
-
"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 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."
|
|
839
|
-
),
|
|
840
|
-
theme_source: VERIFY_INPUT_SHAPE.theme_source.describe(
|
|
841
|
-
"With the theme surface: the same text everfur_plan_integration takes as theme_source (the project's tailwind config, the CSS that declares its theme, or a React Native theme or colours file). The branding is judged merged with the theme prop the plan writes from it, as the SDK merges them. Read statically and never executed."
|
|
842
|
-
)
|
|
843
|
-
};
|
|
844
|
-
async function verifyIntegrationTool(args, context, ports = {}) {
|
|
845
|
-
const built = clientFor(context, ports);
|
|
846
|
-
if (built.ok === false) {
|
|
847
|
-
return refusal("WIRE_NOT_CONFIGURED", "The API connection could not be configured.", { errors: built.errors });
|
|
848
|
-
}
|
|
849
|
-
const origin = args.origin === void 0 ? { origin: null, error: null } : parseOrigin(args.origin);
|
|
850
|
-
if (origin.error !== null) {
|
|
851
|
-
return refusal("INVALID_ORIGIN", origin.error, { origin: args.origin });
|
|
852
|
-
}
|
|
853
|
-
const loader = args.loader_url === void 0 ? { url: null, error: null } : parseLoaderUrl(args.loader_url);
|
|
854
|
-
if (loader.error !== null) {
|
|
855
|
-
return refusal("INVALID_LOADER_URL", loader.error, { loader_url: args.loader_url });
|
|
856
|
-
}
|
|
857
|
-
if (context.credentialSource === "hosted" && loader.url !== null && !isEverfurLoaderHost(loader.url)) {
|
|
858
|
-
return refusal(
|
|
859
|
-
"INVALID_LOADER_URL",
|
|
860
|
-
`On the hosted server loader_url must be on ${[...EVERFUR_LOADER_HOSTS].join(" or ")}; omit it to probe the loader that pairs with the API.`,
|
|
861
|
-
{ loader_url: args.loader_url, allowed_hosts: [...EVERFUR_LOADER_HOSTS] }
|
|
862
|
-
);
|
|
863
|
-
}
|
|
864
|
-
if (context.credentialSource === "hosted" && args.identity_mode === "anonymous" && resolveCredential(context.env, PARTNER_KEY).status !== "present") {
|
|
865
|
-
return hostedPublishableKeyRefusal(context.env);
|
|
866
|
-
}
|
|
867
|
-
const verification = await runVerification({
|
|
868
|
-
client: built.client,
|
|
869
|
-
clock: ports.clock ?? systemClock,
|
|
870
|
-
env: context.env,
|
|
871
|
-
// On the hosted server, the synthetic test user the session was minted for (CLAUDE-CONN-18), never the raw name.
|
|
872
|
-
userRef: context.syntheticUserRef ?? args.user_ref ?? DEFAULT_VERIFY_USER_REF,
|
|
873
|
-
attempts: args.attempts ?? 3,
|
|
874
|
-
pollDelayMs: 500,
|
|
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,
|
|
878
|
-
loaderUrl: loader.url,
|
|
879
|
-
// The MCP server's documented environment holds no secret key, so it verifies only with a session the partner
|
|
880
|
-
// backend minted (EVERFUR_SESSION_TOKEN) and never reads EVERFUR_PARTNER_SECRET_KEY, even when one is set.
|
|
881
|
-
mintWithSecretKey: false,
|
|
882
|
-
// On the Everfur-hosted server the session was minted by the Everfur API for the signed-in sandbox tenant
|
|
883
|
-
// before this call, and the step reports that outcome in words that name no variable.
|
|
884
|
-
sessionSource: context.credentialSource ?? "env",
|
|
885
|
-
hostedMint: context.hostedMint ?? null,
|
|
886
|
-
transport: ports.transport ?? fetchTransport,
|
|
887
|
-
// Normalized the way the CLI's `--surfaces` parser does: contract order, duplicates collapsed, so
|
|
888
|
-
// `["televet","televet"]` mints one handoff and reports each step once.
|
|
889
|
-
surfaces: normalizeSurfaces(args.surfaces ?? []),
|
|
890
|
-
// A root that is not a directory is reported by the project checks as a FAIL step, not refused here.
|
|
891
|
-
project: args.project_root === void 0 ? null : { fs: nodeFileSystem, root: args.project_root },
|
|
892
|
-
// Anonymous verifies the path an anonymous integration uses: the publishable key and a derived session id.
|
|
893
|
-
identityMode: args.identity_mode ?? "partner-user-ref",
|
|
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.
|
|
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
|
-
})()
|
|
900
|
-
});
|
|
901
|
-
const result = {
|
|
902
|
-
ok: verification.status === "passed",
|
|
903
|
-
status: verification.status,
|
|
904
|
-
base_url: built.client.baseUrl,
|
|
905
|
-
steps: verification.steps,
|
|
906
|
-
// Attribution: a wrongly generated integration has to be traceable to the corpus revision that
|
|
907
|
-
// produced it, or a bad recommendation is unfalsifiable after the fact.
|
|
908
|
-
attribution: { cli_version: CLI_VERSION, verified_at: (/* @__PURE__ */ new Date()).toISOString() }
|
|
909
|
-
};
|
|
910
|
-
if (verification.status !== "failed") return { isError: false, document: result };
|
|
911
|
-
const failed = verification.steps.filter((step) => step.result === "FAIL");
|
|
912
|
-
return failedResult(result, "VERIFICATION_FAILED", "At least one verification step failed.", {
|
|
913
|
-
failed_steps: failed.map((step) => step.id),
|
|
914
|
-
request_id: failed.find((step) => step.requestId !== null)?.requestId ?? null
|
|
915
|
-
});
|
|
916
|
-
}
|
|
917
|
-
var VERIFY_OUTPUT_SHAPE = {
|
|
918
|
-
...ENVELOPE_SHAPE,
|
|
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."),
|
|
920
|
-
base_url: z.string().optional().describe("The API base URL the checks ran against."),
|
|
921
|
-
steps: z.array(z.object({
|
|
922
|
-
id: z.string(),
|
|
923
|
-
title: z.string(),
|
|
924
|
-
result: z.enum(["PASS", "FAIL", "SKIP", "GATED", "N/A"]),
|
|
925
|
-
reason: z.string(),
|
|
926
|
-
requestId: z.string().nullable(),
|
|
927
|
-
details: z.array(z.string())
|
|
928
|
-
}).passthrough()).optional().describe("One entry per check, in the order they ran."),
|
|
929
|
-
attribution: z.object({ cli_version: z.string(), verified_at: z.string() }).passthrough().optional()
|
|
930
|
-
};
|
|
931
|
-
var DIAGNOSE_INPUT_SHAPE = {
|
|
932
|
-
bundle: z.boolean().optional().describe("Return one support bundle instead of the diagnosis alone: project and framework, @everfur/sdk and CLI versions, the contract digest, which credentials are set (names and public prefixes, never values), and the diagnosis with its request id. Produced whatever state the integration is in; give it to the developer for a support ticket."),
|
|
933
|
-
project_root: z.string().optional().describe("With bundle: absolute path to the application root to describe. Never defaulted; without it the bundle reports the project as not read.")
|
|
934
|
-
};
|
|
935
|
-
var HOSTED_DIAGNOSE_INPUT_SHAPE = {
|
|
936
|
-
bundle: DIAGNOSE_INPUT_SHAPE.bundle.describe(
|
|
937
|
-
"Return one support bundle instead of the diagnosis alone: the @everfur/sdk and CLI versions, the contract digest, which credentials the connected tenant resolved (names and public prefixes, never values), and the diagnosis with its request id. Produced whatever state the integration is in, for a support ticket. This server reads no project."
|
|
938
|
-
)
|
|
939
|
-
};
|
|
940
|
-
async function diagnoseTool(args, context, ports = {}) {
|
|
941
|
-
const built = clientFor(context, ports);
|
|
942
|
-
if (args.bundle === true) {
|
|
943
|
-
const bundle = await buildSupportBundle({
|
|
944
|
-
fs: nodeFileSystem,
|
|
945
|
-
root: args.project_root ?? null,
|
|
946
|
-
env: context.env,
|
|
947
|
-
cliVersion: CLI_VERSION,
|
|
948
|
-
client: built.ok ? built.client : null,
|
|
949
|
-
wireErrors: built.ok ? [] : built.errors,
|
|
950
|
-
// The MCP client is built without an error registry (see `clientFor`), so no projection was loaded
|
|
951
|
-
// for error codes. `null` is the truth here; the CLI reports the file it loaded.
|
|
952
|
-
contractSource: null,
|
|
953
|
-
contract: contractProviderFor(context, ports),
|
|
954
|
-
credentialSource: context.credentialSource ?? "env"
|
|
955
|
-
});
|
|
956
|
-
return { isError: false, document: { ok: true, bundle } };
|
|
957
|
-
}
|
|
958
|
-
if (built.ok === false) {
|
|
959
|
-
return refusal("WIRE_NOT_CONFIGURED", "The API connection could not be configured.", { errors: built.errors });
|
|
960
|
-
}
|
|
961
|
-
const auth = chooseWidgetAuth(context.env);
|
|
962
|
-
if (auth.ok === false) {
|
|
963
|
-
if (context.credentialSource === "hosted") return hostedPublishableKeyRefusal(context.env);
|
|
964
|
-
const { error } = auth.outcome.json;
|
|
965
|
-
return refusal("WIDGET_CREDENTIAL_UNUSABLE", error.message, {
|
|
966
|
-
// The CLI's own code and remedy lines, so the two surfaces are visibly the same refusal.
|
|
967
|
-
cli_error_code: error.code,
|
|
968
|
-
how_to_fix: [...error.details]
|
|
969
|
-
});
|
|
970
|
-
}
|
|
971
|
-
const result = await built.client.request({
|
|
972
|
-
method: "GET",
|
|
973
|
-
path: DIAGNOSTICS_PATH,
|
|
974
|
-
credentials: auth.auth.headers
|
|
975
|
-
});
|
|
976
|
-
if (!result.ok && result.kind === "http" && result.status === 404) {
|
|
977
|
-
return refusal(
|
|
978
|
-
"DIAGNOSTICS_NOT_DEPLOYED",
|
|
979
|
-
"This deployment does not serve GET /widget/v1/diagnostics, so the four causes of an empty entitlement verdict cannot be told apart here. This is a gap in the PLATFORM, not in the integration being diagnosed.",
|
|
980
|
-
{
|
|
981
|
-
// No internal planning reference (plan numbers, estimates, dependencies) belongs here: this goes to partners.
|
|
982
|
-
base_url: built.client.baseUrl,
|
|
983
|
-
meanwhile: "everfur_verify_integration hard-asserts a non-empty verdict and will still catch the most common failure, an unprovisioned tenant, without naming which of the four causes it is."
|
|
984
|
-
}
|
|
985
|
-
);
|
|
986
|
-
}
|
|
987
|
-
if (result.ok === false) {
|
|
988
|
-
const diagnosed = diagnoseFailure(result, auth.auth);
|
|
989
|
-
return refusal("DIAGNOSTICS_FAILED", `The diagnostics call did not succeed: ${diagnosed.summary}.`, {
|
|
990
|
-
cause: diagnosed.cause,
|
|
991
|
-
evidence: diagnosed.evidence,
|
|
992
|
-
remedy: diagnosed.nextAction,
|
|
993
|
-
kind: result.kind,
|
|
994
|
-
...result.kind === "http" ? { status: result.status, code: result.code } : {},
|
|
995
|
-
request_id: result.requestId ?? null,
|
|
996
|
-
// Which credential actually asked the question. A 401 answered under `partner_key` says nothing
|
|
997
|
-
// about whether the bearer path would have worked, and the reverse.
|
|
998
|
-
auth_mode: auth.auth.mode
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
1001
|
-
const parsed = parseDiagnostics(result.value, result.requestId);
|
|
1002
|
-
if (parsed.ok === false) {
|
|
1003
|
-
return refusal(
|
|
1004
|
-
"DIAGNOSTICS_MALFORMED",
|
|
1005
|
-
`${DIAGNOSTICS_PATH} answered ${result.status} with a body this CLI does not recognise.`,
|
|
1006
|
-
{
|
|
1007
|
-
detail: parsed.detail,
|
|
1008
|
-
request_id: result.requestId ?? null,
|
|
1009
|
-
base_url: built.client.baseUrl,
|
|
1010
|
-
check: "that EVERFUR_API_BASE_URL points at the Everfur API and not at a proxy or a login page"
|
|
1011
|
-
}
|
|
1012
|
-
);
|
|
1013
|
-
}
|
|
1014
|
-
return {
|
|
1015
|
-
isError: false,
|
|
1016
|
-
document: {
|
|
1017
|
-
ok: true,
|
|
1018
|
-
diagnosis: buildDiagnosisDocument({
|
|
1019
|
-
diagnosis: parsed.diagnosis,
|
|
1020
|
-
baseUrl: built.client.baseUrl,
|
|
1021
|
-
path: DIAGNOSTICS_PATH,
|
|
1022
|
-
cliVersion: CLI_VERSION
|
|
1023
|
-
}),
|
|
1024
|
-
auth_mode: auth.auth.mode
|
|
1025
|
-
}
|
|
1026
|
-
};
|
|
1027
|
-
}
|
|
1028
|
-
var DIAGNOSE_OUTPUT_SHAPE = {
|
|
1029
|
-
...ENVELOPE_SHAPE,
|
|
1030
|
-
diagnosis: z.object({}).passthrough().optional().describe("One reason per capability, shaped by everfur://schema/diagnosis.v1. Present when ok is true."),
|
|
1031
|
-
auth_mode: z.string().optional().describe("Which credential asked the question."),
|
|
1032
|
-
bundle: z.object({}).passthrough().optional().describe("The support bundle, when bundle: true was passed. Present whatever state the integration is in.")
|
|
1033
|
-
};
|
|
1034
|
-
var EXPLAIN_ERROR_INPUT_SHAPE = {
|
|
1035
|
-
code: z.string().min(1).describe("The wire error code, exactly as the response carried it. CASE SENSITIVE: three naming families are live at once, so `authExpired` and `AuthorizationException` are two different 401s and `authexpired` is neither."),
|
|
1036
|
-
contract_path: z.string().optional().describe("Path to a contract-public projection to read the registry from. Defaults to EVERFUR_CONTRACT_PUBLIC, then to the contract fetched from the Everfur API with EVERFUR_DOCS_KEY.")
|
|
1037
|
-
};
|
|
1038
|
-
var HOSTED_EXPLAIN_ERROR_INPUT_SHAPE = {
|
|
1039
|
-
code: EXPLAIN_ERROR_INPUT_SHAPE.code
|
|
1040
|
-
};
|
|
1041
|
-
async function explainErrorTool(args, context, ports = {}) {
|
|
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
|
-
}
|
|
1049
|
-
const offline = resolveOffline(code);
|
|
1050
|
-
if (offline?.kind === "sdk_member") return offlineExplanation(offline, "no_registry");
|
|
1051
|
-
const contractPath = args.contract_path ?? context.env["EVERFUR_CONTRACT_PUBLIC"] ?? null;
|
|
1052
|
-
let load;
|
|
1053
|
-
if (contractPath === null) {
|
|
1054
|
-
const fetched = await contractProviderFor(context, ports)();
|
|
1055
|
-
if (fetched.kind === "unavailable") {
|
|
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
|
-
);
|
|
1071
|
-
}
|
|
1072
|
-
load = loadErrorRegistryFromContract(fetched.contract);
|
|
1073
|
-
} else {
|
|
1074
|
-
load = loadErrorRegistry(nodeFileSystem, contractPath);
|
|
1075
|
-
}
|
|
1076
|
-
if (load.kind === "contract_rejected") {
|
|
1077
|
-
return refusal(
|
|
1078
|
-
"CONTRACT_UNUSABLE",
|
|
1079
|
-
"The contract-public projection named for this call could not be used, so no code was looked up.",
|
|
1080
|
-
{
|
|
1081
|
-
path: contractPath,
|
|
1082
|
-
reason: load.outcome.kind,
|
|
1083
|
-
remedy: "run `everfur contract validate --path <file>` for the specific failure, or omit `contract_path` and unset EVERFUR_CONTRACT_PUBLIC to fetch the contract with EVERFUR_DOCS_KEY"
|
|
1084
|
-
}
|
|
1085
|
-
);
|
|
1086
|
-
}
|
|
1087
|
-
if (load.kind === "no_registry") {
|
|
1088
|
-
return refusal("NO_ERROR_REGISTRY", `${load.path} carries no error registry.`, {
|
|
1089
|
-
detail: "the projection has no `errors.yaml` document with an `errors` list; it was built before the registry was published, or it is a partial projection"
|
|
1090
|
-
});
|
|
1091
|
-
}
|
|
1092
|
-
const entry = load.registry.lookup(code);
|
|
1093
|
-
if (entry === null) {
|
|
1094
|
-
const fallback = resolveAfterRegistryMiss(code, load.registry);
|
|
1095
|
-
if (fallback.kind !== "unknown") return offlineExplanation(fallback, "registry_miss");
|
|
1096
|
-
return refusal(
|
|
1097
|
-
"UNKNOWN_ERROR_CODE",
|
|
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.`,
|
|
1099
|
-
{
|
|
1100
|
-
did_you_mean: fallback.suggestions,
|
|
1101
|
-
case_sensitive: true,
|
|
1102
|
-
searched: {
|
|
1103
|
-
source: load.path,
|
|
1104
|
-
digest: load.digest,
|
|
1105
|
-
codes: load.registry.codes.length,
|
|
1106
|
-
// The SDK taxonomy and the SDK contract table were searched too, so a reader knows the refusal is not
|
|
1107
|
-
// just "this one file does not have it".
|
|
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
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
);
|
|
1114
|
-
}
|
|
1115
|
-
return {
|
|
1116
|
-
isError: false,
|
|
1117
|
-
document: {
|
|
1118
|
-
ok: true,
|
|
1119
|
-
explanation: {
|
|
1120
|
-
...buildExplanationDocument({
|
|
1121
|
-
entry,
|
|
1122
|
-
contractPath: load.path,
|
|
1123
|
-
digest: load.digest,
|
|
1124
|
-
cliVersion: CLI_VERSION
|
|
1125
|
-
}),
|
|
1126
|
-
...registryAlsoIn(code)
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
};
|
|
1130
|
-
}
|
|
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;
|
|
1151
|
-
return {
|
|
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
|
|
1161
|
-
}
|
|
1162
|
-
};
|
|
1163
|
-
}
|
|
1164
|
-
function offlineExplanation(resolution, context) {
|
|
1165
|
-
return { isError: false, document: { ok: true, explanation: offlineDocument(resolution, CLI_VERSION, context) } };
|
|
1166
|
-
}
|
|
1167
|
-
var EXPLAIN_ERROR_OUTPUT_SHAPE = {
|
|
1168
|
-
...ENVELOPE_SHAPE,
|
|
1169
|
-
explanation: z.object({}).passthrough().optional().describe("The registry's explanation, shaped by everfur://schema/error-explanation.v1. Present when ok is true.")
|
|
1170
|
-
};
|
|
1171
|
-
var ACCESS_TOKEN_SHAPE = /^mcp_at_[0-9a-f]{64}$/;
|
|
1172
|
-
var CONTEXT_PATH = "/mcp/context";
|
|
1173
|
-
var CONTEXT_CACHE_TTL_MS = 15e3;
|
|
1174
|
-
var REFUSAL_CACHE_TTL_MS = 3e4;
|
|
1175
|
-
var MAX_CACHE_ENTRIES = 1e4;
|
|
1176
|
-
function isAccessTokenShaped(raw) {
|
|
1177
|
-
return ACCESS_TOKEN_SHAPE.test(raw);
|
|
1178
|
-
}
|
|
1179
|
-
function tokenCacheKey(token) {
|
|
1180
|
-
return createHash("sha256").update(token, "utf8").digest("hex");
|
|
1181
|
-
}
|
|
1182
|
-
var TOKEN_BUCKET_HEX_CHARS = 8;
|
|
1183
|
-
function tokenBucketOf(presented) {
|
|
1184
|
-
return tokenCacheKey(presented).slice(0, TOKEN_BUCKET_HEX_CHARS);
|
|
1185
|
-
}
|
|
1186
|
-
var MCP_SCOPE = "mcp:tools";
|
|
1187
|
-
var REVOKED_AUTHORIZATION_REMEDY = "the authorization was revoked or expired since it was verified; reconnect the editor to sign in again";
|
|
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"]);
|
|
1199
|
-
function canonicalResource(value) {
|
|
1200
|
-
let parsed;
|
|
1201
|
-
try {
|
|
1202
|
-
parsed = new URL(value);
|
|
1203
|
-
} catch {
|
|
1204
|
-
return null;
|
|
1205
|
-
}
|
|
1206
|
-
let path = parsed.pathname;
|
|
1207
|
-
while (path.endsWith("/")) path = path.slice(0, -1);
|
|
1208
|
-
return `${parsed.origin}${path}${parsed.search}${parsed.hash}`;
|
|
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
|
-
}
|
|
1227
|
-
function parseTenantContext(body) {
|
|
1228
|
-
if (!isJsonObject(body)) return null;
|
|
1229
|
-
const tenant = body["tenant"];
|
|
1230
|
-
if (!isJsonObject(tenant)) return null;
|
|
1231
|
-
const clientId = tenant["client_id"];
|
|
1232
|
-
const slug = tenant["slug"];
|
|
1233
|
-
const livemode = tenant["livemode"];
|
|
1234
|
-
const userId = body["user_id"];
|
|
1235
|
-
const publishableKey = body["publishable_key"];
|
|
1236
|
-
const apiBaseUrl = body["api_base_url"];
|
|
1237
|
-
const scopes = body["scopes"];
|
|
1238
|
-
const expiresAt = body["expires_at"];
|
|
1239
|
-
const resource = body["resource"];
|
|
1240
|
-
if (typeof clientId !== "string" || clientId.length === 0) return null;
|
|
1241
|
-
if (typeof slug !== "string") return null;
|
|
1242
|
-
if (typeof livemode !== "boolean") return null;
|
|
1243
|
-
if (typeof userId !== "string" || userId.length === 0) return null;
|
|
1244
|
-
if (publishableKey !== null && typeof publishableKey !== "string") return null;
|
|
1245
|
-
if (typeof apiBaseUrl !== "string" || apiBaseUrl.length === 0) return null;
|
|
1246
|
-
if (!Array.isArray(scopes) || !scopes.every((scope) => typeof scope === "string")) return null;
|
|
1247
|
-
if (typeof expiresAt !== "string" || Number.isNaN(Date.parse(expiresAt))) return null;
|
|
1248
|
-
if (resource !== void 0 && resource !== null && typeof resource !== "string") return null;
|
|
1249
|
-
const test = parseTestMode(body["test"]);
|
|
1250
|
-
if (!test.ok) return null;
|
|
1251
|
-
return Object.freeze({
|
|
1252
|
-
tenant: Object.freeze({ clientId, slug, livemode }),
|
|
1253
|
-
userId,
|
|
1254
|
-
publishableKey: publishableKey === null || publishableKey.length === 0 ? null : publishableKey,
|
|
1255
|
-
apiBaseUrl,
|
|
1256
|
-
scopes: Object.freeze([...scopes]),
|
|
1257
|
-
expiresAt,
|
|
1258
|
-
resource: typeof resource === "string" ? resource : null,
|
|
1259
|
-
test: test.test
|
|
1260
|
-
});
|
|
1261
|
-
}
|
|
1262
|
-
function createTtlCache(ttlMs, now, live) {
|
|
1263
|
-
const entries = /* @__PURE__ */ new Map();
|
|
1264
|
-
return {
|
|
1265
|
-
get(key) {
|
|
1266
|
-
const entry = entries.get(key);
|
|
1267
|
-
if (entry === void 0) return null;
|
|
1268
|
-
const at = now();
|
|
1269
|
-
if (at - entry.at >= ttlMs() || !live(entry.value, at)) {
|
|
1270
|
-
entries.delete(key);
|
|
1271
|
-
return null;
|
|
1272
|
-
}
|
|
1273
|
-
return entry.value;
|
|
1274
|
-
},
|
|
1275
|
-
set(key, value) {
|
|
1276
|
-
const at = now();
|
|
1277
|
-
if (entries.size >= MAX_CACHE_ENTRIES) {
|
|
1278
|
-
for (const [entryKey, entry] of entries) {
|
|
1279
|
-
if (at - entry.at >= ttlMs() || !live(entry.value, at)) entries.delete(entryKey);
|
|
1280
|
-
}
|
|
1281
|
-
while (entries.size >= MAX_CACHE_ENTRIES) {
|
|
1282
|
-
const oldest = entries.keys().next().value;
|
|
1283
|
-
if (oldest === void 0) break;
|
|
1284
|
-
entries.delete(oldest);
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
entries.set(key, { value, at });
|
|
1288
|
-
},
|
|
1289
|
-
delete(key) {
|
|
1290
|
-
entries.delete(key);
|
|
1291
|
-
}
|
|
1292
|
-
};
|
|
1293
|
-
}
|
|
1294
|
-
var EXTRA_KEY = "everfur";
|
|
1295
|
-
function authInfoFrom(token, context, ownResource) {
|
|
1296
|
-
return {
|
|
1297
|
-
token,
|
|
1298
|
-
// The context names the tenant this bearer acts for, not the editor's OAuth client id, which the
|
|
1299
|
-
// backend does not return. The tenant is the principal every tool runs as, so it is the id kept here.
|
|
1300
|
-
clientId: context.tenant.clientId,
|
|
1301
|
-
scopes: [...context.scopes],
|
|
1302
|
-
expiresAt: Math.floor(Date.parse(context.expiresAt) / 1e3),
|
|
1303
|
-
// The audience the TOKEN names, already checked to be this server's own (`verifyWithBackend`); this server's own
|
|
1304
|
-
// when the backend named none (the rollout case).
|
|
1305
|
-
resource: new URL(context.resource ?? ownResource),
|
|
1306
|
-
extra: { [EXTRA_KEY]: context }
|
|
1307
|
-
};
|
|
1308
|
-
}
|
|
1309
|
-
function tenantContextOf(authInfo) {
|
|
1310
|
-
const extra = authInfo?.extra;
|
|
1311
|
-
if (extra === void 0) return null;
|
|
1312
|
-
const context = extra[EXTRA_KEY];
|
|
1313
|
-
if (typeof context !== "object" || context === null) return null;
|
|
1314
|
-
const candidate = context;
|
|
1315
|
-
return typeof candidate.userId === "string" && typeof candidate.apiBaseUrl === "string" && typeof candidate.tenant === "object" ? candidate : null;
|
|
1316
|
-
}
|
|
1317
|
-
function createHostedTokenVerifier(options) {
|
|
1318
|
-
const endpoint = resolveEndpoint(null, { EVERFUR_API_BASE_URL: options.apiBaseUrl });
|
|
1319
|
-
if (endpoint.ok === false) throw new Error(`the hosted verifier needs a usable API base URL: ${endpoint.errors.join("; ")}`);
|
|
1320
|
-
const client = createApiClient({
|
|
1321
|
-
transport: options.transport ?? fetchTransport,
|
|
1322
|
-
clock: options.clock ?? systemClock,
|
|
1323
|
-
endpoint: endpoint.endpoint,
|
|
1324
|
-
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
|
|
1325
|
-
});
|
|
1326
|
-
const now = options.now ?? Date.now;
|
|
1327
|
-
const ttlMs = options.cacheTtlMs ?? CONTEXT_CACHE_TTL_MS;
|
|
1328
|
-
const refusalTtlMs = options.refusalTtlMs ?? REFUSAL_CACHE_TTL_MS;
|
|
1329
|
-
const ownResource = canonicalResource(options.resource);
|
|
1330
|
-
if (ownResource === null) throw new Error("the hosted verifier needs an absolute resource URL");
|
|
1331
|
-
const acceptWithoutResource = options.acceptContextWithoutResource ?? ACCEPT_CONTEXT_WITHOUT_RESOURCE;
|
|
1332
|
-
function expired(authInfo, at) {
|
|
1333
|
-
return typeof authInfo.expiresAt !== "number" || Number.isNaN(authInfo.expiresAt) || authInfo.expiresAt * 1e3 <= at;
|
|
1334
|
-
}
|
|
1335
|
-
const verified = createTtlCache(() => ttlMs, now, (authInfo, at) => !expired(authInfo, at));
|
|
1336
|
-
const refused = createTtlCache(() => refusalTtlMs, now, () => true);
|
|
1337
|
-
const inflight = /* @__PURE__ */ new Map();
|
|
1338
|
-
async function verifyWithBackend(token) {
|
|
1339
|
-
const result = await client.request({
|
|
1340
|
-
method: "GET",
|
|
1341
|
-
path: CONTEXT_PATH,
|
|
1342
|
-
credentials: { authorization: withPrefix(ephemeralCredential("editor access token", token), "Bearer ") }
|
|
1343
|
-
});
|
|
1344
|
-
if (result.ok === false) {
|
|
1345
|
-
if (result.kind === "http" && result.status === 401) {
|
|
1346
|
-
throw new InvalidTokenError("The Everfur API refused this access token.");
|
|
1347
|
-
}
|
|
1348
|
-
throw new TemporarilyUnavailableError("The Everfur API could not verify this access token.");
|
|
1349
|
-
}
|
|
1350
|
-
const context = parseTenantContext(result.value);
|
|
1351
|
-
if (context === null) {
|
|
1352
|
-
throw new TemporarilyUnavailableError("The Everfur API answered with a context this server does not recognise.");
|
|
1353
|
-
}
|
|
1354
|
-
if (context.resource === null) {
|
|
1355
|
-
if (!acceptWithoutResource) {
|
|
1356
|
-
throw new TemporarilyUnavailableError("The Everfur API answered with a context that names no resource.");
|
|
1357
|
-
}
|
|
1358
|
-
} else if (canonicalResource(context.resource) !== ownResource) {
|
|
1359
|
-
throw new InvalidTokenError("The access token was not issued for this server.");
|
|
1360
|
-
}
|
|
1361
|
-
return authInfoFrom(token, context, options.resource);
|
|
1362
|
-
}
|
|
1363
|
-
return Object.freeze({
|
|
1364
|
-
forget(token) {
|
|
1365
|
-
verified.delete(tokenCacheKey(token));
|
|
1366
|
-
},
|
|
1367
|
-
async verifyAccessToken(token, context = {}) {
|
|
1368
|
-
if (!isAccessTokenShaped(token)) {
|
|
1369
|
-
throw new InvalidTokenError("The bearer is not an Everfur MCP access token.");
|
|
1370
|
-
}
|
|
1371
|
-
const key = tokenCacheKey(token);
|
|
1372
|
-
const cached = verified.get(key);
|
|
1373
|
-
if (cached !== null) return cached;
|
|
1374
|
-
const refusal2 = refused.get(key);
|
|
1375
|
-
if (refusal2 !== null) throw refusal2;
|
|
1376
|
-
const budget = options.failureBudget;
|
|
1377
|
-
if (budget !== void 0 && context.clientIp !== void 0 && budget.exhausted(context.clientIp, context.bucket ?? tokenBucketOf(token))) {
|
|
1378
|
-
throw new TooManyRequestsError("Too many unauthenticated requests from this client; retry later.");
|
|
1379
|
-
}
|
|
1380
|
-
const pending = inflight.get(key);
|
|
1381
|
-
if (pending !== void 0) return pending;
|
|
1382
|
-
const verification = verifyWithBackend(token).then((authInfo) => {
|
|
1383
|
-
if (expired(authInfo, now())) throw new InvalidTokenError("The access token has expired.");
|
|
1384
|
-
verified.set(key, authInfo);
|
|
1385
|
-
return authInfo;
|
|
1386
|
-
}).catch((error) => {
|
|
1387
|
-
if (error instanceof InvalidTokenError) refused.set(key, error);
|
|
1388
|
-
throw error;
|
|
1389
|
-
}).finally(() => {
|
|
1390
|
-
inflight.delete(key);
|
|
1391
|
-
});
|
|
1392
|
-
inflight.set(key, verification);
|
|
1393
|
-
return verification;
|
|
1394
|
-
}
|
|
1395
|
-
});
|
|
1396
|
-
}
|
|
1397
|
-
var JSON_MIME = "application/json";
|
|
1398
|
-
var MARKDOWN_MIME = "text/markdown";
|
|
1399
|
-
function projection(contract) {
|
|
1400
|
-
return contract.validation.projection;
|
|
1401
|
-
}
|
|
1402
|
-
function sourceOf(contract) {
|
|
1403
|
-
return { generated_at: contract.generatedAt, digest: contract.digest };
|
|
1404
|
-
}
|
|
1405
|
-
async function requireContract(provider) {
|
|
1406
|
-
const load = await provider();
|
|
1407
|
-
if (load.kind === "loaded") return load.contract;
|
|
1408
|
-
throw new Error([`${load.message}.`, ...load.details].join(" "));
|
|
1409
|
-
}
|
|
1410
|
-
async function optionalContract(provider) {
|
|
1411
|
-
const load = await provider();
|
|
1412
|
-
return load.kind === "loaded" ? load.contract : null;
|
|
1413
|
-
}
|
|
1414
|
-
function json(value) {
|
|
1415
|
-
return JSON.stringify(value, null, 2);
|
|
1416
|
-
}
|
|
1417
|
-
function contractDocument(contract, key) {
|
|
1418
|
-
return json({
|
|
1419
|
-
source: { ...sourceOf(contract), document: key },
|
|
1420
|
-
...projection(contract)[key] ?? {}
|
|
1421
|
-
});
|
|
1422
|
-
}
|
|
1423
|
-
function errorRows(contract) {
|
|
1424
|
-
const errors = projection(contract)["errors.yaml"]?.errors;
|
|
1425
|
-
return Array.isArray(errors) ? errors.filter((row) => typeof row === "object" && row !== null && typeof row.code === "string") : [];
|
|
1426
|
-
}
|
|
1427
|
-
function errorCodes(contract) {
|
|
1428
|
-
return errorRows(contract).map((row) => row.code).sort();
|
|
1429
|
-
}
|
|
1430
|
-
function readErrorEntry(contract, code) {
|
|
1431
|
-
const row = errorRows(contract).find((entry) => entry.code === code);
|
|
1432
|
-
return row === void 0 ? null : json({ source: sourceOf(contract), ...row });
|
|
1433
|
-
}
|
|
1434
|
-
function trees(contract) {
|
|
1435
|
-
const load = loadDecisionSpaceFromContract(contract);
|
|
1436
|
-
return load.kind === "loaded" ? load.space.trees : [];
|
|
1437
|
-
}
|
|
1438
|
-
function guideSections(contract) {
|
|
1439
|
-
return trees(contract).map((tree) => tree.treeId).sort();
|
|
1440
|
-
}
|
|
1441
|
-
function renderOutcome(option) {
|
|
1442
|
-
const { outcome } = option;
|
|
1443
|
-
switch (outcome.kind) {
|
|
1444
|
-
case "branch":
|
|
1445
|
-
return `next question \`${outcome.nextQuestionId}\``;
|
|
1446
|
-
case "blocked":
|
|
1447
|
-
return `blocked: ${outcome.reason} (owner: ${outcome.unblockOwner})`;
|
|
1448
|
-
case "terminal":
|
|
1449
|
-
if (outcome.agentBlocked) {
|
|
1450
|
-
return `answered with UI for the person's own tap; blocked for agents: ${outcome.agentBlocked.reason} (owner: ${outcome.agentBlocked.unblockOwner})`;
|
|
1451
|
-
}
|
|
1452
|
-
return outcome.thenAskTreeId === null ? "answered" : `answered, then section \`${outcome.thenAskTreeId}\``;
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
function renderTree(contract, tree) {
|
|
1456
|
-
const lines = [`# ${tree.title}`, "", tree.decides, ""];
|
|
1457
|
-
lines.push(`Source: contract generated ${contract.generatedAt}, digest ${contract.digest}.`, "");
|
|
1458
|
-
if (tree.principles.length > 0) {
|
|
1459
|
-
lines.push("## Principles", "");
|
|
1460
|
-
for (const principle of tree.principles) {
|
|
1461
|
-
lines.push(`### ${principle.title}`, "", principle.statement, "");
|
|
1462
|
-
if (principle.docPath !== null) lines.push(`Reference: \`${principle.docPath}\``, "");
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
lines.push("## Questions", "");
|
|
1466
|
-
for (const question of tree.questions) {
|
|
1467
|
-
lines.push(`### ${question.prompt} (\`${question.id}\`)`, "", question.whyItIsAsked, "");
|
|
1468
|
-
for (const option of question.options) {
|
|
1469
|
-
lines.push(`- \`${option.id}\`, ${option.label}. ${option.when} Outcome: ${renderOutcome(option)}.`);
|
|
1470
|
-
}
|
|
1471
|
-
lines.push("");
|
|
1472
|
-
}
|
|
1473
|
-
return lines.join("\n");
|
|
1474
|
-
}
|
|
1475
|
-
function readGuideSection(contract, section) {
|
|
1476
|
-
const tree = trees(contract).find((entry) => entry.treeId === section);
|
|
1477
|
-
return tree === void 0 ? null : renderTree(contract, tree);
|
|
1478
|
-
}
|
|
1479
|
-
function stringField(row, key) {
|
|
1480
|
-
const value = row[key];
|
|
1481
|
-
return typeof value === "string" ? value : "";
|
|
1482
|
-
}
|
|
1483
|
-
function capabilityPacks(contract) {
|
|
1484
|
-
const packs = projection(contract)["capabilities.yaml"]?.packs;
|
|
1485
|
-
if (!Array.isArray(packs)) return [];
|
|
1486
|
-
return packs.flatMap((pack) => {
|
|
1487
|
-
if (typeof pack !== "object" || pack === null) return [];
|
|
1488
|
-
const row = pack;
|
|
1489
|
-
const id = stringField(row, "id");
|
|
1490
|
-
if (id === "") return [];
|
|
1491
|
-
const actions = Array.isArray(row["actions"]) ? row["actions"].flatMap((action) => typeof action === "object" && action !== null ? [`${stringField(action, "id")} ${stringField(action, "detail")}`] : []) : [];
|
|
1492
|
-
return [{ id, title: stringField(row, "title"), actions }];
|
|
1493
|
-
});
|
|
1494
|
-
}
|
|
1495
|
-
function searchableDocuments(contract) {
|
|
1496
|
-
const errors = errorRows(contract).map((row) => ({
|
|
1497
|
-
uri: `everfur://contract/errors/${encodeURIComponent(row.code)}`,
|
|
1498
|
-
kind: "error",
|
|
1499
|
-
id: row.code,
|
|
1500
|
-
title: row.code,
|
|
1501
|
-
summary: stringField(row, "note") || stringField(row, "display"),
|
|
1502
|
-
text: [stringField(row, "display"), stringField(row, "note"), stringField(row, "naming_family"), String(row["http_status"] ?? "")].join(" ")
|
|
1503
|
-
}));
|
|
1504
|
-
const packs = capabilityPacks(contract).map((pack) => ({
|
|
1505
|
-
uri: "everfur://contract/capabilities",
|
|
1506
|
-
kind: "capability",
|
|
1507
|
-
id: pack.id,
|
|
1508
|
-
title: pack.title,
|
|
1509
|
-
summary: pack.title,
|
|
1510
|
-
text: pack.actions.join(" ")
|
|
1511
|
-
}));
|
|
1512
|
-
const guides = trees(contract).map((tree) => ({
|
|
1513
|
-
uri: `everfur://guide/${tree.treeId}`,
|
|
1514
|
-
kind: "guide",
|
|
1515
|
-
id: tree.treeId,
|
|
1516
|
-
title: tree.title,
|
|
1517
|
-
summary: tree.decides,
|
|
1518
|
-
text: renderTree(contract, tree)
|
|
1519
|
-
}));
|
|
1520
|
-
return [...errors, ...packs, ...guides];
|
|
1521
|
-
}
|
|
1522
|
-
function renderGuideIndex(contract) {
|
|
1523
|
-
const lines = ["# Everfur integration guide", ""];
|
|
1524
|
-
for (const tree of [...trees(contract)].sort((a, b) => a.treeId.localeCompare(b.treeId))) {
|
|
1525
|
-
lines.push(`- [${tree.title}](everfur://guide/${tree.treeId}): ${tree.decides}`);
|
|
1526
|
-
}
|
|
1527
|
-
return `${lines.join("\n")}
|
|
1528
|
-
`;
|
|
1529
|
-
}
|
|
1530
|
-
var SCHEMAS = {
|
|
1531
|
-
"plan.v1": { schema: plan_v1_schema_default, title: "Plan document schema" },
|
|
1532
|
-
"diagnosis.v1": { schema: diagnosis_v1_schema_default, title: "Diagnosis document schema" },
|
|
1533
|
-
"verification.v1": { schema: verification_v1_schema_default, title: "Verification document schema" },
|
|
1534
|
-
"error-explanation.v1": { schema: error_explanation_v1_schema_default, title: "Error explanation document schema" }
|
|
1535
|
-
};
|
|
1536
|
-
function staticResources(provider) {
|
|
1537
|
-
const contract = (key, slug, title, description) => ({
|
|
1538
|
-
uri: `everfur://contract/${slug}`,
|
|
1539
|
-
name: `contract-${slug}`,
|
|
1540
|
-
title,
|
|
1541
|
-
description,
|
|
1542
|
-
mimeType: JSON_MIME,
|
|
1543
|
-
read: async () => contractDocument(await requireContract(provider), key)
|
|
1544
|
-
});
|
|
1545
|
-
return [
|
|
1546
|
-
contract(
|
|
1547
|
-
"errors.yaml",
|
|
1548
|
-
"errors",
|
|
1549
|
-
"Wire error registry",
|
|
1550
|
-
"Every error code the Everfur API emits: HTTP status, retryability, naming family, envelope."
|
|
1551
|
-
),
|
|
1552
|
-
contract(
|
|
1553
|
-
"capabilities.yaml",
|
|
1554
|
-
"capabilities",
|
|
1555
|
-
"Capability packs",
|
|
1556
|
-
"Every capability: its entitlement key, actions, exposure, runtime gate and data class."
|
|
1557
|
-
),
|
|
1558
|
-
contract(
|
|
1559
|
-
"credentials.yaml",
|
|
1560
|
-
"credentials",
|
|
1561
|
-
"Credential shapes",
|
|
1562
|
-
"The shape of each Everfur credential: prefix, length, holder, and redaction pattern. Never a key."
|
|
1563
|
-
),
|
|
1564
|
-
contract(
|
|
1565
|
-
"decision-trees.yaml",
|
|
1566
|
-
"decision-trees",
|
|
1567
|
-
"Integration decision trees",
|
|
1568
|
-
"The decision trees everfur_plan_integration walks, as published."
|
|
1569
|
-
),
|
|
1570
|
-
contract(
|
|
1571
|
-
"action-classes.yaml",
|
|
1572
|
-
"action-classes",
|
|
1573
|
-
"Action classes and exposures",
|
|
1574
|
-
"How each capability action is classified and whether an agent may perform it."
|
|
1575
|
-
),
|
|
1576
|
-
{
|
|
1577
|
-
uri: "everfur://guide",
|
|
1578
|
-
name: "guide",
|
|
1579
|
-
title: "Integration guide",
|
|
1580
|
-
description: "The sections of the integration guide, one per decision tree.",
|
|
1581
|
-
mimeType: MARKDOWN_MIME,
|
|
1582
|
-
read: async () => renderGuideIndex(await requireContract(provider))
|
|
1583
|
-
},
|
|
1584
|
-
{
|
|
1585
|
-
uri: "everfur://mcp/refusal-codes",
|
|
1586
|
-
name: "mcp-refusal-codes",
|
|
1587
|
-
title: "Tool refusal codes",
|
|
1588
|
-
description: "Every error.code an Everfur MCP tool can return, with retryable, next_action and docs_path.",
|
|
1589
|
-
mimeType: JSON_MIME,
|
|
1590
|
-
read: async () => json({
|
|
1591
|
-
next_actions: NEXT_ACTIONS,
|
|
1592
|
-
codes: Object.entries(REFUSAL_POLICY).map(([code, policy]) => ({
|
|
1593
|
-
code,
|
|
1594
|
-
retryable: policy.retryable,
|
|
1595
|
-
next_action: policy.nextAction,
|
|
1596
|
-
docs_path: policy.docsPath
|
|
1597
|
-
}))
|
|
1598
|
-
})
|
|
1599
|
-
},
|
|
1600
|
-
...Object.entries(SCHEMAS).map(([id, entry]) => ({
|
|
1601
|
-
uri: `everfur://schema/${id}`,
|
|
1602
|
-
name: `schema-${id}`,
|
|
1603
|
-
title: entry.title,
|
|
1604
|
-
description: `The JSON Schema for the ${id} document the tools return.`,
|
|
1605
|
-
mimeType: "application/schema+json",
|
|
1606
|
-
read: async () => json(entry.schema)
|
|
1607
|
-
}))
|
|
1608
|
-
];
|
|
1609
|
-
}
|
|
1610
|
-
function registerResources(server, provider) {
|
|
1611
|
-
const annotations = { audience: ["assistant"] };
|
|
1612
|
-
for (const resource of staticResources(provider)) {
|
|
1613
|
-
server.registerResource(
|
|
1614
|
-
resource.name,
|
|
1615
|
-
resource.uri,
|
|
1616
|
-
{ title: resource.title, description: resource.description, mimeType: resource.mimeType, annotations },
|
|
1617
|
-
async (uri) => ({ contents: [{ uri: uri.href, mimeType: resource.mimeType, text: await resource.read() }] })
|
|
1618
|
-
);
|
|
1619
|
-
}
|
|
1620
|
-
server.registerResource(
|
|
1621
|
-
"contract-error",
|
|
1622
|
-
new ResourceTemplate("everfur://contract/errors/{code}", {
|
|
1623
|
-
list: async () => {
|
|
1624
|
-
const contract = await optionalContract(provider);
|
|
1625
|
-
return {
|
|
1626
|
-
resources: (contract === null ? [] : errorCodes(contract)).map((code) => ({
|
|
1627
|
-
uri: `everfur://contract/errors/${encodeURIComponent(code)}`,
|
|
1628
|
-
name: code,
|
|
1629
|
-
mimeType: JSON_MIME
|
|
1630
|
-
}))
|
|
1631
|
-
};
|
|
1632
|
-
},
|
|
1633
|
-
complete: {
|
|
1634
|
-
code: async (value) => {
|
|
1635
|
-
const contract = await optionalContract(provider);
|
|
1636
|
-
return contract === null ? [] : errorCodes(contract).filter((code) => code.startsWith(value));
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
}),
|
|
1640
|
-
{
|
|
1641
|
-
title: "One wire error code",
|
|
1642
|
-
description: "A single entry of the wire error registry. The code is case sensitive.",
|
|
1643
|
-
mimeType: JSON_MIME,
|
|
1644
|
-
annotations
|
|
1645
|
-
},
|
|
1646
|
-
async (uri, variables) => {
|
|
1647
|
-
const code = decodeURIComponent(String(variables["code"] ?? ""));
|
|
1648
|
-
const text = readErrorEntry(await requireContract(provider), code);
|
|
1649
|
-
if (text === null) {
|
|
1650
|
-
throw new Error(`"${code}" is not in the error registry; call everfur_explain_error for near misses.`);
|
|
1651
|
-
}
|
|
1652
|
-
return { contents: [{ uri: uri.href, mimeType: JSON_MIME, text }] };
|
|
1653
|
-
}
|
|
1654
|
-
);
|
|
1655
|
-
server.registerResource(
|
|
1656
|
-
"guide-section",
|
|
1657
|
-
new ResourceTemplate("everfur://guide/{section}", {
|
|
1658
|
-
list: async () => {
|
|
1659
|
-
const contract = await optionalContract(provider);
|
|
1660
|
-
return {
|
|
1661
|
-
resources: (contract === null ? [] : trees(contract)).map((tree) => ({
|
|
1662
|
-
uri: `everfur://guide/${tree.treeId}`,
|
|
1663
|
-
name: `guide-${tree.treeId}`,
|
|
1664
|
-
title: tree.title,
|
|
1665
|
-
mimeType: MARKDOWN_MIME
|
|
1666
|
-
}))
|
|
1667
|
-
};
|
|
1668
|
-
},
|
|
1669
|
-
complete: {
|
|
1670
|
-
section: async (value) => {
|
|
1671
|
-
const contract = await optionalContract(provider);
|
|
1672
|
-
return contract === null ? [] : guideSections(contract).filter((id) => id.startsWith(value));
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
}),
|
|
1676
|
-
{
|
|
1677
|
-
title: "Integration guide section",
|
|
1678
|
-
description: "One section of the integration guide, rendered from its decision tree.",
|
|
1679
|
-
mimeType: MARKDOWN_MIME,
|
|
1680
|
-
annotations
|
|
1681
|
-
},
|
|
1682
|
-
async (uri, variables) => {
|
|
1683
|
-
const section = String(variables["section"] ?? "");
|
|
1684
|
-
const text = readGuideSection(await requireContract(provider), section);
|
|
1685
|
-
if (text === null) {
|
|
1686
|
-
throw new Error(`"${section}" is not a guide section; read everfur://guide for the list.`);
|
|
1687
|
-
}
|
|
1688
|
-
return { contents: [{ uri: uri.href, mimeType: MARKDOWN_MIME, text }] };
|
|
1689
|
-
}
|
|
1690
|
-
);
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
// src/mcp/connector-tools.ts
|
|
1694
|
-
var TIMESTAMP = z.string();
|
|
1695
|
-
var COUNT = z.number().int().min(0);
|
|
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
|
-
});
|
|
1704
|
-
var TENANT_STATUS = z.object({
|
|
1705
|
-
tenant: TENANT,
|
|
1706
|
-
mode: z.object({
|
|
1707
|
-
livemode: z.boolean(),
|
|
1708
|
-
has_sandbox_pair: z.boolean(),
|
|
1709
|
-
sandbox_client_slug: z.string().nullable(),
|
|
1710
|
-
live_client_slug: z.string().nullable()
|
|
1711
|
-
}),
|
|
1712
|
-
plan: z.object({
|
|
1713
|
-
revision: z.number().int(),
|
|
1714
|
-
allowed_features: z.array(z.string()),
|
|
1715
|
-
partner_can_enable: z.array(z.string()),
|
|
1716
|
-
limits: z.array(z.object({ limit: z.string(), value: z.number().int().nullable(), in_use: z.number().int().nullable() }))
|
|
1717
|
-
}).nullable(),
|
|
1718
|
-
features: z.array(z.object({
|
|
1719
|
-
feature: z.string(),
|
|
1720
|
-
in_plan: z.boolean(),
|
|
1721
|
-
enabled: z.boolean(),
|
|
1722
|
-
partner_toggleable: z.boolean(),
|
|
1723
|
-
can_disable: z.boolean(),
|
|
1724
|
-
can_enable: z.boolean(),
|
|
1725
|
-
surface_available: z.boolean()
|
|
1726
|
-
})).nullable(),
|
|
1727
|
-
embed_origins: z.object({ origins: z.array(z.string()), max_embed_origins: z.number().int().nullable() }),
|
|
1728
|
-
entitlements: z.object({ capabilities: z.array(z.string()) }),
|
|
1729
|
-
// The id and the first 12 characters, never the value: a longer "prefix" is refused as outside the shape rather
|
|
1730
|
-
// than relayed, because it could be the key itself.
|
|
1731
|
-
publishable_key: z.object({ key_id: z.string(), prefix: z.string().max(12) }).nullable(),
|
|
1732
|
-
televet: z.object({ return_destination_keys: z.array(z.string()), booking_lead_minutes: z.number().int().nullable() })
|
|
1733
|
-
});
|
|
1734
|
-
var WEBHOOK_ENDPOINT = z.object({
|
|
1735
|
-
endpoint_id: z.string(),
|
|
1736
|
-
url: z.string(),
|
|
1737
|
-
description: z.string().nullable(),
|
|
1738
|
-
enabled_events: z.array(z.string()),
|
|
1739
|
-
status: z.string(),
|
|
1740
|
-
api_version: z.string(),
|
|
1741
|
-
failing_since: TIMESTAMP.nullable(),
|
|
1742
|
-
created_at: TIMESTAMP,
|
|
1743
|
-
updated_at: TIMESTAMP
|
|
1744
|
-
});
|
|
1745
|
-
var WEBHOOK_DELIVERY = z.object({
|
|
1746
|
-
delivery_id: z.string(),
|
|
1747
|
-
event_id: z.string(),
|
|
1748
|
-
endpoint_id: z.string(),
|
|
1749
|
-
type: z.string(),
|
|
1750
|
-
status: z.string(),
|
|
1751
|
-
attempts: COUNT,
|
|
1752
|
-
is_manual_resend: z.boolean(),
|
|
1753
|
-
last_status_code: z.number().int().nullable(),
|
|
1754
|
-
last_error_class: z.string().nullable(),
|
|
1755
|
-
next_attempt_at: TIMESTAMP.nullable(),
|
|
1756
|
-
succeeded_at: TIMESTAMP.nullable(),
|
|
1757
|
-
created_at: TIMESTAMP,
|
|
1758
|
-
updated_at: TIMESTAMP
|
|
1759
|
-
});
|
|
1760
|
-
var WEBHOOK_DELIVERY_DETAIL = WEBHOOK_DELIVERY.extend({
|
|
1761
|
-
attempt_history: z.array(z.object({
|
|
1762
|
-
attempt_no: z.number().int(),
|
|
1763
|
-
started_at: TIMESTAMP,
|
|
1764
|
-
duration_ms: COUNT,
|
|
1765
|
-
status_code: z.number().int().nullable(),
|
|
1766
|
-
error_class: z.string().nullable()
|
|
1767
|
-
}))
|
|
1768
|
-
});
|
|
1769
|
-
var EVENT = z.object({ id: z.string(), type: z.string(), api_version: z.string(), created: z.number().int(), livemode: z.boolean() });
|
|
1770
|
-
var USAGE = z.object({
|
|
1771
|
-
window_days: z.number().int(),
|
|
1772
|
-
conversations: COUNT,
|
|
1773
|
-
user_messages: COUNT,
|
|
1774
|
-
active_users: COUNT,
|
|
1775
|
-
last_activity_at: TIMESTAMP.nullable(),
|
|
1776
|
-
generated_at: TIMESTAMP
|
|
1777
|
-
});
|
|
1778
|
-
var RECORDS_STATUS = z.object({
|
|
1779
|
-
counts: z.array(z.object({ simple_status: z.string(), count: COUNT })),
|
|
1780
|
-
total: COUNT,
|
|
1781
|
-
generated_at: TIMESTAMP
|
|
1782
|
-
});
|
|
1783
|
-
var TENANT_SURFACE_NOT_ENABLED_CODE = "tenant_surface_not_enabled";
|
|
1784
|
-
var TENANT_RESOURCE_NOT_FOUND_CODE = "tenant_resource_not_found";
|
|
1785
|
-
var SURFACE_NAMES = Object.freeze({
|
|
1786
|
-
status: "The tenant status read",
|
|
1787
|
-
webhooks: "The webhooks surface",
|
|
1788
|
-
records: "The records status read"
|
|
1789
|
-
});
|
|
1790
|
-
function failedRead(route, surface, failure) {
|
|
1791
|
-
if (surface !== null && failure.kind === "http" && failure.status === 409 && failure.code === TENANT_SURFACE_NOT_ENABLED_CODE) {
|
|
1792
|
-
return refusal(
|
|
1793
|
-
"TENANT_SURFACE_NOT_ENABLED",
|
|
1794
|
-
`${SURFACE_NAMES[surface]} is not switched on for the connected tenant, so there is nothing to read. This is a setting Everfur holds, not a fault in the caller, and the partner console's page for it is off for this tenant too.`,
|
|
1795
|
-
{ surface, request_id: failure.requestId }
|
|
1796
|
-
);
|
|
1797
|
-
}
|
|
1798
|
-
if (failure.kind === "http" && failure.status === 404) {
|
|
1799
|
-
const missingEntry = failure.code === TENANT_RESOURCE_NOT_FOUND_CODE;
|
|
1800
|
-
return refusal(
|
|
1801
|
-
"TENANT_READ_FAILED",
|
|
1802
|
-
missingEntry ? `GET /mcp/tenant/${route} answered 404 ${TENANT_RESOURCE_NOT_FOUND_CODE}: there is no such entry on the connected tenant.` : `The Everfur API answered 404 for GET /mcp/tenant/${route} without naming an entry: this deployment does not serve the tenant reads (the hosted surface is dark there, or the route is not deployed yet).`,
|
|
1803
|
-
{ kind: "http", status: 404, ...failure.code === null ? {} : { code: failure.code }, request_id: failure.requestId }
|
|
1804
|
-
);
|
|
1805
|
-
}
|
|
1806
|
-
return refusal("TENANT_READ_FAILED", `${describeFailure(failure)}.`, {
|
|
1807
|
-
kind: failure.kind,
|
|
1808
|
-
...failure.kind === "http" ? { status: failure.status, ...failure.code === null ? {} : { code: failure.code } } : {},
|
|
1809
|
-
request_id: failure.requestId,
|
|
1810
|
-
...failure.kind === "http" && failure.status === 401 ? { how_to_fix: [REVOKED_AUTHORIZATION_REMEDY] } : {}
|
|
1811
|
-
});
|
|
1812
|
-
}
|
|
1813
|
-
async function readTenant(context, route, surface, schema, query = {}) {
|
|
1814
|
-
const reads = context.tenantReads;
|
|
1815
|
-
if (reads === void 0 || reads === null) {
|
|
1816
|
-
throw new Error("a hosted read tool ran without the tenant reads of the hosted server");
|
|
1817
|
-
}
|
|
1818
|
-
const result = await reads(route, query);
|
|
1819
|
-
if (result.ok === false) return { ok: false, result: failedRead(route, surface, result) };
|
|
1820
|
-
const parsed = schema.safeParse(result.value);
|
|
1821
|
-
if (!parsed.success) {
|
|
1822
|
-
return {
|
|
1823
|
-
ok: false,
|
|
1824
|
-
result: refusal("TENANT_READ_MALFORMED", `GET /mcp/tenant/${route} answered a body outside its documented shape, so nothing from it is relayed.`, {
|
|
1825
|
-
// Where the body departs from the shape, never what it holds there.
|
|
1826
|
-
issues: parsed.error.issues.slice(0, 10).map((issue) => `${issue.path.join(".") || "(body)"}: ${issue.code}`),
|
|
1827
|
-
request_id: result.requestId
|
|
1828
|
-
})
|
|
1829
|
-
};
|
|
1830
|
-
}
|
|
1831
|
-
return { ok: true, value: parsed.data };
|
|
1832
|
-
}
|
|
1833
|
-
var MASKED_WEBHOOK_PATH = "/\u2026";
|
|
1834
|
-
function webhookUrlForDisplay(url) {
|
|
1835
|
-
let parsed;
|
|
1836
|
-
try {
|
|
1837
|
-
parsed = new URL(url);
|
|
1838
|
-
} catch {
|
|
1839
|
-
return "";
|
|
1840
|
-
}
|
|
1841
|
-
const path = parsed.pathname === "" || parsed.pathname === "/" ? parsed.pathname : MASKED_WEBHOOK_PATH;
|
|
1842
|
-
return `${parsed.protocol}//${parsed.host}${path}`;
|
|
1843
|
-
}
|
|
1844
|
-
function ok(document) {
|
|
1845
|
-
return { isError: false, document: { ok: true, ...document } };
|
|
1846
|
-
}
|
|
1847
|
-
var PAGE_LIMIT = z.number().int().min(1).max(100).optional().describe("How many entries to return, 1 to 100 (20 when omitted).");
|
|
1848
|
-
var DELIVERY_STATUSES = ["pending", "succeeded", "failed", "dead", "skipped"];
|
|
1849
|
-
var TENANT_STATUS_TOOL = {
|
|
1850
|
-
name: "everfur_get_tenant_status",
|
|
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.",
|
|
1852
|
-
input: {},
|
|
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
|
-
},
|
|
1861
|
-
async run(_args, context) {
|
|
1862
|
-
const read = await readTenant(context, "status", "status", TENANT_STATUS);
|
|
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
|
-
});
|
|
1882
|
-
}
|
|
1883
|
-
};
|
|
1884
|
-
var WEBHOOK_ENDPOINTS_TOOL = {
|
|
1885
|
-
name: "everfur_list_webhook_endpoints",
|
|
1886
|
-
description: "List the connected tenant's webhook endpoints: each one's id, its URL without the query string, the event types it receives, its status and since when it has been failing. Never returns a signing secret. Read-only.",
|
|
1887
|
-
input: {},
|
|
1888
|
-
output: { ...ENVELOPE_SHAPE, endpoints: z.array(WEBHOOK_ENDPOINT).optional().describe("The endpoints, newest first.") },
|
|
1889
|
-
async run(_args, context) {
|
|
1890
|
-
const read = await readTenant(context, "webhook-endpoints", "webhooks", z.object({ data: z.array(WEBHOOK_ENDPOINT) }));
|
|
1891
|
-
if (!read.ok) return read.result;
|
|
1892
|
-
return ok({ endpoints: read.value.data.map((endpoint) => ({ ...endpoint, url: webhookUrlForDisplay(endpoint.url) })) });
|
|
1893
|
-
}
|
|
1894
|
-
};
|
|
1895
|
-
var DELIVERIES_INPUT = {
|
|
1896
|
-
endpoint_id: z.string().uuid().optional().describe("Only the deliveries to this endpoint."),
|
|
1897
|
-
status: z.enum(DELIVERY_STATUSES).optional().describe("Only the deliveries in this status."),
|
|
1898
|
-
type: z.string().min(1).max(64).optional().describe("Only the deliveries of this event type, such as visit.booked."),
|
|
1899
|
-
cursor: z.string().min(1).max(512).optional().describe("The next_cursor of the previous page."),
|
|
1900
|
-
limit: PAGE_LIMIT
|
|
1901
|
-
};
|
|
1902
|
-
var WEBHOOK_DELIVERIES_TOOL = {
|
|
1903
|
-
name: "everfur_list_webhook_deliveries",
|
|
1904
|
-
description: "List the connected tenant's webhook deliveries, newest first, optionally for one endpoint, status or event type: each delivery's status, attempts, last HTTP status and error class, and next retry. Never returns an event payload or the receiving server's response. Read-only.",
|
|
1905
|
-
input: DELIVERIES_INPUT,
|
|
1906
|
-
output: {
|
|
1907
|
-
...ENVELOPE_SHAPE,
|
|
1908
|
-
deliveries: z.array(WEBHOOK_DELIVERY).optional().describe("The deliveries on this page."),
|
|
1909
|
-
has_more: z.boolean().optional().describe("Whether another page follows."),
|
|
1910
|
-
next_cursor: z.string().nullable().optional().describe("The cursor of the next page, or null.")
|
|
1911
|
-
},
|
|
1912
|
-
async run(args, context) {
|
|
1913
|
-
const read = await readTenant(context, "webhook-deliveries", "webhooks", z.object({
|
|
1914
|
-
data: z.array(WEBHOOK_DELIVERY),
|
|
1915
|
-
has_more: z.boolean(),
|
|
1916
|
-
next_cursor: z.string().nullable()
|
|
1917
|
-
}), { endpoint_id: args.endpoint_id, status: args.status, type: args.type, cursor: args.cursor, limit: args.limit });
|
|
1918
|
-
if (!read.ok) return read.result;
|
|
1919
|
-
return ok({ deliveries: read.value.data, has_more: read.value.has_more, next_cursor: read.value.next_cursor });
|
|
1920
|
-
}
|
|
1921
|
-
};
|
|
1922
|
-
var DELIVERY_INPUT = {
|
|
1923
|
-
delivery_id: z.string().uuid().describe("The delivery's id, as everfur_list_webhook_deliveries returns it.")
|
|
1924
|
-
};
|
|
1925
|
-
var WEBHOOK_DELIVERY_TOOL = {
|
|
1926
|
-
name: "everfur_get_webhook_delivery",
|
|
1927
|
-
description: "Read one webhook delivery of the connected tenant with its attempt history: when each attempt started, how long it took, and its HTTP status and error class. Never returns the event payload. Read-only.",
|
|
1928
|
-
input: DELIVERY_INPUT,
|
|
1929
|
-
output: { ...ENVELOPE_SHAPE, delivery: WEBHOOK_DELIVERY_DETAIL.optional().describe("The delivery and its attempts.") },
|
|
1930
|
-
async run(args, context) {
|
|
1931
|
-
const read = await readTenant(context, `webhook-deliveries/${args.delivery_id}`, "webhooks", WEBHOOK_DELIVERY_DETAIL);
|
|
1932
|
-
return read.ok ? ok({ delivery: read.value }) : read.result;
|
|
1933
|
-
}
|
|
1934
|
-
};
|
|
1935
|
-
var EVENTS_INPUT = {
|
|
1936
|
-
type: z.string().min(1).max(64).optional().describe("Only the events of this type, such as visit.booked."),
|
|
1937
|
-
created_gte: z.string().datetime({ offset: true }).optional().describe("Only the events created at or after this ISO 8601 time."),
|
|
1938
|
-
starting_after: z.string().regex(/^evt_[0-9a-f]{32}$/).optional().describe("The id of the last event of the previous page."),
|
|
1939
|
-
limit: PAGE_LIMIT
|
|
1940
|
-
};
|
|
1941
|
-
var EVENTS_TOOL = {
|
|
1942
|
-
name: "everfur_list_events",
|
|
1943
|
-
description: "List the events Everfur emitted to the connected tenant, newest first, optionally of one type or since a time: each event's id, type, API version, creation time (Unix seconds) and mode. Never returns an event's member content. Read-only.",
|
|
1944
|
-
input: EVENTS_INPUT,
|
|
1945
|
-
output: {
|
|
1946
|
-
...ENVELOPE_SHAPE,
|
|
1947
|
-
events: z.array(EVENT).optional().describe("The events on this page."),
|
|
1948
|
-
has_more: z.boolean().optional().describe("Whether another page follows.")
|
|
1949
|
-
},
|
|
1950
|
-
async run(args, context) {
|
|
1951
|
-
const read = await readTenant(context, "events", "webhooks", z.object({ data: z.array(EVENT), has_more: z.boolean() }), {
|
|
1952
|
-
type: args.type,
|
|
1953
|
-
created_gte: args.created_gte,
|
|
1954
|
-
starting_after: args.starting_after,
|
|
1955
|
-
limit: args.limit
|
|
1956
|
-
});
|
|
1957
|
-
if (!read.ok) return read.result;
|
|
1958
|
-
return ok({ events: read.value.data, has_more: read.value.has_more });
|
|
1959
|
-
}
|
|
1960
|
-
};
|
|
1961
|
-
var USAGE_INPUT = {
|
|
1962
|
-
window_days: z.number().int().min(1).max(365).optional().describe("The trailing window in days, 1 to 365 (30 when omitted).")
|
|
1963
|
-
};
|
|
1964
|
-
var USAGE_TOOL = {
|
|
1965
|
-
name: "everfur_get_usage_summary",
|
|
1966
|
-
description: "Read the connected tenant's aggregate chat usage over a trailing window: conversations, user messages, active users and the time of the last activity. Counts only, no conversation content. Read-only.",
|
|
1967
|
-
input: USAGE_INPUT,
|
|
1968
|
-
output: { ...ENVELOPE_SHAPE, usage: USAGE.optional().describe("The usage summary.") },
|
|
1969
|
-
async run(args, context) {
|
|
1970
|
-
const read = await readTenant(context, "usage", null, USAGE, { window_days: args.window_days });
|
|
1971
|
-
return read.ok ? ok({ usage: read.value }) : read.result;
|
|
1972
|
-
}
|
|
1973
|
-
};
|
|
1974
|
-
var RECORDS_STATUS_TOOL = {
|
|
1975
|
-
name: "everfur_get_records_status_summary",
|
|
1976
|
-
description: "Count the connected tenant's records requests per simple status (requested, awaiting clinic, received, parsing, published, failed, revoked). Counts only, nothing about a request, a pet or a record. Read-only.",
|
|
1977
|
-
input: {},
|
|
1978
|
-
output: { ...ENVELOPE_SHAPE, records_status: RECORDS_STATUS.optional().describe("The counts, in lifecycle order.") },
|
|
1979
|
-
async run(_args, context) {
|
|
1980
|
-
const read = await readTenant(context, "records-status", "records", RECORDS_STATUS);
|
|
1981
|
-
return read.ok ? ok({ records_status: read.value }) : read.result;
|
|
1982
|
-
}
|
|
1983
|
-
};
|
|
1984
|
-
var MAX_SEARCH_RESULTS = 20;
|
|
1985
|
-
var DEFAULT_SEARCH_RESULTS = 5;
|
|
1986
|
-
var EXCERPT_CHARS = 240;
|
|
1987
|
-
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
1988
|
-
"a",
|
|
1989
|
-
"an",
|
|
1990
|
-
"and",
|
|
1991
|
-
"are",
|
|
1992
|
-
"as",
|
|
1993
|
-
"at",
|
|
1994
|
-
"be",
|
|
1995
|
-
"by",
|
|
1996
|
-
"can",
|
|
1997
|
-
"do",
|
|
1998
|
-
"does",
|
|
1999
|
-
"for",
|
|
2000
|
-
"from",
|
|
2001
|
-
"how",
|
|
2002
|
-
"i",
|
|
2003
|
-
"in",
|
|
2004
|
-
"is",
|
|
2005
|
-
"it",
|
|
2006
|
-
"my",
|
|
2007
|
-
"of",
|
|
2008
|
-
"on",
|
|
2009
|
-
"or",
|
|
2010
|
-
"the",
|
|
2011
|
-
"to",
|
|
2012
|
-
"what",
|
|
2013
|
-
"when",
|
|
2014
|
-
"where",
|
|
2015
|
-
"which",
|
|
2016
|
-
"why",
|
|
2017
|
-
"with"
|
|
2018
|
-
]);
|
|
2019
|
-
function words(text) {
|
|
2020
|
-
return text.toLowerCase().split(/[^a-z0-9]+/).filter((word) => word.length > 0);
|
|
2021
|
-
}
|
|
2022
|
-
function excerpt(text) {
|
|
2023
|
-
const flat = text.replace(/\s+/g, " ").trim();
|
|
2024
|
-
return flat.length <= EXCERPT_CHARS ? flat : `${flat.slice(0, EXCERPT_CHARS - 3).trimEnd()}...`;
|
|
2025
|
-
}
|
|
2026
|
-
function searchDocuments(documents, query, limit) {
|
|
2027
|
-
const all = [...new Set(words(query))];
|
|
2028
|
-
const meaningful = all.filter((word) => !STOPWORDS.has(word));
|
|
2029
|
-
const terms = meaningful.length > 0 ? meaningful : all;
|
|
2030
|
-
if (terms.length === 0) return [];
|
|
2031
|
-
const scored = documents.flatMap((document, index) => {
|
|
2032
|
-
const head = new Set(words(`${document.id} ${document.title}`));
|
|
2033
|
-
const body = words(`${document.summary} ${document.text}`);
|
|
2034
|
-
let matched = 0;
|
|
2035
|
-
let weight = 0;
|
|
2036
|
-
for (const term of terms) {
|
|
2037
|
-
const inHead = head.has(term);
|
|
2038
|
-
const inBody = body.filter((word) => word === term).length;
|
|
2039
|
-
if (inHead || inBody > 0) matched += 1;
|
|
2040
|
-
weight += (inHead ? 10 : 0) + Math.min(inBody, 5);
|
|
2041
|
-
}
|
|
2042
|
-
return matched === 0 ? [] : [{ document, matched, weight, index }];
|
|
2043
|
-
});
|
|
2044
|
-
scored.sort((a, b) => b.matched - a.matched || b.weight - a.weight || a.index - b.index);
|
|
2045
|
-
return scored.slice(0, limit).map(({ document }) => ({
|
|
2046
|
-
uri: document.uri,
|
|
2047
|
-
kind: document.kind,
|
|
2048
|
-
id: document.id,
|
|
2049
|
-
title: document.title,
|
|
2050
|
-
excerpt: excerpt(document.summary)
|
|
2051
|
-
}));
|
|
2052
|
-
}
|
|
2053
|
-
var SEARCH_INPUT = {
|
|
2054
|
-
query: z.string().min(2).max(200).describe("What to look for: an error code, a capability, or a few words about an integration question."),
|
|
2055
|
-
limit: z.number().int().min(1).max(MAX_SEARCH_RESULTS).optional().describe(`How many results to return, 1 to ${MAX_SEARCH_RESULTS} (${DEFAULT_SEARCH_RESULTS} when omitted).`)
|
|
2056
|
-
};
|
|
2057
|
-
var SEARCH_DOCS_TOOL = {
|
|
2058
|
-
name: "everfur_search_docs",
|
|
2059
|
-
description: "Search the Everfur contract served to the connected partner: the error registry, the capability packs, and the integration guide rendered from its decision trees. Returns the matching entries, best first, each with the everfur:// resource that holds it in full. Read-only.",
|
|
2060
|
-
input: SEARCH_INPUT,
|
|
2061
|
-
output: {
|
|
2062
|
-
...ENVELOPE_SHAPE,
|
|
2063
|
-
query: z.string().optional().describe("The query, as given."),
|
|
2064
|
-
results: z.array(z.object({
|
|
2065
|
-
uri: z.string(),
|
|
2066
|
-
kind: z.enum(["error", "capability", "guide"]),
|
|
2067
|
-
id: z.string(),
|
|
2068
|
-
title: z.string(),
|
|
2069
|
-
excerpt: z.string()
|
|
2070
|
-
})).optional().describe("The matching entries, best first."),
|
|
2071
|
-
contract: z.object({ digest: z.string(), generated_at: z.string() }).optional().describe("The contract revision searched.")
|
|
2072
|
-
},
|
|
2073
|
-
async run(args, context, ports) {
|
|
2074
|
-
const load = await contractProviderFor(context, ports)();
|
|
2075
|
-
if (load.kind === "unavailable") return contractRefusal(load);
|
|
2076
|
-
const results = searchDocuments(searchableDocuments(load.contract), args.query, args.limit ?? DEFAULT_SEARCH_RESULTS);
|
|
2077
|
-
return ok({ query: args.query, results, contract: { digest: load.contract.digest, generated_at: load.contract.generatedAt } });
|
|
2078
|
-
}
|
|
2079
|
-
};
|
|
2080
|
-
var CONNECTOR_TOOLS = Object.freeze([
|
|
2081
|
-
TENANT_STATUS_TOOL,
|
|
2082
|
-
WEBHOOK_ENDPOINTS_TOOL,
|
|
2083
|
-
WEBHOOK_DELIVERIES_TOOL,
|
|
2084
|
-
WEBHOOK_DELIVERY_TOOL,
|
|
2085
|
-
EVENTS_TOOL,
|
|
2086
|
-
USAGE_TOOL,
|
|
2087
|
-
RECORDS_STATUS_TOOL,
|
|
2088
|
-
SEARCH_DOCS_TOOL
|
|
2089
|
-
]);
|
|
2090
|
-
function contractChoices(provider, treeId, questionId) {
|
|
2091
|
-
return async () => {
|
|
2092
|
-
const load = await provider();
|
|
2093
|
-
if (load.kind !== "loaded") return [];
|
|
2094
|
-
const space = loadDecisionSpaceFromContract(load.contract);
|
|
2095
|
-
return space.kind === "loaded" ? space.space.choices(treeId, questionId) : [];
|
|
2096
|
-
};
|
|
2097
|
-
}
|
|
2098
|
-
function startingWith(options) {
|
|
2099
|
-
return async (value) => (await options()).filter((option) => option.startsWith(value ?? ""));
|
|
2100
|
-
}
|
|
2101
|
-
function completeList(options) {
|
|
2102
|
-
return async (value) => {
|
|
2103
|
-
const available = await options();
|
|
2104
|
-
const parts = (value ?? "").split(",");
|
|
2105
|
-
const last = (parts.pop() ?? "").trim();
|
|
2106
|
-
const head = parts.map((part) => part.trim()).filter((part) => part.length > 0);
|
|
2107
|
-
return available.filter((option) => option.startsWith(last) && !head.includes(option)).map((option) => [...head, option].join(","));
|
|
2108
|
-
};
|
|
2109
|
-
}
|
|
2110
|
-
function knownArguments(args) {
|
|
2111
|
-
const lines = [];
|
|
2112
|
-
for (const [name, value] of Object.entries(args)) {
|
|
2113
|
-
if (value === void 0 || value.trim() === "") continue;
|
|
2114
|
-
if (name === "capabilities") {
|
|
2115
|
-
const list = value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
2116
|
-
lines.push(` ${name}: ${JSON.stringify(list)}`);
|
|
2117
|
-
} else {
|
|
2118
|
-
lines.push(` ${name}: ${JSON.stringify(value.trim())}`);
|
|
2119
|
-
}
|
|
2120
|
-
}
|
|
2121
|
-
return lines.length === 0 ? "" : `
|
|
2122
|
-
Pass these arguments, which I have already decided:
|
|
2123
|
-
${lines.join("\n")}
|
|
2124
|
-
`;
|
|
2125
|
-
}
|
|
2126
|
-
function sharedRules(options) {
|
|
2127
|
-
const hosted = options.sessionSource === "hosted";
|
|
2128
|
-
const project = hosted ? `- Describe the project with project_details (dependencies, package manager, file-presence markers; never source
|
|
2129
|
-
files or credentials). This server runs on Everfur's hosts and cannot read a path on my machine. To match the
|
|
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
|
|
2131
|
-
(dependencies, package manager, file-presence markers; never source files or credentials) for a remote MCP, plus
|
|
2132
|
-
theme_source (the text of my tailwind config or the CSS that declares my colors) so the plan matches my theme.`;
|
|
2133
|
-
const session = hosted ? `- Verification runs with a session this server obtains from the Everfur API for my sandbox tenant; on a live
|
|
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.";
|
|
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.";
|
|
2136
|
-
return `
|
|
2137
|
-
Rules for every step:
|
|
2138
|
-
${project}
|
|
2139
|
-
${session}
|
|
2140
|
-
- Branch on error.code, error.retryable and error.next_action in a tool result, never on its message text.
|
|
2141
|
-
- When any Everfur response carries an error code, call everfur_explain_error with that exact code. If it
|
|
2142
|
-
says the code is unknown, say so; do not explain it yourself.
|
|
2143
|
-
${keys}`;
|
|
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
|
-
}
|
|
2162
|
-
function integrateWebPrompt(args, options = {}) {
|
|
2163
|
-
const { origin, ...planArgs } = args;
|
|
2164
|
-
return {
|
|
2165
|
-
description: PROMPT_DESCRIPTIONS.integrateWeb,
|
|
2166
|
-
text: `Integrate Everfur into this web project. Follow these steps in order.
|
|
2167
|
-
${knownArguments(planArgs)}
|
|
2168
|
-
1. Read everfur://guide/platform and everfur://guide/identity-mode.
|
|
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.
|
|
2175
|
-
3. If it refuses with PLATFORM_NOT_SUPPORTED or CAPABILITIES_BLOCKED, tell me the reason and the owner from
|
|
2176
|
-
error.details and stop. Do not build around a blocked surface.
|
|
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.
|
|
2181
|
-
5. Build the project and fix any build error before going on.
|
|
2182
|
-
6. ${verifyCall(args.embedding, origin)} Report each step's result with its request id.
|
|
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.
|
|
2186
|
-
${sharedRules(options)}`
|
|
2187
|
-
};
|
|
2188
|
-
}
|
|
2189
|
-
function integrateReactNativePrompt(args, options = {}) {
|
|
2190
|
-
return {
|
|
2191
|
-
description: PROMPT_DESCRIPTIONS.integrateReactNative,
|
|
2192
|
-
text: `Integrate Everfur into this React Native app. Follow these steps in order.
|
|
2193
|
-
${knownArguments(args)}
|
|
2194
|
-
1. Read everfur://guide/platform and everfur://guide/identity-mode.
|
|
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.
|
|
2201
|
-
3. If it refuses with PLATFORM_NOT_SUPPORTED or CAPABILITIES_BLOCKED, tell me the reason and the owner from
|
|
2202
|
-
error.details and stop. Do not build around a blocked surface.
|
|
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.
|
|
2207
|
-
5. Typecheck the app and fix any error before going on.
|
|
2208
|
-
6. Call everfur_verify_integration. Report each step's result with its request id.
|
|
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.
|
|
2212
|
-
${sharedRules(options)}`
|
|
2213
|
-
};
|
|
2214
|
-
}
|
|
2215
|
-
function diagnoseIntegrationPrompt(args, options = {}) {
|
|
2216
|
-
const symptom = args.symptom?.trim() ? `
|
|
2217
|
-
What I see: ${args.symptom.trim()}
|
|
2218
|
-
` : "";
|
|
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";
|
|
2220
|
-
const requestId = args.request_id?.trim() ? ` The failing request id was ${JSON.stringify(args.request_id.trim())}; quote it in your report.` : "";
|
|
2221
|
-
return {
|
|
2222
|
-
description: PROMPT_DESCRIPTIONS.diagnoseIntegration,
|
|
2223
|
-
text: `Diagnose my Everfur integration. Do not change any code until the cause is named.
|
|
2224
|
-
${symptom}
|
|
2225
|
-
1. First, ${code}, and follow its retryable flag and next action.
|
|
2226
|
-
2. Call everfur_diagnose to read one reason per capability for the configured credential.
|
|
2227
|
-
3. Call everfur_verify_integration and report each FAIL with its request id.${requestId}
|
|
2228
|
-
4. Tell me which cause applies, whether it is fixed in my integration or by Everfur, and the next action.
|
|
2229
|
-
${sharedRules(options)}`
|
|
2230
|
-
};
|
|
2231
|
-
}
|
|
2232
|
-
function userMessage(prompt) {
|
|
2233
|
-
return { description: prompt.description, messages: [{ role: "user", content: { type: "text", text: prompt.text } }] };
|
|
2234
|
-
}
|
|
2235
|
-
function registerPrompts(server, provider, options = {}) {
|
|
2236
|
-
const capabilities = contractChoices(provider, CAPABILITIES_TREE, CAPABILITY_QUESTION);
|
|
2237
|
-
const identityModes = contractChoices(provider, IDENTITY_TREE, IDENTITY_QUESTION);
|
|
2238
|
-
const embeddings = contractChoices(provider, PLATFORM_TREE, WEB_EMBEDDING_QUESTION);
|
|
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
|
-
};
|
|
2244
|
-
const capabilityList = () => completable(
|
|
2245
|
-
z.string().optional().describe("Comma-separated capabilities, as the contract names them."),
|
|
2246
|
-
completeList(capabilities)
|
|
2247
|
-
);
|
|
2248
|
-
const identityMode = () => completable(
|
|
2249
|
-
z.string().optional().describe("How end users are named, as the contract names the identity modes."),
|
|
2250
|
-
startingWith(identityModes)
|
|
2251
|
-
);
|
|
2252
|
-
const environment = () => completable(
|
|
2253
|
-
z.string().optional().describe(`The Everfur deployment the configuration targets, one of: ${ENVIRONMENTS.join(", ")}.`),
|
|
2254
|
-
startingWith(async () => ENVIRONMENTS)
|
|
2255
|
-
);
|
|
2256
|
-
server.registerPrompt(
|
|
2257
|
-
"integrate-web",
|
|
2258
|
-
{
|
|
2259
|
-
title: "Integrate Everfur into a web project",
|
|
2260
|
-
description: PROMPT_DESCRIPTIONS.integrateWeb,
|
|
2261
|
-
argsSchema: {
|
|
2262
|
-
...projectRoot,
|
|
2263
|
-
capabilities: capabilityList(),
|
|
2264
|
-
identity_mode: identityMode(),
|
|
2265
|
-
embedding: completable(
|
|
2266
|
-
z.string().optional().describe("How the surface is embedded, as the contract names the embedding modes."),
|
|
2267
|
-
startingWith(embeddings)
|
|
2268
|
-
),
|
|
2269
|
-
environment: environment(),
|
|
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
|
-
)
|
|
2273
|
-
}
|
|
2274
|
-
},
|
|
2275
|
-
(args) => userMessage(integrateWebPrompt(args, options))
|
|
2276
|
-
);
|
|
2277
|
-
server.registerPrompt(
|
|
2278
|
-
"integrate-react-native",
|
|
2279
|
-
{
|
|
2280
|
-
title: "Integrate Everfur into a React Native app",
|
|
2281
|
-
description: PROMPT_DESCRIPTIONS.integrateReactNative,
|
|
2282
|
-
argsSchema: {
|
|
2283
|
-
...projectRoot,
|
|
2284
|
-
capabilities: capabilityList(),
|
|
2285
|
-
identity_mode: identityMode(),
|
|
2286
|
-
environment: environment()
|
|
2287
|
-
}
|
|
2288
|
-
},
|
|
2289
|
-
(args) => userMessage(integrateReactNativePrompt(args, options))
|
|
2290
|
-
);
|
|
2291
|
-
server.registerPrompt(
|
|
2292
|
-
"diagnose-integration",
|
|
2293
|
-
{
|
|
2294
|
-
title: "Diagnose an Everfur integration",
|
|
2295
|
-
description: PROMPT_DESCRIPTIONS.diagnoseIntegration,
|
|
2296
|
-
argsSchema: {
|
|
2297
|
-
error_code: completable(
|
|
2298
|
-
z.string().optional().describe("The error code a response carried, exactly as it appeared."),
|
|
2299
|
-
async (value) => {
|
|
2300
|
-
const load = await provider();
|
|
2301
|
-
return load.kind === "loaded" ? errorCodes(load.contract).filter((code) => code.startsWith(value ?? "")) : [];
|
|
2302
|
-
}
|
|
2303
|
-
),
|
|
2304
|
-
request_id: z.string().optional().describe("The request id of the failing call, when known."),
|
|
2305
|
-
symptom: z.string().optional().describe("What the developer sees, in their own words.")
|
|
2306
|
-
}
|
|
2307
|
-
},
|
|
2308
|
-
(args) => userMessage(diagnoseIntegrationPrompt(args, options))
|
|
2309
|
-
);
|
|
2310
|
-
}
|
|
2311
|
-
var DEADLINE_MS = 3e4;
|
|
2312
|
-
var CONVERSATION_PAGE = 100;
|
|
2313
|
-
var MESSAGE_PAGE = 200;
|
|
2314
|
-
var DEFAULT_TEST_PET_REF = "everfur-mcp-test-pet";
|
|
2315
|
-
var PET_PROFILE_SHAPE = z.object({
|
|
2316
|
-
name: z.string().min(1).max(100).optional().describe("The pet's name."),
|
|
2317
|
-
species: z.string().min(1).max(50).optional().describe("Species, e.g. dog or cat."),
|
|
2318
|
-
breed: z.string().min(1).max(100).optional().describe("Breed."),
|
|
2319
|
-
age_years: z.number().min(0).max(50).optional().describe("Age in years."),
|
|
2320
|
-
weight_kg: z.number().min(0).max(500).optional().describe("Weight in kilograms.")
|
|
2321
|
-
}).strict();
|
|
2322
|
-
function sendTestUserRef(idempotencyKey) {
|
|
2323
|
-
return `${SYNTHETIC_USER_REF_PREFIX}${uuidFromKey(idempotencyKey)}`;
|
|
2324
|
-
}
|
|
2325
|
-
function sendTestConversationTitle(idempotencyKey) {
|
|
2326
|
-
return `everfur-mcp-test-${uuidFromKey(idempotencyKey)}`;
|
|
2327
|
-
}
|
|
2328
|
-
function frameOf(block) {
|
|
2329
|
-
const line = block.split("\n").find((l) => l.startsWith("data:"));
|
|
2330
|
-
if (line === void 0) return null;
|
|
2331
|
-
const raw = line.slice(5).trim();
|
|
2332
|
-
if (raw.length === 0) return null;
|
|
2333
|
-
try {
|
|
2334
|
-
const parsed = JSON.parse(raw);
|
|
2335
|
-
if (typeof parsed !== "object" || parsed === null) return { kind: "unknown" };
|
|
2336
|
-
const obj = parsed;
|
|
2337
|
-
if (typeof obj["token"] === "string") return { kind: "delta", text: obj["token"] };
|
|
2338
|
-
if (typeof obj["status"] === "string") return { kind: `status:${obj["status"]}` };
|
|
2339
|
-
if (obj["error"] !== void 0 || typeof obj["code"] === "string") {
|
|
2340
|
-
return {
|
|
2341
|
-
kind: "error",
|
|
2342
|
-
...typeof obj["code"] === "string" ? { code: obj["code"] } : {},
|
|
2343
|
-
...typeof obj["retryable"] === "boolean" ? { retryable: obj["retryable"] } : {}
|
|
2344
|
-
};
|
|
2345
|
-
}
|
|
2346
|
-
if (obj["done"] === true) return { kind: "done" };
|
|
2347
|
-
if (obj["metadata"] === true) return { kind: "metadata" };
|
|
2348
|
-
return { kind: "unknown" };
|
|
2349
|
-
} catch {
|
|
2350
|
-
return { kind: "malformed" };
|
|
2351
|
-
}
|
|
2352
|
-
}
|
|
2353
|
-
var SEND_TEST_MESSAGE_OUTPUT_SHAPE = {
|
|
2354
|
-
...ENVELOPE_SHAPE,
|
|
2355
|
-
livemode: z.literal(false).optional().describe("Always false on a result: the tool refuses before sending to anything but a sandbox tenant."),
|
|
2356
|
-
replayed: z.boolean().optional().describe("True when an earlier call with this idempotency_key already asked this and its stored answer is returned."),
|
|
2357
|
-
identity: z.enum(["hosted_session", "session_token", "anonymous"]).optional().describe("Which identity asked: hosted_session, a session the Everfur API minted for a synthetic test user of this sandbox tenant; session_token, a session the partner backend minted and this server was given; anonymous, the publishable key and a derived session id."),
|
|
2358
|
-
pet_ref: z.string().optional().describe("The pet the conversation is scoped to, when one was named or registered."),
|
|
2359
|
-
pet_registered: z.boolean().optional().describe("True when pet_profile was registered for this identity before the send."),
|
|
2360
|
-
conversation_id: z.string().optional(),
|
|
2361
|
-
answer: z.string().optional().describe("The answer text, streamed now or replayed from the stored turn."),
|
|
2362
|
-
frame_histogram: z.record(z.number()).optional().describe("How many frames of each kind the stream carried. Absent on a replay."),
|
|
2363
|
-
elapsed_ms: z.number().optional()
|
|
2364
|
-
};
|
|
2365
|
-
function petProfileBody(profile) {
|
|
2366
|
-
const body = {};
|
|
2367
|
-
for (const key of ["name", "species", "breed", "age_years", "weight_kg"]) {
|
|
2368
|
-
const value = profile[key];
|
|
2369
|
-
if (value !== void 0) body[key] = value;
|
|
2370
|
-
}
|
|
2371
|
-
return body;
|
|
2372
|
-
}
|
|
2373
|
-
function failureFacts(failure) {
|
|
2374
|
-
return failure.kind === "http" ? { status: failure.status, ...failure.code === null ? {} : { code: failure.code }, request_id: failure.requestId } : { kind: failure.kind };
|
|
2375
|
-
}
|
|
2376
|
-
function isObject(value) {
|
|
2377
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2378
|
-
}
|
|
2379
|
-
var CHAT_CAPABILITY = "chat.message.create";
|
|
2380
|
-
function isCapabilityEnabled(entry) {
|
|
2381
|
-
return isObject(entry) && entry["enabled"] === true;
|
|
2382
|
-
}
|
|
2383
|
-
function capabilityReasonCode(entry) {
|
|
2384
|
-
if (!isObject(entry)) return null;
|
|
2385
|
-
const code = entry["reason_code"];
|
|
2386
|
-
return typeof code === "string" && code.length > 0 ? code : null;
|
|
2387
|
-
}
|
|
2388
|
-
function availableCapabilities(map) {
|
|
2389
|
-
return Object.keys(map).filter((key) => isCapabilityEnabled(map[key])).sort();
|
|
2390
|
-
}
|
|
2391
|
-
function unavailableCapabilities(map) {
|
|
2392
|
-
const groups = {};
|
|
2393
|
-
for (const key of Object.keys(map).sort()) {
|
|
2394
|
-
if (isCapabilityEnabled(map[key])) continue;
|
|
2395
|
-
const code = capabilityReasonCode(map[key]) ?? "unknown";
|
|
2396
|
-
(groups[code] ??= []).push(key);
|
|
2397
|
-
}
|
|
2398
|
-
return groups;
|
|
2399
|
-
}
|
|
2400
|
-
var INCOMPLETE_STREAM_SENTINEL = "Something interrupted my response. Could you ask again?";
|
|
2401
|
-
function storedAnswer(content) {
|
|
2402
|
-
if (typeof content !== "string") return null;
|
|
2403
|
-
const trimmed = content.trim();
|
|
2404
|
-
if (trimmed === "" || trimmed.startsWith(INCOMPLETE_STREAM_SENTINEL)) return null;
|
|
2405
|
-
return content;
|
|
2406
|
-
}
|
|
2407
|
-
async function findPriorTurn(client, auth, message, petRef, conversationTitle) {
|
|
2408
|
-
const listed = await client.request({
|
|
2409
|
-
method: "GET",
|
|
2410
|
-
path: `/widget/v1/conversations?limit=${CONVERSATION_PAGE}${petRef === null ? "" : `&pet_ref=${encodeURIComponent(petRef)}`}`,
|
|
2411
|
-
...auth
|
|
2412
|
-
});
|
|
2413
|
-
if (listed.ok === false) {
|
|
2414
|
-
if (petRef !== null && listed.kind === "http" && listed.status === 404) {
|
|
2415
|
-
return {
|
|
2416
|
-
ok: false,
|
|
2417
|
-
result: refusal("PET_NOT_REGISTERED", "The pet is not registered for this identity: the conversation list scoped to it answered 404. Nothing was sent.", {
|
|
2418
|
-
pet_ref: petRef,
|
|
2419
|
-
how_to_fix: "Pass pet_profile, which registers it for this identity.",
|
|
2420
|
-
...failureFacts(listed)
|
|
2421
|
-
})
|
|
2422
|
-
};
|
|
2423
|
-
}
|
|
2424
|
-
return {
|
|
2425
|
-
ok: false,
|
|
2426
|
-
result: refusal(
|
|
2427
|
-
"CONVERSATIONS_UNREADABLE",
|
|
2428
|
-
"Could not list the conversations this idempotency_key already opened, so a send could repeat an earlier one. Refusing rather than sending twice.",
|
|
2429
|
-
failureFacts(listed)
|
|
2430
|
-
)
|
|
2431
|
-
};
|
|
2432
|
-
}
|
|
2433
|
-
const rows = isObject(listed.value) && Array.isArray(listed.value["conversations"]) ? listed.value["conversations"] : [];
|
|
2434
|
-
let emptyConversationId = null;
|
|
2435
|
-
for (const row of rows) {
|
|
2436
|
-
const conversationId = isObject(row) ? row["conversation_id"] : void 0;
|
|
2437
|
-
if (typeof conversationId !== "string") continue;
|
|
2438
|
-
const rowPet = isObject(row) && typeof row["pet_ref"] === "string" && row["pet_ref"].length > 0 ? row["pet_ref"] : null;
|
|
2439
|
-
if (rowPet !== null && rowPet !== petRef) continue;
|
|
2440
|
-
if (conversationTitle !== null && (!isObject(row) || row["title"] !== conversationTitle)) continue;
|
|
2441
|
-
const read = await client.request({
|
|
2442
|
-
method: "GET",
|
|
2443
|
-
path: `/widget/v1/conversations/${encodeURIComponent(conversationId)}/messages?limit=${MESSAGE_PAGE}`,
|
|
2444
|
-
...auth
|
|
2445
|
-
});
|
|
2446
|
-
if (read.ok === false) {
|
|
2447
|
-
return {
|
|
2448
|
-
ok: false,
|
|
2449
|
-
result: refusal(
|
|
2450
|
-
"CONVERSATIONS_UNREADABLE",
|
|
2451
|
-
"Could not read a conversation this idempotency_key already opened, so a send could repeat an earlier one. Refusing rather than sending twice.",
|
|
2452
|
-
{ conversation_id: conversationId, ...failureFacts(read) }
|
|
2453
|
-
)
|
|
2454
|
-
};
|
|
2455
|
-
}
|
|
2456
|
-
const messages = (isObject(read.value) && Array.isArray(read.value["messages"]) ? read.value["messages"] : []).filter(isObject);
|
|
2457
|
-
if (messages.length === 0) {
|
|
2458
|
-
emptyConversationId ??= conversationId;
|
|
2459
|
-
continue;
|
|
2460
|
-
}
|
|
2461
|
-
const question = messages.findIndex((m) => m["role"] === "user");
|
|
2462
|
-
const asked = messages[question]?.["content"];
|
|
2463
|
-
if (typeof asked !== "string" || asked.trim() !== message.trim()) continue;
|
|
2464
|
-
const reply = messages.slice(question + 1).find((m) => m["role"] === "assistant");
|
|
2465
|
-
const answer = storedAnswer(reply?.["content"]);
|
|
2466
|
-
return {
|
|
2467
|
-
ok: true,
|
|
2468
|
-
prior: {
|
|
2469
|
-
conversationId,
|
|
2470
|
-
answer,
|
|
2471
|
-
unanswered: answer !== null ? null : typeof reply?.["content"] === "string" ? "interrupted" : "pending"
|
|
2472
|
-
},
|
|
2473
|
-
emptyConversationId
|
|
2474
|
-
};
|
|
2475
|
-
}
|
|
2476
|
-
return { ok: true, prior: null, emptyConversationId };
|
|
2477
|
-
}
|
|
2478
|
-
async function resolveIdentity(args, env, identity) {
|
|
2479
|
-
if (identity.hostedMint !== void 0 && identity.hostedMint !== null) {
|
|
2480
|
-
const minted = await identity.hostedMint();
|
|
2481
|
-
const outcome = minted.outcome;
|
|
2482
|
-
if (outcome.kind === "minted" && minted.sessionToken !== null) {
|
|
2483
|
-
return {
|
|
2484
|
-
ok: true,
|
|
2485
|
-
identity: {
|
|
2486
|
-
kind: "hosted_session",
|
|
2487
|
-
auth: { headers: {}, credentials: { authorization: withPrefix(ephemeralCredential("hosted test session", minted.sessionToken), "Bearer ") } },
|
|
2488
|
-
conversationTitle: null
|
|
2489
|
-
}
|
|
2490
|
-
};
|
|
2491
|
-
}
|
|
2492
|
-
if (outcome.kind === "gated") {
|
|
2493
|
-
return {
|
|
2494
|
-
ok: false,
|
|
2495
|
-
result: refusal(
|
|
2496
|
-
"NOT_A_SANDBOX_TENANT",
|
|
2497
|
-
"This tool sends a REAL message and only runs against a sandbox tenant, and the signed-in tenant is live. Refusing: a test message landing in a tenant that holds real veterinary records is not recoverable by deleting it afterwards.",
|
|
2498
|
-
{ livemode: true, request_id: outcome.requestId }
|
|
2499
|
-
)
|
|
2500
|
-
};
|
|
2501
|
-
}
|
|
2502
|
-
const failure = outcome.kind === "failed" ? outcome.failure : void 0;
|
|
2503
|
-
return {
|
|
2504
|
-
ok: false,
|
|
2505
|
-
result: refusal("SESSION_NOT_MINTED", "The Everfur API did not mint a session for this sandbox tenant, so nothing was sent.", {
|
|
2506
|
-
reason: outcome.kind === "failed" ? outcome.reason : "the Everfur API reported a minted session without the session itself",
|
|
2507
|
-
details: outcome.kind === "failed" ? [...outcome.details] : [],
|
|
2508
|
-
request_id: outcome.requestId,
|
|
2509
|
-
...failure === void 0 ? {} : failure.kind === "http" ? { status: failure.status, ...failure.code === null ? {} : { code: failure.code } } : { kind: failure.kind }
|
|
2510
|
-
})
|
|
2511
|
-
};
|
|
2512
|
-
}
|
|
2513
|
-
const session = resolveCredential(env, SESSION_TOKEN);
|
|
2514
|
-
if (session.status === "present" && session.credential !== null) {
|
|
2515
|
-
return {
|
|
2516
|
-
ok: true,
|
|
2517
|
-
identity: {
|
|
2518
|
-
kind: "session_token",
|
|
2519
|
-
auth: { headers: {}, credentials: { authorization: withPrefix(session.credential, "Bearer ") } },
|
|
2520
|
-
// The session names its own user, so the key cannot scope the identity; it scopes the conversations instead.
|
|
2521
|
-
conversationTitle: sendTestConversationTitle(args.idempotency_key)
|
|
2522
|
-
}
|
|
2523
|
-
};
|
|
2524
|
-
}
|
|
2525
|
-
if (session.status === "malformed") {
|
|
2526
|
-
return { ok: false, result: refusal("WIDGET_CREDENTIAL_UNUSABLE", `${session.reason ?? `${SESSION_TOKEN.envVar} is malformed`}.`) };
|
|
2527
|
-
}
|
|
2528
|
-
const partnerKey = resolveCredential(env, PARTNER_KEY);
|
|
2529
|
-
if (partnerKey.status === "absent" || partnerKey.credential === null) {
|
|
2530
|
-
if (partnerKey.status === "malformed") {
|
|
2531
|
-
return { ok: false, result: refusal("PARTNER_KEY_MALFORMED", `${partnerKey.reason ?? `${PARTNER_KEY.envVar} is malformed`}.`) };
|
|
2532
|
-
}
|
|
2533
|
-
return { ok: false, result: refusal("NO_PARTNER_KEY", `Set ${PARTNER_KEY.envVar} to the publishable key of the sandbox tenant.`) };
|
|
2534
|
-
}
|
|
2535
|
-
return {
|
|
2536
|
-
ok: true,
|
|
2537
|
-
identity: {
|
|
2538
|
-
kind: "anonymous",
|
|
2539
|
-
auth: {
|
|
2540
|
-
headers: { "x-everfur-session-id": uuidFromKey(args.idempotency_key) },
|
|
2541
|
-
credentials: { "x-everfur-partner-key": partnerKey.credential }
|
|
2542
|
-
},
|
|
2543
|
-
conversationTitle: null
|
|
2544
|
-
}
|
|
2545
|
-
};
|
|
2546
|
-
}
|
|
2547
|
-
async function sendTestMessage(args, env, ports = {}, identitySources = {}) {
|
|
2548
|
-
const endpoint = resolveEndpoint(null, env);
|
|
2549
|
-
if (endpoint.ok === false) {
|
|
2550
|
-
return refusal("WIRE_NOT_CONFIGURED", "The API connection could not be configured.", {
|
|
2551
|
-
errors: endpoint.errors
|
|
2552
|
-
});
|
|
2553
|
-
}
|
|
2554
|
-
const resolved = await resolveIdentity(args, env, identitySources);
|
|
2555
|
-
if (resolved.ok === false) return resolved.result;
|
|
2556
|
-
const identity = resolved.identity;
|
|
2557
|
-
const petRef = args.pet_ref ?? (args.pet_profile === void 0 ? null : DEFAULT_TEST_PET_REF);
|
|
2558
|
-
const transport = ports.transport ?? fetchTransport;
|
|
2559
|
-
const clock = ports.clock ?? systemClock;
|
|
2560
|
-
const client = createApiClient({ transport, clock, endpoint: endpoint.endpoint });
|
|
2561
|
-
const streamClient = createApiClient({ transport, clock, endpoint: endpoint.endpoint, timeoutMs: DEADLINE_MS });
|
|
2562
|
-
const auth = identity.auth;
|
|
2563
|
-
const verdict = await client.request({ method: "GET", path: "/widget/v1/entitlements", ...auth });
|
|
2564
|
-
if (verdict.ok === false) {
|
|
2565
|
-
return refusal("ENTITLEMENTS_UNREADABLE", "Could not read the entitlement verdict, so the sandbox status of this tenant is unknown. Refusing rather than guessing.", failureFacts(verdict));
|
|
2566
|
-
}
|
|
2567
|
-
const body = isObject(verdict.value) ? verdict.value : {};
|
|
2568
|
-
if (body.livemode !== false) {
|
|
2569
|
-
return refusal(
|
|
2570
|
-
"NOT_A_SANDBOX_TENANT",
|
|
2571
|
-
"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.",
|
|
2572
|
-
{
|
|
2573
|
-
livemode: body.livemode ?? null,
|
|
2574
|
-
// A sandbox is a TENANT, and the verdict is the authority: a sandbox tenant is issued a pk_test_ key (EFBackend
|
|
2575
|
-
// #1630), an older one may still carry pk_live_, and either way its verdict reports livemode false
|
|
2576
|
-
// (commands/plan/environment.ts).
|
|
2577
|
-
// `everfur sandbox create` is Everfur-internal (it needs an admin token), so it is no partner's remedy; the
|
|
2578
|
-
// documented path is the partner docs' testing page (tester run, 2026-10-01).
|
|
2579
|
-
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").'
|
|
2580
|
-
}
|
|
2581
|
-
);
|
|
2582
|
-
}
|
|
2583
|
-
const capabilities = isObject(body.capabilities) ? body.capabilities : {};
|
|
2584
|
-
const chat = capabilities[CHAT_CAPABILITY];
|
|
2585
|
-
if (!isCapabilityEnabled(chat)) {
|
|
2586
|
-
const listed = Object.prototype.hasOwnProperty.call(capabilities, CHAT_CAPABILITY);
|
|
2587
|
-
const reasonCode = capabilityReasonCode(chat);
|
|
2588
|
-
const facts = {
|
|
2589
|
-
capability: CHAT_CAPABILITY,
|
|
2590
|
-
reason_code: reasonCode,
|
|
2591
|
-
available: availableCapabilities(capabilities),
|
|
2592
|
-
unavailable: unavailableCapabilities(capabilities)
|
|
2593
|
-
};
|
|
2594
|
-
if (!listed) {
|
|
2595
|
-
return refusal("CHAT_NOT_GRANTED", `This sandbox tenant does not grant ${CHAT_CAPABILITY}, so a message would be refused by the server.`, facts);
|
|
2596
|
-
}
|
|
2597
|
-
return refusal("CHAT_NOT_GRANTED", `This sandbox tenant is granted ${CHAT_CAPABILITY} and the verdict reports it switched off, so a message would be refused by the server.`, {
|
|
2598
|
-
...facts,
|
|
2599
|
-
how_to_fix: nextActionFor(reasonCode ?? "unknown")
|
|
2600
|
-
});
|
|
2601
|
-
}
|
|
2602
|
-
if (args.pet_profile !== void 0 && petRef !== null) {
|
|
2603
|
-
const registered = await client.request({
|
|
2604
|
-
method: "POST",
|
|
2605
|
-
path: `/widget/v1/pets/${encodeURIComponent(petRef)}`,
|
|
2606
|
-
body: petProfileBody(args.pet_profile),
|
|
2607
|
-
...auth
|
|
2608
|
-
});
|
|
2609
|
-
if (registered.ok === false) {
|
|
2610
|
-
return refusal("PET_NOT_REGISTERED", "Could not register the sandbox pet, so nothing was sent.", {
|
|
2611
|
-
pet_ref: petRef,
|
|
2612
|
-
...failureFacts(registered)
|
|
2613
|
-
});
|
|
2614
|
-
}
|
|
2615
|
-
}
|
|
2616
|
-
const lookup = await findPriorTurn(client, auth, args.message, petRef, identity.conversationTitle);
|
|
2617
|
-
if (lookup.ok === false) return lookup.result;
|
|
2618
|
-
const scope = { identity: identity.kind, ...petRef === null ? {} : { pet_ref: petRef } };
|
|
2619
|
-
if (lookup.prior !== null) {
|
|
2620
|
-
if (lookup.prior.answer === null && lookup.prior.unanswered === "interrupted") {
|
|
2621
|
-
return refusal("PRIOR_TURN_INTERRUPTED", "An earlier call with this idempotency_key already sent this message and no answer was stored for it. Sending again would put the question in twice, so nothing was sent.", {
|
|
2622
|
-
conversation_id: lookup.prior.conversationId,
|
|
2623
|
-
how_to_fix: "To ask again, call with a new idempotency_key: the same key returns this turn instead of sending the message a second time."
|
|
2624
|
-
});
|
|
2625
|
-
}
|
|
2626
|
-
if (lookup.prior.answer === null) {
|
|
2627
|
-
return refusal("PRIOR_TURN_UNANSWERED", "An earlier call with this idempotency_key already sent this message and no answer was stored for it. Sending again would put the question in twice, so nothing was sent.", {
|
|
2628
|
-
conversation_id: lookup.prior.conversationId,
|
|
2629
|
-
how_to_fix: "Wait for the earlier answer and call again, or use a new idempotency_key to ask again."
|
|
2630
|
-
});
|
|
2631
|
-
}
|
|
2632
|
-
return {
|
|
2633
|
-
isError: false,
|
|
2634
|
-
document: {
|
|
2635
|
-
ok: true,
|
|
2636
|
-
livemode: false,
|
|
2637
|
-
replayed: true,
|
|
2638
|
-
conversation_id: lookup.prior.conversationId,
|
|
2639
|
-
answer: lookup.prior.answer,
|
|
2640
|
-
...scope,
|
|
2641
|
-
...args.pet_profile === void 0 ? {} : { pet_registered: true }
|
|
2642
|
-
}
|
|
2643
|
-
};
|
|
2644
|
-
}
|
|
2645
|
-
let conversationId = lookup.emptyConversationId;
|
|
2646
|
-
if (conversationId === null) {
|
|
2647
|
-
const created = await client.request({
|
|
2648
|
-
method: "POST",
|
|
2649
|
-
path: "/widget/v1/conversations",
|
|
2650
|
-
body: {
|
|
2651
|
-
...petRef === null ? {} : { pet_ref: petRef },
|
|
2652
|
-
...identity.conversationTitle === null ? {} : { title: identity.conversationTitle }
|
|
2653
|
-
},
|
|
2654
|
-
...auth
|
|
2655
|
-
});
|
|
2656
|
-
if (created.ok === false) {
|
|
2657
|
-
return refusal("CONVERSATION_NOT_CREATED", "Could not open a conversation.", failureFacts(created));
|
|
2658
|
-
}
|
|
2659
|
-
const createdId = isObject(created.value) ? created.value["conversation_id"] : void 0;
|
|
2660
|
-
if (typeof createdId !== "string") {
|
|
2661
|
-
return refusal("CONVERSATION_NOT_CREATED", "The create call returned no conversation_id.");
|
|
2662
|
-
}
|
|
2663
|
-
conversationId = createdId;
|
|
2664
|
-
}
|
|
2665
|
-
const started = Date.now();
|
|
2666
|
-
const streamed = await streamClient.request({
|
|
2667
|
-
method: "POST",
|
|
2668
|
-
path: `/widget/v1/conversations/${encodeURIComponent(conversationId)}/messages`,
|
|
2669
|
-
// `message` is the ONLY field this route accepts. `content` returns a 422 naming body.message,
|
|
2670
|
-
// which is an hour of a developer's life if the tool gets it wrong.
|
|
2671
|
-
body: { message: args.message },
|
|
2672
|
-
responseType: "text",
|
|
2673
|
-
headers: {
|
|
2674
|
-
...auth.headers,
|
|
2675
|
-
accept: "text/event-stream",
|
|
2676
|
-
// Sent for a server that honours it. Today's does not, which is why reuse above exists.
|
|
2677
|
-
"idempotency-key": args.idempotency_key
|
|
2678
|
-
},
|
|
2679
|
-
credentials: auth.credentials
|
|
2680
|
-
});
|
|
2681
|
-
if (streamed.ok === false) {
|
|
2682
|
-
if (streamed.kind === "http") {
|
|
2683
|
-
return refusal("SEND_FAILED", "The send did not open a stream.", {
|
|
2684
|
-
conversation_id: conversationId,
|
|
2685
|
-
...failureFacts(streamed)
|
|
2686
|
-
});
|
|
2687
|
-
}
|
|
2688
|
-
const timedOut = streamed.kind === "timeout";
|
|
2689
|
-
return refusal(
|
|
2690
|
-
timedOut ? "DEADLINE_EXCEEDED" : "STREAM_FAILED",
|
|
2691
|
-
timedOut ? `The turn did not finish within ${DEADLINE_MS / 1e3}s.` : "The stream failed before it completed.",
|
|
2692
|
-
{ conversation_id: conversationId, ...streamed.kind === "timeout" ? {} : { detail: streamed.detail } }
|
|
2693
|
-
);
|
|
2694
|
-
}
|
|
2695
|
-
const histogram = {};
|
|
2696
|
-
let answer = "";
|
|
2697
|
-
let streamError = null;
|
|
2698
|
-
const blocks = (typeof streamed.value === "string" ? streamed.value : "").split("\n\n");
|
|
2699
|
-
blocks.pop();
|
|
2700
|
-
for (const block of blocks) {
|
|
2701
|
-
const frame = frameOf(block);
|
|
2702
|
-
if (frame === null) continue;
|
|
2703
|
-
histogram[frame.kind] = (histogram[frame.kind] ?? 0) + 1;
|
|
2704
|
-
if (frame.text !== void 0) answer += frame.text;
|
|
2705
|
-
if (frame.kind === "error") streamError ??= frame;
|
|
2706
|
-
}
|
|
2707
|
-
const deltas = histogram["delta"] ?? 0;
|
|
2708
|
-
const completed = (histogram["done"] ?? 0) > 0 && streamError === null;
|
|
2709
|
-
const result = {
|
|
2710
|
-
ok: deltas > 0 && completed,
|
|
2711
|
-
livemode: false,
|
|
2712
|
-
replayed: false,
|
|
2713
|
-
conversation_id: conversationId,
|
|
2714
|
-
answer,
|
|
2715
|
-
frame_histogram: histogram,
|
|
2716
|
-
elapsed_ms: Date.now() - started,
|
|
2717
|
-
...scope,
|
|
2718
|
-
...args.pet_profile === void 0 ? {} : { pet_registered: true }
|
|
2719
|
-
};
|
|
2720
|
-
if (deltas > 0 && completed) return { isError: false, document: result };
|
|
2721
|
-
if (deltas > 0) {
|
|
2722
|
-
const failed = streamError;
|
|
2723
|
-
return failedResult(result, "STREAM_FAILED", "The stream failed before it completed.", {
|
|
2724
|
-
conversation_id: conversationId,
|
|
2725
|
-
...failed?.code === void 0 ? {} : { stream_error_code: failed.code },
|
|
2726
|
-
...failed?.retryable === void 0 ? {} : { stream_error_retryable: failed.retryable }
|
|
2727
|
-
});
|
|
2728
|
-
}
|
|
2729
|
-
return failedResult(
|
|
2730
|
-
result,
|
|
2731
|
-
"EMPTY_STREAM",
|
|
2732
|
-
"The stream carried no delta frames, so no answer text arrived. Check the frame histogram.",
|
|
2733
|
-
{ conversation_id: conversationId }
|
|
2734
|
-
);
|
|
2735
|
-
}
|
|
2736
|
-
|
|
2737
|
-
// src/mcp/server.ts
|
|
2738
|
-
var MCP_INSTRUCTIONS = `Everfur partner integration tools.
|
|
2739
|
-
|
|
2740
|
-
Everfur is a pet-health API. Partners embed chat, medical-records and Televet (vet visit) surfaces in their own apps and pages
|
|
2741
|
-
via @everfur/sdk (React Native and Expo; the web through @everfur/sdk/web and a hosted frame; Flutter
|
|
2742
|
-
through everfur_sdk). On the web the plan follows the contract: a React tree embeds the SDK in the page,
|
|
2743
|
-
any other page includes the loader and gets the frame, and a Shopify storefront gets the Everfur app's
|
|
2744
|
-
theme block with Shopify vouching for the signed-in customer (identity_mode shopify-app-proxy). A
|
|
2745
|
-
capability the web cannot render (photo in the page, anything but chat in the frame) is reported as
|
|
2746
|
-
blocked by the platform; do not substitute the React Native entry points and do not invent an
|
|
2747
|
-
integration from the HTTP routes.
|
|
2748
|
-
|
|
2749
|
-
Use everfur_plan_integration FIRST. Describe the project with project_root when this server runs on the same
|
|
2750
|
-
machine as the project, or with project_details (package dependencies, package manager and file-presence markers,
|
|
2751
|
-
never source files or credentials) when this is a remote MCP that cannot read the project. The plan matches the
|
|
2752
|
-
generated surface to the project's own theme (a Tailwind v4 @theme block, tailwind.config, :root custom properties,
|
|
2753
|
-
a React Native theme file), read statically and never executed; a remote MCP passes that text as theme_source.
|
|
2754
|
-
When the theme cannot be read the plan says so in its match-theme step and never guesses one. A plan comes back as
|
|
2755
|
-
a summary with a plan_id and a number of pages (the goal, the detected facts, each step's id, title, one-line intent
|
|
2756
|
-
and page, and the checklist); call it again with the same arguments plus plan_id and page, for page 1 to pages, for
|
|
2757
|
-
the steps in full, with their commands and the complete body of each file, before writing them. Page 1 also carries
|
|
2758
|
-
the detection, the warnings and the checklist in full, and the principles. (The same arguments plus plan_id and steps,
|
|
2759
|
-
step ids, return those steps instead, a page at a time, and remaining_steps names the rest.) Fetch every page
|
|
2760
|
-
before any command runs or any file is written: each call recomputes the plan from the project, and the plan's own
|
|
2761
|
-
commands and files change it. everfur_plan_integration refuses
|
|
2762
|
-
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
|
|
2763
|
-
answers and do not proceed without them: an integration built on a guessed identity mode is wrong in a way
|
|
2764
|
-
that only shows up in production. Each missing fact carries a fixed question and its options: ask them all in one
|
|
2765
|
-
message, worded as given. When the plan's match-theme step says the theme could not be read, find the host app's
|
|
2766
|
-
brand colours in its own code as that step lists, pass the file's text as theme_source and plan again; never pick a
|
|
2767
|
-
colour. The tenant's console branding (partner console) is the other side of the theme. A ready plan carries a
|
|
2768
|
-
checklist: the integration is finished only when every item holds, each proved by its verify.
|
|
2769
|
-
|
|
2770
|
-
Then everfur_verify_integration to exercise the real wire, and everfur_diagnose when a working integration
|
|
2771
|
-
stops returning capabilities. Verification uses EVERFUR_SESSION_TOKEN, a short-lived session the partner's backend
|
|
2772
|
-
minted for a test user; EVERFUR_PARTNER_SECRET_KEY never belongs in this server's environment and is never read by
|
|
2773
|
-
it. When the session step is skipped, ask the developer to mint a session on their backend and set it. An anonymous
|
|
2774
|
-
integration is verified with identity_mode anonymous instead: the publishable key and a derived session id, with
|
|
2775
|
-
nothing minted.
|
|
2776
|
-
|
|
2777
|
-
Whenever an Everfur response carries an error code, call everfur_explain_error with that code rather than
|
|
2778
|
-
explaining it yourself; that includes the error.code these tools return. Three naming families are live at once and
|
|
2779
|
-
one code is the default for four different HTTP statuses, so a code explained from the shape of its string
|
|
2780
|
-
produces a confident, wrong retry policy. If the tool says the code is unknown, say so; do not fill it in.
|
|
2781
|
-
|
|
2782
|
-
Every tool returns structured content. A failed call carries error.code, error.retryable and
|
|
2783
|
-
error.next_action: branch on those, not on the message. Reference material is served as resources
|
|
2784
|
-
(everfur://guide, everfur://contract/errors, everfur://mcp/refusal-codes), and the prompts
|
|
2785
|
-
integrate-web, integrate-react-native and diagnose-integration lay out the order to call the tools in.
|
|
2786
|
-
|
|
2787
|
-
The Everfur contract (the error registry, capabilities and decision trees the planner, everfur_explain_error,
|
|
2788
|
-
the guide and the contract resources read) is served only to an authenticated partner. It is fetched with
|
|
2789
|
-
EVERFUR_DOCS_KEY from EVERFUR_API_BASE_URL. When a tool refuses with CONTRACT_CREDENTIAL_MISSING,
|
|
2790
|
-
tell the developer to set those variables in this server's environment; do not answer from memory instead.
|
|
2791
|
-
The documented environment is EVERFUR_DOCS_KEY, EVERFUR_API_BASE_URL, EVERFUR_PARTNER_KEY and, for verification,
|
|
2792
|
-
EVERFUR_SESSION_TOKEN.`;
|
|
2793
|
-
function toContent(result) {
|
|
2794
|
-
return {
|
|
2795
|
-
content: [{ type: "text", text: JSON.stringify(result.document) }],
|
|
2796
|
-
structuredContent: result.document,
|
|
2797
|
-
isError: result.isError
|
|
2798
|
-
};
|
|
2799
|
-
}
|
|
2800
|
-
async function guarded(body) {
|
|
2801
|
-
try {
|
|
2802
|
-
return toContent(await body());
|
|
2803
|
-
} catch (error) {
|
|
2804
|
-
return toContent(refusal("INTERNAL_ERROR", "The tool failed before it could produce a result.", {
|
|
2805
|
-
error_type: error instanceof Error ? error.name : typeof error
|
|
2806
|
-
}));
|
|
2807
|
-
}
|
|
2808
|
-
}
|
|
2809
|
-
var TOOL_DESCRIPTIONS = Object.freeze({
|
|
2810
|
-
env: Object.freeze({
|
|
2811
|
-
planContract: "Reads the contract from GET /partners/contract with EVERFUR_DOCS_KEY unless contract_path or EVERFUR_CONTRACT_PUBLIC names a projection file.",
|
|
2812
|
-
explainContract: "The contract is fetched with EVERFUR_DOCS_KEY unless contract_path or EVERFUR_CONTRACT_PUBLIC names a projection file."
|
|
2813
|
-
}),
|
|
2814
|
-
hosted: Object.freeze({
|
|
2815
|
-
planContract: "Reads the contract the Everfur API serves to the connected partner.",
|
|
2816
|
-
explainContract: "The contract is the one the Everfur API serves to the connected partner."
|
|
2817
|
-
})
|
|
2818
|
-
});
|
|
2819
|
-
var HOSTED_TOOL_DESCRIPTIONS = Object.freeze({
|
|
2820
|
-
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.",
|
|
2821
|
-
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.",
|
|
2822
|
-
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."
|
|
2823
|
-
});
|
|
2824
|
-
var EFFECT_HINTS = Object.freeze({
|
|
2825
|
-
reads: Object.freeze({ readOnlyHint: true, destructiveHint: false, idempotentHint: true }),
|
|
2826
|
-
writes_sandbox: Object.freeze({ readOnlyHint: false, destructiveHint: true, idempotentHint: false }),
|
|
2827
|
-
writes_live: Object.freeze({ readOnlyHint: false, destructiveHint: true, idempotentHint: false }),
|
|
2828
|
-
destroys_live: Object.freeze({ readOnlyHint: false, destructiveHint: true, idempotentHint: false })
|
|
2829
|
-
});
|
|
2830
|
-
var TOOL_DECLARATIONS = Object.freeze({
|
|
2831
|
-
everfur_plan_integration: { title: "Plan an Everfur integration", effect: "reads", openWorld: false },
|
|
2832
|
-
everfur_verify_integration: { title: "Verify the wire end to end", effect: "writes_sandbox", openWorld: true },
|
|
2833
|
-
everfur_diagnose: { title: "Explain why a capability is unavailable", effect: "reads", openWorld: false },
|
|
2834
|
-
everfur_explain_error: { title: "Explain an Everfur API error code", effect: "reads", openWorld: false },
|
|
2835
|
-
everfur_send_test_message: { title: "Send a real message against a sandbox tenant", effect: "writes_sandbox", openWorld: false },
|
|
2836
|
-
everfur_get_tenant_status: { title: "Read the tenant status", effect: "reads", openWorld: false },
|
|
2837
|
-
everfur_list_webhook_endpoints: { title: "List webhook endpoints", effect: "reads", openWorld: false },
|
|
2838
|
-
everfur_list_webhook_deliveries: { title: "List webhook deliveries", effect: "reads", openWorld: false },
|
|
2839
|
-
everfur_get_webhook_delivery: { title: "Read one webhook delivery", effect: "reads", openWorld: false },
|
|
2840
|
-
everfur_list_events: { title: "List emitted events", effect: "reads", openWorld: false },
|
|
2841
|
-
everfur_get_usage_summary: { title: "Read the usage summary", effect: "reads", openWorld: false },
|
|
2842
|
-
everfur_get_records_status_summary: { title: "Count records requests by status", effect: "reads", openWorld: false },
|
|
2843
|
-
everfur_search_docs: { title: "Search the Everfur docs", effect: "reads", openWorld: false }
|
|
2844
|
-
});
|
|
2845
|
-
var TOOL_ANNOTATIONS = Object.freeze(
|
|
2846
|
-
Object.fromEntries(
|
|
2847
|
-
Object.entries(TOOL_DECLARATIONS).map(([name, declared]) => [
|
|
2848
|
-
name,
|
|
2849
|
-
Object.freeze({ title: declared.title, ...EFFECT_HINTS[declared.effect], openWorldHint: declared.openWorld })
|
|
2850
|
-
])
|
|
2851
|
-
)
|
|
2852
|
-
);
|
|
2853
|
-
Object.freeze(CONNECTOR_TOOLS.map((tool) => tool.name));
|
|
2854
|
-
var PATH_ARGUMENTS = Object.freeze(["project_root", "contract_path"]);
|
|
2855
|
-
function argumentsSchema(local, hosted, readsLocalFiles) {
|
|
2856
|
-
return readsLocalFiles ? local : z.object(hosted).passthrough();
|
|
2857
|
-
}
|
|
2858
|
-
function localPathRefusal(args) {
|
|
2859
|
-
const named = PATH_ARGUMENTS.filter((name) => Object.hasOwn(args, name) && args[name] !== void 0);
|
|
2860
|
-
if (named.length === 0) return null;
|
|
2861
|
-
return refusal(
|
|
2862
|
-
"LOCAL_PATH_NOT_READABLE",
|
|
2863
|
-
"This server does not run on the developer's machine and reads no path. Describe the project with `project_details` instead of `project_root`; `contract_path` is not accepted, the contract is served to the signed-in partner; the offline project checks of everfur_verify_integration and everfur_diagnose are not available on a hosted server.",
|
|
2864
|
-
{ arguments: named }
|
|
2865
|
-
);
|
|
2866
|
-
}
|
|
2867
|
-
function buildServer(context, ports = {}, options = {}) {
|
|
2868
|
-
const shared = {
|
|
2869
|
-
...ports,
|
|
2870
|
-
contract: ports.contract ?? createContractProvider({
|
|
2871
|
-
env: context.env,
|
|
2872
|
-
transport: ports.transport ?? fetchTransport,
|
|
2873
|
-
clock: ports.clock ?? systemClock,
|
|
2874
|
-
cache: ports.contractCache ?? nodeContractCache(context.env)
|
|
2875
|
-
})
|
|
2876
|
-
};
|
|
2877
|
-
const server = new McpServer(
|
|
2878
|
-
{ name: "everfur", version: CLI_VERSION },
|
|
2879
|
-
{ capabilities: { tools: {}, resources: {}, prompts: {} }, instructions: options.instructions ?? MCP_INSTRUCTIONS }
|
|
2880
|
-
);
|
|
2881
|
-
const contextFor = options.contextFor ?? (async () => context);
|
|
2882
|
-
const readsLocalFiles = options.readsLocalFiles ?? true;
|
|
2883
|
-
const hosted = context.credentialSource === "hosted";
|
|
2884
|
-
const describe = hosted ? TOOL_DESCRIPTIONS.hosted : TOOL_DESCRIPTIONS.env;
|
|
2885
|
-
const run = (tool, args, authInfo, body) => guarded(async () => {
|
|
2886
|
-
if (!readsLocalFiles) {
|
|
2887
|
-
const refused = localPathRefusal(args);
|
|
2888
|
-
if (refused !== null) return refused;
|
|
2889
|
-
}
|
|
2890
|
-
return body(await contextFor({ tool, args, authInfo }));
|
|
2891
|
-
});
|
|
2892
|
-
server.registerTool(
|
|
2893
|
-
"everfur_plan_integration",
|
|
2894
|
-
{
|
|
2895
|
-
title: TOOL_ANNOTATIONS.everfur_plan_integration.title,
|
|
2896
|
-
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}`,
|
|
2897
|
-
inputSchema: argumentsSchema(PLAN_INPUT_SHAPE, HOSTED_PLAN_INPUT_SHAPE, readsLocalFiles),
|
|
2898
|
-
outputSchema: PLAN_OUTPUT_SHAPE,
|
|
2899
|
-
annotations: { ...TOOL_ANNOTATIONS.everfur_plan_integration }
|
|
2900
|
-
},
|
|
2901
|
-
// The planner computes the full plan, as the CLI prints it; what the wire carries is its summary or the steps asked
|
|
2902
|
-
// for (plan-wire.ts).
|
|
2903
|
-
(args, extra) => run("everfur_plan_integration", args, extra.authInfo, async (resolved) => planOnTheWire(await planIntegrationTool(args, resolved, shared), args))
|
|
2904
|
-
);
|
|
2905
|
-
server.registerTool(
|
|
2906
|
-
"everfur_verify_integration",
|
|
2907
|
-
{
|
|
2908
|
-
title: TOOL_ANNOTATIONS.everfur_verify_integration.title,
|
|
2909
|
-
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.",
|
|
2910
|
-
inputSchema: argumentsSchema(VERIFY_INPUT_SHAPE, HOSTED_VERIFY_INPUT_SHAPE, readsLocalFiles),
|
|
2911
|
-
outputSchema: VERIFY_OUTPUT_SHAPE,
|
|
2912
|
-
annotations: { ...TOOL_ANNOTATIONS.everfur_verify_integration }
|
|
2913
|
-
},
|
|
2914
|
-
(args, extra) => run("everfur_verify_integration", args, extra.authInfo, (resolved) => verifyIntegrationTool(args, resolved, shared))
|
|
2915
|
-
);
|
|
2916
|
-
server.registerTool(
|
|
2917
|
-
"everfur_diagnose",
|
|
2918
|
-
{
|
|
2919
|
-
title: TOOL_ANNOTATIONS.everfur_diagnose.title,
|
|
2920
|
-
description: hosted ? HOSTED_TOOL_DESCRIPTIONS.diagnose : "Separate the four byte-identical causes of an empty entitlement verdict. 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 (project, framework, SDK and CLI versions, contract digest, credential presence without values, and the diagnosis request id) whatever state the integration is in.",
|
|
2921
|
-
inputSchema: argumentsSchema(DIAGNOSE_INPUT_SHAPE, HOSTED_DIAGNOSE_INPUT_SHAPE, readsLocalFiles),
|
|
2922
|
-
outputSchema: DIAGNOSE_OUTPUT_SHAPE,
|
|
2923
|
-
annotations: { ...TOOL_ANNOTATIONS.everfur_diagnose }
|
|
2924
|
-
},
|
|
2925
|
-
(args, extra) => run("everfur_diagnose", args, extra.authInfo, (resolved) => diagnoseTool(args, resolved, shared))
|
|
2926
|
-
);
|
|
2927
|
-
server.registerTool(
|
|
2928
|
-
"everfur_explain_error",
|
|
2929
|
-
{
|
|
2930
|
-
title: TOOL_ANNOTATIONS.everfur_explain_error.title,
|
|
2931
|
-
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.`,
|
|
2932
|
-
inputSchema: argumentsSchema(EXPLAIN_ERROR_INPUT_SHAPE, HOSTED_EXPLAIN_ERROR_INPUT_SHAPE, readsLocalFiles),
|
|
2933
|
-
outputSchema: EXPLAIN_ERROR_OUTPUT_SHAPE,
|
|
2934
|
-
annotations: { ...TOOL_ANNOTATIONS.everfur_explain_error }
|
|
2935
|
-
},
|
|
2936
|
-
(args, extra) => run("everfur_explain_error", args, extra.authInfo, (resolved) => explainErrorTool(args, resolved, shared))
|
|
2937
|
-
);
|
|
2938
|
-
server.registerTool(
|
|
2939
|
-
"everfur_send_test_message",
|
|
2940
|
-
{
|
|
2941
|
-
title: TOOL_ANNOTATIONS.everfur_send_test_message.title,
|
|
2942
|
-
description: hosted ? HOSTED_TOOL_DESCRIPTIONS.sendTestMessage : "Send a REAL message and stream a real veterinary answer, against a SANDBOX tenant only. Refuses unless the entitlement verdict reports livemode false. Returns the answer plus a frame histogram, so a stream that died mid-answer is distinguishable from a short one. Asks with a signed session when one is available, and with pet_profile 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.",
|
|
2943
|
-
inputSchema: {
|
|
2944
|
-
message: z.string().min(1).max(4e3).describe("What to ask. A real pet-health question gets a real answer."),
|
|
2945
|
-
idempotency_key: z.string().min(8).max(128).describe("Required and explicit. A call with a key and message already sent reuses that conversation and returns its stored answer instead of sending again. This is done by the tool reading the conversation first; the server does not deduplicate sends."),
|
|
2946
|
-
pet_ref: z.string().min(1).max(128).optional().describe("Optional pet to scope the conversation to. Must already be registered for this identity, unless pet_profile is passed, which registers it."),
|
|
2947
|
-
pet_profile: PET_PROFILE_SHAPE.optional().describe("Optional sandbox pet to register for this call's identity before the send (under pet_ref, else a fixed test ref), so the answer is scoped to it. Registered only once the sandbox fence has passed.")
|
|
2948
|
-
},
|
|
2949
|
-
outputSchema: SEND_TEST_MESSAGE_OUTPUT_SHAPE,
|
|
2950
|
-
annotations: { ...TOOL_ANNOTATIONS.everfur_send_test_message }
|
|
2951
|
-
},
|
|
2952
|
-
(args, extra) => run("everfur_send_test_message", args, extra.authInfo, (resolved) => (
|
|
2953
|
-
// On the hosted server the session comes from the Everfur API's mint for the signed-in sandbox tenant.
|
|
2954
|
-
sendTestMessage(args, resolved.env, shared, { hostedMint: resolved.hostedMint ?? null })
|
|
2955
|
-
))
|
|
2956
|
-
);
|
|
2957
|
-
if (hosted) {
|
|
2958
|
-
for (const tool of CONNECTOR_TOOLS) {
|
|
2959
|
-
const annotations = TOOL_ANNOTATIONS[tool.name];
|
|
2960
|
-
server.registerTool(
|
|
2961
|
-
tool.name,
|
|
2962
|
-
{ title: annotations.title, description: tool.description, inputSchema: tool.input, outputSchema: tool.output, annotations: { ...annotations } },
|
|
2963
|
-
(args, extra) => run(tool.name, args, extra.authInfo, (resolved) => tool.run(args, resolved, shared))
|
|
2964
|
-
);
|
|
2965
|
-
}
|
|
2966
|
-
}
|
|
2967
|
-
registerResources(server, shared.contract);
|
|
2968
|
-
registerPrompts(server, shared.contract, { sessionSource: hosted ? "hosted" : "env" });
|
|
2969
|
-
return server;
|
|
2970
|
-
}
|
|
2971
|
-
async function runStdioServer(env) {
|
|
2972
|
-
const server = buildServer({ env });
|
|
2973
|
-
await server.connect(new StdioServerTransport());
|
|
2974
|
-
}
|
|
2975
|
-
|
|
2976
|
-
export { MCP_SCOPE, REVOKED_AUTHORIZATION_REMEDY, SYNTHETIC_USER_REF_PREFIX, buildServer, createHostedTokenVerifier, hostedVerifyUserRef, isAccessTokenShaped, runStdioServer, sendTestUserRef, tenantContextOf, tokenBucketOf };
|