feat(keycardai-mcp): interrupt-compatible auth mode for the langchain adapter (ECO-332) - #246
Merged
Merged
Conversation
… adapter An agent that combines keycardai-langchain's KeycardGrantMiddleware with this adapter had two auth UXs: the middleware pauses the run with an `authorization_required` interrupt, while the adapter handed the model auth-request tools. `interrupt_on_auth=True` makes the adapter raise the same payload the middleware's `_interrupt_payload` produces, with MCP servers in place of resource URLs, sourced from the pending challenge of a session whose `requires_user_action` is true. Off by default: the auth-tools path is untouched unless the mode is enabled. Also adds `tool_allowlist`, so a large server cannot flood the model's context, and `get_lazy_tools()` (`list_mcp_tools` / `call_mcp_tool`) for agents whose tool list must exist before any user has connected: they connect on first call and then expose the server's real tool schemas rather than a hand-written wrapper that hides its filtering parameters. Co-Authored-By: Larry Osakwe <larry@keycard.ai>
Covers when to use middleware grants versus the MCP client, the empty tool_resources mapping for MCP-backed tools, the shared /auth/mcp/callback route wired to coordinator.handle_completion(...), and the MCP adapter's opt-in interrupt mode. Co-Authored-By: Larry Osakwe <larry@keycard.ai>
Contributor
Author
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
get_tools() reads the server list that only __aenter__ (or the lazy path) populates; the documented pattern called client.connect() directly and returned zero tools every time. Enter the adapter instead.
Larry-Osakwe
approved these changes
Aug 29, 2026
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.
Summary
An agent that mixes
keycardai-langchain'sKeycardGrantMiddleware(brokered REST tools, pauses with anauthorization_requiredinterrupt) with the MCP client's langchain adapter (hands the model arequest_authenticationtool) had to render two auth UXs. This unifies them.Interrupt mode, opt-in.
LangChainClient(mcp_client, interrupt_on_auth=True)makes a wrapped tool pause the run with the same payload the middleware's_interrupt_payloadproduces, with MCP servers standing in for resource URLs:The challenge is read from sessions where
session.requires_user_actionis true. While the mode is on,get_auth_tools()returns[]; with the mode off (the default) nothing about the auth-tools path changes.langgraphis imported lazily inside_interrupt, so the adapter still depends onlangchain-corealone.Dynamic tool binding.
create_agentfixes its tool list at import, before any user has connected, which is what pushed the demo into a hand-written wrapper that hid the server'sstate/query/team/projectparameters. Two supported shapes now, both documented: build the agent's tools afterconnect()(get_tools(), per request), orget_lazy_tools()—list_mcp_tools/call_mcp_tool, which connect on first call and then report and invoke the server's real schemas.tool_allowlist=[...]restricts what is exposed either way, so a 67-tool server does not flood the context window.Note on what is not possible: LangGraph's
ToolNoderesolves tool calls against the tools bound atcreate_agenttime, so tools discovered later cannot be registered retroactively — hence thelist_mcp_tools+call_mcp_toolpair rather than per-tool lazy stubs.Docs (both READMEs): middleware grants vs. MCP client,
tool_resources={"call_mcp_tool": []}so the middleware exchanges nothing for MCP-backed tools, the shared/auth/mcp/callbackroute callingcoordinator.handle_completion(dict(request.query_params)), and the interrupt opt-in.Tests run the tool inside a real checkpointed LangGraph run (an interrupt only exists inside a run) and compare the payload against
KeycardGrantMiddleware._interrupt_payloaditself, not a copy of its literal.packages/mcp553 passed (65% coverage, gate 60%),packages/langchain46 passed,ruff checkclean. No code in the langchain middleware package was touched — only its README.Link to Devin session: https://app.devin.ai/sessions/cbdad0905a154ec4855c7c644a247eed
Open in Devin Desktop: https://app.devin.ai/desktop/session/cbdad0905a154ec4855c7c644a247eed?variant=devin
Requested by: @Larry-Osakwe