MCP tools over plain HTTP
Call any MCP tool with a GET request, no MCP client needed.
Prerequisites#
- curl, Node 18 or newer, or Python 3 with only its standard library. The curl examples also use
jqto pick fields; leave it off to see the whole answer. - No account. Every MCP tool answers
GET /api/tools/{tool}, with the tool's arguments as query parameters. - Without a key, this endpoint allows 30 requests an hour for each IP address. Limits and keys covers the free key.
Steps#
Find the tool's parameters
Each tool has a schema file at
/schema/mcp/<tool>.json. ItsinputSchemalists the parameters, which are required, and the allowed values. ItsoutputSchemadescribes the answer. The tool names are listed on the MCP overview.Read a schemacurl -s https://everloot.app/schema/mcp/get_by_name.json | jq '{title, description, required: .inputSchema.required, properties: .inputSchema.properties}'{ "title": "Get records by exact name", "description": "Exact full-name match (case-insensitive) for disambiguation.", "required": [ "name" ], "properties": { "name": { "type": "string", "maxLength": 200, "description": "Full record name." }, "kind": { "description": "Narrow to one record kind.", "type": "string", "enum": [ "item", "npc", "object", "quest", "zone", "spell", "lore_page", "creator", "realm" ] } } }So
get_by_nameneeds aname, and takes an optionalkindfrom the list shown. Numbers and booleans go in the query string as plain text, such asitemId=19019ortrueandfalse.Call it with curl
Call get_by_namecurl -s "https://everloot.app/api/tools/get_by_name?name=Thrall" | jq '{tool, arguments, text, schema}'
{ "tool": "get_by_name", "arguments": { "name": "Thrall" }, "text": "[lore_page/lore] Thrall\nThrall (birthname Go'el), son of Durotan and Draka, is the former Warchief of the restored shamanistic Horde, founder of the nation of Durotar in Kalimdor and o…\nSource: https://warcraft.wiki.gg/wiki/Thrall (CC-BY-SA-4.0, retrieved 2023-11-13)", "schema": "https://everloot.app/schema/mcp/get_by_name.json" }The answer has five fields.
toolandargumentsrepeat what you asked, with defaults applied.answeris the tool's structured result, in the shape itsoutputSchemadeclares.textis the same answer as words, as an MCP client sees it.schemais the address of the schema file. Thejqfilter leavesanswerout to keep this short; it holds the hits.Call it from JavaScript or Python
This script takes a tool name and
name=valuepairs, so it works for any tool. It prints the words, then the keys of the structured answer. Errors go to standard error and the script exits with 1.tool// Call one MCP tool over HTTP. Run: node tool.mjs talent_tree class=warrior const BASE = "https://everloot.app"; const [tool, ...pairs] = process.argv.slice(2); const query = new URLSearchParams(pairs.map((pair) => pair.split(/=(.*)/s).slice(0, 2))); const res = await fetch(`${BASE}/api/tools/${tool}?${query}`); const body = await res.json(); if (res.status === 429) { console.error(`Rate limited. Try again in ${res.headers.get("retry-after")} seconds.`); process.exit(1); } if (!res.ok) { // 400 says which argument is wrong; 404 lists the tools. console.error(`${res.status}: ${body.error}`); process.exit(1); } console.log(body.text); // the answer as words console.log(Object.keys(body.answer)); // the structured answer, in body.answer
"""Call one MCP tool over HTTP. Run: python tool.py talent_tree class=warrior""" import json import sys from urllib.error import HTTPError from urllib.parse import urlencode from urllib.request import urlopen sys.stdout.reconfigure(encoding="utf-8") # the text can hold characters a Windows console does not BASE = "https://everloot.app" tool, *pairs = sys.argv[1:] query = urlencode([tuple(pair.split("=", 1)) for pair in pairs]) try: with urlopen(f"{BASE}/api/tools/{tool}?{query}", timeout=30) as res: body = json.load(res) except HTTPError as err: if err.code == 429: raise SystemExit(f"Rate limited. Try again in {err.headers['Retry-After']} seconds.") # 400 says which argument is wrong; 404 lists the tools. raise SystemExit(f"{err.code}: {json.load(err)['error']}") print(body["text"]) # the answer as words print(list(body["answer"])) # the structured answer, in body["answer"]
Runnode tool.mjs get_by_name name=Thrall
python tool.py get_by_name name=Thrall
Output[lore_page/lore] Thrall Thrall (birthname Go'el), son of Durotan and Draka, is the former Warchief of the restored shamanistic Horde, founder of the nation of Durotar in Kalimdor and o… Source: https://warcraft.wiki.gg/wiki/Thrall (CC-BY-SA-4.0, retrieved 2023-11-13) [ 'hits' ]
[lore_page/lore] Thrall Thrall (birthname Go'el), son of Durotan and Draka, is the former Warchief of the restored shamanistic Horde, founder of the nation of Durotar in Kalimdor and o… Source: https://warcraft.wiki.gg/wiki/Thrall (CC-BY-SA-4.0, retrieved 2023-11-13) ['hits']
Handle a 400
A wrong or missing parameter is a 400, and
errorsays which one and where its schema is.A missing and a wrong argumentcurl -s -w "\n%{http_code}\n" "https://everloot.app/api/tools/get_by_name?kind=npc"
{"ok":false,"error":"name: Invalid input: expected string, received undefined. The parameters are described at https://everloot.app/schema/mcp/get_by_name.json."} 400{"ok":false,"error":"kind: Invalid option: expected one of \"item\"|\"npc\"|\"object\"|\"quest\"|\"zone\"|\"spell\"|\"lore_page\"|\"creator\"|\"realm\". The parameters are described at https://everloot.app/schema/mcp/get_by_name.json."} 400Handle a 404
An unknown tool name is a 404, and the error lists every tool.
curl https://everloot.app/api/tools/nope{"ok":false,"error":"No tool named nope. Tools: search_knowledge, get_entity, get_by_name, search, fetch, answer_sources, profession_path, recipe_thresholds, item_page, npc_page, object_page, quest_page, vendor_page, world_map_near, entity_relations, item_categories, talent_tree, talent_plan, collection_gap, completion_progress, guild_board, auction_history, market_value, gold_ledger, population, recent_changes, ecosystem_projects, everloot_plan."} 404Handle a 429
Over the limit, the answer is a 429. The
Retry-Afterheader says how many seconds to wait, and the body says where to get a free key. The scripts above print the wait and stop. Do not retry before it is over. A key sent asAuthorization: Bearer <key>raises the limit.A 429HTTP/1.1 429 Too Many Requests Cache-Control: no-store Content-Type: application/json Retry-After: 2758 {"ok":false,"error":"Without a key this endpoint answers 30 requests per hour per address. Try again in 2758 seconds, or get a free key at https://everloot.app/docs/api/keys and send it as \"Authorization: Bearer <key>\".","getAKey":"https://everloot.app/docs/api/keys"}
What each status means#
| Status | Meaning | What to do |
|---|---|---|
200 | The tool answered. | Read answer or text. |
400 | An argument is missing or wrong. error says which. | Fix it from the schema file. |
404 | No tool by that name. error lists them. | Pick a name from the list. |
429 | Over the hourly limit. | Wait Retry-After seconds, or use a free key. |
503 | The data could not be read. The answer is unrecorded, not zero. | Try again later. |
Check your work#
- The curl call returns a JSON object with
tool,arguments,answer,textandschema, andargumentsechoes what you sent. - The JavaScript and Python scripts print the same text as curl.
- Leaving out
namegives the 400 above. A made-up tool name gives the 404. - Answers built on player uploads carry the credit line, in the text and in the
X-Everloot-Creditheader. Show it if you publish them.
A tool that reads player uploads can answer with "unrecorded", in the text and in the structured answer. That is an answer, not an error. It means players have not reported enough yet.
Next steps#
- /tools/{tool}: the endpoint reference.
- MCP overview: every tool, with its own page.
- Ask Claude about WoW: Forever: use the same tools in a chat.