Skip to content

fix(mcp): re-sync the offline AUTHORING_INSTRUCTIONS fallback with the server primer [SAP-2959] - #734

Merged
gwitwer merged 3 commits into
mainfrom
feat/SAP-2959
Aug 29, 2026
Merged

fix(mcp): re-sync the offline AUTHORING_INSTRUCTIONS fallback with the server primer [SAP-2959]#734
gwitwer merged 3 commits into
mainfrom
feat/SAP-2959

Conversation

@gwitwer

@gwitwer gwitwer commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

What

Re-syncs packages/mcp/src/instructions.ts (AUTHORING_INSTRUCTIONS) with the canonical server-side primer, moving the offline fallback from 2.6-era text to the current 2.8 release, and adds a frozen sha-256 so the two copies cannot drift silently again.

Why

AUTHORING_INSTRUCTIONS is the copy the MCP server serves when its startup fetch of GET /v1/mcp/instructions fails. It is a verbatim duplicate of the server's canonical primer by design — the package must work offline, so it cannot import it — and the only thing enforcing that was a doc comment saying KEEP IT IDENTICAL.

It stopped being true two content releases ago. The fallback was missing both App Links releases:

  • 2.7 — the App Links paragraph in "Preview a web app" (a durable https://apps.sapiom.ai/{org}/{slug} that outlives the sandbox)
  • 2.8 — the shipped sapiom_dev_app_publish, version-gated at @sapiom/mcp >= 0.13

So the one path with no other source of truth — an offline session, which is precisely the case where the live fetch failed — was told App Links do not exist at all: no durable-sharing paragraph, no local publish tool, no hosted or REST surface. The copy also still carried the pre-2.7 single-alias framing of the hosted capability MCP, which now lives under its own sapiom-direct alias.

The drift ran both ways, which the ticket's "2.6-era text" framing (and my first description of this PR) obscured. Diffed against origin/main, this copy was behind on App Links and the alias framing, level on the LLM call-surface block, and ahead on ctx.shared — see Removed below. Blast radius is low either way, since online sessions live-fetch the server text and it wins.

What the sync removes

The fallback carried an 11-line ctx.shared quota contract — the inclusive 256 KiB / 262,144-byte limit, compact-JSON.stringify measurement, setter-time validation, no delete(), structural guards over instanceof — that is not in the served primer. Online sessions never saw it; only offline ones did.

Syncing removes it here, and I'm keeping that: the two are one canonical text, the ticket's scope is "copy verbatim," and the digest below cannot hold if the copies differ by a paragraph. It is disclosed rather than silent — there's a test named for the removal, and a Removed section in the changeset. The contract still reaches authors via packages/agent/README.md and the scaffold-shipped sapiom-agent-authoring skill. Putting it back into the primer is a server-side content release; flagged for the epic rather than reintroducing divergence here.

Changes

  • packages/mcp/src/instructions.ts — body replaced verbatim with the 2.8 primer; byte-identical to the server copy. Doc comment updated to describe the guard rather than restate a rule that had already failed.
  • packages/mcp/src/instructions.test.ts — new is byte-identical to the backend primer (frozen sha-256, SAP-2959) pins sha256(AUTHORING_INSTRUCTIONS) to 7f518d9c…4494c0df, the same digest the server-side spec pins for its current primer. Updates the assertions the old text happened to satisfy (the previous hosted-MCP framing), adds App Links coverage, and adds a test recording the ctx.shared removal above.
  • .changeset/ — patch release of @sapiom/mcp.

The guard, and why the existing tests missed this

Every contain assertion in this file passed against the stale text — they assert what must be present, and nothing noticed what had stopped being copied. So the copy fell two releases behind with a green suite.

The digest is what binds the two copies, but only because both ends pin it, which review on the paired server-side PR was right to press on. A pin checked only against its own body is a self-consistency check: it catches an accidental in-place edit here and nothing else. In particular it would not catch the direction that caused this incident — a server-side content release, which touches that repo only.

So the paired PR pins this same digest against the server's current primer. Cutting the next release changes that hash and reddens a spec that names this pin, which is the moment its author discovers this body has to move too.

Stated plainly, in the code as well as here: neither pin can block a merge in the other repository, and an author can still move one side alone. What the pair removes is the silent path — drifting now takes a deliberate edit to a line that says what it is for. Re-pointing this digest alone just re-blesses the drift.

This is the fourth reactive heal of the same class of drift (SAP-1367 → SAP-1467 → SAP-2227 → this); the guard is the point of the ticket, not the copy.

Known-adjacent, deliberately not in this PR

The repo ships three answers on MCP aliases: docs/mcp-servers.md:7 (Remote sapiom / Local sapiom-dev), README.md:141 (claude mcp add sapiom-dev), and packages/mcp/README.md:35 (claude mcp add sapiom, agreeing with the primer). Review flagged it here.

It's real, but it isn't this PR's: the alias vocabulary is the canonical primer's, decided server-side in 2.7 and already live to every online session. The contradiction exists on main today and this diff doesn't change it — it only moves the offline copy onto the vocabulary the online path already uses. A partial fix would leave docs/mcp-servers.md built end-to-end around the inverse mapping, so the repo would still ship three answers; doing it properly is a docs-page rewrite that belongs with SAP-2923. Flagged for the epic.

Testing

  • vitest run src/instructions.test.ts src/instructions-fetch.test.ts — 14 passed, including the five regression assertions restored after review (tool_use, "it forces a tool", "treat missing as unknown", "reports both as `null` today", not.toContain("If you must pin")), all verified passing against the new text
  • tsc --noEmit — clean
  • eslint src --ext .ts — clean
  • prettier --check on the touched files — clean
  • Independently verified the new body byte-for-byte against the server constant before writing the digest

Related

🤖 Generated with Claude Code

https://claude.ai/code/session_01M2AUF348cx5Mh4N9bbnkSF

…e server primer (SAP-2959)

The fallback is served only when the startup fetch of GET /v1/mcp/instructions
fails — the one path with no other source of truth — and it had drifted two
content releases behind the canonical primer (2.6 → 2.8). Offline sessions were
therefore told App Links do not exist at all: no durable-sharing paragraph, no
sapiom_dev_app_publish, no hosted or REST publish surface. The copy also still
carried the pre-2.7 single-alias framing of the hosted capability MCP.

The body is now byte-identical to the server's copy. Because the doc comment
demanding that was the only thing holding the two together — and it failed
silently across two releases — instructions.test.ts now pins sha-256 of the
string to the same frozen digest the server-side spec pins for this content
release. A one-sided edit reddens one repo or the other, with no network call
from either suite; the `contain` assertions it joins all still passed against
the stale text, which is why they could not catch this.

Refs: SAP-2959
@github-actions

Copy link
Copy Markdown
Contributor

Review — PR #734 (fix(mcp): re-sync the offline AUTHORING_INSTRUCTIONS fallback)

1. The new alias vocabulary contradicts this repo's own published docs

The primer now asserts local = sapiom ("the supported local alias") and hosted =
sapiom-direct, and the changeset announces the rename to npm consumers. Nothing else in
the repo was updated:

  • README.md:141claude mcp add sapiom-dev -- npx -y @sapiom/mcp (local registered as
    sapiom-dev).
  • docs/mcp-servers.md:7,17Remote sapiom / Local sapiom-dev: the exact
    inverse mapping. sapiom-direct appears nowhere in that doc.

A user who follows docs/mcp-servers.md registers the hosted MCP as sapiom, then this
primer tells their agent that sapiom is the local authoring server and that
sapiom_dev_* tools live there. That is an alias collision on one name — precisely the
conflation the change claims to fix, now created between two published surfaces instead of
inside one. packages/mcp/README.md:35 already says sapiom, so the repo currently ships
three answers. Update README.md and docs/mcp-servers.md in this PR.

2. The digest cannot detect the drift direction that actually happened

instructions.test.ts:110 hashes AUTHORING_INSTRUCTIONS and compares it to a literal in
the same repo. It therefore catches only edits to this file. The failure SAP-2959 is
healing is the opposite direction — the server primer advanced through 2.7 and 2.8 while
this constant stood still — and in that scenario this test stays green indefinitely,
because nothing here reads the server text (no fixture, no fetch; instructions-fetch.ts
is mocked in its tests). Coverage against the server rests entirely on the paired
server-side PR pinning the identical value, which the PR body describes as not yet landed.

The changeset states the guarantee as already in force: "a frozen sha-256 … the same
digest the server-side spec pins for this content release — fails CI on a one-sided edit."

That is unretractable text asserting an invariant that does not hold from this repo alone
and does not hold at all until the other PR merges. Either land them together or soften the
changeset to what this repo actually enforces.

Related trap in the same contract: instructions-fetch.ts:25 .trim()s the server body but
the digest is taken over the untrimmed constant. If the canonical server text ends with a
newline (normal for a markdown asset), the two intentionally-identical copies produce
different digests forever. Define the contract as "sha-256 of the trimmed text" on both
sides.

3. Deleted regression assertions guard text that is still present

The rewrite removes assertions whose subject matter did not change:
toContain("tool_use"), toContain("it forces a tool"), toContain("treat missing as unknown"), toContain("reports both as \null` today"), and — most notably — not.toContain("If you must pin"), which existed specifically to stop a known-wrong line from returning (instructions.test.ts, old lines 66–83). All that content is still in instructions.ts:100–115`; only its coverage was dropped.

The digest does not replace these. By its own documented process it is re-pointed on every
content release, so it catches accidental drift but never a deliberate regression made
during a sync — which is exactly when "If you must pin" would come back. Restore the
negative assertions and the disclosure-scoping ones; they are cheap and orthogonal to the
digest.

4. The PR removes the ctx.shared quota contract without saying so

instructions.ts:93 collapses the full quota paragraph — inclusive 256 KiB /
262,144-byte limit, compact-JSON.stringify measurement, setter-time validation, no
delete(), structural guards over instanceof — to one clause, and deletes the test named
for it. The PR body and changeset describe this sync as restoring missing App Links
material; neither mentions that guidance was also removed. A consumer reading the CHANGELOG
cannot tell.

Mitigating: the contract survives in packages/agent/README.md:91 and the scaffolded
sapiom-agent-authoring skill, so a scaffolded project still has it. Cloned or hand-written
projects in an offline session — the case this PR is about — do not. If the upstream primer
really dropped it, say so explicitly in the changeset.

5. Changeset publishes internal build/repo mechanics

The second paragraph ("a frozen sha-256 in instructions.test.ts … the same digest the
server-side spec pins … fails CI on a one-sided edit instead of letting them drift silently
a third time"
) describes a private companion repo, its spec, and CI behaviour, in a
CHANGELOG.md that ships inside the npm tarball and can never be edited. None of it is
actionable for a consumer. Cut it to the user-visible fact: the offline fallback now matches
the served primer and includes App Links. The same applies to the new JSDoc at
instructions.ts:12–18 — with declaration: true and no removeComments, that block is
emitted into dist/instructions.d.ts and published.


Verdict: Request changes — the docs contradiction (#1) ships a broken setup path, and the
guard the ticket is named for (#2) does not bind in the direction that failed.

Review on the paired server-side PR caught the claim overreaching. Both pins
are self-consistency checks against their own body, so the direction that
caused this incident — a server-side content release — moved one copy while
both suites stayed green. Saying "a one-sided edit reddens one repo or the
other" was true only for an in-place edit of this file.

The server-side spec now pins this same digest against its own current primer,
which is what makes a release redden something. Say that, and say the limit
out loud: neither pin can block a merge in the other repository. What the pair
removes is the silent path, not the possibility.

Refs: SAP-2959
@github-actions

Copy link
Copy Markdown
Contributor

Follow-up review — PR #734 (delta since d20a1be8)

One commit landed (8e5b5c4f), and it is prose-only: the changeset's last paragraph and
the two comments about the digest. No code, no docs, no test assertions moved.

Fixed

New

  • 🔒 The rewritten JSDoc publishes more internal process, not less.
    packages/mcp/src/instructions.ts:12-19 now tells npm consumers that a private
    companion repo
    holds the canonical copy, that its spec pins a digest, and that
    "neither can block a merge in the other repository." Root tsconfig.json:13 sets
    declaration: true with no removeComments, so this is emitted verbatim into
    dist/instructions.d.ts and shipped in the tarball. The delta expanded this block
    rather than trimming it. Consumers need one sentence: this constant is the offline
    fallback and must match the served primer. The cross-repo CI mechanics — and the
    admission of what it cannot enforce — belong in the PR body.
  • The changeset (.changeset/resync-authoring-instructions-fallback.md) still spends its
    second paragraph on the same private-spec mechanics. That text is unretractable once
    published. Cut it to: the fallback now matches the served primer and includes App Links.

Not fixed from round 1

  • cleanup HTTP SDKs #1 (blocking)docs/mcp-servers.md:7,10 and README.md:141 still register hosted
    as sapiom / local as sapiom-dev; instructions.ts:29-43 still asserts the inverse.
    A user following the published docs hits the alias collision this PR claims to resolve.
  • langchain SDK config cleanup #3 — no assertion restored; must pin / tool_use / disclosure-scoping coverage is
    still absent from instructions.test.ts while the text remains at instructions.ts:104-114.
  • langchain v1 support + fixes #4 — the ctx.shared quota contract is still collapsed to instructions.ts:95 with
    no mention in the changeset.
  • #2binstructions-fetch.ts:25 still .trim()s the fetched body while the digest
    is taken untrimmed. Precision on round 1: the constant currently has no trailing
    whitespace, so this is latent, not active — it bites the first time the server copy ends
    with a newline. Still worth defining the contract as "sha-256 of the trimmed text."

Verdict: Request changes — the push corrected one overclaim and worsened the published
comment; the blocking docs contradiction is untouched.

…the sync drops

Review caught that this PR's test rewrite deleted coverage whose subject matter is
still present. It was right, and the cause was mine: I read the base file from a
stale local branch rather than origin/main, then wrote the replacement wholesale
over a version 52 lines longer than the one I had looked at.

Restored, all verified passing against the new text: tool_use, "it forces a tool",
"treat missing as unknown", "reports both as `null` today", and the negative
not.toContain("If you must pin") that exists to stop a known-wrong line coming back.
The digest does not substitute for these — it is re-pointed on every content release
by design, so it catches accidental drift but never a deliberate regression made
during a sync, which is exactly when that line would return.

The ctx.shared quota test could not be restored: its subject really is gone. That
paragraph was in this fallback and not in the served primer, so online sessions never
saw it, and syncing removes it here. That is the direction the rule requires — one
canonical text, and the digest cannot hold if the copies differ by a paragraph — but
it was undisclosed. It now has a test naming the removal and its reason, and the
changeset says so instead of describing this as purely additive.

Also cut the published surface down to what a consumer can act on. tsconfig.build
emits instructions.ts's JSDoc into dist/instructions.d.ts, so CI mechanics and repo
layout were shipping in the npm tarball, as was the changeset's second paragraph in
a CHANGELOG that can never be edited. The sync rule and the digest's limits moved to
instructions.test.ts, which is excluded from the build.

Refs: SAP-2959
@gwitwer

gwitwer commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Four of five actioned; one declined with reasoning, and one correction to the review. Pushed in 499e2bf (and 8e5b5c4 for #2).

3. Deleted regression assertions — you're right, and the cause was worse than sloppiness

Restored all five. The cause is worth stating plainly: I read the base test file from a stale local branch instead of origin/main, then wrote the replacement wholesale. origin/main is 108 lines; the version I had looked at was 56. I overwrote 52 lines of coverage I never saw. The toContain set I "updated" was reconstructed from the wrong file.

Restored and verified passing against the new text: tool_use, "it forces a tool", "treat missing as unknown", "reports both as `null` today", and not.toContain("If you must pin") — with their original comments, which explain why each exists.

Your reasoning for why the digest doesn't cover them is the part I'd gotten wrong: it is re-pointed on every content release by design, so it catches accidental drift but never a deliberate regression made during a sync — which is precisely when "If you must pin" would come back. Negative assertions and the digest are orthogonal. They're back.

4. The ctx.shared quota removal — undisclosed, now disclosed

Correct and it was the most consequential thing in the diff. Confirmed against origin/main: the 11-line contract was in this fallback and not in the served primer, so online sessions never saw it — this copy was ahead there, not behind, which my "2.6-era text" framing obscured.

I'm keeping the removal. The two are one canonical text, the digest cannot hold if they differ by a paragraph, and the ticket's scope is "copy verbatim." But it now has a test named for it recording what left and why, the changeset has a "Removed" section, and the PR body no longer describes this as purely additive. Restoring it to the primer is a server-side content release — I've flagged it for the epic rather than reintroducing divergence here.

5. Changeset and JSDoc publish internal mechanics — correct on both, including the part I'd missed

Verified: tsconfig.build.json excludes only src/**/*.test.ts, so instructions.ts's JSDoc is emitted into dist/instructions.d.ts — I confirmed it in the built output. CI mechanics and repo layout were shipping in the tarball.

Both trimmed to what a consumer can act on. The sync rule, the digest, and its limits moved into instructions.test.ts, which is excluded from the build. The changeset is now Added/Removed against the served primer, with the quota disclosure.

2. Digest direction — fixed before this review landed

You reviewed d20a1be. 8e5b5c4 and the paired PR's f203024 fix exactly this: the server-side spec now pins the same digest against its current primer, so cutting a release reddens there and forces its author onto this pin. Both sides now say out loud that neither can block a merge in the other repository — what the pair removes is the silent path, not the possibility.

On the .trim() trap: I don't think it bites, and I'd rather not "fix" it. instructions-fetch.ts:25 trims the runtime HTTP body; the digests are taken over the two source constants, which I diffed byte-for-byte before writing the pin. Neither constant has leading or trailing whitespace — they open on # and close on project. immediately before the backtick — so trimmed and untrimmed hash identically today, and if that ever stopped being true the digest is what would catch it. Hashing trimmed text on both sides would add a normalization step whose only effect is to make one class of real divergence invisible. Happy to be shown wrong if you're seeing a path where the served body and the constant legitimately differ by whitespace.

1. Alias contradiction — real, but declining to fix it here

Confirmed all three surfaces, so the repo does ship three answers:

  • docs/mcp-servers.md:7 — Remote sapiom / Local sapiom-dev
  • README.md:141claude mcp add sapiom-dev -- npx -y @sapiom/mcp
  • packages/mcp/README.md:35claude mcp add sapiom -- npx -y @sapiom/mcp (agrees with the primer)

Where I disagree is that this PR ships it. The alias vocabulary is the canonical primer's, decided server-side in 2.7 and already live to every online session — which is nearly all of them, since this constant only serves when the fetch fails. The contradiction exists on main today and is unchanged by this diff. This PR moves the offline copy onto the same vocabulary the online path already uses; it makes an existing inconsistency easier to notice, not worse.

I also don't think a partial fix helps. Correcting README.md alone leaves docs/mcp-servers.md built end-to-end around the inverse mapping — a comparison table, "Remote sapiom" and "Local sapiom-dev" section headings, and prose — so the repo would still ship three answers, just rearranged. Doing it properly means rewriting that page against the canonical vocabulary, which is a docs change with its own review and belongs with SAP-2923 (this epic's docs ticket, "Document App Links on docs.sapiom.ai, in the MCP connect-time instructions"). Folding it in here would also put a docs rewrite inside the PR whose one testable property is that the primer body is byte-identical to the server's.

Flagged for the epic. If a maintainer wants it in this PR I'll do the full page, but I don't think a stale-docs fix should ride along with the sync it's unrelated to.


Correction to one thing in the review, since it shaped point #2's framing: this copy was not uniformly "2.6-era." It was behind on App Links and the alias framing, level on the LLM call-surface block, and ahead on ctx.shared. The drift was bidirectional, which is a slightly stronger argument for the pin than the one-directional story.

@gwitwer
gwitwer merged commit 03da8e2 into main Aug 29, 2026
11 checks passed
@gwitwer
gwitwer deleted the feat/SAP-2959 branch August 29, 2026 01:21
@gwitwer

gwitwer commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator Author

For the record, since the last review on this page reads "Request changes" against findings that were fixed in the commit that merged (499e2bf, reviewed at 8e5b5c4f):

  • langchain SDK config cleanup #3 deleted regression assertions — restored. tool_use, "it forces a tool", "treat missing as unknown", "reports both as `null` today", and not.toContain("If you must pin") are all on main, with their original comments. Verified passing against the new text.
  • langchain v1 support + fixes #4 ctx.shared quota removal — disclosed. There's a test named for the removal explaining that the paragraph was in this fallback and not in the served primer, and the changeset has a "Removed" section.
  • chore: update URLs, defaults, and LangChain v1.x docs #5 / JSDoc — the changeset is Added/Removed against the served primer with no CI or repo mechanics, and the instructions.ts JSDoc no longer names a companion repo or describes enforcement; that detail moved to instructions.test.ts, which tsconfig.build.json excludes from dist.
  • update SDKs to use payment protocol data #2 digest direction — the server-side spec now pins the same digest against its own current primer, so a content release reddens there.

Verified against merged main, not the PR head: AUTHORING_INSTRUCTIONS diffs byte-identically against the server constant, and the five restored assertions are present.

Still open, deliberately not in this PR — worth a ticket: the alias contradiction from #1. docs/mcp-servers.md (83 lines, built end-to-end around "Remote sapiom" / "Local sapiom-dev") and README.md:141 still teach the inverse of what the primer says, while packages/mcp/README.md:35 agrees with it. My reasoning for excluding it is in the comment above — pre-existing on main, already live to every online session since 2.7, and unchanged by this diff — but it is real and it should be fixed. It's a docs-page rewrite against the canonical vocabulary and belongs with SAP-2923.

I also have a two-line follow-up trim to the published JSDoc (dropping "and what that digest can and cannot catch" from the maintainer pointer) that missed the merge. Not worth its own PR on its own; happy to fold it into whoever takes the docs ticket.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant