Your MCP server's registry URL shouldn't need a key
Our MCP registry listing advertised a URL with an API-key placeholder, so every install needed an account before the client could connect. Hundreds of clients a day hit it and left. The fix was one line, because the gateway already spoke OAuth.
Kognite team7 min read
On 31 August we listed Kognite in the official MCP registry as dev.kognite/memory. Directories that ingest the registry picked it up, and clients started arriving. For two weeks, the clients it sent us failed at the first request. The cause was the one line that was supposed to be our onboarding: the URL.
What we published
The registry entry described a remote server whose URL carried the API key in its path, declared as a required, secret variable:
{
"name": "dev.kognite/memory",
"version": "1.0.0",
"remotes": [
{
"type": "streamable-http",
"url": "https://mcp.kognite.dev/t/{api_key}",
"variables": {
"api_key": {
"description": "Your Kognite API key (create one free at app.kognite.dev/api-keys)",
"isRequired": true,
"isSecret": true
}
}
}
]
}That looks reasonable from the inside: the key-in-path URL is how many of our users connect, and the variable tells a client to ask for it. From the outside it is a closed loop. To connect you need a key. To get a key you need an account. And the account is the thing the connection was supposed to lead you to. Every install from the registry required signing up somewhere else before the client could even say hello.
What the logs showed
When we finally went looking, the gateway was receiving POST requests to the literal, unsubstituted path /t/{api_key} — the placeholder itself, braces and all. Around 330 a day, from dozens of distinct addresses. A couple were obviously automated pollers; most were a single attempt that got a 401 and never came back.
We can’t tell how many of those were people and how many were automation. But a URL containing an unfilled {api_key} only exists because something read our listing and tried to use it, and a single attempt that never returns is exactly what someone trying a server and giving up looks like.
The part that stung: we already spoke OAuth
Since 2 September the gateway has implemented the MCP authorization flow — OAuth 2.1 with PKCE and dynamic client registration. A client that connects without a credential is supposed to get a challenge, discover the authorization server, register itself, open a browser tab for the user to sign in, and come back with a short-lived token. Ours did all of that:
- An unauthenticated request to
/mcpanswers401with aWWW-Authenticate: Bearerheader carryingresource_metadata. /.well-known/oauth-protected-resource(and its path-suffixed form/.well-known/oauth-protected-resource/mcp) names the authorization server.- The authorization server’s metadata publishes a
registration_endpoint, PKCE withS256, and the scopesmemory:readandmemory:write. /oauth/authorizeredirects to the sign-in page, and the first sign-in creates the account.
Nothing advertised it. The registry — the one channel with real inbound volume — still pointed at the URL that needed a key.
The fix was one line
We changed remotes[].url to the sign-in endpoint, removed variables, bumped the version and republished. This is the manifest in the repository and in the registry today:
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "dev.kognite/memory",
"title": "Kognite",
"description": "Hosted agent memory: store, search, and recall facts across sessions from any MCP client.",
"version": "1.1.0",
"websiteUrl": "https://kognite.dev",
"remotes": [
{
"type": "streamable-http",
"url": "https://mcp.kognite.dev/mcp"
}
]
}Two things made us comfortable doing it. The registry’s schema does not require variables — a remote URL with no placeholder is valid. And we weren’t first: in a sample of 30 comparable memory servers, 12 already publish a plain URL with OAuth sign-in.
Publishing went through the registry’s mcp-publisher CLI, which proves you own the dev.kognite namespace by fetching a public key from https://kognite.dev/.well-known/mcp-registry-auth. One practical note: that file is served by our marketing site, so a republish attempted while that site is mid-deploy fails the domain check. Deploy first, publish after.
We made the same inversion everywhere a reader could land next — the quickstart, the client setup page and the memory-for guides now lead with https://mcp.kognite.dev/mcp instead of “create a key first”.
Keys still work
Not every client can do a browser handshake, so nothing was removed. The key URL https://mcp.kognite.dev/t/<key> and an Authorization: Bearer kgn_… header work exactly as before, and are documented for those clients. And a client that still sends the literal placeholder gets a 401 whose error description says the URL still contains a placeholder, rather than a bare refusal.
The lesson: the URL you publish is your onboarding
We tested the gateway thoroughly — with our own accounts, our own keys, our own clients already signed in. Every test started from a state no stranger is ever in. The only test that would have caught this is the boring one: a fresh client, no account, the exact string from the listing, pasted in.
If you run a remote MCP server, here is the check we now do before publishing a URL anywhere.
1. Does an anonymous request get a usable challenge?
Send an initialize with no credential. You want a 401 and a WWW-Authenticate header with resource_metadata pointing at your protected-resource document. A 401 without that header is a dead end for a client.
$ curl -si -X POST https://mcp.kognite.dev/mcp \
-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"}}}' \
| grep -i '^HTTP\|www-authenticate'
HTTP/1.1 401 Unauthorized
www-authenticate: Bearer realm="kognite", resource_metadata="https://mcp.kognite.dev/.well-known/oauth-protected-resource", scope="memory:read memory:write"2. Does the protected-resource document name an authorization server?
$ curl -s https://mcp.kognite.dev/.well-known/oauth-protected-resource
{
"resource": "https://mcp.kognite.dev",
"authorization_servers": ["https://app.kognite.dev"],
"scopes_supported": ["memory:read", "memory:write"],
"bearer_methods_supported": ["header"],
...
}3. Can a client register itself and use PKCE?
Fetch /.well-known/oauth-authorization-server on the issuer from step 2. Look for a registration_endpoint (a client that has never seen your server needs dynamic registration) and S256 in code_challenge_methods_supported.
$ curl -s https://app.kognite.dev/.well-known/oauth-authorization-server
{
"issuer": "https://app.kognite.dev",
"authorization_endpoint": "https://app.kognite.dev/oauth/authorize",
"token_endpoint": "https://app.kognite.dev/api/oauth/token",
"registration_endpoint": "https://app.kognite.dev/api/oauth/register",
"code_challenge_methods_supported": ["S256"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"scopes_supported": ["memory:read", "memory:write"],
...
}4. Does authorize land somewhere a new user can act?
The authorize endpoint should send an unauthenticated browser to a page where someone without an account can create one — not to an error, and not to a login form that assumes they already have one.
$ curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' \
'https://app.kognite.dev/oauth/authorize?response_type=code&client_id=test'
307 https://app.kognite.dev/login?callbackUrl=%2Foauth%2Fauthorize%3F...5. Then do it for real, as a stranger
- Use a client profile with no saved credentials for your service, and a browser session that isn’t signed in.
- Paste the URL exactly as your registry entry, README or directory listing shows it — copy it from there, not from your own config.
- Count the steps until the first tool call succeeds. Every one that happens outside the client is a place people leave.
- Read your gateway logs for requests that contain the literal text of a placeholder. If you see
{api_key}in a path, someone installed your listing and failed.
Whether the new URL converts better than the old one is too early to say, and we won’t guess. What we can say is that the old one could not convert anyone who didn’t already have an account — and that is the definition of a listing doing nothing.