Agents become useful when they can act, and acting means calling internal systems. The Model Context Protocol standardizes that boundary: a server advertises tools, an agent calls them, and the transport is uniform across providers. What the protocol does not decide is your security and error model. That is the architecture.
Tools, not a generic executor
The fastest way to create an unsafe agent is a single run_query tool. Model each capability as a narrow tool with a typed schema, so the agent’s surface area is legible and reviewable.
server.tool(
'get_invoice_status',
{ invoiceId: z.string().uuid() },
async ({ invoiceId }, ctx) => {
const invoice = await billing.getInvoice(invoiceId, ctx.auth.scopes);
if (!invoice) return { error: 'not_found' as const };
return { status: invoice.status, dueAt: invoice.dueAt };
}
);Credentials are per-tool
A shared service account turns every agent into a superuser. Issue scoped credentials per tool call, derived from the caller identity, and deny by default.
Errors are part of the interface
Agents plan around outcomes. Return typed, non-throwing errors such as not_found or rate_limited and document them, so the agent retries or escalates instead of fabricating a result.
| Concern | Weak default | Enterprise contract |
|---|---|---|
| Tooling | One generic executor | Narrow typed tools |
| Auth | Shared service account | Per-call scoped credentials |
| Errors | Thrown exceptions | Typed, documented outcomes |