DEV Community

Cover image for Talking to Salesforce Through MCP: How We Solved the Context Bloat Problem
Murali Gour
Murali Gour

Posted on • Originally published at datagrout.ai

Talking to Salesforce Through MCP: How We Solved the Context Bloat Problem

Salesforce has a huge API surface. Objects, fields, flows, custom actions — it goes deep. When we started building our Salesforce MCP integration on top of it, we ran into a design conundrum.

Option A: Expose a handful of generic tools.

  • Pro: The agent avoids context bloat.
  • Con: You lose the granular control, predictable outputs, and security enforcement that enterprise workflows actually need.

Option B: Expose 100s of specific tools, one per object and one per action.

  • Pro: You get the precision and auditability you need.
  • Con: The agent's context window fills up with schemas before it has done any real work.

We built DataGrout’s Conduit SDK around the idea that you shouldn’t have to pick one or the other. This post walks through the design we used and what tradeoffs you still need to account for.

The trade-off: granularity vs. context efficiency

A standard MCP call to tools/list gets back every tool the server exposes, and the model reasons over all of it before deciding what to call. That’s not an issue if your server has 10 tools. But if you’re running multi-system enterprise workflows where Salesforce, QuickBooks, and other connected systems are all wired to the same agent, you’re looking at a combined tool catalog with 100s of schemas that load into context on every single turn.

Our answer in Conduit was to collapse the entire tool surface into two entry points:

from datagrout.conduit import Client

async with Client("https://gateway.datagrout.ai/servers/{uuid}/mcp") as client:

tools = await client.list_tools()

# -> [discovery.discover, discovery.perform]
Enter fullscreen mode Exit fullscreen mode

Instead of the model scanning 100s of schemas, it describes what it wants in plain language and a server-side discovery step resolves that to the right tool:

results = await client.discover(query="find leads created this week with no owner", limit=5)
Enter fullscreen mode Exit fullscreen mode

This moves tool selection out of the LLM's context entirely. The upside is smaller prompts and cheaper calls. The tradeoff is that you’re now trusting a semantic matching step to pick the right tool. For read operations that’s fine. For anything destructive, test your edge cases before relying on it in production.

The granular tools still exist underneath. They give you the control and governance layer. Conduit's intelligence layer just makes sure they don’t all land in context at once.

Calling Salesforce tools directly

If you already know which tool you need, skip discovery and call it directly. Tools are namespaced by integration and version:

result = await client.call_tool("salesforce@1/get_lead@1", {"id": "00Q5f000003abcXYZ"})
Enter fullscreen mode Exit fullscreen mode

We expose the same call shape across all five SDKs: Python, TypeScript, Rust, Elixir, and Ruby. The choice of language comes down to whatever your agent runtime already uses.

Chaining actions across systems

Single tool calls are easy. Where Conduit makes the biggest difference is multi-step workflows that span systems. The flow.run() primitive lets you chain tool calls and pass output from one step directly into the next:

outcome = await client.flow.run(plan=[

{"tool": "salesforce@1/get_lead@1", "args": {"id": "00Q5f000003abcXYZ"}},

{"tool": "quickbooks@1/create_invoice@1", "args": {"$prev.result": True}},

])
Enter fullscreen mode Exit fullscreen mode

Each step's output is in the next step's args. For example, you can look up a lead in Salesforce, which can then feed directly into an invoice creation in QuickBooks. Two completely separate systems in one agent call.

For anything with financial or irreversible side effects, insert an approval gate before the write happens:

await client.flow.request_approval(
    action="create invoice from lead 00Q5f000003abcXYZ",
    reason="First invoice for this account, needs manual sign-off"
)
Enter fullscreen mode Exit fullscreen mode

We built this because giving an agent the keys to take financial actions unsupervised introduces risks that aren’t worth taking for enterprise teams. At least not yet. The approval gate keeps a human in the loop without breaking the workflow entirely.

Authentication: built for agents that run unattended

Conduit supports bearer tokens, OAuth 2.1 client credentials, and mTLS across all five SDKs. The tool calls look identical regardless of which auth method you pick.

The mTLS path is the one we recommend for production agents that run unattended for extended periods:

client = await Client.bootstrap_identity(

url="https://gateway.datagrout.ai/servers/{uuid}/mcp",

auth_token="your-access-token",

name="my-agent",

)

# subsequent runs auto-discover the cert from ~/.conduit/

# no token refresh logic needed
Enter fullscreen mode Exit fullscreen mode

The private key is generated locally and never leaves your machine. Our CA signs a certificate binding the public key to a named agent identity. This matters in practice because bearer tokens in env vars are the thing that gets rotated incorrectly at 2 am and takes down your production agent. With mTLS, you don’t have to maintain refresh logic and you’re not leaving a long-lived secret sitting in an environment variable.

Try it yourself

Check out Conduit SDK on GitHub and share any feedback. There are installation instructions for all five languages, and the quick start gets you to a working Salesforce tool call in under five minutes. If it’s useful, give it a star so others can discover it.

Top comments (1)

Collapse
 
alexshev profile image
Alex Shev

The context-bloat fix that usually matters is making discovery staged. The agent should not carry every Salesforce object and field all the time; it should first identify the business object, then fetch only the schema and examples needed for the task. That is also how local SEO tooling should treat GBP/location data: narrow scope first, then query.