Model Context ProtocolMCP Python SDK 2.2 · LangChain 1.4 · Python 3.10+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
27 small wins to finish your pathNext lesson →

Integrations

Integrations are the real backends that replace each local piece this course ran on: a client over a real transport, another model provider, a ready-made agent loop, and a deployed server.

Last updated: 29 Sep, 2026 · MCP 2.2

Every runnable lesson so far used an in-process Client(mcp), and part 6 used one free hosted model. This lesson maps each piece to what you run in production, and shows that a swap changes one line, not the agent.

What you ran on, and its real version

What you ran onA real oneThe package or transport
Client(mcp), a server objectA client over stdio or Streamable HTTPStdioServerParameters or a URL, from mcp (the stdio and Streamable HTTP lessons)
The Groq model, openai/gpt-oss-120bAny chat model LangChain supports: Gemini, Anthropic, OpenAIinit_chat_model with that provider's string, and its package
run_agent, the loop you wrotecreate_agent with MCPAdapterlangchain[mcp] (the agent-loop lesson's video example)
A local python shop.pyA deployed, remote MCP serverStreamable HTTP behind a host name, with transport_security
The ORDERS dictA real databaseA driver opened once in the lifespan (the Lifespan lesson)

Swapping the client to stdio

The agent from the agent-loop lesson talks to a client. It never cares how that client connects, so moving off the in-process client changes only the argument you pass to Client:

python
# in memory, as the earlier lessons ran it:
async with Client(mcp) as client:
    ...

# the same agent, now over stdio:
server = StdioServerParameters(command=sys.executable, args=["shop.py"])
async with Client(server) as client:
    ...
Project files used on this pageThis lesson builds on a project from earlier lessons. The code below imports these files. Click a file to see its code, or follow the link to the lesson that wrote it. To run the code yourself, keep them in the same folder.
View the code here
shop.py
from typing import Annotated, Literal

from pydantic import BaseModel, Field

from mcp.server import MCPServer
from mcp.server.mcpserver.prompts.base import AssistantMessage, Message, UserMessage
from mcp.server.mcpserver.exceptions import ResourceNotFoundError, ToolError
from mcp.types import ToolAnnotations

mcp = MCPServer("Shop support")

ORDERS = {
    "A17": {"item": "blue mug", "status": "shipped", "total": 12.50},
    "B42": {"item": "desk lamp", "status": "processing", "total": 48.00},
}

ARTICLES = {
    "Where is my order?": "orders",
    "Changing an order": "orders",
    "How refunds work": "refunds",
    "Refunds for damaged items": "refunds",
    "Resetting your password": "account",
}


class Order(BaseModel):
    id: str
    item: str
    status: Literal["processing", "shipped", "delivered"]
    total: float


@mcp.tool(annotations=ToolAnnotations(read_only_hint=True))
def lookup_order(order_id: str) -> Order:
    """Look up an order by its id."""
    if order_id not in ORDERS:
        raise ToolError(f"No order with id {order_id!r}. Order ids look like A17.")
    return Order(id=order_id, **ORDERS[order_id])


@mcp.tool()
def search_help(
    query: Annotated[str, Field(description="Words to look for in the help articles.")],
    topic: Literal["orders", "refunds", "account"] | None = None,
    limit: Annotated[int, Field(ge=1, le=5)] = 3,
) -> str:
    """Search the help centre and return matching article titles."""
    found = [title for title, t in ARTICLES.items() if query.lower() in title.lower() and topic in (None, t)]
    return "; ".join(found[:limit]) or "No articles found."


@mcp.tool(
    title="Refund an order",
    annotations=ToolAnnotations(read_only_hint=False, destructive_hint=False, idempotent_hint=False),
)
def refund_order(order_id: str, reason: str) -> str:
    """Refund the full total of an order to the customer's card."""
    if order_id not in ORDERS:
        raise ToolError(f"No order with id {order_id!r}.")
    return f"Refunded {ORDERS[order_id]['total']:.2f} for order {order_id}: {reason}."


@mcp.resource("policy://refunds", mime_type="text/markdown")
def refund_policy() -> str:
    """The shop's refund policy."""
    return "# Refunds\n\nFull refund within 30 days of delivery. Damaged items: refund or replacement."


@mcp.resource("orders://{order_id}", mime_type="application/json")
def order_record(order_id: str) -> Order:
    """The full record for one order."""
    if order_id not in ORDERS:
        raise ResourceNotFoundError(f"No order with id {order_id!r}.")
    return Order(id=order_id, **ORDERS[order_id])


@mcp.prompt(title="Reply to a customer")
def reply_to_customer(ticket: str, tone: str = "friendly") -> list[Message]:
    """Draft a reply to a support ticket."""
    return [
        UserMessage(f"Write a {tone} reply to this support ticket:\n\n{ticket}"),
        AssistantMessage("Hello, and thank you for getting in touch."),
    ]


if __name__ == "__main__":
    mcp.run()
agent.py
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage, SystemMessage, ToolMessage

model = init_chat_model("groq:openai/gpt-oss-120b", temperature=0)

SYSTEM = (
    "You are the support assistant for a small online shop. "
    "Answer in one or two short sentences, using only what the tools returned."
)


def to_model_tools(tools):
    return [
        {
            "type": "function",
            "function": {
                "name": tool.name,
                "description": tool.description,
                "parameters": tool.input_schema,
            },
        }
        for tool in tools
    ]


async def run_tool(client, call):
    print(f"  calling {call['name']} {call['args']}")
    result = await client.call_tool(call["name"], call["args"])
    status = "error" if result.is_error else "success"
    return ToolMessage(result.content[0].text, tool_call_id=call["id"], status=status)


async def run_agent(client, message):
    listed = await client.list_tools()
    llm = model.bind_tools(to_model_tools(listed.tools))
    messages = [SystemMessage(SYSTEM), HumanMessage(message)]
    for _ in range(3):
        reply = await llm.ainvoke(messages)
        messages.append(reply)
        if not reply.tool_calls:
            return reply.text
        for call in reply.tool_calls:
            messages.append(await run_tool(client, call))
    return "Stopped after three rounds of tool calls."

The agent runs unchanged over stdio, against shop.py started as a child process. First put the stdio ending back at the end of shop.py, mcp.run() from the stdio lesson: with the Streamable HTTP ending, the server listens on port 8200 instead of standard input, and this client waits forever.

ExampleAPI keydesk_stdio.py
import asyncio
import sys

from mcp import Client, StdioServerParameters
from agent import run_agent

server = StdioServerParameters(command=sys.executable, args=["shop.py"])


async def main():
    async with Client(server) as client:
        for message in ["Where is my order B42?", "I need help with my refund"]:
            print(message)
            print(" ", await run_agent(client, message))


asyncio.run(main())
  • The agent code did not change. Only the object handed to Client did, from mcp to StdioServerParameters.
  • The server ran as its own process, the way a desktop host launches it, and the model made the same tool calls it made in memory.

Swapping the model provider

The model is one line in agent.py. Any provider init_chat_model supports drops in. Gemini's free tier, for example, needs pip install "langchain-google-genai==4.4.0" and a GOOGLE_API_KEY from aistudio.google.com/apikey:

python
model = init_chat_model("google_genai:gemini-2.5-flash", temperature=0)

The rest of run_agent stays as it is: it still lists the tools, calls the chosen one over MCP, and passes errors on. Gemini's reply object keeps its words in .text as well, which is why the loop returns reply.text rather than reply.content. The model decides which tool to call; the server still decides what each tool is allowed to do.

Where real backends replace the local pieces

  • The client swaps to stdio or Streamable HTTP with no change to the agent loop.
  • The Groq model swaps to another provider by changing the model string, and the loop swaps to create_agent when you want LangChain to run it.
  • The ORDERS dict swaps to a database opened once in the lifespan.
Watch out
A real model is not deterministic and may call a tool you did not expect. Keep the offered tool list tight and enforce every rule in the server, as the refund resolver does, rather than trusting the model.
Try it yourself
  • Change desk_stdio.py to the HTTP client from the Streamable HTTP lesson, start python shop.py, and run the same messages.
  • Switch agent.py to Gemini with the line above, and run desk_stdio.py again.
  • Replace the ORDERS dict with a small SQLite database opened in a lifespan (the Lifespan lesson).

Every expert started right here.