Skip to content

sdk: host callback tools in the author's app - #120

Merged
weilueluo merged 4 commits into
mainfrom
capability-callback-path
Aug 31, 2026
Merged

sdk: host callback tools in the author's app#120
weilueluo merged 4 commits into
mainfrom
capability-callback-path

Conversation

@weilueluo

Copy link
Copy Markdown
Contributor

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 → Outcome envelope 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 (new src/app.ts)

One register({name, description, input, output?}, fn) shape in both transport directions:

  • Backend (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_ms is raced app-side too and cancel aborts the in-flight call's signal.
  • Browser/outbound (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 the tool/v1 callback manifests (hosting: "callback", no payload, empty requires/binding_names) for composition.

Environment side — author.route.callbacks() (new src/callbacks.ts)

  • Bare route.callbacks() terminates the app's outbound WebSocket channel, token-authenticated from the environment's channelToken option; the generated handler now exposes handle.channel.upgrade(request, socket, head) for the host server's upgrade event. 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.
  • On invoke, tools that arrived as attach provisions keep the environment's own run path; everything else routes to the app. The environment enforces deadline_ms itself (timeout outcome + best-effort cancel downstream), maps Brain-side cancellation to a cancelled outcome, and answers a down/unreachable app with typed app_disconnected / app_unreachable errors — never a hang.
  • Attach receipts keep provides capability-empty: the contract has no field for callback tool names, and callback tools have empty requires, 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 sends hosting when set.

Tests

fetchHandler roundtrips (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.mdx covering both directions and route.callbacks(); docs/concepts/tool.mdx gains the two-hostings section.

Comment thread packages/brain-sdk/src/app.ts Fixed
Comment thread packages/brain-sdk/src/app.ts Fixed
@weilueluo
weilueluo merged commit 8ad9f2f into main Aug 31, 2026
14 checks passed
@weilueluo
weilueluo deleted the capability-callback-path branch August 31, 2026 13:30
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.

2 participants