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.
- 1Run the command in any terminal.
- 2Approve the browser prompt that opens.
- 3Type /mcp inside Claude Code to confirm it connected.
Terminalclaude mcp add --transport http onymu https://onymu.com/api/v1/mcpNo 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/checkThe 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
| Name | Type | Required | Description |
|---|---|---|---|
names | string[] | Yes | Bare labels to check, without a dot. Up to 25 per call. |
tlds | string[] | No | Extensions 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.
{
"name": "search_domains",
"arguments": {
"names": ["northbeam"],
"tlds": ["com", "io", "dev"]
}
}{
"results": [
{
"name": "northbeam",
"available": ["dev"],
"taken": ["com"]
}
],
"tldsChecked": 3,
"tldSource": "requested"
}check_usernames
POST /api/v1/usernames/checkThe 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
| Name | Type | Required | Description |
|---|---|---|---|
usernames | string[] | Yes | Handles to check, without an @. Up to 10 per call. |
platforms | string[] | No | Limit 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.
{
"name": "check_usernames",
"arguments": {
"usernames": ["northbeam"]
}
}{
"results": [
{
"username": "northbeam",
"available": ["twitch", "telegram"],
"taken": ["x", "github", "youtube"],
"unknown": [],
"invalid": []
}
]
}generate_domains
PUT /api/v1/domains/generateApplies 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
| Name | Type | Required | Description |
|---|---|---|---|
keyword | string | Yes | The word to build names from. Letters and digits; anything else is stripped. |
categories | string[] | No | Restrict output to these categories. Defaults to all eight. |
count | number | No | Candidates per category. Defaults to 8, capped at 25. |
minLength | number | No | Discard candidates shorter than this. |
maxLength | number | No | Discard candidates longer than this. |
prefix | string | No | Keep only candidates starting with this string. |
suffix | string | No | Keep only candidates ending with this string. |
mustContain | string | No | Keep only candidates containing this string. |
tlds | string[] | No | Extensions 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.
{
"name": "generate_domains",
"arguments": {
"keyword": "orbit",
"categories": ["brandable"],
"count": 3
}
}{
"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/tldsReturns 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.
{
"name": "list_favorite_tlds",
"arguments": {}
}{
"tlds": ["com", "io", "dev", "co"],
"total": 4,
"limit": 200
}add_favorite_tld
POST /api/v1/favorites/tldsSaves 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
| Name | Type | Required | Description |
|---|---|---|---|
tld | string | Yes | The 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.
{
"name": "add_favorite_tld",
"arguments": { "tld": ".ai" }
}{
"added": "ai",
"total": 5,
"limit": 200
}remove_favorite_tld
DELETE /api/v1/favorites/tlds/:tldRemoves 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
| Name | Type | Required | Description |
|---|---|---|---|
tld | string | Yes | The extension to remove, with or without the leading dot. |
Returns
What was removed, and the account's new total.
{
"name": "remove_favorite_tld",
"arguments": { "tld": "xyz" }
}{
"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/domainsReturns 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.
{
"name": "list_saved_domains",
"arguments": {}
}{
"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/domainsBookmarks 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
| Name | Type | Required | Description |
|---|---|---|---|
domain | string | Yes | The name to save, with or without an extension. |
Returns
What was saved, and the account's new total against its cap.
{
"name": "save_domain",
"arguments": { "domain": "northbeam.dev" }
}{
"saved": "northbeam.dev",
"total": 3,
"limit": 500
}remove_saved_domain
DELETE /api/v1/favorites/domains/:domainRemoves 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
| Name | Type | Required | Description |
|---|---|---|---|
domain | string | Yes | The saved name to remove, exactly as it was saved. |
Returns
What was removed, and the account's new total.
{
"name": "remove_saved_domain",
"arguments": { "domain": "sprigly.io" }
}{
"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/searchesReturns 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
| Name | Type | Required | Description |
|---|---|---|---|
limit | number | No | How many entries to return. Defaults to 20, capped at the account's 200. |
Returns
A list of searched terms with timestamps, newest first.
{
"name": "get_recent_searches",
"arguments": { "limit": 3 }
}{
"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/generationsReturns 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
| Name | Type | Required | Description |
|---|---|---|---|
limit | number | No | How many entries to return. Defaults to 20, capped at the account's 200. |
Returns
A list of seed words with timestamps, newest first.
{
"name": "get_recent_generations",
"arguments": { "limit": 3 }
}{
"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
401 | No key and no session, or a key that has been revoked. Mint one on the MCP page. |
429 | Rate limited. 60 calls a minute or 600 an hour, per account. The response carries Retry-After. |
405 | A GET asking only for text/event-stream. This server sends no server-to-client stream, so everything answers on the POST. |
503 | The server is switched off (MCP_ENABLED=false). |
JSON-RPC: the protocol rejected the message
-32700 | The body is not valid JSON. |
-32600 | Valid JSON, but not a JSON-RPC 2.0 request. |
-32601 | Unknown method. This server implements initialize, ping, tools/list and tools/call. |
-32602 | Unknown tool name, or a missing one. |
-32603 | The 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.