Last month, I spent three days debugging an MCP server I'd built to wrap one of our APIs. The symptoms were classic: Claude would make requests, get responses back, but then ask the user to manually enter their API key anyway. I was convinced it was a Claude Desktop bug. Turns out, I'd made a fundamental mistake about how authentication actually works at the boundary between an AI client and your API. Once I understood the separation of concerns, everything clicked.
This is the kind of thing that doesn't appear in most tutorials. You can find plenty of "build your first MCP server" guides, but they skip right over the authentication layer, the place where most of us actually get stuck in production. I wish I'd understood this earlier.
The Two-Credential Model Changed How I Think About This
Here's what I was doing wrong: I was treating MCP authentication as a pass-through mechanism. The user authenticates to Claude, Claude authenticates to my MCP server, and my MCP server just... forwards everything to the API. No separation, no boundary, no thought.
The actual architecture is cleaner. Your MCP server handles two separate authentication flows. The client (Claude or ChatGPT) authenticates to your MCP server with one set of credentials. Your MCP server authenticates to your API with a completely different set. They're independent systems.
Think of it like this: Claude doesn't need to know about your API's authentication at all. Your MCP server acts as an intermediary that speaks the client's language and the API's language, but keeps them separate. This is where I'd been conflating concerns.
Shared Keys Versus Per-User OAuth: A Real Trade-off
This is where the design decision matters. I can either:
Use a static shared key, One API key, known to the MCP server at runtime (via headers, environment variables, whatever), and used for every call. Everyone who accesses the tool gets the same API permissions.
Use per-user OAuth, Each user grants their own credentials, and the MCP server validates that token before calling your API with their permissions.
The choice depends on what you're building. If your tool is meant to be a shared service account (everyone does the same thing, sees the same data), static keys work fine. But if Claude needs to read that user's specific invoices or act on their behalf, you need OAuth.
I went with OAuth because we have per-tenant data. But here's what surprised me: Claude and ChatGPT don't natively support the full OAuth 2.1 dance yet. They support it at the MCP layer through the Protected Resource Metadata specification, but the setup is still more manual than I expected.
The Tool Design Matters More Than You'd Think
Once authentication is settled, I discovered that how you structure your tools heavily influences whether Claude will actually use them correctly.
A generic api_request tool with a method parameter sounds elegant in theory. In practice, Claude gets confused. Should it use POST or PUT? Does the model even know what fields are required? Instead, Anthropic's connector review criteria requires focused tools: list_invoices, get_invoice, create_invoice. Each one has a clear purpose, obvious inputs, and honest descriptions of what it does.
I built separate tools for reads and writes. Read tools are idempotent and don't need confirmation. Write tools return explicit handles (an invoice ID, for example) that subsequent calls can reference. This forces me to validate on every call that the handle belongs to the current user, it's a safety mechanism built into the flow.
Error Handling Is Part of Your API Contract
Here's something I'd overlooked: how you return errors shapes whether Claude can recover from them.
A 404 that says "Invoice not found" is useless. Claude doesn't know what to do next. But a 404 that says "No invoice with ID 123. Call list_invoices to find a valid ID" gives the model actionable guidance. Rate limits need a retry delay in the response. Validation failures need the expected format clearly stated.
I stopped passing through raw upstream responses and stack traces. Instead, I translate them into information Claude can act on. It's extra work, but it's the difference between a tool that sometimes works and one that's genuinely reliable.
My Take
I think the biggest insight here is that MCP isn't a magic tunnel to your API. It's a controlled interface with its own security model. The fact that two separate authentication layers exist isn't a limitation, it's the right design. It means your API keeps full control over permissions, rate limits, and business logic. The MCP server just enforces them consistently.
The part I'm still uncertain about: how well this scales when you need fine-grained, per-user OAuth at real volume. The spec exists, but I haven't seen many production examples. If you're doing this at scale, I'd love to hear how.
Source: This post was inspired by "Shared API key or per-user OAuth: how to put an MCP server over your existing API" by Dev.to. Read the original article