Skip to content

feat(keycardai-mcp): interrupt-compatible auth mode for the langchain adapter (ECO-332) - #246

Merged
Larry-Osakwe merged 3 commits into
mainfrom
devin/1787958800-eco-332-mcp-interrupt-mode
Aug 29, 2026
Merged

feat(keycardai-mcp): interrupt-compatible auth mode for the langchain adapter (ECO-332)#246
Larry-Osakwe merged 3 commits into
mainfrom
devin/1787958800-eco-332-mcp-interrupt-mode

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Summary

An agent that mixes keycardai-langchain's KeycardGrantMiddleware (brokered REST tools, pauses with an authorization_required interrupt) with the MCP client's langchain adapter (hands the model a request_authentication tool) 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_payload produces, with MCP servers standing in for resource URLs:

async def invoke_tool(**kwargs):
    if self._interrupt_on_auth:
        await self._interrupt_if_authorization_required()  # -> langgraph interrupt(payload)
    ...

# payload, keys identical to KeycardGrantMiddleware's:
{"type": "authorization_required", "resources": ["linear"],
 "authorization_url": challenges[0]["authorization_url"],
 "errors": {"linear": {...}}, "message": "Access to the resources above ..."}

The challenge is read from sessions where session.requires_user_action is true. While the mode is on, get_auth_tools() returns []; with the mode off (the default) nothing about the auth-tools path changes. langgraph is imported lazily inside _interrupt, so the adapter still depends on langchain-core alone.

Dynamic tool binding. create_agent fixes 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's state/query/team/project parameters. Two supported shapes now, both documented: build the agent's tools after connect() (get_tools(), per request), or get_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 ToolNode resolves tool calls against the tools bound at create_agent time, so tools discovered later cannot be registered retroactively — hence the list_mcp_tools + call_mcp_tool pair 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/callback route calling coordinator.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_payload itself, not a copy of its literal.

packages/mcp 553 passed (65% coverage, gate 60%), packages/langchain 46 passed, ruff check clean. 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

devin-ai-keycard and others added 2 commits August 28, 2026 23:13
… 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>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

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
Larry-Osakwe merged commit 6350810 into main Aug 29, 2026
5 checks passed
@Larry-Osakwe
Larry-Osakwe deleted the devin/1787958800-eco-332-mcp-interrupt-mode branch August 29, 2026 00:35
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