← Back to blog
March 20, 2025·7 min read

Building a Human-in-the-Loop Multi-Agent Platform with LangGraph and MCP

How I designed an agent platform on LangGraph's StateGraph with Model Context Protocol tool access, persistent checkpointed memory, and human approval steps before sensitive actions.

LangGraphModel Context ProtocolAI AgentsHuman-in-the-Loop

The Problem

A client wanted an agent that could search the web, query a database, read internal documents, and take action on the results — but they were, correctly, nervous about handing an LLM unsupervised write access to anything. The brief was simple to state and hard to build well: give the agent real capability, but never let it act on anything sensitive without a human checking first.

Why LangGraph Over a Plain Tool-Calling Loop

Most "agent" demos are a while-loop: call the model, execute whatever tool it picked, feed the result back, repeat. That works for a demo. It falls apart the moment you need the agent to survive a server restart mid-conversation, or pause and wait for a human to approve a step before continuing.

LangGraph's StateGraph gives you both of those for free. The agent's state — conversation history, intermediate results, which step it's on — is a first-class object that gets checkpointed, not something you're manually serializing into a database table. I backed the checkpointer with async SQLite, so a conversation can be interrupted, the process can restart, and the agent picks up exactly where it left off.

Model Context Protocol for Tool Access

Instead of hand-rolling tool schemas for every integration (web search, financial data lookups, internal docs), I wired the agent to an MCP client. The practical benefit isn't philosophical — it's that adding a new tool means pointing at a new MCP server, not writing and maintaining a bespoke function-calling schema and re-testing the model's tool-selection behavior every time.

Designing the Human-in-the-Loop Interrupts

The tempting shortcut is to just prompt the model: "ask the user before doing anything risky." Don't do this. It's unreliable exactly when you need it most — under-tested edge cases, ambiguous instructions, or a model that's confident it already has enough information.

The reliable version is a real interrupt() in the graph itself, placed before any node that calls a sensitive tool. The graph execution pauses, the pending action is surfaced to a human for approval or rejection, and only then does execution resume — with the state, including everything the agent had already reasoned through, fully intact.

What I'd Do Differently

State design took longer than the agent logic itself, and I underestimated that going in. It's worth spending real time up front deciding exactly what belongs in the checkpointed state versus what can be recomputed, because retrofitting that decision later means migrating every in-flight conversation.

I also learned to test the interrupt paths as rigorously as the happy path. It's easy to thoroughly test "agent completes the task" and barely touch "agent gets rejected at the approval step, then has to recover gracefully" — but that second path is the entire point of building human-in-the-loop in the first place.