sdk: host callback tools in the author's app - #120
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the callback third of the capability contract (#119): tools whose implementation stays in the author's app, invoked through the bound environment with the same
invoke → Outcomeenvelope as provisioned tools. MCP data shapes over plain HTTP, no JSON-RPC framing:{call_id, name, arguments, deadline_ms}per call, the shared Outcome back, best-effort{cancel: call_id}down the same channel.App side —
appTools(newsrc/app.ts)One
register({name, description, input, output?}, fn)shape in both transport directions:appTools({signingKey})):.fetchHandler()returns a WHATWG fetch handler. HMAC-SHA256 over the raw body (x-brain-signature) is verified before anything else — unsigned/tampered → 401, malformed → 400, and the tool function is never reached. Schema-invalid input, a thrown handler, or a broken output contract are in-band error outcomes;deadline_msis raced app-side too and cancel aborts the in-flight call'ssignal.appTools.connect({url, token})): holds an outbound WebSocket (Node ≥22 / browser built-in client; token in the URL query since the standard client cannot set headers), answers invoke frames with{call_id, ...outcome}, honors{cancel}, reconnects with backoff (250ms → 10s), and is silent-safe when down..manifests()returns thetool/v1callback manifests (hosting: "callback", no payload, emptyrequires/binding_names) for composition.Environment side —
author.route.callbacks()(newsrc/callbacks.ts)route.callbacks()terminates the app's outbound WebSocket channel, token-authenticated from the environment'schannelTokenoption; the generated handler now exposeshandle.channel.upgrade(request, socket, head)for the host server'supgradeevent. The server half of RFC 6455 is ~120 lines in-tree (handshake, masking, continuation, ping/pong, close, 4 MiB bound) — no dependency, not even a dev one: the end-to-end tests run Node's built-in client against it.route.callbacks(({options}) => ({mode: "post", url, signingKey}))POSTs HMAC-signed invocations to a backend app instead.invoke, tools that arrived as attachprovisionskeep the environment's ownrunpath; everything else routes to the app. The environment enforcesdeadline_msitself (timeout outcome + best-effort cancel downstream), maps Brain-side cancellation to a cancelled outcome, and answers a down/unreachable app with typedapp_disconnected/app_unreachableerrors — never a hang.providescapability-empty: the contract has no field for callback tool names, and callback tools have emptyrequires, so they bind. No contract schema changes were needed.Composition —
appTool(...)appTool({name, description, input, output?}).useIn(appEnv)produces a bindable tool whose wire manifest is callback-shaped (hosting: "callback", no payload); the client now sendshostingwhen set.Tests
fetchHandlerroundtrips (signed ok / 401 / 400 / error outcomes / deadline / cancel), connect-mode end-to-end through the generated environment handler over a real HTTP upgrade (invoke → result, deadline → timeout with cancel observed app-side, Brain-side cancel → cancelled, disconnected → typed error, transport-drop → reconnect → recovery), signed-POST mode end-to-end incl. unreachable app, bad channel token refused, provisioned/callback routing split. All existing SDK and Rust tests untouched and green.Docs
New guide
docs/guides/app-tools.mdxcovering both directions androute.callbacks();docs/concepts/tool.mdxgains the two-hostings section.