LangChain agents and Binions speak the same protocol. Binions exposes a Model Context Protocol (MCP) server on every install, and LangChain consumes MCP servers natively through the langchain-mcp-adapters library. That gives you two production-grade integration patterns from a single endpoint: an agent that calls host capabilities you have published as tools, and an agent that programs the host by writing and deploying playbooks itself. This page walks through both, end to end, with the exact configuration, YAML, and Python that a working integration uses.
Scope. Everything here also applies to LangGraph graphs and Deep Agents — they share the same tool interface. If you use a different MCP client (Claude Code, an IDE, a custom agent), the server side of this page is identical; only the Python changes.
| What you get | LangChain agents call Binions playbooks as typed tools; optionally, agents write and hot-deploy new playbooks |
| Endpoint | https://<binions-host>:8446/mcp (edge, TLS + Basic Auth) · http://127.0.0.1:9099/mcp (loopback, same host only) |
| Protocol | MCP over stateless streamable HTTP (JSON-RPC 2.0): initialize, ping, tools/list, tools/call |
| LangChain side | langchain-mcp-adapters 0.3+ (MultiServerMCPClient), any tool-calling chat model; Deep Agents 0.6+ for the builder pattern |
| Binions side | ships enabled — [mcp] is on by default with an empty tool list; the deploy tools are a separate opt-in ([mcp.deploy]) |
Decide first which of the two roles your agent plays. They use the same endpoint but different tools, and — deliberately — different credentials:
| Pattern 1 — operator agent | Pattern 2 — builder agent | |
|---|---|---|
| The agent… | calls tools you defined in the showman config, each backed by a playbook | writes playbook YAML and installs it through the deploy meta-tools |
| Playbooks are… | fixed — the agent cannot create, change, or remove them | the agent's output — validated, installed, and hot-reloaded per call |
| Needs | [[mcp.tools]] entries + the edge credential | [mcp.deploy] enabled = true + the dedicated deploy credential + the binions-playbooks skill |
| Trust level | bounded — the agent can only do what the published playbooks do | operator-equivalent — treat the credential like SSH access |
| Typical use | ops assistants, chat front-ends, cross-system orchestration | AI-driven automation engineering, self-extending installs |
Key idea. In Binions, an MCP tool is a playbook route. A
tools/callbecomes aFact.Http.Receivedevent withmethod: MCP; whatever playbook triggers on that route runs its steps and replies through the gateway; the reply becomes the tool result the model sees. No connector code anywhere — the integration surface is your playbook vocabulary.
[mcp] enabled = true) and exposes nothing until you list tools or enable deploy.8446 on the Binions host. The edge terminates TLS with the install's self-signed certificate and enforces HTTP Basic Auth in front of /mcp./opt/binions/showman-service/secrets/mcp-deploy.credential holds the whole string mcp-deploy:<password> — use it verbatim as the Basic Auth user:pass, do not split or re-derive it.pip install langchain langchain-mcp-adapters deepagents httpx
plus the provider package for your chat model (any LangChain chat model with tool calling works; the transcripts on this page were produced with langchain-deepseek).The adapter turns every tool the server advertises into a regular LangChain StructuredTool. Three Binions-specific details matter: the Basic Auth header, the self-signed certificate, and the rate limit on the edge.
import base64
import httpx
from langchain_mcp_adapters.client import MultiServerMCPClient
# The credential file's WHOLE content is the user:pass string.
CRED = open("mcp-deploy.credential").read().strip()
AUTH = "Basic " + base64.b64encode(CRED.encode()).decode()
# The edge presents the install's self-signed certificate. Export it once
# and PIN it - you keep both encryption and authenticity:
# openssl s_client -connect binions-host.example:8446 binions-edge.pem
def client_factory(headers=None, timeout=None, auth=None):
return httpx.AsyncClient(headers=headers, timeout=timeout, auth=auth,
verify="binions-edge.pem")
client = MultiServerMCPClient({
"binions": {
"transport": "streamable_http",
"url": "https://binions-host.example:8446/mcp",
"headers": {"Authorization": AUTH},
"httpx_client_factory": client_factory,
}
})
Use a session, not one-shot calls.
MultiServerMCPClientis stateless by default: every tool invocation opens a fresh MCP session (aninitializeround-trip plus the call itself). The Binions edge rate-limits/mcpto an average of 2 requests/second with a burst of 10 as brute-force protection, so a stateless agent making several calls in a row will hit429 Too Many Requests. Open one session and load the tools on it —initializethen happens exactly once:
from langchain_mcp_adapters.tools import load_mcp_tools
async with client.session("binions") as session:
tools = await load_mcp_tools(session)
# ... build the agent and run it INSIDE this block ...
TLS. Always keep certificate verification on — disabling it (
verify=False) opens the connection to man-in-the-middle attacks even on a LAN. Pinning the exported edge certificate as shown above (or installing it into your client's trust store) is the supported way to talk to a self-signed edge; the certificate lists the install's hostname and IP addresses in its subject alternative names, so connect using one of those. Also note the edge answers 401 to everything without the credential — includinginitializeandtools/list. A 401 on first contact means “credential missing or wrong”, never “wrong endpoint”. Anonymous discovery works only on the loopback address, from the host itself.
You publish a tool by adding one [[mcp.tools]] entry to the showman configuration and deploying one playbook that answers on the entry's route. The agent then sees a typed, described tool — and can do nothing else.
application.toml)[[mcp.tools]]
name = "count-rows"
description = "Count rows in a registered database table. Arguments: {table: string}."
route = "mcp-demo-count"
input_schema = { type = "object", properties = { table = { type = "string" } }, required = ["table"] }
[[mcp.tools]]
name = "record-lead"
description = "Record a sales lead (customer + email) into the integration_demo table."
route = "mcp-record-lead"
input_schema = { type = "object", properties = { customer = { type = "string" }, email = { type = "string" } }, required = ["customer", "email"] }
Two practical notes: the configuration is read at daemon start, so restart the showman service after editing it; and always provide input_schema — without it the tool accepts a free-form object, and language models call schema-typed tools far more reliably.
The playbook triggers on the route with method: MCP and must end by replying through the gateway — that reply is the tool result. The reply route requires the shared ingest bearer (the install bridges it into the playbook secret store as SHOWMAN_INGEST), and headers on webhook.send is a list of pairs:
name: mcp-demo-count
description: |
MCP tool "count-rows": counts rows in the requested table and replies
synchronously - the reply becomes the MCP tool result.
trigger:
event: Fact.Http.Received
filter:
route: mcp-demo-count
method: MCP
steps:
- id: count
run: database.count
with:
table: ${trigger.body.table}
- id: reply
run: webhook.send
with:
url: "http://127.0.0.1:9099/in/_reply/${trigger.correlation_id}"
method: POST
headers:
- ["authorization", "Bearer ${secret.SHOWMAN_INGEST}"]
body:
table: ${prev.table}
count: ${prev.count}
Do not omit the bearer. The reply gateway refuses unauthenticated posts — loopback is not an identity on Binions. A playbook whose reply step lacks the
authorizationheader still runs (its earlier steps execute and their side effects land), but the reply is rejected and the MCP client getsno reply from any playbook within 10000 ms. If a tool times out yet its work visibly happened, this header is the first thing to check.
One authenticated endpoint may advertise the deploy meta-tools alongside your published ones. An operator agent should not carry them — filter the tool list before handing it to the model (and, better, keep [mcp.deploy] disabled on installs that only serve operator tools):
from deepagents import create_deep_agent
from langchain_mcp_adapters.tools import load_mcp_tools
OPERATOR_TOOLS = {"count-rows", "record-lead"}
async with client.session("binions") as session:
all_tools = await load_mcp_tools(session)
tools = [t for t in all_tools if t.name in OPERATOR_TOOLS]
agent = create_deep_agent(
model=chat_model, # any LangChain chat model with tool calling
tools=tools,
system_prompt=(
"You are an operations agent for a Binions automation host. "
"Binions exposes host capabilities as MCP tools backed by playbooks. "
"Use the tools to fulfil requests; report plain facts."
),
)
result = await agent.ainvoke({"messages": [{"role": "user", "content":
"How many rows are in the integration_demo table? "
"Record a lead for Umbrella Corp (contact@umbrella.example), "
"then report the new count."}]})
A real run of exactly this setup produces the following tool trace — the model plans the sequence on its own:
CALL: count-rows {'table': 'integration_demo'}
RESULT: {"table":"integration_demo","count":6}
CALL: record-lead {'customer': 'Umbrella Corp', 'email': 'contact@umbrella.example'}
RESULT: {"recorded":"yes","customer":"Umbrella Corp"}
CALL: count-rows {'table': 'integration_demo'}
RESULT: {"table":"integration_demo","count":7}
With [mcp.deploy] enabled = true the same endpoint additionally advertises six meta-tools. They are the full deploy surface — validate-and-install, inspect, roll back, and seed secrets:
| Tool | Arguments | What it does |
|---|---|---|
deploy-playbook | name (slug, must equal the YAML name:), yaml (≤64 KiB), run_on_load (default true), namespace (default mcp-deployed) | Validates, installs, hot-reloads, and — unless the playbook is request-reply — runs it once. On any problem it returns the validation errors instead of installing. |
list-playbooks | namespace (optional) | Inventory across all namespaces: name, namespace, trigger. |
get-playbook | name, namespace (optional) | Reads back the deployed YAML plus a summary — your eyes on what actually runs. |
remove-playbook | name, namespace (optional) | Removes and hot-reloads — the rollback. |
put-secret | key ([A-Z0-9_]), value | Writes a host secret so ${secret.KEY} resolves in playbooks; only a digest is ever logged. |
get-page | name or path | Reads back a web page this install serves — useful when playbooks publish dashboards. |
Every Binions release ships a downloadable binions-playbooks skill — the complete playbook grammar, the generic verb vocabulary with per-verb argument contracts, trigger events, patterns, and validation rules, generated from the same contract the platform itself enforces. Customers download it from the portal (it is versioned with the release, so the skill always matches the install). The skill follows the open Agent Skills layout (a SKILL.md plus reference files), which Deep Agents load natively with progressive disclosure — the model reads the metadata first and pulls in reference files only when it needs them:
from deepagents import create_deep_agent
from deepagents.backends.filesystem import FilesystemBackend
BUILDER_TOOLS = {"deploy-playbook", "get-playbook", "list-playbooks", "remove-playbook", "put-secret"}
async with client.session("binions") as session:
all_tools = await load_mcp_tools(session)
tools = [t for t in all_tools if t.name in BUILDER_TOOLS]
agent = create_deep_agent(
model=chat_model,
tools=tools,
backend=FilesystemBackend(root_dir="/opt/agent"),
skills=["skills/"], # contains skills/binions-playbooks/
system_prompt=(
"You are a Binions automation engineer. You have the binions-playbooks "
"skill installed - ALWAYS read its SKILL.md and the reference files it "
"points to BEFORE writing any playbook YAML, and follow the grammar "
"exactly. Deploy via the deploy-playbook tool. If deployment returns "
"validation errors, fix the YAML and retry."
),
)
Given the task “write and deploy a playbook that runs once when loaded and writes one row into the integration_demo table”, an agent built exactly as above produced this trace — note it studies the skill before writing a line of YAML, and verifies its own deployment afterwards:
CALL: read_file skills/binions-playbooks/SKILL.md
CALL: read_file skills/binions-playbooks/references/verbs.md
CALL: read_file skills/binions-playbooks/references/events.md
CALL: deploy-playbook {'name': 'langchain-agent-demo', 'yaml': 'name: langchain-agent-demo\n...'}
RESULT: {"deployed":true,"name":"langchain-agent-demo","namespace":"mcp-deployed",
"trigger":"Fact.System.Boot","loaded":16,"ran_now":true}
CALL: get-playbook {'name': 'langchain-agent-demo'}
RESULT: {"found":true,"name":"langchain-agent-demo","steps":1,...}
The row it promised appeared in the table on the same tick — run_on_load fired the playbook immediately after the hot reload (request-reply playbooks are the exception: they only ever run when their route is called).
Deployment is fail-closed: the host validates the YAML against the full grammar and the per-verb argument contracts before anything is written, and returns machine-readable errors the model can act on. A deliberately broken submission answers like this:
{"deployed": false,
"name": "langchain-demo-invalid",
"errors": ["steps[0]: unknown verb `run: webhook.nonsense` - operation 'nonsense' not declared for daemon 'webhook'"]}
Nothing was installed; the agent reads errors, fixes the step, and resubmits — deploying under the same name overwrites the previous version, so iteration is natural. This loop (deploy → errors → fix → redeploy) is what makes Binions an adaptive tool for a LangChain agent: the platform is the type-checker.
Every tool you publish under [[mcp.tools]] follows one contract, worth memorising:
Fact.Http.Received filtered on your route and method: MCP. The tool's arguments arrive as the fact body — reach them with ${trigger.body.<field>}; the request's correlation id is ${trigger.correlation_id}.http://127.0.0.1:9099/in/_reply/${trigger.correlation_id} with the ingest bearer header. The posted body is returned verbatim to the MCP client as the tool result.reply_timeout_ms (default 10 000 ms, configurable under [mcp]) and then returns a timeout error to the client. Long-running work belongs in an async playbook that a second, quick tool can poll.The integration is guarded in layers; know which layer produced the error you are looking at:
| Layer | Mechanism | Notes |
|---|---|---|
| Transport | TLS on the edge (install's self-signed certificate) | Pin the certificate in production clients. |
| Edge auth | HTTP Basic Auth on /mcp + rate limit (avg 2 r/s, burst 10) | 401 to everything without the credential, including initialize. |
| Deploy fortress | The deploy meta-tools re-validate the dedicated credential in-app and lock out after repeated wrong guesses (default: 5 failures, 15-minute lockout) | Defence in depth — the app does not trust the edge, or loopback. |
| Opt-in power | [mcp.deploy] enabled = false by default | Enabling it makes the credential holder operator-equivalent — secrets, SSH-backed verbs, and external webhooks included. Treat the credential like an SSH key. |
| Audit | Every deploy, removal, and secret write is a fact on the event bus (Fact.Playbook.Deployed with source: mcp, etc.); secret values are logged as digests only | Your playbooks can even trigger on these facts — alert on deploys, for instance. |
Practical least-privilege recipe: give operator agents an install (or at least a credential) where [mcp.deploy] is off, filter tool lists in the client as shown above, and reserve the deploy credential for the builder agent. Deep Agents can additionally require human approval per tool (its interrupt_on option) — a sensible default for deploy-playbook in unattended setups.
| Symptom | Cause | Fix |
|---|---|---|
401 on initialize | Missing/wrong Basic credential — the edge answers 401 to everything unauthenticated | Send the credential file's whole content as user:pass; it is not a token to split. |
429 Too Many Requests | Stateless client re-initialises per tool call and trips the edge rate limit | Use client.session() + load_mcp_tools(session); keep bursts under 10 requests. |
no reply … within 10000 ms, side effects did happen | The reply step lacks the ingest bearer header, so the gateway rejected the reply | Add headers: - ["authorization", "Bearer ${secret.SHOWMAN_INGEST}"] to the reply step. |
no reply … within 10000 ms, nothing happened | No playbook triggers on that route, or the playbook is slower than the deadline | get-playbook / list-playbooks to verify the route; raise reply_timeout_ms for slow work. |
Tool missing from tools/list | Config edited but daemon not restarted; or you queried anonymously — deploy tools only appear on authenticated requests | Restart the showman service after config changes; authenticate to see the deploy tools. |
deployed: false with errors[] | The YAML failed grammar or per-verb argument validation | Working as designed — feed the errors back to the model and redeploy under the same name. |