This FastAPI/uvicorn server exposes the Bunpro MCP bundle to other agents via HTTP streaming at /mcp. It registers grammar, vocabulary, reading, user statistics, and review tools that proxy Bunpro API endpoints.
This is a personal learning project built to:
- Explore how MCP (Model Context Protocol) servers work
- Assist with Japanese language study via Bunpro integration
Not affiliated with Bunpro. Built for educational purposes.
- Python 3.14 or later (matching Bunpro MCP dependencies)
- Astral's
uvCLI. Install it viapip install uvor follow https://github.com/astral-sh/uv, then runuv syncfrom the repo root before starting the server.
- Copy
.env.exampleto.envand supply your Bunpro frontend API token (BUNPRO_FRONTEND_API_TOKEN=line). Treat the token like any other secret; do not commit it. - If requests arrive through HTTP, the server first reads Bunpro's
frontend_api_tokencookie and uses that token for outbound Bunpro API calls. - For local runs without that cookie, the server falls back to
BUNPRO_FRONTEND_API_TOKEN, then to legacyBUNPRO_JWT.
BUNPRO_FRONTEND_API_TOKEN(preferred): Bunpro frontend API token used by every tool when no request cookie is present.BUNPRO_JWT(legacy alias): backward-compatible fallback forBUNPRO_FRONTEND_API_TOKEN.BUNPRO_API_BASE_URL(optional): override the Bunpro API root (defaults to built-in client value).BUNPRO_HOST(optional): host the server listens on (defaults to127.0.0.1).BUNPRO_PORT(optional): port the server listens on (defaults to8000).BUNPRO_STREAM_PATH(optional): MCP stream path (defaults to/mcp).
From the repo root, run the FastAPI app with the frontend API token on the same line so the server picks it up immediately. uv run reuses the environment that uv sync prepared for this repo.
BUNPRO_FRONTEND_API_TOKEN=your_frontend_api_token_here uv run server.py
OpenCode can connect to this server as an MCP client. The server runs at http://127.0.0.1:8000/mcp by default.
- Install OpenCode (see OpenCode docs)
- Use
.env.exampleto create a.envfile with your Bunpro API token - Start the MCP server:
uv --env-file .env run server.py
- Configure OpenCode to connect to the server. Add to your
opencode.jsonc: - Run
opencodeand start chatting with access to Bunpro tools
This repo includes a pre-configured agent at .opencode/agent/bunpro-tutor.md that provides Japanese tutoring assistance. To use it:
- Ensure the
.opencode/directory is in your project - In OpenCode, switch to the bunpro-tutor agent
- Ask questions about your Japanese study progress
Example prompt:
I'm currently reviewing. Why is 行きませんか used instead of 行きましょうか here?
Use the bundled MCP Inspector to list and invoke tools while the server is running:
BUNPRO_FRONTEND_API_TOKEN=your_frontend_api_token_here uv run mcp dev server.py
With the inspector running you can explore the registered tools, view their signatures, and pass arguments interactively from the UI.
Choose between the MCP Inspector and Python scripts for invoking tools. MCP Inspector exposes the registered tool list whenever you run uv run mcp dev server.py; you pick a tool name, fill its arguments, and send the request interactively. Automation or debugging can call the async tool functions directly with uv run python.
Replace your_frontend_api_token_here with the value you exported (and keep it out of version control):
search_grammar(query)uv run python - <<'PY' import asyncio from src.tools.grammar import search_grammar async def main(): print(await search_grammar("particles")) asyncio.run(main()) PY
get_grammar_point(slug)uv run python - <<'PY' import asyncio from src.tools.grammar import get_grammar_point async def main(): print(await get_grammar_point("particles-1")) asyncio.run(main()) PY
get_vocab_level()uv run python - <<'PY' import asyncio from src.tools.vocabulary import get_vocab_level async def main(): print(await get_vocab_level()) asyncio.run(main()) PY
get_vocab_items(vocab_slug_or_id)uv run python - <<'PY' import asyncio from src.tools.vocabulary import get_vocab_items async def main(): print(await get_vocab_items("genki-lesson-1")) asyncio.run(main()) PY
search_vocab(query, result_limit=None)uv run python - <<'PY' import asyncio from src.tools.vocabulary import search_vocab async def main(): print(await search_vocab("greetings", result_limit=10)) asyncio.run(main()) PY
get_reading_passages()uv run python - <<'PY' import asyncio from src.tools.reading import get_reading_passages async def main(): print(await get_reading_passages()) asyncio.run(main()) PY
search_reading_passages(query)uv run python - <<'PY' import asyncio from src.tools.reading import search_reading_passages async def main(): print(await search_reading_passages("vacation")) asyncio.run(main()) PY
get_user_stats()uv run python - <<'PY' import asyncio from src.tools.user_stats import get_user_stats async def main(): print(await get_user_stats()) asyncio.run(main()) PY
get_jlpt_progress()uv run python - <<'PY' import asyncio from src.tools.user_stats import get_jlpt_progress async def main(): print(await get_jlpt_progress()) asyncio.run(main()) PY
get_srs_forecast(granularity="daily")uv run python - <<'PY' import asyncio from src.tools.user_stats import get_srs_forecast async def main(): print(await get_srs_forecast(granularity="daily")) asyncio.run(main()) PY
get_study_configuration()uv run python - <<'PY' import asyncio from src.tools.review import get_study_configuration async def main(): print(await get_study_configuration()) asyncio.run(main()) PY
get_due_count()uv run python - <<'PY' import asyncio from src.tools.review import get_due_count async def main(): print(await get_due_count()) asyncio.run(main()) PY
get_pending_reviews()uv run python - <<'PY' import asyncio from src.tools.review import get_pending_reviews async def main(): print(await get_pending_reviews()) asyncio.run(main()) PY
{ "mcpServers": { "bunpro": { "url": "http://127.0.0.1:8000/mcp" } } }