/** * The vocabulary both transports speak. * * `@shipstatic/mcp` (stdio) and the hosted Streamable-HTTP server are one * product with two doors in. Everything an agent observes that is NOT forced * apart by the transport lives here and is IMPORTED by both — because a fact * with two owners is a fact that drifts. This pair kept ten such strings * byte-identical by hand for a year, and the hand slipped: a tool description * diverged unnoticed, a one-word correction had to be applied at three sites, * and a test mock invented constraint numbers production never used. A * coordination table written in prose is a specification for drift, not a * defence against it. * * What belongs here: anything true of a ShipStatic deploy regardless of how * the bytes arrived. What does not, and why: * * - **The file-input schema.** A filesystem path here, inline content there: * Workers has no filesystem. Structurally forced apart. * - **Tool descriptions**, as whole strings. They are deliberately rewritten * hosted-side for an Apps-SDK caller that must be told not to base64-encode * text — a failure mode the filesystem path does not have. What genuinely * overlaps is exported as `DESCRIPTION_BLOCKS` and composed per transport. * - **Anything Apps-SDK** (widget, `_meta`, `outputSchema`): hosted-only by * nature. * * Each of those is recorded in `cloudflare/mcp/CLAUDE.md`'s divergence table. * Everything else should be here, and adding a shared fact anywhere else is * how the next year's drift starts. */ /** * Two packages, and the split is a rule rather than an accident: **read a * constant from whatever will act on it.** * * The label, password and idempotency-key constraints come from * `@shipstatic/ship` because the SDK is what validates a value against them * before it reaches the wire — describing a bound the client in the same * process will not honour is the drift that matters, and reading both from one * module makes it impossible. `@shipstatic/types` declares them, but ship * bundles its own copy, so importing them from types here would let a describe * advertise a limit the validator beside it rejects. * * The public-deploy lifetime is the other kind of fact. Ship never reads it — * the API stamps it — so there is no validator to agree with, and taking it * from the package that merely forwards it would mean a ship release every * time the platform's own vocabulary grows. */ /** * The server name every transport reports in `serverInfo`. * * Shared because it is not only prose: the Apps-SDK widget's bridge handshake * sends `appInfo.name`, which the HOST correlates against `serverInfo.name` to * tie the rendered view to the connector. Two literals kept equal by comment * is exactly the shape this package exists to delete. */ export declare const SERVER_NAME = "shipstatic"; /** * The one tool authored per transport — its INPUT differs (a filesystem path * over stdio, inline bytes over HTTP), its NAME must not. Exported so the * hosted parity fence can build the expected catalogue as * `[UPLOAD_TOOL_NAME, ...ACCOUNT_TOOL_NAMES]` rather than counting to fifteen. */ export declare const UPLOAD_TOOL_NAME = "deployments_upload"; /** * The upload tool's human-readable `title`, shared for the same reason the * name is — and NOT for the reason the description is not. * * A title names the OPERATION, and the operation is identical on both doors: * a user reading "Deploy Static Site" in a client's tool list learns nothing * about how the bytes got there. The description is the opposite — hosted * spends a paragraph telling an Apps-SDK caller not to base64-encode text, a * hazard a filesystem path cannot have — which is why one is exported whole * and the other only in fragments. * * The other fourteen titles live inline in `tools.ts`: one definition, both * transports, nothing to keep in agreement. This one is authored per transport * (upload is the tool each door writes for itself), so without an owner it * would be two literals in two repos with nothing comparing them. * * Titles are not decoration here: the Claude connectors directory refuses * submission for a tool that lacks one. */ export declare const UPLOAD_TOOL_TITLE = "Deploy Static Site"; /** * How long an anonymous deployment lives, in the words an agent reads. * * **Derived, since `@shipstatic/types@2.5.0-beta.19`.** It was a restatement * until then, and deliberately the only one — the duration had appeared in * eight places across the two servers and the widget, so a TTL change had to * find all eight. Both halves of the fix landed together: types declares the * number and `cloudflare/api` imports it back, because exporting without the * import-back would have given the fact two owners instead of ending the * duplication. * * A phrase rather than a number because every consumer is prose: the value has * to carry its own unit, and dividing by 86400 at eight sites would restate the * unit eight times instead of the number. * * The unit stays literal, and that is the one assumption here: this reads * correctly while the TTL is a whole number of days, which it has always been. * A TTL of hours would need the prose reviewed anyway — the widget's own * `formatExpires` speaks in days and hours too — so the honest failure is a * sentence someone must rewrite, not a number that silently rounds. */ export declare const PUBLIC_EXPIRY: string; /** * What is true of one tool, on the three axes a host reads. * * One row per tool lives in the registry (`tools.ts`, `TOOLS`), and every * other statement about the tool is derived from it: the MCP annotations * (`annotate`), the account-tool set (and so the hosted door's * `securitySchemes` and challenge scope), the listing justifications in * `integrations/gpt`, and the catalogue tests. Until 1.11.0 these facts had * three owners: four annotation classes here, a hand-written name list for * the auth need, and the upload tool annotated by hand on both transports. * Three owners for facts about one tool is how the classes came to spread * `openWorldHint: true` over ten reads that touch nothing public. */ export interface ToolContract { /** `optional`: works without an account, does more with one. */ auth: 'optional' | 'required'; /** * What a call does to the platform's state. `add` creates something new * on every call; `replace` overwrites what is there; `remove` deletes it. */ mutation: 'none' | 'add' | 'replace' | 'remove'; /** Whether the call changes public internet state, or only the account's. */ reach: 'account' | 'public'; } /** * The MCP annotations a tool's contract implies. An agent reads them to * decide whether it may call speculatively (`readOnlyHint`), whether it must * confirm with the user first (`destructiveHint`), whether a retry is free * (`idempotentHint`), and whether the call reaches beyond the account * (`openWorldHint`). Both hosts read the same four: Anthropic's review asks * for an accurate `readOnlyHint` and `destructiveHint`, OpenAI additionally * reads `openWorldHint` as "can change public state". * * **`idempotentHint` is promised only for a remove**, the one mutation where * a repeat is MEASURED to have no further effect (both deletes are fenced in * the API: a repeat neither flips state nor writes an audit row). A replace * writes an activity row per call, and `domains_set` notifies and may enqueue * DNS work besides, so it is not claimed. An add creates on every call, and * `idempotencyKey` cannot rescue the claim: that property is conditional on * an argument the caller may not pass, while the annotation is static per * tool. Reads carry none because the spec defines the hint only for tools * that mutate. */ export declare function annotate({ mutation, reach }: ToolContract): { readonly openWorldHint: boolean; readonly idempotentHint?: boolean | undefined; readonly readOnlyHint: boolean; readonly destructiveHint: boolean; }; /** * One title, two wire slots: project a tool config's top-level `title` into * `annotations.title` at registration. * * The MCP spec carries a tool title in two places. The 2025-06-18 revision * put it top-level on the Tool object (where every registration here states * it), and `ToolAnnotations.title` is the older 2025-03-26 slot that some * readers still key on. The Claude connectors portal is one of them, measured * on submission day 2026-08-31: its Tools step rendered every top-level title * as the heading and simultaneously flagged all fifteen tools with "Missing * annotations: title". So both slots must be populated, and they must agree. * * The top-level `title` stays the single owner. This projection is the only * writer of the annotations copy, applied at every registration on both * transports, and the catalogue fences hold the two slots equal off a real * `tools/list` so a registration that bypasses it goes red rather than * shipping a bare slot back to the portal. */ export declare function titled(config: C): C; /** * INSTRUCTIONS sentences both transports say. * * `initialize`'s instructions are the other half of what an agent reads * before acting (the catalogue is the first). Each transport composes its own * from these blocks plus the two things that are genuinely its own: how files * arrive, and how a caller authenticates. * * Three of these are duplicated prose TODAY, kept equal by review. The last * two are stdio-only only because the hosted transport has no domain tools * yet — when it gains them with OAuth they become shared too, which is * precisely when someone would otherwise copy them across. */ export declare const INSTRUCTION_BLOCKS: { readonly opening: "ShipStatic deploys static websites instantly. Free, no account required."; readonly live: "The site is live immediately."; /** * A block of its own rather than a clause of `live`: two sentences, two * facts, and a door may compose them independently. * * The read-back rule lives HERE and not in the upload description: a * password the user never sees locks them out, and `instructions` is the * one place a server may say what an agent should do. A tool description * describes the tool; both listing reviews reject one that instructs the * model, and the catalogue tests grep for it. */ readonly password: "To make the site private, pass `password`; visitors must unlock before viewing, including on any custom domains pointing at it. Tell the user any password you set, since one they never see locks them out."; readonly claim: "The response includes a claim URL — always show the deployment URL and the claim URL to the user so they can keep the site permanently."; readonly conceptsHeader: "Concepts:"; readonly deploymentConcept: "- Deployment: an immutable set of files with an instant URL (e.g. happy-cat-abc1234.shipstatic.com). No setup needed."; readonly domainConcept: "- Domain: a custom domain (e.g. www.example.com) pointing to a deployment. Optional. Subdomains only — not apex domains."; readonly domainWorkflow: "To add a custom domain: domains_validate → domains_set → domains_records (show DNS records to user) → user configures DNS → domains_verify."; }; /** * The fragments of the upload tool's description that both transports say. * * The surrounding descriptions diverge on purpose — hosted opens for an * Apps-SDK caller and spends a paragraph on plain-text-vs-base64, stdio takes * a filesystem path and has no such hazard — so what is shared is smaller than * a sentence in one case and exactly a sentence in the other. Both were pinned * by a `toContain` on each side, which meant three copies of each fragment * (two sources and a test literal) held equal by nobody. * * Same reasoning as `INSTRUCTION_BLOCKS`, one altitude down: blocks are shared, * composition is per transport. */ export declare const DESCRIPTION_BLOCKS: { /** The no-account promise, mid-sentence in both openings. */ readonly free: "free, no account or API key required"; /** * The option, stated as a capability. The read-back rule that used to * follow it ("always show the password to the user") is behavioural * guidance, which belongs to `INSTRUCTION_BLOCKS.password`. */ readonly password: "Pass `password` to make the site private."; }; /** * Deploy-parameter descriptions shared by both transports. * * The numbers interpolate from `@shipstatic/types` rather than being written * out, so a platform constraint change reaches every agent-facing string * without anyone editing prose — the same reason the API and the SDK import * them instead of restating them. */ export declare const PARAM_DESCRIPTIONS: { readonly labels: "Labels for organizing deployments (e.g. [\"production\", \"v1.2\"]). Lowercase, 3-25 chars, allows . _ - separators. Up to 10."; readonly password: "Optional password to gate the deployment behind an unlock prompt (6–128 characters; whitespace significant). Visitors must enter this password before viewing the site, including on any custom domains pointing at it."; /** * Shared even though only stdio offers the option today: the hosted door * gains it with OAuth (it can scope a replay per user once callers have an * identity), and the law this teaches — key the ATTEMPT, never the try — is * the same one on both. The window is derived, never typed out. */ readonly idempotencyKey: `Makes this deploy replayable instead of repeatable. A deploy is not naturally idempotent: if a call times out you cannot tell "it never landed" from "it landed and the response was lost", and retrying creates a second deployment. Send the same key on the retry and the original deployment is replayed instead (within ${number} hours). Key the ATTEMPT \u2014 a run id, a commit sha, a uuid minted before the first try \u2014 never one minted fresh on each retry, which would defeat the point.`; /** * Shared and spoken by BOTH doors — stdio declared it first (2026-08-13) * and the hosted door converged the same day, importing this describe * verbatim (its catalogue fence holds it to the import). The two refusals * this teaches belong to the platform, so they read identically on every * door. * * **Two things are deliberately absent, and both are the same rule.** The * RANGE, because `@shipstatic/ship` validates it in-process before a byte is * uploaded and relays the constitution's own sentence — a second copy here * could only ever disagree with it. And the name of any credential: stdio * owns `SHIP_TOKEN`, the hosted door owns "connect an account", and a SHARED * string that named either would put one door's fact in the other's mouth. * `tests/vocabulary.test.ts` fences the second half for every member. */ readonly ttl: "Seconds until this deployment expires and the platform reclaims it; omit for one that never does. Only for authenticated deploys — an anonymous deployment already expires on the platform's schedule, and a requested ttl on one is refused. A deployment carrying a ttl cannot be linked to a custom domain: deploy without one if the site needs a domain."; };