Add an MCP-server adapter and four hosted-server tool packages - #413
Add an MCP-server adapter and four hosted-server tool packages#413TheGreatAxios wants to merge 2 commits into
Conversation
interchange.tools can load ordinary npm packages that declare a static tool factory, but Interchange has no MCP support of its own. Each hosted, OAuth-protected MCP server (Linear, Slack, Granola, Exa) now ships as its own thin workspace package configuring a shared adapter, rather than a bundle or duplicated connection logic per server. Tool names are declared statically at construction and the adapter connects lazily -- including any OAuth round trip -- on a bundle's first run(), reusing the callback-server and token-store patterns Corbits Code already uses for its own MCP client, ported here so the packages carry no import back into this repository's src/ tree and can move to their own repos later without a rewrite.
Reviewers found three real defects in the MCP adapter: a failed connect cached its rejection forever so every later tool call replayed the same stale error instead of retrying; a dispose() racing an in-flight connect never closed the connection once it resolved, leaking its transport and callback server; and every declared tool across all four server packages used an empty input schema, giving the model no signal on what arguments a tool takes -- exactly the dispatch-proxy ergonomics problem the static per-tool declarations were meant to avoid. run() now also threads its own abort signal into the connect call, since a first-time authorization blocks on a loopback HTTP callback with no other way to bound or cancel that wait.
|
Closing until the plugin registration surface exists. Blocked on three findings, each sufficient alone: nothing in src/ can reach the packages (workspace glob makes them typecheck; no config, registry, or consumer wires them); they are built against a tool-definition shape upstream has already changed, so the next dependency bump breaks all four at once; and the static tool declarations are never reconciled against what a server reports on connect, so drift is silent in both directions. The adapter inside — connect, OAuth provider, loopback callback server, hardened token store — was judged genuine and worth keeping. When the registration surface lands, rebuild from this branch: land the adapter with ONE reachable, integration-tested server package, then replicate. The branch is preserved. |
Summary
@corbits/mcp-adapter: shared logic exposing a hosted, OAuth-protected MCP server as aninterchange.toolsfactory. Tool definitions (with real per-tool argument schemas, not empty objects) are set synchronously at bundle construction; the real connection (and OAuth, when required) happens lazily on firstrun(), threading the tool call's own abort signal through so a hung authorization wait is cancellable. Self-contained OAuth callback server and token store, ported (not imported) fromsrc/mcp/so the package has no dependency back into this repo'ssrc/tree.@corbits/linear-mcp,@corbits/slack-mcp,@corbits/granola-mcp,@corbits/exa-mcp: thin per-server config over the adapter. Slack's transport was confirmed hosted (https://mcp.slack.com/mcp, OAuth, GA Feb 2026) before writing the package — see https://mcpservers.org/remote-mcp-servers/slack. Exa's API key is optional end to end — the package connects and works with it unset.packages/*), matching the operator's stated goal of eventual independent repos/publishes.The tool-declaration tradeoff (static list vs. single dispatch tool vs. cache-from-prior-connection) is documented in
packages/mcp-adapter/README.md, including what happens when a server's real tool list drifts from the declared one.Second commit fixes three defects reviewers found in the first pass: a failed connect cached its rejection forever (no retry),
dispose()racing an in-flight connect leaked the connection, and every declared tool used an empty input schema instead of a real one.Known open item, not fixed in this PR: the OAuth/token-store/callback-server logic in
packages/mcp-adapter/src/is a deliberate port (not an import) ofsrc/mcp/'s existing implementation, per this pass's explicit scope (no imports back intosrc/, so the package can move to its own repo later without a rewrite). Reviewers correctly flagged this as two independent implementations of security-sensitive OAuth handling that can drift; resolving it (e.g. extracting a shared package bothsrc/mcp/andmcp-adapterimport) is a real tradeoff against the extraction goal and is left for the operator to decide, not fixed unilaterally here.Linear: CL-5728 (this work), CL-5729 (tracks the four packages), CL-5730 (follow-up CLI, not in this PR).
Test plan
bun run typecheck— no new errors (pre-existing errors elsewhere, confirmed present without this change)bun run test(full suite) — 4263 pass / 0 failpackages/mcp-adapter/src/*.test.tscover: structural tool-factory shape, no network activity before firstrun(), lazy OAuth-URL surfacing (againstfactory.ts's contract withconnect.ts, notconnect.ts's real network path, which remains untested), optional-API-key query-param wiring, retry after a failed connect, dispose racing an in-flight connect, and signal propagation into the connect call — all with fake credentials only