Unified MCP Server with LangChain and LangGraph
September 25, 2026
Use Unified's MCP server 67k+ tools as LangChain tools in LangChain and LangGraph agents
LangChain and LangGraph give you a framework for building AI agents with tools, memory and multi-step workflows. With the official LangChain MCP adapter, every tool exposed by the Unified MCP server becomes a native LangChain tool. Your agents get real-time read and write access to your customers' CRM, ATS, HRIS, accounting, ticketing, file storage, messaging and commerce data, across all of Unified.to's integrations. You don't need custom integration code.
The adapter converts each Unified MCP tool into a LangChain BaseTool. You can pass the tools to create_agent, bind them to any chat model, or run them in a LangGraph ToolNode.
Prerequisites
- A Unified.to workspace and your workspace API key
- At least one end-customer connection (for example, a HubSpot or Greenhouse connection), and its connection ID from Connections
- Python 3.10+ or Node.js 18+
Install
Python
pip install langchain langchain-mcp-adapters langchain-anthropic
TypeScript
npm install langchain @langchain/mcp-adapters @langchain/anthropic
You can use any LangChain-supported model provider. The examples below use Anthropic, but OpenAI, Google, Mistral and others work the same way.
Quickstart (Python)
Connect LangChain to the Unified MCP server with a few lines of code:
import asyncio
import os
from langchain.agents import create_agent
from langchain_mcp_adapters.client import MultiServerMCPClient
UNIFIED_API_KEY = os.environ["UNIFIED_API_KEY"]
CONNECTION_ID = os.environ["UNIFIED_CONNECTION_ID"] # end-customer connection
async def main():
client = MultiServerMCPClient(
{
"unified": {
"transport": "streamable_http",
"url": f"https://mcp-api.unified.to/mcp?connection={CONNECTION_ID}",
"headers": {"Authorization": f"bearer {UNIFIED_API_KEY}"},
}
}
)
tools = await client.get_tools()
print(f"Loaded {len(tools)} Unified tools")
agent = create_agent("anthropic:claude-sonnet-4-5", tools)
result = await agent.ainvoke(
{"messages": [{"role": "user", "content": "List the 10 most recently updated deals and summarize their stages."}]}
)
print(result["messages"][-1].content)
asyncio.run(main())
The tools available depend on the connection's integration and the permissions granted. A CRM connection exposes tools for contacts, companies, deals, and so on. An ATS connection exposes tools for candidates, applications, jobs, and so on.
Quickstart (TypeScript)
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
import { createAgent } from "langchain";
const client = new MultiServerMCPClient({
mcpServers: {
unified: {
transport: "http",
url: `https://mcp-api.unified.to/mcp?connection=${process.env.UNIFIED_CONNECTION_ID}`,
headers: { Authorization: `bearer ${process.env.UNIFIED_API_KEY}` },
},
},
});
const tools = await client.getTools();
const agent = createAgent({ model: "anthropic:claude-sonnet-4-5", tools });
const result = await agent.invoke({
messages: [{ role: "user", content: "Find open jobs and count applications per job." }],
});
console.log(result.messages.at(-1)?.content);
await client.close();
Authentication
The Unified MCP server accepts a token either as a token URL parameter or as an Authorization: bearer {token} header. All other options must be sent as URL parameters. See Authentication for full details.
| Use case | Token | Other URL parameters |
|---|---|---|
| Your backend runs the agent (recommended) | Workspace API key, in the Authorization header | connection={connectionId} |
| Token is handed to a less-trusted runtime or end user | Public end-user token: {connectionId}-{nonce}-{signature} | none |
A workspace API key grants access to all of your connections and your Unified.to account. Keep it server-side, prefer the Authorization header over the URL, and never log full MCP URLs that contain it. |
Controlling which tools the agent sees
Agents choose tools more reliably when they see a smaller, focused list, and a shorter list also saves context. Use these server options as URL parameters to shape the tools LangChain loads:
| Option | What it does |
|---|---|
tools | Comma-delimited list of tool IDs. Only these tools are exposed. |
permissions | Comma-delimited list of Unified.to permissions. Only tools allowed by these permissions are exposed. For example, pass read-only permissions for an agent that should never write. |
hide_sensitive | Hides PII fields (names, emails, telephones, and so on) from results. |
url = (
"https://mcp-api.unified.to/mcp"
f"?connection={CONNECTION_ID}"
"&permissions=crm_contact_read,crm_deal_read"
"&hide_sensitive=true"
)
Using Unified tools in a LangGraph workflow
For more control over agent flow, bind the Unified tools to a model and route tool calls through a LangGraph ToolNode:
from langchain.chat_models import init_chat_model
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.graph import StateGraph, MessagesState, START
from langgraph.prebuilt import ToolNode, tools_condition
client = MultiServerMCPClient(
{
"unified": {
"transport": "streamable_http",
"url": f"https://mcp-api.unified.to/mcp?connection={CONNECTION_ID}",
"headers": {"Authorization": f"bearer {UNIFIED_API_KEY}"},
}
}
)
tools = await client.get_tools()
model = init_chat_model("anthropic:claude-sonnet-4-5").bind_tools(tools)
async def call_model(state: MessagesState):
return {"messages": [await model.ainvoke(state["messages"])]}
builder = StateGraph(MessagesState)
builder.add_node("call_model", call_model)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "call_model")
builder.add_conditional_edges("call_model", tools_condition)
builder.add_edge("tools", "call_model")
graph = builder.compile()
result = await graph.ainvoke(
{"messages": [{"role": "user", "content": "Which candidates applied this week, and for which roles?"}]}
)
Multi-tenant agents
If your product serves many end customers, each customer has their own Unified connection. Create the MCP client per request or session, scoped to the current customer's connection, so an agent can only reach that customer's data:
def unified_client_for(connection_id: str) -> MultiServerMCPClient:
return MultiServerMCPClient(
{
"unified": {
"transport": "streamable_http",
"url": f"https://mcp-api.unified.to/mcp?connection={connection_id}",
"headers": {"Authorization": f"bearer {UNIFIED_API_KEY}"},
}
}
)
async def handle_request(customer, prompt: str):
tools = await unified_client_for(customer.unified_connection_id).get_tools()
agent = create_agent("anthropic:claude-sonnet-4-5", tools)
return await agent.ainvoke({"messages": [{"role": "user", "content": prompt}]})
Because Unified normalizes data across integrations, the same agent code works whether one customer uses Salesforce and another uses HubSpot.
Calling tools directly
Unified tools are standard LangChain tools, so deterministic workflows can call them without an agent:
tools_by_name = {t.name: t for t in await client.get_tools()}
contacts = await tools_by_name["list_crm_contacts"].ainvoke({"limit": 50})
Tool names vary by integration and category. Print [t.name for t in tools] to see what a connection exposes.
Regions
| Region | Streamable HTTP endpoint |
|---|---|
| US | https://mcp-api.unified.to/mcp |
| EU | https://mcp-api-eu.unified.to/mcp |
| Use the endpoint for the region where your Unified.to workspace is hosted. |
Troubleshooting
- 401 / unauthorized: Check that the token is a valid workspace API key or a correctly signed end-user token. When you use a workspace API key, make sure the
connectionparameter is also present. - Fewer tools than expected: The tool list reflects the connection's integration, the scopes granted when the customer authorized it, and any
toolsorpermissionsfilters in the URL. - Agent picks the wrong tool: Narrow the tool list with
toolsorpermissions, and describe the task more specifically in your system prompt. - Options not applied: Only the token can go in a header. Every other option must be a URL parameter.