Munin with the OpenAI Agents SDK.
Every other integration page here is about a client somebody else wrote. This one is about the case where you are the client — one MCPServerStreamableHttp, a tool filter, an approval policy you control, and a review queue underneath that holds regardless of what your code decides.

There is a moment in every agent project where the hosted clients stop being enough. You want the loop to run at 06:00 without anyone opening a laptop. You want it to read from your own queue, write to your own logs, and fail in a way your on-call rota understands. At that point you are not looking for a chat window. You are writing a program, and the program needs tools.
How do you connect the OpenAI Agents SDK to a CRM?
Point an MCPServerStreamableHttp at an MCP server that already has the customer data in it. Munin serves CRM, Conversations, Knowledge Base, CMS, Outreach, and Analytics as MCP tools at one HTTPS endpoint, so a single server object in your Python process gives an Agent the whole customer platform — no per-module SDK, no REST client, no schema of your own to maintain.
import asyncio
import os
from agents import Agent, Runner
from agents.mcp import MCPServerStreamableHttp
async def main() -> None:
async with MCPServerStreamableHttp(
name="Munin",
params={
"url": "https://mcp.getmunin.com",
"headers": {"Authorization": f"Bearer {os.environ['MUNIN_API_KEY']}"},
"timeout": 30,
},
cache_tools_list=True,
max_retry_attempts=3,
) as server:
agent = Agent(
name="Desk",
instructions="Use the Munin tools. Quote the customer's own words.",
mcp_servers=[server],
)
result = await Runner.run(
agent,
"Which conversations from the last seven days never got a reply?",
)
print(result.final_output)
asyncio.run(main())That is the whole integration. pip install openai-agents on Python 3.10 or newer, an admin key minted in Settings → API keys, and the agent can reach crm_search_contacts, conv_list_conversations, kb_search, cms_create_entry and the rest of the catalogue by name.
Two constructor arguments are worth setting deliberately rather than copying. cache_tools_list=True stops the SDK re-listing tools on every run — the tool definitions do not change between your 06:00 and 06:05 passes, and listing a large catalogue over the network is the slowest thing in the loop. Call invalidate_tools_cache() on the server if you need a fresh list mid-process. max_retry_attempts adds automatic retries around list_tools() and call_tool(), which matters more for an unattended cron job than it does for a human sitting in front of a chat window who can simply try again.
Self-hosting instead of Cloud? The same object, pointed at http://localhost:3001/mcp. Your process makes the connection, so a private endpoint is fine here in a way it is not for every route — more on that below.
Which of the SDK's four MCP integrations should you use?
The Python SDK supports four, and the choice is really a question about where the tool call executes. For a customer platform holding real records, Streamable HTTP inside your own process is the right default: your code holds the credential, your code sees every call, and the endpoint never has to be reachable from anywhere but your infrastructure.
- MCPServerStreamableHttpYour process opens the connection and makes the calls. The credential stays in your environment, the traffic stays on your network path, and a private or self-hosted endpoint works fine. This is the one to reach for.
- HostedMCPToolYou hand OpenAI's Responses API a
server_labelandserver_url, and OpenAI's infrastructure calls the server on the model's behalf. Less code, and no round trip back into Python — but the docs are explicit that the server must be publicly reachable, and it currently works with OpenAI models supporting the Responses API's hosted MCP integration. - MCPServerSseThe older HTTP-with-SSE transport. The MCP project has deprecated it; the SDK docs say to keep it for legacy servers and prefer Streamable HTTP for anything new.
- MCPServerStdioThe SDK spawns a local subprocess and talks over stdin and stdout. Right for a filesystem server on your own machine, wrong for a multi-tenant platform that lives behind an HTTPS endpoint.
The hosted route has a network shape, not a quality difference
HostedMCPTool is genuinely less code. It also means OpenAI's servers, not yours, must be able to open a connection to your MCP endpoint — so a laptop install or a VPC-only deployment is out.
Munin Cloud is publicly reachable and works either way. Pick the hosted route when you want the Responses API to own the round trip; pick Streamable HTTP when you want the credential and the call log inside your own process.
How do I stop the agent seeing every tool in the catalogue?
Use tool_filter. A full customer platform exposes a large tool surface across six modules, and an agent whose one job is triaging the inbox has no business holding a tool that merges contacts. create_static_tool_filter takes an allow-list, a block-list, or both — the SDK applies the allow-list first and then removes anything blocked from what remains.
This is the highest-value ten lines in the file. Scoping tools is not only a latency and token argument; it is the difference between an agent that cannot do the wrong thing and an agent you are trusting not to.
from agents import Agent
from agents.mcp import MCPServerStreamableHttp, create_static_tool_filter
triage = MCPServerStreamableHttp(
name="Munin (triage)",
params={"url": "https://mcp.getmunin.com", "headers": headers},
tool_filter=create_static_tool_filter(
allowed_tool_names=[
"conv_list_conversations",
"conv_search_messages",
"conv_set_topic",
"conv_set_subject",
"crm_lookup_contact",
"kb_search",
],
),
)
agent = Agent(
name="Triage",
mcp_servers=[triage],
mcp_config={
"convert_schemas_to_strict": True,
"include_server_in_tool_names": True,
},
)mcp_config is agent-level rather than server-level, and both keys there earn their place. convert_schemas_to_strict is best-effort — where a tool schema can be converted to strict JSON schema it will be, and where it cannot the original is used, so it costs nothing to leave on. include_server_in_tool_names prefixes local MCP tool names with the server name, which stops collisions the moment you attach a second server; if you are running Munin alongside a filesystem or search server, turn it on before the first name clash rather than after.
For logic a static list cannot express, pass a callable instead. It receives a ToolFilterContext carrying the active run_context, the agent asking for the tools, and the server_name, and returns True when the tool should be exposed — so one server object can present a read-only surface to a summarising agent and a wider one to the agent that actually files things.
How do I require approval before a tool runs?
Pass require_approval to the server. MCPServerStdio, MCPServerSse, and MCPServerStreamableHttp all accept it, in four forms: "always" or "never" for everything, the booleans True and False meaning the same, a per-tool map like {"conv_send_message": "always", "kb_search": "never"}, or a grouped object naming tools by policy.
The grouped form is the one to write, because it reads like a policy document and a reviewer can check it in a code review.
async with MCPServerStreamableHttp(
name="Munin",
params={"url": "https://mcp.getmunin.com", "headers": headers},
require_approval={
"always": {"tool_names": ["conv_send_message", "crm_update_contact"]},
"never": {"tool_names": ["conv_search_messages", "kb_search"]},
},
) as server:
...The test
If someone deleted your approval policy tonight, what could the agent do to a customer by morning?
What happens if my runner just approves everything?
The irreversible actions still do not fire. Munin splits writes by cost rather than by permission: cheap reversible ones like tagging a conversation or setting its subject execute directly, while the ones you cannot take back — publishing a knowledge-base article, merging two contact records, sending outreach — exist only as propose tools that file into a review queue a signed-in human clears.
That distinction is deliberate, and it is the reason a custom runner is a reasonable thing to hand a production org. Client-side approval is a property of your code: it is as good as your last deploy, and require_approval="never" is one careless commit away. The review queue is a property of the platform, so it survives your code being wrong. outreach_propose_first_touch drafts an email and files it; there is no tool in the catalogue that sends it. kb_propose_curation_candidate writes an admin-only draft; promoting it is a human action. The outreach queue works the same way from every client, which is the point — the safety is not in the client.
Write the approval policy anyway. Two layers that agree with each other is the correct number, and the client-side one is what stops a wasted model call before it happens rather than after.
What does a real unattended pass look like?
The overnight desk pass, which is the job most people write a runner for in the first place. Four steps, all real tool names, none of which require a human to be awake:
- 01Find what went unanswered.
conv_list_conversationsfiltered to open threads, thenconv_search_messagesto read what was actually asked. The customer's phrasing, not a summary of it. - 02Work out who is asking.
crm_lookup_contactresolves the sender to a contact record;analytics_get_contact_journeysays which pages they read before they wrote in. Both sit on the same Postgres row, so this is a join, not an integration. - 03Check whether you have already answered it.
kb_searchruns vector and full-text search over the knowledge base. If the answer exists, the reply writes itself; if it does not, that absence is the more useful finding. - 04File, do not send.
conv_set_topicandconv_set_subjectwrite directly because they are trivially reversible. Anything that reaches the customer goes through a proposal the morning shift approves.
Your runner reads text customers wrote
A support message is untrusted input, and it lands in the model's context. Treat a ticket as a plausible prompt-injection vector rather than as data.
The mitigation is scope, not vigilance: a tool_filter allow-list, require_approval on anything outbound, and a key scoped to the modules the job needs. A Munin admin key is full access to the org — keep it in the environment, never in the repo.
Who should not write their own runner?
Most people, honestly, and it is worth being clear about what Munin does and does not ship. Munin is a customer platform, not an agent framework: there is no runner, no prompt manager, no eval harness, no tracing UI in it. That is the Agents SDK's territory, and the SDK is well built for it — tracing captures MCP tool listing and tool calls automatically, Runner.run_streamed gives you incremental output, and MCPServerManager handles connecting several servers with drop_failed_servers and reconnect semantics already thought through. Take that machinery from the SDK, and let Munin be the thing it calls.
What you get from writing the loop yourself is the two things a hosted client cannot give you: a schedule, and a place to put the output. A cron entry, a Slack post, a row in your own table, a test that fails when the pass produces nothing. If your agent work is conversational — someone asking questions and reading answers — a client is less work, and the Claude Code and Cursor walkthroughs cover that setup end to end. Write a runner when the thing you need is a job rather than a conversation, and when you want the loop, the retries, and the failure mode to be yours. The tools are identical either way, because it is one endpoint and one schema underneath.
Is there a TypeScript version?
Yes. The JavaScript and TypeScript Agents SDK ships from @openai/agents and exposes the same two shapes under the same names: MCPServerStreamableHttp when your process owns the connection, and hostedMcpTool when the Responses API should own it. The concepts map one to one — a server object with a URL, tool filtering, an approval policy.
The option spellings differ from Python in places, and rather than guess at them here, read them off the TypeScript MCP guide before you write the config. A config key invented in a blog post costs an afternoon.
Frequently asked questions
Does the OpenAI Agents SDK support remote MCP servers?
Yes. MCPServerStreamableHttp connects to a Streamable HTTP server anywhere your process can reach, with params taking url, headers, and timeout. HostedMCPTool is the alternative, where OpenAI's Responses API calls a publicly reachable server on the model's behalf.
How do I authenticate an MCP server in the Agents SDK?
Put an Authorization header in the params dict: {"url": ..., "headers": {"Authorization": f"Bearer {token}"}}. Read the token from the environment. For Munin, mint the key under Settings → API keys and scope it to the modules the job needs.
Can I limit which MCP tools an agent can call?
Yes, with tool_filter. Use create_static_tool_filter(allowed_tool_names=[...], blocked_tool_names=[...]) for a fixed list — the allow-list applies first, then blocked names are removed — or pass a callable receiving a ToolFilterContext for per-run logic.
How do I add human approval to MCP tool calls?
Pass require_approval to the server object as "always", "never", a per-tool map, or a grouped {"always": {"tool_names": [...]}} object. For hosted tools, set require_approval in tool_config and supply an on_approval_request callback to decide in Python.
Does this work with a self-hosted Munin?
Yes, over Streamable HTTP. docker compose up serves MCP at /mcp on the backend port, so point params["url"] at http://localhost:3001/mcp. The hosted-tool route needs a publicly reachable URL, so use the Streamable HTTP server for a private deployment.
Do I have to use OpenAI models?
For the local transports — Streamable HTTP, SSE, stdio — the MCP tools are converted into ordinary function tools, so this is a normal Agents SDK model choice. HostedMCPTool is the exception: the SDK documents it as working with OpenAI models that support the Responses API's hosted MCP integration.
What does Munin cost to run this against? Cloud Free is €0 a month with 5,000 MCP calls, 250 contacts, and 100 MB of storage — enough to run a nightly pass while you decide. Self-hosting is free forever under MIT, with no enterprise directory held back.
The short version
MCPServerStreamableHttpwith aurland anAuthorizationheader gives an Agents SDK agent the whole Munin catalogue — CRM, Conversations, Knowledge Base, CMS, Outreach, Analytics — in about fifteen lines of Python.- Four integration paths exist; Streamable HTTP is the default for real customer data, because your process holds the credential and the endpoint never needs to be public.
create_static_tool_filterscopes the agent to the tools its job needs. Setinclude_server_in_tool_namesbefore you attach a second server, not after.require_approvalin its grouped form reads like a policy and reviews like one. Underneath it, Munin only lets agents propose the irreversible actions — so the review queue holds whatever your code decides.- Write a runner when you need a schedule and somewhere to put the output. For conversational work, an existing MCP client is less work and reaches the same tools.
Point an MCPServerStreamableHttp at your org and ask it which conversations from last week never got a reply — the MCP tool reference has the names, and Munin Cloud is free to start on.
Write the loop. Everything it needs to call is already there.