MCP server reference#
QuizBase exposes the public catalog over Model Context Protocol — Streamable HTTP transport, MCP spec 2025-11-25.
| Endpoint | https://quizbase.runriva.com/mcp |
| Discovery (no key) | POST /mcp with initialize, tools/list, prompts/list, resources/list, ping — anonymous, metadata only. Per-IP rate limit applies (50 req/s, 5000 / 10min). |
| Execution auth | OAuth 2.1 + DCR (RFC 7591, agent surfaces) or Authorization: Bearer qb_(pk\|sk)_* (code-driven clients) for tools/call, prompts/get, resources/read. |
| Auth discovery | /.well-known/oauth-protected-resource (RFC 9728) → /.well-known/oauth-authorization-server (RFC 8414) |
| Transport | Streamable HTTP, JSON response (no SSE streaming — tools are synchronous) |
| Rate limit | Same as REST. 1 tools/call = 1 request against your per-user quota; see /pricing for the current burst and daily limits per tier. OAuth and API key calls share the same counter. |
| Catalog listings | MCP Registry · Smithery · Glama · PulseMCP |
For step-by-step setup in Claude.ai / Cursor / Inspector see the MCP guide.
Tools (12)#
Every tool wraps the matching /api/v1/* endpoint with the same query semantics. EN-only descriptions for LLM-aware language; output language is controlled by the lang argument.
| Tool | Wraps | Input shape (selected) |
|---|---|---|
quizbase_random | /v1/questions/random | amount (1-50), lang, category, difficulty (trivial | easy | medium | hard | expert — LLM-calibrated), type, subcategory, tags, tags_any, topic, topics_any, regions, source, license, quality (default high), exclude |
quizbase_list | /v1/questions | full filter parity with quizbase_random minus amount/exclude, plus cursor, limit (1-100), updated_since, count (exact | none), ids (batch ≤250), content_language (map a set across languages) |
quizbase_question_by_id | /v1/questions/:id | id (UUID), lang |
quizbase_stats | /v1/stats | lang |
quizbase_topics | /v1/topics | q, kind (tag|subcategory), cursor, limit (1-500) |
quizbase_topic_by_slug | /v1/topics/:slug | slug, lang |
quizbase_tags | /v1/tags | q, cursor, limit |
quizbase_subcategories | /v1/subcategories | q, cursor, limit |
quizbase_categories | /v1/categories | lang |
quizbase_languages | /v1/languages | lang |
quizbase_regions | /v1/regions | lang, q, kind (country|cultural), cursor, limit (1-500) |
quizbase_report | POST /v1/report | one of (questionId, questionText, questionUrl), type, comment, reporterEmail |
Output format#
Every tool returns CallToolResult with both representations for client compatibility:
content[0].text— JSON-stringified payload (for clients on SDK older than 1.18)structuredContent— typed object (preferred, parsed withoutJSON.parse)
Every tool returns each question with full per-record attribution (source, author, license, licenseVersion, licenseUrl, sourceId, url, modifications, lastModified) — identical shape to REST /api/v1/questions/*:
{
"questions": [
{
"id": "019db549-…",
"text": "…",
"correctAnswer": "…",
"attribution": {
"source": "opentdb",
"author": "OpenTDB community",
"license": "CC-BY-SA-4.0",
"licenseVersion": "4.0",
"licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0/",
"sourceId": "opentdb:17243",
"url": "https://opentdb.com/",
"modifications": ["translated_pl"],
"lastModified": "2026-03-12T10:14:00Z"
}
}
],
"meta": { "count": 3, "language": "pl", "nextCursor": null }
} Per-question attribution lets you credit each record individually (CC-BY-SA § 3(a)(1)(B) compliance) without an extra quizbase_question_by_id round-trip. If you need a deduplicated Credits-page summary, derive it client-side: [...new Set(questions.map(q => q.attribution.source))].
Tool errors#
When a tool can’t fulfil a request (e.g. UUID not found, invalid input, rate limit), the result is marked with isError: true and the payload contains an error code:
{
"isError": true,
"content": [{ "type": "text", "text": "{ "error": "not_found", "message": "…" }" }],
"structuredContent": { "error": "not_found", "message": "…", "id": "…" }
} Codes: not_found, invalid_input, rate_limit_exceeded, internal_error. Auth failures (401) and rate-limit on the API key (429) are handled at the HTTP layer — your MCP client surfaces them as protocol errors.
Resources (3)#
Static MCP resources cached by URI. For non-default language use the matching tool with a lang argument.
| URI | Returns |
|---|---|
mcp://quizbase/categories | List of 24 top-level categories (English labels) |
mcp://quizbase/languages | Supported languages whitelist + question counts |
mcp://quizbase/topics/top-100 | Top-100 curated topics by question count (English labels) |
mimeType is always application/json.
Prompts (4)#
Curated workflow templates for LLM agents. Arguments are strings (per MCP spec); the prompt template injects them into a user message with a step-by-step plan invoking the tools above.
| Prompt | Arguments | What it does |
|---|---|---|
build_quiz | topic, lang, difficulty?, rounds, questions_per_round | Multi-round quiz orchestration. Resolves the topic, then fetches questions per round; each question carries its own attribution. |
explore_topic | topic, lang | Discursive topic exploration — facets (byCategory, byDifficulty, coOccurringTags) + sample questions. |
warmup_round | topic?, lang | Lightweight 5-question icebreaker. Single tool call, no multi-round orchestration. |
client_mechanics_patterns | mechanic?, lang | Catalogue or single walkthrough for 9 client-side mechanics on top of stable question IDs. Call without mechanic for the menu; pass mechanic=daily_challenge \| no_repeat \| multi_language_same_q \| multiplayer_sync \| streak_persistence \| anti_cheat \| anki_cards \| lead_capture_followup \| report_wrong_content for a step-by-step plan on one pattern. Full code samples live at /docs/api/questions-by-id. |
Authentication#
Anonymous discovery (no key)#
POST /mcp with JSON-RPC method ∈ {initialize, notifications/initialized, ping, tools/list, prompts/list, resources/list, resources/templates/list} is accepted without an Authorization header. The server returns metadata only — tool names, input schemas, prompt templates, resource URIs — the same surface published at /openapi.json. No question content, no user data.
This exists so MCP catalogs (Glama, Smithery, PulseMCP, MCP.so) can index the server, and so MCP clients can probe capabilities before prompting the user for a key.
A per-IP rate limit applies (50 req/s, 5000 / 10 min). Execution methods (tools/call, prompts/get, resources/read, completion/complete, logging/setLevel) and GET / DELETE /mcp still return 401 with WWW-Authenticate: Bearer realm="quizbase".
Authenticated requests#
Two paths, both accepted on /mcp for execution methods:
OAuth 2.1 + Dynamic Client Registration#
For Claude.ai (web / desktop / mobile), ChatGPT custom connectors, Gemini agents, MCP Inspector, and any client that implements the MCP OAuth profile.
- Client fetches
/.well-known/oauth-protected-resource(RFC 9728). - Client fetches
/.well-known/oauth-authorization-server(RFC 8414). - Client
POST /api/auth/oauth2/register— Dynamic Client Registration (RFC 7591, public client,token_endpoint_auth_method: "none"). - Client opens
/api/auth/oauth2/authorize?response_type=code&client_id=…&code_challenge=…&code_challenge_method=S256&scope=quizbase:read offline_access— PKCE S256 is required. - User signs in on
/login, accepts the consent screen on/oauth/consent, gets redirected back to the client withcode. - Client exchanges the code at
/api/auth/oauth2/tokenfor a JWT access token (1 hour) and refresh token (90 days). Refresh tokens rotate on use. - Client sends
Authorization: Bearer <jwt>to/mcp.
Token claims: iss = https://quizbase.runriva.com/api/auth, aud = https://quizbase.runriva.com/mcp, sub = <user id>, scope = "quizbase:read" (plus OIDC standard if requested). Signed with EdDSA Ed25519; the JWKS is served at /api/auth/jwks.
Available scopes: openid, profile, email, offline_access, quizbase:read. We do not currently expose a quizbase:write scope — quizbase_report is gated by quizbase:read along with everything else (we will split it out only when there is a real write surface to protect).
Bearer API key#
For Cursor, Continue, Claude Desktop config files, curl, and your own code. Create a key in API keys, send Authorization: Bearer qb_pk_… (or qb_sk_…) — same flow as /api/v1/*.
Security#
- Use
pkkeys for code-driven clients.skkeys work but are intended for server-side agents. - OAuth tokens are scoped + short-lived: 1 h access + 90 d refresh with rotation. A leaked access token expires by itself before most incidents are detected.
- DNS rebinding — only an allowlist of known
Hostheaders (the production domain,localhost(:port), and internal preview hosts) is accepted; others get 403. - CORS — allow-list of
claude.ai,app.cursor.sh, andlocalhost. Server-side clients (noOriginheader) are unaffected. WWW-Authenticateon 401 — includes aresource_metadatalink to the RFC 9728 PRM endpoint so clients can discover where to obtain credentials./mcpresponses are never cached (Cache-Control: private, no-store) — protects against intermediary caches replaying a 401 for everyone after one bad request.
Versioning#
The MCP server tracks the same major version as /api/v1/*. Tool inputs / outputs are additive within a major; a breaking change would bump to a new path (/mcp/v2/). The current spec is 2025-11-25; the SDK we run is @modelcontextprotocol/sdk@<SdkVersion sdk="mcp" />.
See also#
- MCP guide — step-by-step setup for Claude.ai / Cursor / Inspector
- MCP playground — list and call the tools from your browser, no client setup
- Authentication — key types and rotation