MCP client guard
Screen dynamic client registrations and the accounts those clients bind to.
Registration endpoint
POST https://nolgard.com/v1/mcp/register
{"secret":"sk_…","clientId":"c_7f21","clientName":"Claude","redirectUris":["https://claude.ai/api/mcp/auth_callback"],"ip":"198.51.100.9","userAgent":"…"}
{"decision":"allow","reasons":[]}Call this inside your Dynamic Client Registration endpoint, before you persist the client. A remote MCP server has to accept open registration under the MCP authorization spec (2026-07-28), so the registration itself is the only place to judge the client.
Registration request fields
| Field | Type | Notes |
|---|---|---|
| secret | string | Required. Site secret. |
| clientId | string | Required. The client id you are about to issue, up to 256 characters. |
| clientName | string | Optional. The name the client claims, up to 200 characters. |
| redirectUris | string[] | Required. 1 to 10 redirect URIs. |
| ip | string | Optional. IP address of the registering caller. |
| userAgent | string | Optional. User agent of the registering caller. |
Response: decision is allow, challenge or block, with reasons.
Registration reasons and defaults
| Reason | Default | Decision |
|---|---|---|
| mcp_register_burst | More than 10 registrations an hour from one network prefix, IPv4 /24 or IPv6 /48 (networkRegistrations 10), or more than 500 an hour for the site (siteRegistrations 500) | challenge |
| mcp_redirect_insecure | A redirect that is neither https nor loopback http per RFC 8252, an IP-literal host that is not loopback, or userinfo or a fragment in the URI. Private-use schemes such as cursor:// are allowed | block |
| mcp_name_spoof | The client name claims Claude or ChatGPT while sending the browser to another web host. The documented callbacks are https://claude.ai/api/mcp/auth_callback and https://chatgpt.com/connector/oauth/… Native apps on loopback or custom schemes are never spoofs | block |
| mcp_redirect_spread | More than 3 distinct redirect hosts (redirectHosts 3) | challenge |
Bind endpoint
POST https://nolgard.com/v1/mcp/bind
{"secret":"sk_…","clientId":"c_7f21","accountId":"user_42"}
{"decision":"allow","reasons":[]}Call this after the user finishes OAuth consent, when the client becomes linked to an account. decision is allow, limit or block.
| Reason | Default | Decision |
|---|---|---|
| mcp_client_accounts | One client bound to more than 5 accounts in a day (clientAccounts 5): a resold or shared client | limit |
| mcp_account_clients | One account binding more than 10 new clients in a day (accountClients 10): a client farm | limit |
| mcp_client_blocked | The client was blocked at registration | block |
| mcp_client_unregistered | The client never passed registration. Telemetry only | no change |
Errors
| Status | Error |
|---|---|
| 400 | invalid-input |
| 401 | invalid-secret |
| 403 | read-only (demo site) |
| 429 | rate-limited |
Webhook
mcp.registration
{"clientIdHash":"a19f…","clientName":"Claude","decision":"block","reasons":["mcp_name_spoof"]}Fires on a blocked registration, once per network per day. See Webhooks and clawback.
SDK
const registration = await nolgard.mcp.register({
clientId,
clientName: metadata.client_name,
redirectUris: metadata.redirect_uris,
ip: clientIp
});
if (registration.decision === 'block') return reject();
const bind = await nolgard.mcp.bind({ clientId, accountId: user.id });See SDKs.
Per-client usage abuse
Registration and binding cover who the client is. For what it then does, pass the OAuth client_id as apiKeyId to /v1/usage and /v1/spend; the API key guard then tracks that client like any other key.
Dashboard and data retention
Settings → MCP clients shows registrations, bindings and the thresholds behind them. All defaults are owner-editable: networkRegistrations 10, siteRegistrations 500, redirectHosts 3, clientAccounts 5, accountClients 10.
Client ids are stored only as salted hashes. Client name and redirect hostnames are kept 30 days.
Limits
- Defaults are logical starting points rather than measured guarantees. Tune them for your client population.
- Shared office networks and CI runners register from one prefix and can hit the burst threshold.
- A legitimate client with several deployment hosts can trip redirect spread.
- Name checks cover Claude and ChatGPT callbacks only. Any other brand name is not verified.
- Bind counts reflect one day. A shared client used by a small team looks like a resold client.
- Infrastructure rate limits still matter; this guard judges clients rather than request volume.