LEARN · PHASE 07 · LESSON 54

OAuth, Headers, and MCP Tool Search

You can authenticate a remote server without pasting a token into a tracked

Type
Learn
Prerequisites
Lessons 50–53
Time
~35 minutes

Learning Objectives

  • Choose between a static header token and OAuth for a remote server, and use /mcp to authenticate
  • Set headers and OAuth client details in a config without committing a secret
  • Explain what tool search defers and what it costs to turn off
  • Set alwaysLoad deliberately, and say when a tool earns it

The Problem

Two failures, both common, both felt as "the server is broken."

The first: a remote server needs a token. You add it with --header "Authorization: Bearer sk-...", it works, and six weeks later you find the token in a commit, in a shell history file, and in a screenshot of /mcp somebody posted in a channel.

The second: you connect a big server — a whole SaaS surface, fifty tools — and every session starts slower, /compact happens sooner, and you don't know which of your other tools got squeezed out. Nothing errored. The window just filled up with descriptions you'd never have read.

The first is a secrets problem and the second is a context problem. Same phase, different doors.


The Concept

Two ways to authenticate a remote server

Static headerOAuth
Good forYour own token, a service account key, a local gatewayAnything where a human logs in
Set by--header "Authorization: Bearer …" or the headers field/mcp, or --client-id / --callback-port at add time
RevocableYou rotate itThe user re-auths; tokens refresh
In a committed .mcp.jsonNever. Use ${API_KEY}Yes — the config holds clientId, not a secret
claude mcp add --transport http secure-api https://api.example.com/mcp \
  --header "Authorization: Bearer ${API_KEY}"

The ${API_KEY} form keeps the committed file free of the value while still working for local, user, and project scopes. Expansion happens in headers.

OAuth at add time:

claude mcp add --transport http \
  --client-id your-client-id --callback-port 8080 \
  my-server https://mcp.example.com/mcp

Or in JSON, with the secret still supplied on the command line rather than in the file:

claude mcp add-json my-server \
  '{"type":"http","url":"https://mcp.example.com/mcp","oauth":{"clientId":"your-client-id","callbackPort":8080}}' \
  --client-secret

clientSecret is never written into the config. That asymmetry is the design: clientId and callbackPort are public facts, the secret is not.

In a session, /mcp shows each server's auth state and starts the flow for you. Companion commands: claude mcp login <name>, claude mcp logout <name>.

One thing to know about ws: it has no OAuth story. A WebSocket server needs a static token in headers. That's a real reason to prefer http for anything remote, beyond the transport advice in Lesson 50.

Tool search: the context budget

Every tool definition sits in your window — name, description, schema. Add fifty tools and you have spent a chunk of the budget before the conversation starts (Lesson 32).

Tool search, on by default, changes the order of operations:

  • At startup, only tool names and the server's instructions load.
  • Full descriptions and schemas load on demand, when Claude decides a tool might be relevant and searches for it.

So adding a server with fifty tools costs almost nothing at startup. The budget goes to tools that were actually used.

ENABLE_TOOL_SEARCH=auto:5 claude   # threshold mode: load upfront while under 5% of the window
ENABLE_TOOL_SEARCH=false claude    # everything upfront, always

auto:N is the honest middle: small tool sets load eagerly (no search step for the tools you always want), large ones defer. Unset is threshold-free — defer everything. Turn it off when a server's tools must be visible for correctness, not convenience, or when you're debugging discovery and want to see the full list.

What description length means now

With tool search on, the description is what Claude searches. Claude Code cuts descriptions past a limit and the model sees only what came before the cut — so the words it would search for go first. Limits as documented today: shorter for descriptions that load upfront, longer for ones found through search. The exact numbers are version-gated and have moved more than once; read the doc page rather than quoting them.

alwaysLoad — the deliberate exception

{ "mcpServers": { "core-tools": { "type": "http", "url": "…", "alwaysLoad": true } } }

The server's tools load into context regardless of the setting. Per-tool, "anthropic/alwaysLoad": true in a _meta block does it for one tool.

This is a context purchase, and you pay for it on every turn of every session. Earn it: a "check the deploy status" tool the agent reaches for often qualifies. A hundred-tool server marked alwaysLoad is the mistake this flag exists to make avoidable.

Note the second effect: alwaysLoad: true makes startup wait for the server's tools, because the first prompt needs them present.

Output size, the other budget

A result larger than the threshold doesn't get truncated — it gets persisted to a file and replaced by a file reference in the conversation. You raise it two ways:

export MAX_MCP_OUTPUT_TOKENS=50000   # whole session, set before you start claude

or per tool, which is better because it's scoped to the tool that needs it:

{
  "name": "get_schema",
  "description": "Returns the full database schema",
  "_meta": { "anthropic/maxResultSizeChars": 200000 }
}

The annotation applies independently of the env var, and there's a hard ceiling — read the docs for the current number. Images in tool results go inline to the model and get saved to a file in the session's tool-results directory, so Claude can work on them at full resolution.

The reframe: a remote server's config should never hold a secret — the

secret belongs in the environment and the auth flow, while the file holds

only ${API_KEY}, clientId, and a callback port — and a hundred tools cost

you nothing at startup because of when they load, not how many there are.


Build It

1. Authenticate against a real remote server

claude mcp add --transport http context7 -- https://mcp.context7.com/mcp
claude mcp get context7

Start Claude Code, run /mcp, pick context7, and complete the auth flow it offers. Note that the config entry has no secret in it.

claude mcp get context7

Compare to before the flow.

✅ You pass when the entry names a URL and a type, and contains no credential you would not paste into a public issue.

2. Do the same for a server you already trust

/mcp also shows logout. Log out, then log back in. Write down where the token lives — your OS keychain or the config — and confirm it's not in ~/.claude.json in plain text.

✅ You pass when you can say where the token is stored and can show that ~/.claude.json contains no readable copy of it.

3. Put a static token in a project config without committing it

{
  "mcpServers": {
    "api-server": {
      "type": "http",
      "url": "${API_BASE_URL:-https://api.example.com}/mcp",
      "headers": { "Authorization": "Bearer ${API_KEY}" }
    }
  }
}

Run once without API_KEY set and once with it. Read the error both times.

✅ You pass when the file on disk contains no secret and the two runs behaved differently — the difference is proof the expansion happened at load time.

4. See what a big tool surface actually costs

With tool search on (default), connect any server with a lot of tools. Then:

What tools do you have available right now?

The deferred ones are found by search, not listed. Then compare:

ENABLE_TOOL_SEARCH=false claude

Same question. Now you get everything.

✅ You pass when you can state, in terms of your own two runs, what "deferred" means for what Claude can tell you about its own toolset.

5. Find the deferred tool

With tool search off and two servers connected, ask for something that lives on the second one. Then turn tool search on and ask again, using words that appear near the start of that tool's description.

✅ You pass when the second phrasing worked and you can say which part of the description made the difference.

6. Buy one tool upfront

Take the tool you reach for most — deploy status, or your test runner. Mark it _meta["anthropic/alwaysLoad"]: true on the server side, or add alwaysLoad: true to the server entry from the client side.

✅ You pass when you can say what you just bought (that tool, always visible) and what you declined to buy (the rest of the server).


Use It

Command / fieldWhen you reach for it
${API_KEY} in headersAny static token, especially in a committed .mcp.json
/mcpAuthenticate, disconnect, inspect a server's tools
claude mcp login <name>Re-authenticate outside a session
--client-id / --callback-portPreconfiguring OAuth at add time
oauth.clientId in JSONPublic OAuth facts in config; secret stays on the CLI
ENABLE_TOOL_SEARCH=auto:NA middle ground when some tools must always be visible
ENABLE_TOOL_SEARCH=falseDebugging discovery, or when correctness demands the full list
alwaysLoad: trueOne or two tools you reach for every session
anthropic/maxResultSizeCharsOne tool that legitimately returns something huge
MAX_MCP_OUTPUT_TOKENSRaising the ceiling session-wide — set before launching claude
claude mcp logout <name>Revoking access without removing the server

Exercises

  1. Take every MCP server on your machine. For each, write down: how it authenticates, where the credential lives, and whether that credential is in any tracked file. ✅ You pass when the report has one line per server and, for anything remote, a location for the token that isn't a repo file.
  1. Turn tool search off for a session with three or more servers and run the same task twice — once with it off, once on. Count the difference in how many turns it took and whether the answer changed. ✅ You pass when you have both runs and can say what tool search cost you rather than what it saved you.
  1. Add a tool that returns 200,000 characters. Measure what Claude Code does with it at the default threshold, then again with _meta["anthropic/maxResultSizeChars"] set. ✅ You pass when you can describe what the model saw in each case — inline text versus a file reference — and name the threshold you compared against.
  1. Decide which one of your servers, if any, earns alwaysLoad: true. Write the argument, then estimate what it costs you per turn. ✅ You pass when the answer names the specific tool, not the server, and your estimate is in tokens or tools-per-turn rather than "a bit".

Key Terms

TermWhat people sayWhat it actually means
OAuth"the login"A flow where a human authorises once and tokens refresh. Config holds clientId, never the secret
--header"the token"A static credential sent every request. Use ${VAR} so it isn't in a tracked file
headersHelper"dynamic headers"A shell command a config may run to produce a header. Read it before approving
tool search"lazy loading"Deferring tool descriptions until a query suggests one. On by default; costs you nothing at startup
deferred vs upfront"loaded"Deferred = found by searching descriptions. Upfront = always in the window, costs every turn
ENABLE_TOOL_SEARCH—unset defers; auto:N loads eagerly below N% of the window; false loads everything
alwaysLoad"pin it"Exempt a server (or one tool) from deferral. A context purchase, paid every turn
MAX_MCP_OUTPUT_TOKENS"the output limit"Session-wide ceiling. Results past it are written to a file, not truncated
maxResultSizeChars"raise the limit"Per-tool ceiling via _meta. Scoped, and independent of the env var
requiresUserInteraction"always ask"Per-tool annotation: a permission prompt on every call, no remember-this option

What this lesson skipped

Prompt injection. Lesson 53 — the trust model this config sits inside. Reading this lesson without that one is how you end up optimising the wrong thing.

Enterprise identity. SSO, SAML, SCIM provisioning, and managed MCP policy (managedMcpServers, allowedMcpServers, deniedMcpServers). Real, and behind a gateway. Read Managed MCP configuration when your organisation controls the server set.

Tokens in detail. Refresh flows, scopes, expiry. You use them; you don't implement the server side here.

The exact description-length numbers. Version-gated, and they've changed more than once. Look them up in Connect Claude Code to tools via MCP rather than trusting a figure quoted here.

Which transports support which auth. Only ws's lack of OAuth was mentioned, because it's the one that changes your transport choice. The full matrix moves; check the doc.

Deploying your own authenticated server. You connect to authenticated servers in this lesson. Making one — token storage, refresh, scope handling — is real work and outside this phase.

Check yourself

4 questions. Answer before you reveal — recognising the right option is not the same as being able to produce it.

  1. You commit a .mcp.json so your team gets the same server. It needs a bearer token. Which entry keeps the secret out of git history?

    1. "headers": {"Authorization": "Bearer ${API_KEY}"} — the file holds the variable name, and expansion resolves it per machine at load time
    2. "headers": {"Authorization": "Bearer sk-live-abc123"} — remove it from the file in a later commit before the repo is public
    3. "env": {"API_KEY": "sk-live-abc123"} on the server entry, since env values are not written to .mcp.json
    4. Put the token in headersHelper, which runs as a shell command and so is never stored in the config
    Show answer

    A. "headers": {"Authorization": "Bearer ${API_KEY}"} — the file holds the variable name, and expansion resolves it per machine at load time

    Expansion runs in url, headers, command, args, and env, so ${API_KEY} in a committed file resolves from each machine's environment and the file itself holds no credential. Option B is the failure this prevents — once a secret is in history, deleting it later does not remove it. Option C is factually wrong; env values in .mcp.json are written to the file, which is exactly why expansion exists. Option D is a real mechanism but a shell command executes on load — trading a readable secret for executable config.

  2. A server exposes 60 tools and every session feels slower to start and /compact happens sooner. What does tool search, on by default, actually change here?

    1. It caps each server at a smaller number of tools, dropping the rest permanently
    2. It loads only tool names and server instructions at startup, deferring full descriptions until a query suggests a tool is relevant
    3. It caches the tool definitions on disk so later sessions don't pay for them
    4. It disables tools from servers you haven't used in the last few sessions
    Show answer

    B. It loads only tool names and server instructions at startup, deferring full descriptions until a query suggests a tool is relevant

    Tool search changes when descriptions load, not which tools exist — the full list is always reachable, just found by searching descriptions when something looks relevant, so startup cost stays small as servers multiply. Option A is a cap that doesn't exist. Option C describes a caching layer that isn't the mechanism, and it wouldn't help the first session anyway. Option D is invented; unused servers aren't retired.

  3. You mark a whole 60-tool server with alwaysLoad true because you use three of its tools most days. What did you actually buy?

    1. Faster tool discovery, since everything is visible without a search step
    2. All 60 tool definitions in context on every turn of every session, plus a startup that now waits for that server to connect
    3. Correctness — deferred tools can be missed, while always-loaded ones cannot
    4. Nothing until the tools are first used, after which they stay pinned
    Show answer

    B. All 60 tool definitions in context on every turn of every session, plus a startup that now waits for that server to connect

    alwaysLoad exempts a server from deferral, which means every definition is resident in the window every turn — a standing context cost — and startup waits for the server so the first prompt has its tools. That is the right trade only for one or two tools you reach for constantly; pinning 60 to use 3 spends the budget Lesson 32 told you to protect. Option C is the tempting justification and it is wrong: deferral changes discovery, not availability.

  4. One MCP tool returns a 200,000-character schema dump. At the default threshold, what does the model actually receive?

    1. The first 50,000 characters, with the rest dropped
    2. A file reference to the persisted full result, not the 200,000 characters inline
    3. The full result inline, with older parts dropped from the window by compaction later
    4. An error, because a tool result cannot exceed the limit
    Show answer

    B. A file reference to the persisted full result, not the 200,000 characters inline

    Oversized results aren't truncated — they're persisted and replaced in the conversation by a reference, so the model reads a pointer and can work from the file. That distinction matters: a truncated schema is a wrong schema, whereas a file reference is a redirect the agent can follow. Per-tool _meta anthropic/maxResultSizeChars raises the threshold for that one tool, scoped rather than session-wide.