Skip to content
Forward Deployed

Forward Deployed
Lessons and contents
MCP Applications & RAG over MCPLesson 7 of 9
9 lessons
Learn

Research assistant with MCP and LangGraph

Source
From the RAG & Agents Engineering course
By
Sourav Banerjee
Last updated
9 Nov 2026

Report an issue

MCP + LangGraph

Larkspur's research assistant keeps conversation in its graph and research files on a server. You will separate those memories, inspect the MCP tool loop and count repeated startup and discovery under three session lifetimes.

Step 1: Connect the graph to server-owned tools

The research assistant splits into a graph that thinks and servers that act. client.py builds the graph; two MCP servers supply every tool it can call:

  • research_server (this project's server.py) keeps research in Chroma, one database per topic under research_chroma_dbs/<topic>/. Its five tools: save_research_data(content, topic), search_research_data(query, topic, max_results=5), list_research_topics(), get_topic_info(topic) and delete_research_topic(topic). Saving skips any text whose MD5 hash is already in the topic's content_hashes.json.
  • firecrawl_server is the published firecrawl-mcp package, started with npx -y, which scrapes the web. The client copies FIRECRAWL_API_KEY from its environment into that server's env before it connects.

The graph is the tool loop from T7.1 and T9.4, unchanged:

Diagram: START; chat_node; tools_condition; tool_node; ENDDiagram: START; chat_node; tools_condition; tool_node; END

Step 2: Keep the model and execution loop distinct

MCP only changes where the tools come from. MultiServerMCPClient(mcp_json).get_tools() turns every tool of both servers into a LangChain tool; llm.bind_tools(tools) shows them to Ollama's qwen3; ToolNode(tools=tools) runs whichever the model picks; tools_condition routes. A system prompt tells the model to search saved research before crawling and to save what it finds.

Step 3: Locate conversation and research persistence

The assistant has two memories, held in different processes. The client's MemorySaver keeps the conversation for the thread research_session and loses it when client.py exits. The research server keeps findings in Chroma on disk, across sessions and for any host that connects, Claude Desktop included (it is the research_server entry in the 40.2 config).

Two fixes before it runs here: client.py imports ChatPromptTemplate and MessagesPlaceholder from langchain.prompts, which raises ModuleNotFoundError under langchain 1.4.7 (they live in langchain_core.prompts); and topic names become directory names unchecked, which lesson 7 of T12.3 shows is dangerous once the server is shared.

How long a session lives

Step 1: Count discovery and process startup separately

MultiServerMCPClient.get_tools() returns tools with no open connection behind them. Its docstring in langchain-mcp-adapters 0.3.2 says: "A new session will be created for each tool call". For a stdio server, a new session is a new process.

A tool call with a session per call In the LangGraph client, chat_node returns a tool call and tools_condition routes it to tool_node. Because the tools came from MultiServerMCPClient.get_tools(), the call starts a new server process, which takes the handshake (3 messages), the call and its result (2), and a tools/list the client sends to fetch the output schema (2), then exits: 7 messages and 2,071 bytes for one call in the lab’s recording. The tool result goes back to chat_node. LangGraph client New server process for this call chat_node tools_condition tool_node END tool call no call ToolMessage Start process initialize tools/call tools/list Exit 3 messages 2 messages 2 messages 7 msgs, 2,071 bytes
  1. chat_node calls the model, which replies with a tool call for retrieve.
  2. tools_condition sees the tool call and routes to tool_node.
  3. The tool came from get_tools(), so it opens a new session: a new server process.
  4. initialize, its result and the initialized notification: 3 messages.
  5. tools/call and its result: 2 messages.
  6. The new session has no output schema for retrieve, so ClientSession sends tools/list before it checks the result: 2 more.
  7. The session closes and the process exits: 7 messages and 2,071 bytes for one call. A held session sends only the 2 call messages.
  8. The result goes back to chat_node as a ToolMessage, and the next call starts another process.

Step 2: Explain discovery after a call

The tools/list after the call comes from the SDK. mcp 1.30.0's ClientSession validates each structured tool result against the tool's output schema, and a session that has not listed its tools yet lists them first. A held session pays for that once; a session per call pays for it every time.

Step 3: Compare process and message counts

The interactive lab records the real JSON-RPC traffic of the Larkspur handbook server under three client patterns, with a scripted model so every run makes the same calls. Its numbers:

Questions × calls per question Hold one session Session per question (06 app.py) Session per tool call (07 client)
1 × 1 1 start, 7 messages 1 start, 7 messages 2 starts, 12 messages
3 × 2 1 start, 17 messages 3 starts, 27 messages 7 starts, 47 messages
3 × 3 1 start, 23 messages, 9,963 bytes 3 starts, 33 messages, 12,131 bytes 10 starts, 68 messages, 19,726 bytes
Interactive lab: One server, three clients. Open full screen (opens in a new tab)

Step 4: Choose a session lifetime deliberately

Each pattern has a reason to exist:

  • Hold one session for a conversation: async with client.session("research_server") as session: then tools = await load_mcp_tools(session). One process, one handshake, server-side state kept in memory for as long as the session lives.
  • Session per question isolates questions from each other and suits a request handler such as Streamlit's, at the price of one start per question.
  • Session per tool call needs no lifecycle code, which is why get_tools() is the quickstart, and it is the costliest. The lab counts messages; on a real server each start also pays for the work done at import time. The research server builds its Ollama embeddings client at every start, and npx -y firecrawl-mcp starts Node for every crawl.

Message counts are not latency, and the lab does not time anything, because start-up cost depends on the machine and the server. Measure your own: log one line on the server's stderr at start-up and count the lines per conversation.

Try it yourself

Use three questions with two calls each in the lab. Compare a held session, one per question and one per tool call, then inspect when tools/list repeats. Check process starts and JSON-RPC messages separately from latency. The lab records traffic, not startup time on your deployment machine.

Practice notebook

Run the complete implementation from the practice material after checking the trace or calculation above. Use its stated environment for live model or server calls; recorded model choices and timings are observations, not guaranteed outputs.

Continue learning

Run the notebooks

The code for this lesson is in these notebooks. Colab runs one in your browser: install the packages its first cell imports and set the API keys it reads. To run every notebook as written, clone the repository and use its uv environment.

Notebook GitHub Colab
Research assistant with MCP and LangGraph Open on GitHub Run locally

Key Takeaways

  • MCP changes only where the tools come from. The LangGraph tool loop stays the same, with the graph thinking and the servers acting.
  • Conversation memory lives in the client's checkpointer and ends with the process; research saved by the server persists on disk for every host that connects.
  • Hold one session for a conversation. A session per tool call needs no lifecycle code but restarts the server on every call, so measure start-ups with one stderr line.

Check yourself

1. The research assistant has two memories. Where does each live, and how long does it last?

The client's MemorySaver keeps the conversation for the thread research_session and loses it when client.py exits. The research server keeps findings in Chroma on disk, across sessions and for any host that connects, Claude Desktop included.

2. For 3 questions with 3 tool calls each, what does the lab record for holding one session versus a session per tool call?

Holding one session: 1 start, 23 messages, 9,963 bytes. A session per tool call, as in the 07 client: 10 starts, 68 messages, 19,726 bytes. A session per question sits between, at 3 starts, 33 messages and 12,131 bytes.

3. You turn the research assistant into a chat that holds long conversations. Which session pattern should the client use, and why not the get_tools default?

Hold one session per conversation: async with client.session("research_server") as session: then load_mcp_tools(session), for one process and one handshake. The get_tools() default opens a session per tool call, the costliest pattern: every start rebuilds the Ollama embeddings client, and npx -y firecrawl-mcp starts Node for every crawl. Log one stderr line at server start-up to count the starts.

Rate your confidence and keep notes on the module page.