Which Agents SDK class fits a remote MCP endpoint?
A remote server behind OAuth fits MCPServerStreamableHttp or HostedMCPTool. Stdio is for a server running as a local child process, which a hosted endpoint is not.
The situation: you have an agent written with the OpenAI Agents SDK, it needs to read opportunities in Salesforce and post a digest to Slack, and it has to act as a named person with a record of what it did. OpenAI Agents SDK governed access comes down to three decisions. Which transport class, who holds the OAuth grant, and where the per-tool rules live.
The OpenAI Agents SDK documents four MCP shapes:
| Class | Transport | Who calls the server | Use when |
|---|---|---|---|
MCPServerStdio |
Local process, stdio | Your process | The server runs as a local child process |
MCPServerSse (deprecated) |
HTTP + Server-Sent Events | Your process | Legacy servers only; do not use for new work |
MCPServerStreamableHttp |
HTTP, streamable | Your process | Remote server, your code holds the token and makes the calls |
HostedMCPTool / Responses API mcp tool |
HTTP, streamable | OpenAI's infrastructure | Remote server, OpenAI's backend makes the calls on the model's behalf |
Source: the Agents SDK MCP guide (openai.github.io, checked October 2026). MCP is the Model Context Protocol, the wire format an AI client uses to find and call tools on a server (the specification).
Elaichi is a remote server. It serves standard MCP over Streamable HTTP, JSON-RPC 2.0, stateless, behind OAuth, at one organization-wide address: POST https://api.elaichi.ai/mcp. There are no per-toolbox URLs and no embedded tokens. With MCPServerStreamableHttp your own process makes the calls. With HostedMCPTool, or the Responses API mcp tool, OpenAI's infrastructure calls the server instead.
Disclosure: Elaichi publishes this blog.
One caveat before any code. Elaichi names Claude, ChatGPT, Cursor, any MCP client and the Elaichi Agent as the clients that connect. Neither the Agents SDK nor the Responses API mcp tool is named there specifically. What follows sets Elaichi's documented requirements beside OpenAI's documented options. Prove the pairing in a throwaway organization first.
Copy the address exactly. POST https://api.elaichi.ai/mcp answers 401 with the resource_metadata challenge that starts sign-in. With a trailing slash it answers 404, and on app.elaichi.ai/mcp it answers 405 (probed October 2026). Neither carries the challenge, so neither starts sign-in.
OpenAI Agents SDK governed access starts with one person's grant
Your program holds a real person's OAuth grant, not a service key. OAuth is the sign-in handshake that issues a scoped token, and the grant is the standing permission a person approved for one named client.
Elaichi takes OAuth only. A client registers itself through dynamic client registration (RFC 7591) at /oauth/register, with PKCE S256 required, then sends the person to Elaichi's consent screen in a browser. There is no client ID, secret or header to paste.
The Python SDK will not run that flow. The Python Agents SDK takes a token through headers or an httpx.Auth handler in params["auth"]. The JS SDK accepts an authProvider, the MCP TypeScript SDK's OAuthClientProvider (openai.github.io, checked October 2026). The Responses API mcp tool takes an authorization value that OpenAI says it never stores and sends on every request (developers.openai.com, checked October 2026). The Python SDK and the Responses API tool assume you already have a token; the JS SDK's authProvider can run the browser leg through the MCP TypeScript SDK.
So you build one component: a browser sign-in that captures the grant, plus storage and a refresher. Minimal shape in Python:
from agents.mcp import MCPServerStreamableHttp
import httpx
class ElaichiAuth(httpx.Auth):
"""Wraps a token store that owns refresh, rotation, and the single-writer lock."""
def __init__(self, token_store):
self.token_store = token_store
def auth_flow(self, request):
token = self.token_store.get_access_token() # refreshes if < ~60s to expiry
request.headers["Authorization"] = f"Bearer {token}"
yield request
server = MCPServerStreamableHttp(
params={
"url": "https://api.elaichi.ai/mcp",
"auth": ElaichiAuth(token_store),
}
)
The snippet leaves out the browser consent leg, which runs once per person at onboarding, not on every agent invocation. The token_store is the part worth building carefully. The lifetimes and locking rule below explain why.
Four lifetimes shape the token store. The consent request lasts 30 minutes and works once. An access token lasts 1 hour. A refresh token lasts 30 days and rotates on every use: each refresh call returns a new refresh token and invalidates the old one. Reusing an old refresh token or an authorization code revokes the whole grant, so serialize refreshes through a single writer (a DB advisory lock or a single refresher process). This matters concretely in multi-process deployments: if two workers both see a token expiring and both call refresh concurrently, the second call's refresh token is already dead by the time it lands, and the whole grant goes with it. One writer, one in-flight refresh at a time, is not optional. The grant itself has no expiry, but 30 idle days means somebody signs in again.
Four details trip OAuth libraries here:
- Scopes come only from
mcp:read,mcp:write,mcp:destructive,mcp:tools,openidandemail. A library that addsoffline_accessgetsinvalid_scope. - A
resourceparameter, if sent, must be onhttps://api.elaichi.ai, orinvalid_target. - The
redirect_urimust match registration exactly. A loopback address may change port only, never host, so127.0.0.1never matcheslocalhost. - Rate limits are 30 OAuth requests a minute and 120 MCP requests a minute per token, answered with 429 and
Retry-After: 60.
Sign-in needs a browser. A program running headless in CI cannot complete it. That is a stop, not a configuration problem.
A revoked or expired grant answers 401 invalid_token on the next request, and a refresh answers invalid_grant. Both are fixed by sending the person back through sign-in. The two credential shapes are compared in OAuth or API keys for AI agents.
If your organization runs both patterns at once, one team wiring MCPServerStreamableHttp directly and another going through HostedMCPTool via the Responses API, both see the same tools/list output for the same grant, because the tool list is a property of the grant's role and scopes, not of which SDK class is calling. There's no split-brain case here: change the role, both callers see it change within about two minutes.
Why per-tool control over Salesforce and Slack is written in Elaichi
Client-side filters cannot tell one Salesforce tool from another. Connected tools are never listed in tools/list, however few there are. Elaichi's list carries its own elaichi__ operations plus two meta-tools: the model finds a connected tool with search_tools and runs it with execute_tool.
That bounds what the SDK's controls can do. tool_filter=create_static_tool_filter(...), the Responses API's allowed_tools and require_approval all act on advertised names. Against Elaichi they can allow or gate execute_tool as a whole. Allow it and every connected tool the grant reaches is callable. Block it and the agent reaches no connected app at all. execute_tool is annotated destructive and open-world on purpose, because its real tier is not knowable before the call.
Client-side approval is also inconsistent across OpenAI's own surfaces. The JS hostedMcpTool defaults requireApproval to 'never' (openai.github.io, checked October 2026), while the Responses API mcp tool requires approval by default (developers.openai.com, checked October 2026). A control whose default flips between surfaces is a poor home for a Salesforce write policy.
Write the policy in Elaichi, as a restriction on the role the agent's person holds. A restriction is a rule about which connectors and which individual tools a target may reach. In Elaichi, restriction targets are role or user only; the organization default is the absence of a rule, which means allow everything.
For the Salesforce and Slack pair, two shapes do most of the work:
- Blocks on one app's write tools hold that app to reads and leave the person's other apps untouched.
- An allow rule is stricter and wider. It becomes the role's entire allowlist across every connector, so a role allowed a handful of Salesforce read tools also needs allow rules naming its other apps, Slack included, or it loses them.
A digest-posting agent usually wants the first shape twice. Block the Salesforce write tools so the agent reads opportunities and changes nothing, and block Slack's destructive tools so it posts without deleting. A withheld tool is invisible. It reaches neither tools/list nor search_tools, and its name never goes on the wire, so expect silence rather than a refusal. A role or restriction change takes effect within about two minutes, which matters while you are testing. Which shape fits which case is worked through in restricting one tool or a whole app.
One scope note surprises people. For a connected app's tools, mcp:tools runs reads and writes alike, and only a tool whose method is a delete costs mcp:destructive. Leaving "Create and change data" unticked on the consent screen does not make Slack read-only. Read-only access to a connected app is a restriction, never a consent checkbox.
Every call acts as the member whose grant the program holds. The audit entry records the surface as mcp, names the OAuth client, and names the connection actually reached, so "which Slack workspace did it post to" has an answer. A client that registers itself shows the name it registered with, marked unverified; only clients whose redirect URIs prove them, Claude, ChatGPT and Cursor among them, are marked verified. Argument names and counts are logged. Argument values never are. Offboarding follows the same model. In Elaichi, removing or suspending a member revokes every live grant in the same transaction as the membership change.
What SDK tracing records, and OpenAI's warning about aggregators
Agents SDK tracing is on by default and includes tool inputs and outputs. It goes to OpenAI's Traces dashboard, covers MCP tool listing and calls, and is governed by trace_include_sensitive_data (openai.github.io, checked October 2026). If Salesforce field values should not land in a third party's dashboard, decide that before the first run and reroute with add_trace_processor() or set_trace_processors().
OpenAI also says it does not verify remote MCP servers, that a malicious one can exfiltrate what is in the model's context, and that "aggregators" deserve extra due diligence (developers.openai.com, checked October 2026). That warning points at Elaichi too, and deserves a direct answer rather than a reassurance.
What holds on Elaichi's endpoint: Elaichi authors, maintains and serves its own connectors from its own infrastructure, rather than wrapping a registry of servers other people run. Connector credentials sit in a separate credential service, encrypted at rest with AES-256-GCM, stored in the organization's region, with every organization request routed to run in that region. Role checks apply per operation, a tool classified forbidden is reachable under no scope, results are scrubbed for secret-shaped values as a backstop, OAuth scopes bound the grant, and every attempt is logged. The detail is on the security page.
What does not hold: the prompt-injection write gate in the Elaichi Agent does not apply to POST /mcp, and cannot, because an MCP server never sees a user prompt. Do not count the endpoint as prompt-injection protection. The rest of that surface is in MCP security risks and how to reduce them.
When the app's own MCP server is the better answer
One agent, one app, one vendor: use the vendor's own server. Salesforce hosted MCP servers are generally available for Enterprise Edition orgs and above, where "every transaction runs as the authenticated user" (developer.salesforce.com, checked October 2026). Slack hosts one at https://mcp.slack.com/mcp, where workspace admins approve MCP clients and each client must be backed by a registered Slack app (docs.slack.dev, checked October 2026). Both run under the app's own permission model and add no second vendor. If your agent only ever touches one app, this is the simpler, lower-trust-surface choice, and we'd say so even though we sell the alternative.
The case for a single endpoint like Elaichi's starts when the agent spans several apps and more are coming. One address, one grant per person, restrictions written once against a role, and one audit trail across every app. That consolidation is the actual trade: fewer integration points per agent, at the cost of adding Elaichi itself as a party that touches every credential. Removing someone from Elaichi ends their access through Elaichi and nothing more; their Salesforce and Slack accounts are still deprovisioned separately, where they live. Elaichi does not replace app-level offboarding, it adds a second place that also needs to be checked.
If what you actually need is a budget cap per model or a rate limit on inference, that is a different layer, and a proxy in front of the model is the right shape. Elaichi governs tool execution, not token spend. The 600+ connectors behind the endpoint are listed in the connector catalog, including Salesforce and Slack.
A first run you can check in an afternoon
Start in a throwaway organization, with a member whose role you are willing to break. Sign in once through a browser, store the grant, point MCPServerStreamableHttp at https://api.elaichi.ai/mcp, and call tools/list.
Seeing search_tools and execute_tool means the grant reaches connected tools. Seeing only elaichi__ operations means the grant lacks mcp:tools or no connected tool is reachable yet, for example a connection still pending or needing reauthorization. Reconnecting with "Run your connected tools" ticked fixes the first case. An empty list usually means the role lacks tool:execute, which no amount of re-authorizing grants. The rest of the ladder is in why MCP tools do not show up.
Then prove the governance leg. Block a Salesforce write tool on that role, give the change about two minutes, run the call, and open the audit trail. The entry should name the member, the OAuth client and the connection reached. That record is the evidence a reviewer asks for later.
For the wider shape, read what an MCP control plane is. For the same problem from the human end, where a rep uses their own Salesforce access inside a chat client, see each rep's own Salesforce access in ChatGPT. Team rollouts sit on use cases, and Gold's USD list price is on pricing.