quizbase
Skip to content
Command Palette
Search for a command to run...
QuizBase · Docs

MCP server reference#

QuizBase exposes the public catalog over Model Context Protocol — Streamable HTTP transport, MCP spec 2025-11-25.

Endpointhttps://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 authOAuth 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)
TransportStreamable HTTP, JSON response (no SSE streaming — tools are synchronous)
Rate limitSame 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 listingsMCP 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.

ToolWrapsInput shape (selected)
quizbase_random/v1/questions/randomamount (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/questionsfull 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/:idid (UUID), lang
quizbase_stats/v1/statslang
quizbase_topics/v1/topicsq, kind (tag|subcategory), cursor, limit (1-500)
quizbase_topic_by_slug/v1/topics/:slugslug, lang
quizbase_tags/v1/tagsq, cursor, limit
quizbase_subcategories/v1/subcategoriesq, cursor, limit
quizbase_categories/v1/categorieslang
quizbase_languages/v1/languageslang
quizbase_regions/v1/regionslang, q, kind (country|cultural), cursor, limit (1-500)
quizbase_reportPOST /v1/reportone 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 without JSON.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.

URIReturns
mcp://quizbase/categoriesList of 24 top-level categories (English labels)
mcp://quizbase/languagesSupported languages whitelist + question counts
mcp://quizbase/topics/top-100Top-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.

PromptArgumentsWhat it does
build_quiztopic, lang, difficulty?, rounds, questions_per_roundMulti-round quiz orchestration. Resolves the topic, then fetches questions per round; each question carries its own attribution.
explore_topictopic, langDiscursive topic exploration — facets (byCategory, byDifficulty, coOccurringTags) + sample questions.
warmup_roundtopic?, langLightweight 5-question icebreaker. Single tool call, no multi-round orchestration.
client_mechanics_patternsmechanic?, langCatalogue 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.

  1. Client fetches /.well-known/oauth-protected-resource (RFC 9728).
  2. Client fetches /.well-known/oauth-authorization-server (RFC 8414).
  3. Client POST /api/auth/oauth2/register — Dynamic Client Registration (RFC 7591, public client, token_endpoint_auth_method: "none").
  4. Client opens /api/auth/oauth2/authorize?response_type=code&client_id=…&code_challenge=…&code_challenge_method=S256&scope=quizbase:read offline_accessPKCE S256 is required.
  5. User signs in on /login, accepts the consent screen on /oauth/consent, gets redirected back to the client with code.
  6. Client exchanges the code at /api/auth/oauth2/token for a JWT access token (1 hour) and refresh token (90 days). Refresh tokens rotate on use.
  7. 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 pk keys for code-driven clients. sk keys 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 Host headers (the production domain, localhost(:port), and internal preview hosts) is accepted; others get 403.
  • CORS — allow-list of claude.ai, app.cursor.sh, and localhost. Server-side clients (no Origin header) are unaffected.
  • WWW-Authenticate on 401 — includes a resource_metadata link to the RFC 9728 PRM endpoint so clients can discover where to obtain credentials.
  • /mcp responses 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