Skip to content

protocol/mcp: own the Result to ToolsCallResult adapter - #1718

Merged
sergey-shandar merged 2 commits into
mainfrom
claude/open-issue-fix-76mxy8
Aug 26, 2026
Merged

protocol/mcp: own the Result to ToolsCallResult adapter#1718
sergey-shandar merged 2 commits into
mainfrom
claude/open-issue-fix-76mxy8

Conversation

@sergey-shandar

@sergey-shandar sergey-shandar commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Fixes fjs/protocol/mcp/todo/result-adapter.md, deleted here.

fjs/protocol/mcp already owns this vocabulary — okResult, and errorResult
derived from it. The missing third member is the one that picks between them,
and five tool handlers had each written that dispatch out by hand:

r => pureOk(r[0] === 'error' ? errorResult(evoSummary(r[1])) : okResult(toJson(r[1])))

toolResultStep(e, text, errorText) is that member. It is resultMapStep over
one fallible operation, rendering the ok value with text and the failure
with errorText into an isError result. Its success channel is
ToolsCallResult and its error channel is never — a tool-level failure is a
result the client reads, never a transport error. Every converted handler is
now one line:

({ subject }) => toolResultStep(e.head(subject), hs => hs.join('\n'), evoSummary)

Two corrections to the issue's design

The issue proposed a pure adapter, resultResult, curried over a single ok
renderer, taking a Result whose error channel is already a string and
answering a ToolsCallResult. Neither half survives contact with the code as it
stands, so the design shipped here differs on both points:

  • The error channel is not string. All five sites render a structured
    failure through a summariser (evoSummary in fjs/mcp/evo, errorSummary
    in fjs/mcp/cas), so the adapter takes two renderers, not one. Requiring
    errorText rather than defaulting it is the load-bearing part, for the reason
    unwrapStep gives for its own summary: a renderer written for a particular
    error channel cannot accept a wider one, so a fallible call added upstream
    becomes a compile error at the tool that has to say what the new failure reads
    like, instead of reaching the client as whatever String made of it.
  • A pure adapter leaves most of the noise behind. Every call site is
    resultStep(effect, r => pureOk(…)), so an adapter over the plain Result
    would still be spelled r => pureOk(resultResult(…)(r)) at each one. Taking
    the effect makes the handler a single line and is what closes the duplication
    the issue is about. This is not the step(e, x => pure(f(x))) spelling
    map-step-combinator is retiring — it is written with the combinator that
    issue points at, resultMapStep.

The issue also cited a §6.3 that no longer exists under that number after the
guidelines were restructured; the rule it meant is DESIGN.md §4 (DRY) and §9
(signal-to-noise). Nothing else referenced the file, so it is simply deleted.

Scope

The issue's task list named evo_revision and evo_add. evo_list, evo_head
and cas_list are the identical shape and are converted too — five sites, one
improvement. The per-handler comments explaining why a cache-slot failure is a
tool-level error, and why evo_list encodes JSON rather than lines, are kept
where they were.

Checks

  • npx tsc — clean.
  • fjs test — 3398 pass, 0 fail, including the two new protocol/mcp proofs
    (toolResultStep.okIsRendered, toolResultStep.errorIsRenderedInBand, one
    per branch; the error one asserts isError is set, which is what the never
    error channel claims).
  • npm run cov — 100% lines / branches / functions, thresholds enforced.

Changelog:

  • protocol/mcp: new toolResultStep(e, text, errorText) — answers a tool call
    with the outcome of one fallible operation, rendering the value or the failure
    (as isError) into a ToolsCallResult.

🤖 Generated with Claude Code

https://claude.ai/code/session_014jZKR1C2qxXCueDSBKBnFg

claude added 2 commits August 26, 2026 14:46
Adds `toolResultStep` and converts the five handlers that hand-rolled it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jZKR1C2qxXCueDSBKBnFg
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jZKR1C2qxXCueDSBKBnFg

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2e539bb05d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread fjs/protocol/mcp/module.f.mjs
@cloudflare-workers-and-pages

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
functionalscript 4248009 Commit Preview URL

Branch Preview URL
Aug 26 2026, 02:48 PM

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. The move is exact and neither caller changed.

The five base sites were the same expression differing on two axes only — the error renderer (errorSummary vs evoSummary) and the per-tool ok renderer — and toolResultStep(e, text, errorText) parameterizes precisely those and nothing else. The unfolding is expression-for-expression: resultMapStep(e, f) is resultStep(e, r => pure(f(r))) and pureOk(v) is pure(ok(v)), so head's form is base's form rewritten, not reinterpreted.

I measured it rather than reading it: 44 comparisons through the real registries in both worktrees, dumping the full ToolsCallResult with key order — the evo registry over a 10-value corpus × 4 tools, plus cas_list through the same synthetic-op drive its proof uses. cas_list identical including key order; evo 37/40 identical, and the 3 that differ are type-impossible inputs where only a local variable name inside a JS TypeError message differs. Nothing the type admits diverges.

Mutation testing shows coverage stacked rather than thinned. Five mutations, applied to toolResultStep at head and to all five inline copies at base: all killed on both sides, and head's killer set is base's plus exactly one new unit proof each time — errorIsRenderedInBand on top of the five caller proofs for the isError mutations, okIsRendered on top of the round-trip proofs for the text ones. No mutation survived at head that base killed.

Worth noting M1 (tag === 'error'!==) is killed by tsc with two TS2345s at module.f.mjs:218-219 — which is the empirical demonstration of the README's claim that a required errorText pins the handler's error channel at compile time.

Layering is right: protocol/mcp's import block is byte-identical base to head (resultMapStep and ok were already there), no fjs/mcp/** import anywhere in it, and mcp/evo correctly drops its now-unused imports while mcp/cas keeps the ones cas_add/cas_get still use. Callers lost nothing — assert sites 10 → 10 and 37 → 37, leaf delta is exactly the two new proofs. The deleted todo's tasks are both done and exceeded (5 sites converted, not 2), no dangling links, and the body honestly documents the two departures from its proposal.

One thing, not blocking and not in the Changelog: section — that one is byte-identical to the file and correctly placed. The body prose carries 5 HTML-escaped entities: Effect&lt;O, ToolsCallResult, never&gt;, &lt;T&gt;(render: (value: T) =&gt; string), r =&gt; pureOk(…). Same escaping that hit #1708's changelog item; it's confined to prose here, so nothing malformed ships, but it will land in the squash commit looking like that.

Gates: npm test 3396 → 3398, tsc --noEmit exit 0, purge matched nothing. No **BREAKING CHANGES:** needed — toolResultStep is purely additive and no caller's output moved.

@sergey-shandar

Copy link
Copy Markdown
Contributor Author

@codex review

Copy link
Copy Markdown
Contributor Author

Thanks — and the escaping note is right, with one correction worth recording since it recurs.

The 5 entities you counted were mine, and they are now removed. I had put them there deliberately: the original body wrote the type as Effect with its three arguments in angle brackets, and GitHub's sanitizer swallowed the bracketed run as a tag, so the stored body read as a bare Effect. Escaping them by hand preserved the content but, as you saw, preserved it as entities. The current body avoids angle brackets in prose entirely — "its success channel is ToolsCallResult and its error channel is never" — which loses nothing and needs no escaping. The proposed-signature code block went the same way, into prose.

What is not fixable from the prose side: the remaining &#39; and &gt; inside the two js fences. I submitted this revision with plain ' and => characters and they came back stored as entities, so that transformation is in the API path used to write the body, not in anything the description says. That is the same thing that hit #1708's changelog item. It will land in the squash message looking like that unless it is fixed in the merge box; the Changelog: section itself has no arrows or quotes, so it is unaffected either way.

Head is unchanged at 42480090 — this was a description edit only, no push, so your approval still stands against the reviewed tree.


Generated by Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: 42480090f2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@sergey-shandar
sergey-shandar added this pull request to the merge queue Aug 26, 2026
Merged via the queue into main with commit e30e934 Aug 26, 2026
19 checks passed
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.

3 participants