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.

ts
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.

ConcernWeak defaultEnterprise contract
ToolingOne generic executorNarrow typed tools
AuthShared service accountPer-call scoped credentials
ErrorsThrown exceptionsTyped, documented outcomes