MCP Authentication: OAuth Grants and Bearer API Keys
Pick any authenticated call to a remote MCP server, dump the raw HTTP, and you will find one line doing all the work: Authorization: Bearer <token>. That line is the entire authentication surface. Everything else people describe as "MCP auth" is really a question about where that token came from and how long it stays good.
Two provenances are in common use. A token can be minted by an authorization server after a person clicked Approve in a browser, in which case it expires on a short clock and can be refreshed without bothering anyone again. Or it can be a key you generated yourself in a product dashboard, which lives until you delete it and works fine on a machine that has no browser at all. The server receiving the request cannot always tell the difference from the header shape alone, and mostly does not need to — it resolves the token to an identity and a set of permissions either way.
What follows is organised around the credential rather than around the protocol: how a key is born, where it gets stored, what it looks like in transit, what the server checks before it runs a tool, and how it dies. OAuth's discovery handshake and PKCE exchange get their own treatment, because that machinery is what makes one-click connectors possible and is the part most often half-implemented. Worked examples use ClipSpeedAI's MCP server at https://api.clipspeed.ai/mcp, since it accepts both credential types against one endpoint. Readers who have not met the protocol yet should begin with What Is MCP? Model Context Protocol Explained; the request lifecycle behind these calls is mapped out in How MCP Servers Work: Architecture and Request Flow.
Start at the Header, Then Work Backwards
Here is a complete authenticated MCP request. There is nothing hidden in it and no protocol-specific credential envelope — the transport is HTTP, so the credential rides in the header HTTP already has for the purpose.
POST /mcp HTTP/1.1
Host: api.clipspeed.ai
Authorization: Bearer <token>
Content-Type: application/json
Accept: application/json, text/event-stream
{"jsonrpc":"2.0","id":1,"method":"tools/list"}Substitute either credential into <token> and the request is valid. An OAuth access token is typically a signed JWT the server can verify offline. An API key is typically an opaque random string the server has to look up. ClipSpeedAI's keys are the second kind: the string begins csai_live_ and continues with 48 hexadecimal characters, generated from 24 random bytes — 192 bits of entropy, which is not something anyone guesses.
Two consequences fall out of this immediately, and they explain most of the rest of the page. First, the credential is attached to the connection, not to an individual tool invocation. A model calling check_clips and then publish_to_youtube in the same session sends the identical header both times; any distinction between what those two calls are allowed to do has to be enforced inside the server. Second, because the header is standard, nothing about MCP prevents you from testing it with curl before you touch a client config — which is the single most useful debugging habit on this page.
What Changed When MCP Servers Left stdio
The original shape of an MCP server was a local subprocess. Your client spawned a binary and talked to it over stdin and stdout. There was no authentication because there was no one to authenticate: the process ran as you, on your machine, with your file handles. The operating system was the security boundary, and it was a perfectly good one.
A URL is a different proposition. Once the server answers at https://api.example.com/mcp, anyone on the internet can POST to it, and the server has to answer three questions before executing anything: which account is this, what is that account permitted to do, and was this token actually issued for this resource rather than some other service that happens to share an issuer. The trade-offs between the two deployment shapes — and the hybrid where a local process fronts a remote API — are laid out in Remote MCP vs Local MCP Servers.
Rather than invent an authorization scheme, the specification profiles OAuth 2.1 and the surrounding RFCs. The 2025-06-18 specification revision is explicit that an MCP server acts as an OAuth resource server: it validates tokens, and a separate authorization server issues them. That division is worth taking seriously when you build. A resource server needs a signature check and an audience check. An authorization server needs consent screens, password storage, session management, and a breach plan. Conflating them is how a small tool server acquires responsibilities nobody scoped for it.
The Life of a ClipSpeedAI API Key: Generate, Use, Revoke
Following one key from creation to deletion covers most of what a user needs to know, and it happens entirely over three routes.
POST /auth/api-keys -> create a key (returned in full, once) GET /auth/api-keys -> list keys, metadata only DELETE /auth/api-keys/:id -> revoke a key
Creation is also available in the product UI under Account → API & Integrations → Generate API Key. The response contains the complete secret — csai_live_ plus 48 hex characters — and that is the only time it is ever shown. Afterwards the account stores a display prefix, the first 18 characters, which is exactly csai_live_ plus the first eight hex characters of the random part. Enough to tell two keys apart in a list; not enough to authenticate with.
The listing endpoint is more informative than most, and the fields are worth knowing because they are what makes safe rotation possible later:
id,name,key_prefix— identify a key without exposing itplan,rate_limit— the per-key request ceilingrequests_today,total_requests— usage counterslast_request_at— when this key was last seenis_active,created_at— lifecycle state
Revocation is real and immediate rather than cosmetic: DELETE /auth/api-keys/:id flips is_active to false and stamps a revoked_at timestamp, after which the next request carrying that key is rejected. Nothing about a revoked key can be un-revoked, so the recovery path from a leak is generate-new-then-delete-old, not repair.
Because keys are named and independently counted, the useful convention is one key per machine or per job — laptop, CI runner, the box that runs the nightly batch. A key shared across three environments cannot be revoked without taking down all three, and its last_request_at tells you nothing about which of them is still alive.
The Browser Path: PKCE, Discovery, and the Code Exchange
OAuth exists to keep a long-lived secret out of the user's hands entirely. The user proves who they are to an authorization server they already trust, approves a specific client, and the client walks away with a token that expires. Nothing is copied and pasted.
MCP clients are public clients: a desktop app or CLI shipped to users cannot hold a client secret, because anyone can unpack it. PKCE is therefore required rather than recommended. The client generates a high-entropy code_verifier, sends only its SHA-256 hash as code_challenge when it opens the browser, and presents the original verifier when it redeems the authorization code. Interception of the code alone is useless.
Rather than reading the flow as a ladder diagram, it helps to read it as five legs, each proving one thing:
- Resource metadata fetch. The client asks the MCP server which authorization server governs it. This is what lets a user paste a URL and nothing else.
- Authorization server metadata fetch. The client learns the
/authorizeand/tokenendpoints, the supported grant types, and the PKCE methods available. Some servers additionally support Dynamic Client Registration (RFC 7591), which lets a client obtain aclient_idprogrammatically instead of a human filling in a developer portal form. - Browser redirect to
/authorize. Carries thecode_challengeand aresourceparameter naming the MCP server. That second parameter is what binds the resulting token to one audience. - Code exchange at
/token. The client posts the code plus thecode_verifier. Proof of possession, without a client secret. - The MCP call itself. Access token in the Bearer header, identical in form to the API key case.
The refresh token returned alongside the access token is the piece that makes short lifetimes tolerable. Without silent refresh, a 30-minute access token means a consent prompt every 30 minutes, and users disable the integration.
The 401 That Tells a Client Where Consent Lives
Leg one above raises an obvious question: how does a client that has never seen your server know where to look? The answer is a specific, machine-readable rejection.
An unauthenticated request to a server implementing the discovery pattern returns 401 Unauthorized with a WWW-Authenticate header pointing at a protected resource metadata document, per RFC 9728:
HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer resource_metadata= "https://api.example.com/.well-known/oauth-protected-resource"
The document it points at is short and static:
{
"resource": "https://api.example.com/mcp",
"authorization_servers": ["https://auth.example.com"],
"scopes_supported": ["jobs:read", "jobs:write", "admin"],
"bearer_methods_supported": ["header"]
}Those scope strings are illustrative. Scope names are entirely a server's own choice, and the only authoritative list for any given server is whatever its own metadata document returns — do not copy the ones above into a client and expect them to mean anything.
If you are implementing a server, this handshake repays attention out of proportion to its size. A missing or malformed WWW-Authenticate header is a common reason a GUI client shows a flat "could not connect" instead of an approval prompt: the client was rejected and given no route to fix it, so it has nothing to display. Getting the challenge right is the difference between a one-click connector and a support ticket. How to Build an MCP Server (Practical Guide) is where that wiring gets built out end to end.
Side by Side: Operational Fit, Not a Security Ranking
The comparison below is about where each credential type fits, not which is safer in the abstract. A narrowly scoped key held in a secret manager and rotated quarterly beats a broadly scoped OAuth token with a year-long refresh grant, and vice versa.
| Dimension | OAuth access token | Bearer API key |
|---|---|---|
| What the user does | Approves once in a browser | Generates, copies, pastes |
| Headless environments | Awkward without a pre-provisioned refresh token | Works unchanged |
| Lifetime | Short, refreshed automatically | Until revoked |
| Exposure if leaked | Bounded by expiry and scope | Open-ended until someone notices |
| Revoking one machine | Revoke that grant | Only if that machine had its own key |
| Scope selection | Negotiated per authorization | Fixed when the key is created |
| Audit trail | User plus client plus grant | Key id and its counters |
| Cost to implement server-side | Higher: metadata, AS integration, refresh | Lower: lookup and status check |
| Typical failure | No browser, or a blocked callback port | Key committed to git, or shared team-wide |
Notice that several rows are not properties of the credential at all but of how you provision it. "Revoking one machine" is a solved problem for API keys the moment you stop sharing one key across machines. The columns describe defaults, not ceilings.
Three Places the Secret Ends Up
Storage is where most real incidents originate, and three locations are typical.
- OS keychain or credential store. Where GUI clients put tokens after an OAuth flow. Encrypted at rest, scoped to the OS user, invisible to a filesystem search. The best of the three, and the one you get without doing anything.
- A config file on disk. Readable, editable, easy to reason about — and a plaintext secret in a path that backup tools, cloud sync clients, and dotfile repositories all reach into.
- An environment variable. Standard for CLI and container use. Keeps the value out of files you might commit, though it remains readable by anything that can inspect the process environment, and it turns up in shell history if you export it carelessly.
Client config formats vary, so treat the block below as the general shape rather than a copy-paste target; follow your own client's MCP documentation for its exact key names.
{
"mcpServers": {
"clipspeed": {
"url": "https://api.clipspeed.ai/mcp",
"headers": {
"Authorization": "Bearer <API_KEY>"
}
}
}
}The placeholder is deliberate. Some clients expand ${VAR} inside config JSON and some send it verbatim as a literal string, which produces a puzzling authentication failure with a perfectly valid key sitting in your environment. Do not rely on interpolation unless your client's own documentation states that it performs it. If you must write the literal key into a file, add that file to .gitignore in the same commit.
Local stdio servers skip all of this. No HTTP request means no header, and the specification directs stdio servers to take credentials from the process environment:
{
"mcpServers": {
"local-tool": {
"command": "npx",
"args": ["-y", "some-mcp-server"],
"env": { "SOME_API_KEY": "..." }
}
}
}The trust model there is the operating system again: the server runs with your privileges, so a malicious local server is not a problem authentication can solve. For local installs, provenance matters more than credential hygiene.
One rule cuts across all three locations. Any key that has ever appeared in a chat window, a support ticket, a screenshot, or a pasted log should be treated as burned and replaced, regardless of who saw it.
Wiring Claude Code to ClipSpeedAI with a Bearer Key
A CLI is the case OAuth handles worst and API keys handle best, so this is the concrete version of everything above. Generate a key under Account → API & Integrations → Generate API Key, copy it while it is on screen, then register the server:
claude mcp add --transport http clipspeed https://api.clipspeed.ai/mcp \ --header "Authorization: Bearer <API_KEY>"
That is the whole setup. The transport is streamable HTTP, the endpoint is the same one a GUI client uses, and the tool list you get back is identical. In claude.ai the same server is added as a custom connector and authenticated with OAuth, so no key is ever handled by the user — same server, same tools, different provenance for the token. The step-by-step version of the CLI path lives in ClipSpeedAI MCP for Claude Code: Complete Setup Guide.
Client support is not uniform, and it is worth stating the tiers accurately rather than implying a flat "works everywhere":
- Verified end to end: Claude (claude.ai), Claude Code, Claude Desktop, and Windsurf.
- Compatible, verification in progress: Cursor, Codex, OpenClaw, and Hermes. These speak the same protocol — an HTTP MCP server plus a Bearer credential — but have not been confirmed end to end against this server.
- Rolling out and vendor-gated: ChatGPT, which depends on OpenAI's connector availability and is not verified.
For any client in the second or third group, the instruction is the same in prose: add ClipSpeedAI as an HTTP MCP server pointed at https://api.clipspeed.ai/mcp, with your key in the Authorization header, following that client's own MCP documentation for where those fields go. An npm package, clipspeed-mcp, also exists. Differences in how two clients surface tool calls and approvals are compared in Claude Code vs Cursor for MCP Workflows.
Prove the Credential at the Wire Before Blaming the Client
When a connector misbehaves, the first question is whether the credential is even the problem. curl answers that in one call, with no client config in the way. Streamable HTTP wants an Accept header listing both JSON and SSE; leaving out the SSE type produces content-negotiation errors that are easy to misread as authentication failures.
curl -sS https://api.clipspeed.ai/mcp \
-H "Authorization: Bearer $CLIPSPEED_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": { "name": "curl", "version": "0.0.1" }
}
}'A successful initialize means the credential was accepted. That is authentication settled; authorization is a separate question, so follow it with a listing call:
curl -sS https://api.clipspeed.ai/mcp \
-H "Authorization: Bearer $CLIPSPEED_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'You are looking for the ten tools by name: discover_trending, submit_to_clipspeed, check_clips, creator_pack, list_templates, publish_to_youtube, clip_livestream, check_livestream, stop_livestream, and extend_livestream. A short list is a real signal — it means the token authenticated but resolved to less than you expected.
If curl succeeds and your client still fails, the credential is fine and the fault is in client configuration. If curl fails with 401, the credential was rejected and no amount of client fiddling will help. On a server you are building yourself, add one more test: drop the header entirely and confirm you get a 401 carrying the WWW-Authenticate challenge rather than a 500 or a 200 with an error string in the body. Against someone else's endpoint, a bare 401 is all you can conclude from.
Attaching the Header from TypeScript and Python
Client SDKs expose a hook for injecting headers into the transport. In TypeScript, the streamable HTTP transport accepts a requestInit whose headers are merged into every outbound request:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport }
from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const key = process.env.CLIPSPEED_API_KEY;
if (!key) throw new Error("CLIPSPEED_API_KEY is not set");
const transport = new StreamableHTTPClientTransport(
new URL("https://api.clipspeed.ai/mcp"),
{ requestInit: { headers: { Authorization: `Bearer ${key}` } } }
);
const client = new Client({ name: "my-agent", version: "1.0.0" });
await client.connect(transport);
const { tools } = await client.listTools();
console.log(tools.map(t => t.name));The explicit check for a missing environment variable is not decoration. An undefined key produces the header Bearer undefined, which the server rejects as a bad credential, and you spend twenty minutes suspecting the key you can see in your dashboard.
The wire protocol is plain JSON-RPC over POST, so skipping the SDK is a legitimate way to isolate whether a bug is yours or the library's:
import os, httpx
URL = "https://api.clipspeed.ai/mcp"
HEADERS = {
"Authorization": f"Bearer {os.environ['CLIPSPEED_API_KEY']}",
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
}
def rpc(method, params=None, _id=1):
body = {"jsonrpc": "2.0", "id": _id, "method": method}
if params is not None:
body["params"] = params
r = httpx.post(URL, headers=HEADERS, json=body, timeout=60)
if r.status_code in (401, 403):
raise SystemExit(
f"{r.status_code} {r.headers.get('WWW-Authenticate', '(no challenge)')}"
)
r.raise_for_status()
return r.text
print(rpc("initialize", {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": {"name": "py", "version": "0.1"},
}))Branching on 401 and 403 separately, and printing the challenge header instead of a stack trace, turns most credential bugs into a quick diagnosis rather than a bisect.
Verifying Both Credential Types on the Server
A server that accepts keys and OAuth tokens needs one entry point and two verification strategies behind it. The dispatch can key off a prefix your own issuer controls; what matters is that neither branch skips a check.
const KEY_PREFIX = "key_live_"; // whatever your issuer emits
async function authenticate(req) {
const raw = req.get("authorization") || "";
const m = /^Bearer\s+(.+)$/i.exec(raw);
if (!m) return null;
const token = m[1];
// 1. API key: look up a hash, never the raw value
if (token.startsWith(KEY_PREFIX)) {
const row = await db.apiKeys.findByHash(sha256(token));
if (!row || !row.is_active || row.revoked_at) return null;
return { userId: row.user_id, scopes: row.scopes, via: "api_key" };
}
// 2. OAuth JWT: signature, expiry, issuer, AND audience
const claims = await verifyJwt(token, {
issuer: "https://auth.example.com",
audience: "https://api.example.com/mcp", // do not skip this
});
return {
userId: claims.sub,
scopes: String(claims.scope || "").split(" "),
via: "oauth",
};
}Four details carry weight. Store a hash of each key rather than the key itself, so a database leak yields nothing usable. Compare in constant time. Check the revocation state on every request rather than caching an authorization decision for the life of a session. And validate the audience claim without exception — a token minted for a different resource can have a flawless signature and still be an attack.
Return the right status when it fails. 401 means the credential was missing, malformed, expired, or revoked, and a client is expected to try re-authenticating. 403 means the credential was accepted but the scope does not cover this operation, and re-authenticating changes nothing. Clients branch on that distinction; collapsing both into 401 produces re-auth loops that never converge.
Scoping by Consequence: check_clips and publish_to_youtube Are Not Peers
Authentication asks who is calling. Authorization asks whether they may do this particular thing. MCP sharpens the second question because the caller is a model selecting tools on its own, sometimes on the strength of text it read inside a document.
The instinct is to scope by object — everything touching a project gets one permission. Consequence is the better axis. Take ClipSpeedAI's tool surface as a design exercise (the product does not publish a scope list, so this is illustration, not documentation). check_clips reads finished clips for a project id and returns titles, viral scores, and download links. creator_pack returns suggested titles, hooks, and posting times. list_templates returns the six caption styles — karaoke, hormozi, beasty, fire, youshaei, cinematic — and changes nothing at all. Those three are pure reads.
Then there is submit_to_clipspeed, which starts work that consumes compute, and clip_livestream, which opens an ongoing live session that keeps consuming until stop_livestream ends it. Different tier: they spend something. And publish_to_youtube sits alone, because it emits content to a third-party platform under the user's identity. That it defaults to a private privacy status is a sensible safety default, but a default is not a permission boundary — an argument can override it, and arguments are exactly what an attacker influences.
Which brings up the confused deputy. Your server holds credentials for downstream services; an attacker who can shape tool arguments tries to make your server spend them on their behalf. The defences are unglamorous: bind every token to a single audience, reject tokens minted for another resource, never forward a caller's token downstream verbatim, and require explicit consent for each newly registered client instead of quietly attaching it to an existing grant. Threat modelling past the credential itself is the subject of MCP Security: Scopes, Keys and Safe Tool Design, and the argument that a tool's boundaries should be visible in its schema rather than buried in a handler is made in MCP Tool Design: Writing Tools an Agent Can Actually Use.
Rotation, Revocation, and the Keys You Forgot About
Short access-token lifetimes only work if refresh is silent. The conventional shape is an access token measured in minutes, a refresh token measured in days, and rotation on each refresh so that a stolen refresh token becomes visible when the legitimate client's next attempt fails.
access_token short sent on every MCP request
refresh_token longer used once, then replaced
401 (expired) -> POST /token grant_type=refresh_token
-> new access + new refresh -> retry once
-> still 401? drop the grant, re-prompt the userAPI keys have no automatic equivalent, so rotation is manual and the only thing that matters is that it can be done without an outage. The safe sequence is: create the replacement, deploy it everywhere it is needed, confirm requests are arriving under the new key id, then revoke the old one. That sequence has a hard prerequisite — the account must support more than one live key at a time, and it must record enough per-key usage that you can tell whether the old one is still carrying traffic.
ClipSpeedAI's listing endpoint supplies exactly that: requests_today, total_requests, and last_request_at per key, alongside is_active. Before deleting a key, check whether it was used in the last day. Afterwards, watch for a counter that unexpectedly stops moving somewhere you forgot to update. Without usage data, revoking an old credential is a guess about what might break, and teams facing that guess frequently defer rotation indefinitely.
Log the key id on every request; never log the key. Because only a display prefix is retained anyway, the id and prefix are all you have to correlate with — which is fine, and is the correct amount of information to keep in a log file.
Claims About MCP Auth That Don't Hold Up
- "OAuth is inherently safer than an API key." It reduces credential handling and bounds lifetime, which are real advantages. It does not make an over-scoped, never-expiring grant safe. Compare two specific configurations, not two labels.
- "MCP has its own auth protocol." It profiles OAuth 2.1 and related RFCs for HTTP transports, and defers to the process environment for stdio. There is no MCP-specific credential format.
- "The MCP server issues the tokens." In the current model it validates them. A separate authorization server issues them. Servers that blur this end up storing passwords they never intended to store.
- "
?api_key=in the query string is the same thing." It is not. Query strings persist in access logs, proxy logs, referrer headers, and browser history. Keep the credential in the header. - "A valid token means the call will work." Authentication and authorization are separate gates. A
403, or atools/listshorter than you expected, is the normal shape of a token that authenticated but is scoped narrowly. - "One-click OAuth means less server work." It means more. The click is the payoff for correct metadata documents, a well-formed
401challenge, and PKCE handled properly. - "Each tool call carries its own credential." The header is attached per HTTP request at the transport layer, so it covers the whole session. Restricting one tool is a scope check inside that tool's handler.
- "Revoking a key is a soft delete that might not take effect." On this server it flips
is_activeand stampsrevoked_at, and the next request using that key is rejected.
Triage Order for a Connection That Won't Authenticate
Work down this list rather than across it. Many connection failures resolve in the first few steps, and each step rules out a whole category.
- Reproduce with
curl. Success there plus failure in the client localises the bug to client configuration. - Check the
Acceptheader. Streamable HTTP expectsapplication/jsonandtext/event-streamtogether. A missing SSE type produces content-negotiation errors that read like rejection. - Read the exact status.
401is a rejected credential;403is an accepted credential without sufficient scope;404is usually a path problem — a dropped/mcpor a stray trailing slash. They are three different bugs. - Read
WWW-Authenticate. A well-built server explains itself there withinvalid_token,expired_token, orinsufficient_scope. - Confirm the header actually shipped. Shell quoting mangles long tokens, and an unexpanded
${VAR}in a config file travels as a literal string. - Check expiry and refresh. A repeating expired-token loop usually means the refresh token itself was rejected and the grant needs re-establishing.
- Check audience and issuer. A token for a different resource can pass signature validation and still be correctly refused.
- Confirm the key is still live. A revoked or rotated key fails identically to a typo, so verify
is_activeandlast_request_atbefore assuming a transport bug.
The decision underneath all of this is simple enough to state in two lines. If a person with a browser is present at connect time, use OAuth and avoid distributing a secret at all. If the caller is a CLI, a container, or a scheduled job, use a scoped API key, store it outside your repository, give each machine its own, and rotate on a schedule you will actually keep. Cursor users should follow ClipSpeedAI MCP for Cursor: Complete Setup Guide for that client's current status and steps. And if you are still deciding whether a capability belongs behind MCP at all rather than a conventional endpoint with its own auth story, that question is argued in MCP vs REST API: When to Use Each.
Frequently asked questions
- What does a ClipSpeedAI API key look like, and can I retrieve it later?
- A key is the literal prefix csai_live_ followed by 48 hexadecimal characters, generated from 24 random bytes. The full value is shown once, at creation. Afterwards the account keeps only an 18-character display prefix — csai_live_ plus the first eight hex characters — so the listing endpoint can identify keys without exposing them. If you lose a key, generate a new one and revoke the old; there is no recovery path.
- Do I need OAuth to use a remote MCP server?
- No. OAuth is one way to obtain a token and a Bearer API key is another; both arrive at the server as an Authorization: Bearer header on an ordinary HTTP request. For ClipSpeedAI, GUI clients such as claude.ai use OAuth, and CLI clients connect with a key in a single command — no browser involved.
- How do I revoke a ClipSpeedAI API key?
- Send DELETE /auth/api-keys/:id. That sets is_active to false and stamps a revoked_at timestamp, after which requests carrying the key are rejected. Revocation is permanent, so the recovery sequence for a leaked key is to generate a replacement, deploy it, and then delete the compromised one.
- Is a Bearer API key less secure than an OAuth token?
- Not intrinsically. An OAuth access token expires quickly and is bound to one client, which limits the damage window after a leak. A key does not expire, so the risk depends on handling: held in a secret manager, issued per machine, and rotated, it can be safer than a broadly scoped OAuth grant that refreshes forever. Compare the configurations rather than the labels.
- Where should the key live on disk?
- An OS keychain or secret manager if your client supports one, otherwise an environment variable that your client reads. Avoid literal keys in files that get committed, synced, or backed up, and never put a key in a URL query string, where it lands in access logs, proxy logs, and browser history.
- What is the difference between a 401 and a 403 from an MCP server?
- 401 means the credential was missing, malformed, expired, or revoked, and the client should try authenticating again. 403 means the credential was accepted but lacks the scope for that specific operation, so re-authenticating will not help. A server implementing discovery also attaches a WWW-Authenticate header to the 401 pointing at its protected resource metadata.
- Can I use OAuth from CI or a Docker container?
- Rarely without pain. The authorization code flow needs a browser and a reachable redirect URI, and a CI runner has neither. The usual workaround, pasting a long-lived refresh token into a CI secret, reintroduces the long-lived-credential risk while adding steps. A scoped API key is the straightforward choice for unattended environments.
- How do I rotate a key without downtime?
- Create the replacement first, deploy it everywhere, confirm traffic is arriving under the new key id, then revoke the old one. ClipSpeedAI's listing endpoint returns requests_today, total_requests, and last_request_at per key, which is what lets you check whether the old credential is still in use before you delete it.
- Which MCP clients are verified against ClipSpeedAI?
- Claude (claude.ai), Claude Code, Claude Desktop, and Windsurf are verified end to end. Cursor, Codex, OpenClaw, and Hermes speak the same protocol and are compatible, with verification still in progress. ChatGPT support depends on OpenAI's connector rollout and is not verified. For any unverified client, add the server as an HTTP MCP endpoint with your key in the Authorization header, following that client's own documentation.
- Is the credential attached per tool call or per session?
- Per HTTP request, at the transport layer, which in practice means it covers the whole session and every tool the server exposes. A model calling check_clips and then publish_to_youtube sends the same header both times, so any per-tool restriction has to be a scope check inside the server's handler.