How We Built a Secure Chat for CS Teams on Top of MCP
The decisions behind per-turn credential isolation, session history, and race-safe credit enforcement.
Building chat over live data sounds straightforward. It isn't.
The moment a CSM types "show me account health for Acme Corp," you have a security problem that a generic AI assistant never has to solve. The AI needs to call real APIs using Acme's credentials. Not your credentials. Not a shared key. Not a demo fixture.
Get that wrong and you have a tenant isolation breach. Here's what getting it right required.
The problem is not chat. It's acting as someone else.
Most chat-over-data tools are single-tenant. You connect your own database, ask questions, get answers. The blast radius of a bug is your own data.
CS teams work differently. A CSM queries on behalf of a customer, with that customer's credentials, scoped to that customer's data. The same AI loop that pulls Acme's invoices might handle Beta Inc's subscription status on the next message. Each call needs to prove it's using the right identity.
Two things need to be true before any API call fires:
- The CSM is authenticated to this organization
- The customer account being queried belongs to that same organization
Enforcing this at session start is not enough. We enforce it on every turn.
Per-turn trust: one check, two jobs
Every time the model wants to call a tool, before credentials load, before any upstream request fires, we verify the combination of organization, connector, and customer account in a single query.
It checks: does this customer account exist, does it belong to the connector being queried, does that connector belong to the authenticated organization, and are both currently active?
If anything is off, such as wrong org, paused account, revoked customer, then the tool call is rejected. The response is deliberately uninformative. The caller gets a 404, not a 403. A 403 leaks information about what exists. A 404 doesn't.
This also means the trust check doubles as the tool catalog load. Resolving which tools are available proves the org/connector/customer triple is valid in the same round-trip. One query, two jobs. There's no way to get the tool list without passing both.
The model never sees the credential
Customer API keys are stored encrypted at rest. When the model needs to call an upstream API, the key is decrypted in-process, injected into the outbound HTTP request, and never returned to the model. The LLM sees the tool result. The data that came back. Nothing else.
This is the same pattern as a good OAuth proxy. The agent acts with the customer's authority without having access to the credential itself. You could log every model input and output without leaking a single API key.