|
1 | | -from typing import Annotated |
| 1 | +import typing |
| 2 | +from collections.abc import Callable, Sequence |
| 3 | +from typing import Annotated, Any |
2 | 4 | from unittest.mock import AsyncMock, MagicMock |
3 | 5 |
|
4 | 6 | import httpx |
5 | 7 | import pytest |
6 | 8 | from langchain_core.callbacks import CallbackManagerForToolRun |
7 | | -from langchain_core.messages import ToolMessage |
| 9 | +from langchain_core.language_models import LanguageModelInput |
| 10 | +from langchain_core.messages import AIMessage, HumanMessage, ToolMessage |
| 11 | +from langchain_core.runnables import Runnable |
8 | 12 | from langchain_core.tools import BaseTool, InjectedToolArg, ToolException, tool |
9 | 13 | from mcp.server import FastMCP |
10 | 14 | from mcp.types import ( |
|
19 | 23 | from pydantic import BaseModel |
20 | 24 |
|
21 | 25 | from langchain_mcp_adapters.client import MultiServerMCPClient |
| 26 | +from langchain_mcp_adapters.interceptors import MCPToolCallRequest |
22 | 27 | from langchain_mcp_adapters.tools import ( |
23 | 28 | _convert_call_tool_result, |
24 | 29 | convert_mcp_tool_to_langchain_tool, |
@@ -526,3 +531,141 @@ async def test_convert_mcp_tool_metadata_variants(): |
526 | 531 | "openWorldHint": None, |
527 | 532 | "_meta": {"flag": True}, |
528 | 533 | } |
| 534 | + |
| 535 | + |
| 536 | +def _create_increment_server(): |
| 537 | + server = FastMCP(port=8183) |
| 538 | + |
| 539 | + @server.tool() |
| 540 | + def increment(value: int) -> str: |
| 541 | + """Increment a counter""" |
| 542 | + return f"Incremented to {value + 1}" |
| 543 | + |
| 544 | + return server |
| 545 | + |
| 546 | + |
| 547 | +try: |
| 548 | + import langchain |
| 549 | + |
| 550 | + LANGCHAIN_INSTALLED = True |
| 551 | +except ImportError: |
| 552 | + LANGCHAIN_INSTALLED = False |
| 553 | + |
| 554 | +from langchain_core.language_models.fake_chat_models import GenericFakeChatModel |
| 555 | + |
| 556 | + |
| 557 | +class FixedGenericFakeChatModel(GenericFakeChatModel): |
| 558 | + def bind_tools( |
| 559 | + self, |
| 560 | + tools: Sequence[ |
| 561 | + typing.Dict[str, Any] | type | Callable | BaseTool # noqa: UP006 |
| 562 | + ], |
| 563 | + *, |
| 564 | + tool_choice: str | None = None, |
| 565 | + **kwargs: Any, |
| 566 | + ) -> Runnable[LanguageModelInput, AIMessage]: |
| 567 | + """Override bind-tools.""" |
| 568 | + return self |
| 569 | + |
| 570 | + |
| 571 | +@pytest.mark.skipif(not LANGCHAIN_INSTALLED, reason="langchain not installed") |
| 572 | +async def test_mcp_tools_with_agent_and_command_interceptor(socket_enabled) -> None: |
| 573 | + """Test Command objects from interceptors work end-to-end with create_agent. |
| 574 | +
|
| 575 | + This test verifies that: |
| 576 | + 1. MCP tools can be used with create_agent |
| 577 | + 2. Interceptors can return Command objects to short-circuit execution |
| 578 | + 3. Commands can update custom agent state |
| 579 | + """ |
| 580 | + from langchain.agents import AgentState, create_agent |
| 581 | + from langchain.tools import ToolRuntime |
| 582 | + from langgraph.checkpoint.memory import MemorySaver |
| 583 | + from langgraph.types import Command |
| 584 | + |
| 585 | + from langchain_mcp_adapters.interceptors import MCPToolCallResult |
| 586 | + |
| 587 | + # Interceptor that returns Command to update state |
| 588 | + async def counter_interceptor( |
| 589 | + request: MCPToolCallRequest, |
| 590 | + handler: Callable[[MCPToolCallRequest], typing.Awaitable[MCPToolCallResult]], |
| 591 | + ) -> Command: |
| 592 | + # Instead of calling the tool, return a Command that updates state |
| 593 | + tool_runtime: ToolRuntime = request.runtime |
| 594 | + assert tool_runtime.tool_call_id == "call_1" |
| 595 | + return Command( |
| 596 | + update={ |
| 597 | + "counter": 42, |
| 598 | + "messages": [ |
| 599 | + ToolMessage( |
| 600 | + content="Counter updated!", |
| 601 | + tool_call_id=tool_runtime.tool_call_id, |
| 602 | + ), |
| 603 | + AIMessage(content="hello"), |
| 604 | + ], |
| 605 | + }, |
| 606 | + goto="__end__", |
| 607 | + ) |
| 608 | + |
| 609 | + with run_streamable_http(_create_increment_server, 8183): |
| 610 | + # Initialize client and connect to server |
| 611 | + client = MultiServerMCPClient( |
| 612 | + { |
| 613 | + "increment": { |
| 614 | + "url": "http://localhost:8183/mcp", |
| 615 | + "transport": "streamable_http", |
| 616 | + } |
| 617 | + }, |
| 618 | + tool_interceptors=[counter_interceptor], |
| 619 | + ) |
| 620 | + |
| 621 | + # Get tools from the server |
| 622 | + tools = await client.get_tools(server_name="increment") |
| 623 | + assert len(tools) == 1 |
| 624 | + original_tool = tools[0] |
| 625 | + assert original_tool.name == "increment" |
| 626 | + |
| 627 | + # Custom state schema with counter field |
| 628 | + class CustomAgentState(AgentState): |
| 629 | + counter: typing.NotRequired[int] |
| 630 | + |
| 631 | + model = FixedGenericFakeChatModel( |
| 632 | + messages=iter( |
| 633 | + [ |
| 634 | + AIMessage( |
| 635 | + content="", |
| 636 | + tool_calls=[ |
| 637 | + { |
| 638 | + "name": "increment", |
| 639 | + "args": {"value": 1}, |
| 640 | + "id": "call_1", |
| 641 | + "type": "tool_call", |
| 642 | + } |
| 643 | + ], |
| 644 | + ), |
| 645 | + AIMessage( |
| 646 | + content="The counter has been incremented.", |
| 647 | + ), |
| 648 | + ] |
| 649 | + ) |
| 650 | + ) |
| 651 | + # Create agent with custom state |
| 652 | + agent = create_agent( |
| 653 | + model, |
| 654 | + tools, |
| 655 | + state_schema=CustomAgentState, |
| 656 | + checkpointer=MemorySaver(), |
| 657 | + ) |
| 658 | + |
| 659 | + # Run agent |
| 660 | + result = await agent.ainvoke( |
| 661 | + {"messages": [HumanMessage(content="Increment the counter")], "counter": 0}, |
| 662 | + {"configurable": {"thread_id": "test_1"}}, |
| 663 | + ) |
| 664 | + |
| 665 | + # Verify Command updated the state |
| 666 | + assert result["counter"] == 42 |
| 667 | + # Verify the Command's message was added |
| 668 | + assert any( |
| 669 | + isinstance(msg, ToolMessage) and msg.content == "Counter updated!" |
| 670 | + for msg in result["messages"] |
| 671 | + ) |
0 commit comments