MCP server

MCP documentation

The complete reference for the onymu server: all 11 tools an assistant can call, what each takes, and what comes back. The interface below is fixed. The server answers discovery today and the calls land next.

Endpoint
/api/v1/mcp
Transport
streamable-http
Auth
OAuth 2.1 or a key
Tools
11

Overview

The server exposes the same machinery the site runs on over the Model Context Protocol: check a name against 1,400 extensions in milliseconds, generate candidates from a word, and read and write the same favorites, shortlist and history the browser uses.

search_domains is the tool everything else supports. Availability is read from a registry-derived list of every registered domain, refreshed daily, so a check of a thousand extensions is one request rather than a thousand WHOIS lookups. That is why it answers in milliseconds even when the assistant asks about the whole catalogue. The account tools exist to aim it: favorites narrow which extensions matter, history says what has already been tried, and the shortlist is where anything worth keeping goes.

Find

The reason the server exists: availability for any name, and candidates when there is no name yet.

Favorite TLDs

The extensions this person actually registers under, so suggestions are scoped to them instead of ranking a thousand results they will ignore.

Saved domains

The shortlist. Written from the conversation, read back in the browser: the same library either way.

History

What this person has already looked at on the website, so an assistant can pick up a naming session instead of starting from nothing. Read-only: nothing done through these tools is added to it.

Connect

Point your client at the endpoint. It reads the server's own metadata, discovers where to ask for authorization, and opens a browser. You approve it once and it is connected. There is nothing to copy and no secret to keep.

One command in Claude Code, one URL in Claude Desktop, one block in Cursor, VS Code, Windsurf, Cline, Gemini CLI or Codex. Pick yours below for the file and the field names it wants. Anything that speaks streamable-http works, listed here or not.

  1. 1Run the command in any terminal.
  2. 2Approve the browser prompt that opens.
  3. 3Type /mcp inside Claude Code to confirm it connected.
Terminal
claude mcp add --transport http onymu https://onymu.com/api/v1/mcp

No browser? A cron job, a container or a shared machine can send a key instead. Mint one in the connect card, where it is shown once next to a finished command with the key already in it, and send it as Authorization: Bearer <key>.

Discovery is public: GET /api/v1/mcp returns the server status and this tool list, so a client can probe for support and get an honest answer rather than a 404.

Authentication

Every tool requires an account. This is not a public API. The website lets anyone search and generate. The MCP endpoint does not. A client connects as one specific person, and every call, including search_domains, is attributed to them.

The server acts as you, and there are two ways for a client to prove which you it is acting as.

OAuth 2.1, the one most clients will take. An unauthenticated call answers 401 with a WWW-Authenticate header pointing at /.well-known/oauth-protected-resource, which names the authorization server. The client registers itself, either by hosting a client ID metadata document or through dynamic registration, then sends you to the consent screen and exchanges the code for a token bound to this resource. PKCE (S256) is required, refresh tokens rotate, and access tokens are short-lived.

A bearer key, for anything that cannot open a browser: Authorization: Bearer <key>. A signed-in browser can also use its session cookie. A client with none of the three gets the 401 above before any tool runs, so there is no anonymous path to the tool list.

Access granted through OAuth can be withdrawn by revoking the client. Keys are minted in the connect card, up to 5 at once, so a laptop and a desktop can hold different ones and either can be revoked alone. Only a hash is stored: the key is shown once, at creation, and nothing can print it again. Revoking takes effect on the next call. Minting keys needs the website session, never a key. A leaked key can drive the tools, but it cannot mint its successors.

Find

The reason the server exists: availability for any name, and candidates when there is no name yet.

search_domains

Primary toolPOST /api/v1/domains/check

The main tool. Checks one or more labels against a set of TLDs and sorts every combination into available or taken. Availability comes from a registry-derived list of every registered domain, refreshed daily, so a single call covers every extension at once rather than issuing a WHOIS lookup per name. It answers in milliseconds even across the whole catalogue. Leave `tlds` out and it uses the caller's favorites, falling back to the most popular extensions for a new account.

Parameters

NameTypeRequiredDescription
namesstring[]YesBare labels to check, without a dot. Up to 25 per call.
tldsstring[]NoExtensions to check against, without the leading dot. Defaults to the caller's favorite TLDs, or the most popular ones.

Returns

One row per name, splitting the extensions into `available` (not in the latest registry data) and `taken`. Also reports how many extensions were checked and where that list came from.

Call
{
  "name": "search_domains",
  "arguments": {
    "names": ["northbeam"],
    "tlds": ["com", "io", "dev"]
  }
}
Result
{
  "results": [
    {
      "name": "northbeam",
      "available": ["dev"],
      "taken": ["com"]
    }
  ],
  "tldsChecked": 3,
  "tldSource": "requested"
}

check_usernames

POST /api/v1/usernames/check

The other half of picking a name: a domain is only really available if the handles are too. Checks X, YouTube, TikTok, Snapchat, Pinterest, Discord, GitHub, Telegram and Twitch. Unlike domain search there is no dataset behind this — no platform publishes its username list — so each platform is probed live through a rotating proxy pool and the answers are cached. Results are split four ways, and the last two matter: `unknown` means the check could not be completed (the platform blocked it or timed out) and must never be reported as free, while `invalid` means that platform's own username rules reject the string outright. Checking several names at once skips X, which bans on volume.

Parameters

NameTypeRequiredDescription
usernamesstring[]YesHandles to check, without an @. Up to 10 per call.
platformsstring[]NoLimit the check to these platform ids: x, youtube, tiktok, snapchat, pinterest, discord, github, telegram, twitch. Defaults to all nine.

Returns

One row per username, splitting the platforms into `available`, `taken`, `unknown` and `invalid`. Anything skipped is named in `notChecked` with the reason.

Call
{
  "name": "check_usernames",
  "arguments": {
    "usernames": ["northbeam"]
  }
}
Result
{
  "results": [
    {
      "username": "northbeam",
      "available": ["twitch", "telegram"],
      "taken": ["x", "github", "youtube"],
      "unknown": [],
      "invalid": []
    }
  ]
}

generate_domains

PUT /api/v1/domains/generate

Applies the deterministic naming generators to a keyword and returns candidates by category: brandable, multiword, short, creative, modern, classic, professional and synonyms. No model is involved, so the same keyword always returns the same list. Each candidate arrives with availability already resolved against a short extension list; pass anything promising to search_domains for the full catalogue.

Parameters

NameTypeRequiredDescription
keywordstringYesThe word to build names from. Letters and digits; anything else is stripped.
categoriesstring[]NoRestrict output to these categories. Defaults to all eight.
countnumberNoCandidates per category. Defaults to 8, capped at 25.
minLengthnumberNoDiscard candidates shorter than this.
maxLengthnumberNoDiscard candidates longer than this.
prefixstringNoKeep only candidates starting with this string.
suffixstringNoKeep only candidates ending with this string.
mustContainstringNoKeep only candidates containing this string.
tldsstring[]NoExtensions to check the candidates against. Defaults to the caller's favorites, or the most popular ones.

Returns

A list of candidates, each with its `domain`, the `category` that produced it, and the extensions it is `available` under.

Call
{
  "name": "generate_domains",
  "arguments": {
    "keyword": "orbit",
    "categories": ["brandable"],
    "count": 3
  }
}
Result
{
  "keyword": "orbit",
  "tldsChecked": ["com", "io", "dev"],
  "candidates": [
    { "domain": "orbitly",  "category": "brandable",
      "available": ["io", "dev"] },
    { "domain": "orbitify", "category": "brandable",
      "available": ["com", "io", "dev"] },
    { "domain": "orbitfy",  "category": "brandable",
      "available": ["com", "dev"] }
  ]
}

Favorite TLDs

The extensions this person actually registers under, so suggestions are scoped to them instead of ranking a thousand results they will ignore.

list_favorite_tlds

GET /api/v1/favorites/tlds

Returns the favorite TLDs saved on the calling account, in the order they were saved. Call it before search_domains to scope a check to the extensions this person actually cares about, or leave search_domains to do it, which is what it does when you pass no `tlds`. An empty list can also mean the person keeps their extensions in their browser rather than on the account — that copy is out of reach here.

Parameters

None.

Returns

The saved extensions without their leading dots, how many there are, and the account's cap.

Call
{
  "name": "list_favorite_tlds",
  "arguments": {}
}
Result
{
  "tlds": ["com", "io", "dev", "co"],
  "total": 4,
  "limit": 200
}

add_favorite_tld

POST /api/v1/favorites/tlds

Saves one TLD to the calling account. Every later search checks favorites first and shows them above the rest of the grid. Adding one that is already saved comes back as an error rather than silently doing nothing, so an assistant can tell the difference. If the person has "Save my favorite TLDs" switched off, their extensions live in their browser only: nothing is stored and the answer says so.

Parameters

NameTypeRequiredDescription
tldstringYesThe extension, with or without the leading dot.

Returns

What was saved, and the account's new total against its cap — or `added: null` and a note, when the account keeps its extensions in the browser.

Call
{
  "name": "add_favorite_tld",
  "arguments": { "tld": ".ai" }
}
Result
{
  "added": "ai",
  "total": 5,
  "limit": 200
}

remove_favorite_tld

DELETE /api/v1/favorites/tlds/:tld

Removes one TLD from the calling account. Removing one that is not saved comes back as an error, so a mistaken call is visible rather than silent.

Parameters

NameTypeRequiredDescription
tldstringYesThe extension to remove, with or without the leading dot.

Returns

What was removed, and the account's new total.

Call
{
  "name": "remove_favorite_tld",
  "arguments": { "tld": "xyz" }
}
Result
{
  "removed": "xyz",
  "total": 4,
  "limit": 200
}

Saved domains

The shortlist. Written from the conversation, read back in the browser: the same library either way.

list_saved_domains

GET /api/v1/favorites/domains

Returns the domains bookmarked on the calling account, newest first. Use it to answer “what have I shortlisted so far?” without the person leaving the conversation, or to re-check a shortlist against search_domains before they buy.

Parameters

None.

Returns

A list of saved names with when each was saved, plus the account's cap.

Call
{
  "name": "list_saved_domains",
  "arguments": {}
}
Result
{
  "domains": [
    { "domain": "northbeam.dev", "savedAt": "2026-08-26T11:31:40Z" },
    { "domain": "sprouted.io",   "savedAt": "2026-08-24T09:02:11Z" }
  ],
  "total": 2,
  "limit": 500
}

save_domain

POST /api/v1/favorites/domains

Bookmarks a name on the calling account. It appears in the library on the site immediately and survives the conversation, which is the point: the shortlist outlives the chat window. Accepts a full domain or a bare label, the way the site saves a name being compared across extensions.

Parameters

NameTypeRequiredDescription
domainstringYesThe name to save, with or without an extension.

Returns

What was saved, and the account's new total against its cap.

Call
{
  "name": "save_domain",
  "arguments": { "domain": "northbeam.dev" }
}
Result
{
  "saved": "northbeam.dev",
  "total": 3,
  "limit": 500
}

remove_saved_domain

DELETE /api/v1/favorites/domains/:domain

Removes one bookmarked name from the calling account. It is the counterpart to save_domain, for when a candidate is ruled out mid-conversation. Removing something that was never saved comes back as an error.

Parameters

NameTypeRequiredDescription
domainstringYesThe saved name to remove, exactly as it was saved.

Returns

What was removed, and the account's new total.

Call
{
  "name": "remove_saved_domain",
  "arguments": { "domain": "sprigly.io" }
}
Result
{
  "removed": "sprigly.io",
  "total": 2,
  "limit": 500
}

History

What this person has already looked at on the website, so an assistant can pick up a naming session instead of starting from nothing. Read-only: nothing done through these tools is added to it.

get_recent_searches

GET /api/v1/history/searches

Returns the account's search history, most recent first. It is the cheapest way for an assistant to resume a naming session: what was already tried, and therefore what not to suggest again. This is what the person searched on the website: search_domains does not write here, so checking a name through an assistant never adds to it.

Parameters

NameTypeRequiredDescription
limitnumberNoHow many entries to return. Defaults to 20, capped at the account's 200.

Returns

A list of searched terms with timestamps, newest first.

Call
{
  "name": "get_recent_searches",
  "arguments": { "limit": 3 }
}
Result
{
  "searches": [
    { "query": "northbeam", "searchedAt": "2026-08-26T11:32:02Z" },
    { "query": "harbor",    "searchedAt": "2026-08-26T11:31:58Z" },
    { "query": "ledger",    "searchedAt": "2026-08-25T16:20:44Z" }
  ],
  "count": 3
}

get_recent_generations

GET /api/v1/history/generations

Returns the account's generation history, most recent first: the seed words, not the candidates. Useful for picking up where a naming session left off, or for noticing which direction someone keeps circling back to. Like the search history, it records the website only: generate_domains does not write here.

Parameters

NameTypeRequiredDescription
limitnumberNoHow many entries to return. Defaults to 20, capped at the account's 200.

Returns

A list of seed words with timestamps, newest first.

Call
{
  "name": "get_recent_generations",
  "arguments": { "limit": 3 }
}
Result
{
  "generations": [
    { "keyword": "orbit",  "generatedAt": "2026-08-26T11:31:51Z" },
    { "keyword": "bloom",  "generatedAt": "2026-08-26T10:14:03Z" },
    { "keyword": "harbor", "generatedAt": "2026-08-25T16:19:30Z" }
  ],
  "count": 3
}

Rate limits

60 calls a minute and 600 an hour, counted per account rather than per key. A second key does not buy a second budget. An assistant answering a naming question makes a handful of calls, so tripping this means something is looping.

Over the limit, the endpoint answers 429 with a Retry-After in seconds. Every successful response carries RateLimit-Remaining, so a client can slow down before it gets there. Account caps for favorite TLDs, saved domains and history length are separate, and come back from the tool itself.

Errors

Something a caller can fix, such as a duplicate favorite, a full shortlist or a malformed argument, comes back as a normal tool result with isError: true and a message saying what went wrong, so the model can read it and adjust rather than seeing the connection fail. The two tables below are the failures that happen before a tool ever runs.

HTTP: the request never reached a tool

401No key and no session, or a key that has been revoked. Mint one on the MCP page.
429Rate limited. 60 calls a minute or 600 an hour, per account. The response carries Retry-After.
405A GET asking only for text/event-stream. This server sends no server-to-client stream, so everything answers on the POST.
503The server is switched off (MCP_ENABLED=false).

JSON-RPC: the protocol rejected the message

-32700The body is not valid JSON.
-32600Valid JSON, but not a JSON-RPC 2.0 request.
-32601Unknown method. This server implements initialize, ping, tools/list and tools/call.
-32602Unknown tool name, or a missing one.
-32603The tool threw something unexpected. That is a bug, and the message says what.

Status

Live. All 11 tools are callable, and every one of them runs the same code the website does. A name saved from a conversation is in your library when you reload the page, and a search made there shows up in your history.

GET /api/v1/mcp needs no key and reports the server version, the protocol versions it speaks, the rate limits and this tool list, which is enough for a client to probe before connecting.