A production-grade Model Context Protocol (MCP) server that decomposes competitive programming expertise into 23 modular, provider-independent tools — orchestrated by any MCP-compatible client.
- Why MCP?
- Architecture
- Tool Catalog
- Project Structure
- Getting Started
- Configuration
- Workflows
- How It Works
- Testing
- Contributing
Traditional AI coding assistants force all reasoning through one massive prompt. This approach suffers from:
| Problem | Impact |
|---|---|
| Monolithic prompts | Impossible to test, cache, or reuse individual capabilities |
| Provider lock-in | Switching from OpenAI → Ollama requires rewriting everything |
| No structured output | Raw text responses require fragile regex parsing |
| Redundant LLM calls | Identical problems re-analyzed every time |
This MCP server solves all four. Each capability is an independent tool with its own schema, prompt template, cache key, and validation pipeline.
┌──────────────────┐ MCP Protocol ┌──────────────────────────┐
│ Claude Desktop │◄─────────────────────►│ CP Mentor MCP Server │
│ Cursor IDE │ JSON-RPC / stdio │ │
│ Claude CLI │ │ 23 Tools · 3 Resources │
│ Any MCP Client │ │ 3 Prompts · 2-Tier Cache│
└──────────────────┘ └────────────┬─────────────┘
│
┌────────────▼─────────────┐
│ LLM Provider Layer │
│ ┌────────┐ ┌─────────┐ │
│ │ OpenAI │ │ Ollama │ │
│ │gpt-4o │ │llama3.1 │ │
│ └────────┘ └─────────┘ │
└──────────────────────────┘
graph TD
Client["MCP Client<br/>(Cursor · Claude Desktop · CLI)"]
Server["FastMCP Server<br/>server.py"]
Cache{"Two-Tier Cache<br/>Memory TTL + SQLite Disk"}
Prompt["Jinja2 Prompt Manager<br/>prompts/*.md"]
Provider["LLM Provider Factory<br/>OpenAI | Ollama"]
Validator{"Self-Correcting Validator<br/>Pydantic v2 Schemas"}
Formatter["Response Formatter<br/>+ _meta tracing"]
Client -->|"tool call (JSON-RPC)"| Server
Server -->|"check cache"| Cache
Cache -->|"HIT"| Formatter
Cache -->|"MISS"| Prompt
Prompt -->|"rendered prompt"| Provider
Provider -->|"raw JSON"| Validator
Validator -->|"schema fail → retry prompt"| Provider
Validator -->|"schema pass"| Cache
Cache -->|"store result"| Formatter
Formatter -->|"structured response"| Client
| Principle | Implementation |
|---|---|
| Tool-Service Decoupling | Tools contain zero business logic — they delegate to PromptManager → Provider → Validator → Cache → Formatter |
| Provider Independence | BaseLLMProvider abstract class ensures zero OpenAI/Ollama imports in tool code |
| Schema-First Validation | Every tool has a dedicated Pydantic BaseModel — LLM outputs are validated and auto-corrected |
| Two-Tier Caching | In-memory TTLCache (μs latency) + persistent diskcache (survives restarts) |
| Fail-Safe Registration | Each tool module is try/except imported — one broken tool doesn't crash the server |
| Tool | Description | Schema |
|---|---|---|
detect_patterns |
Identifies algorithmic patterns (DP, Graph, Greedy, Math, etc.) from problem text | PatternResponse |
extract_constraints |
Parses numeric bounds (N, M, K, Q) and system limits (time/memory) | ConstraintResponse |
estimate_difficulty |
Approximates competitive programming difficulty rating | DifficultyResponse |
identify_topics |
Tags primary/secondary topic categories | TopicsResponse |
| Tool | Description | Schema |
|---|---|---|
suggest_algorithms |
Recommends candidate algorithms based on constraints | AlgorithmsListResponse |
compare_algorithms |
Builds a trade-off comparison matrix across candidates | AlgorithmsComparisonResponse |
choose_best_algorithm |
Selects the optimal algorithm with justification | BestAlgorithmResponse |
estimate_runtime |
Calculates operation count vs. time budget feasibility | RuntimeEstimateResponse |
| Tool | Description | Schema |
|---|---|---|
generate_solution |
Produces optimized, contest-ready code in the target language | SolutionResponse |
generate_pseudocode |
Outputs language-agnostic structural pseudocode | PseudocodeResponse |
generate_multi_language |
Generates C++, Java, and Rust implementations simultaneously | MultiLangResponse |
| Tool | Description | Schema |
|---|---|---|
dry_run |
Traces variable states step-by-step through sample inputs | DryRunResponse |
prove_correctness |
Provides formal correctness proofs (loop invariants, induction) | CorrectnessProofResponse |
analyze_complexity |
Computes asymptotic time/space complexity with justification | ComplexityResponse |
| Tool | Description | Schema |
|---|---|---|
generate_testcases |
Creates input/output test pairs covering standard scenarios | TestcasesResponse |
generate_edge_cases |
Targets boundary conditions, zero-cases, and overflow scenarios | EdgeCasesResponse |
stress_testing |
Generates randomized brute-force stress test configurations | StressTestResponse |
| Tool | Description | Schema |
|---|---|---|
review_solution |
Full code review with correctness, efficiency, and style feedback | ReviewResponse |
find_bug |
Pinpoints logical, runtime, or compile-time bugs | BugResponse |
optimize_solution |
Suggests constant-factor and algorithmic optimizations | OptimizationResponse |
| Tool | Description | Schema |
|---|---|---|
get_hint |
Progressive hint system (nudge → approach → partial solution) | HintResponse |
explain_algorithm |
Educational breakdown with examples, when-to-use heuristics | ExplanationResponse |
recommend_next_problem |
Suggests follow-up problems to reinforce learned concepts | RecommendationResponse |
Competitive Programming Mentor MCP Server/
│
├── app.py # Entry point — mcp.run()
├── server.py # FastMCP app, tool/resource/prompt registration
├── config.py # Pydantic-settings configuration from .env
├── pyproject.toml # Dependencies & build config
├── .env.example # Environment template
│
├── services/ # Core business logic layer
│ ├── llm/
│ │ ├── base_provider.py # Abstract LLM interface
│ │ ├── openai_provider.py # OpenAI gpt-4o-mini adapter
│ │ ├── ollama_provider.py # Ollama local LLM adapter
│ │ └── provider_factory.py # Factory: .env → Provider instance
│ ├── prompt_manager.py # Jinja2 template renderer
│ ├── validator.py # Pydantic validation + self-correcting retry
│ ├── cache.py # Two-tier cache (TTLCache + diskcache)
│ ├── problem_parser.py # Regex constraint extractor (N, M, K, Q)
│ └── formatter.py # Response normalization + _meta tags
│
├── tools/ # MCP tool implementations (23 tools)
│ ├── analysis/ # detect_patterns, extract_constraints, ...
│ ├── planning/ # suggest_algorithms, compare_algorithms, ...
│ ├── generation/ # generate_solution, generate_pseudocode, ...
│ ├── verification/ # dry_run, prove_correctness, ...
│ ├── testing/ # generate_testcases, generate_edge_cases, ...
│ ├── review/ # review_solution, find_bug, optimize_solution
│ └── learning/ # get_hint, explain_algorithm, recommend_problem
│
├── schemas/ # Pydantic response models (one per tool)
├── prompts/ # Jinja2 markdown templates (one per tool + personas)
├── resources/ # Static knowledge base (Markdown files)
│ ├── algorithms/graphs/ # dijkstra.md
│ ├── data_structures/ # segment_tree.md
│ └── patterns/ # sliding_window.md
│
├── tests/ # Pytest test suite
│ ├── test_parser.py # Constraint parsing tests
│ ├── test_cache.py # Two-tier cache tests
│ └── test_validator.py # Schema validation + retry tests
│
└── docs/
├── ARCHITECTURE.md # System design documentation
└── TOOLS.md # Tool reference catalog
# Clone the repository
git clone https://github.com/your-username/competitive-programming-mcp.git
cd competitive-programming-mcp
# Install dependencies with uv
uv sync --all-extras
# Copy and configure environment
cp .env.example .env
# Edit .env with your API keys / Ollama settings# Start the MCP server (stdio transport)
uv run app.py
# Or run in development mode with auto-reload
fastmcp dev app.py
# Run tests
uv run pytestAll settings are managed via .env and loaded through Pydantic Settings:
# ─── LLM Provider ────────────────────────────────────
LLM_PROVIDER=openai # "openai" or "ollama"
# ─── OpenAI (when LLM_PROVIDER=openai) ───────────────
OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-4o-mini # ~$0.15/1M input tokens
OPENAI_MAX_TOKENS=4096
OPENAI_TEMPERATURE=0.2 # Low = deterministic code
# ─── Ollama (when LLM_PROVIDER=ollama) ────────────────
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=llama3.1:8b # Run: ollama pull llama3.1:8b
# ─── Cache ────────────────────────────────────────────
CACHE_ENABLED=true
CACHE_TTL_SECONDS=3600 # 1-hour TTL
CACHE_DISK_DIR=.cache # SQLite-backed persistent cache
# ─── Server ──────────────────────────────────────────
LOG_LEVEL=INFO # DEBUG | INFO | WARNING | ERRORAdd the following to your claude_desktop_config.json:
{
"mcpServers": {
"cp-mentor": {
"command": "uv",
"args": [
"--directory",
"YOUR_ABSOLUTE_PATH\\Competitive Programming Mentor MCP Server",
"run",
"app.py"
]
}
}
}Note on Config Locations:
- Standard Windows:
%APPDATA%\Claude\claude_desktop_config.json- Windows Store App:
C:\Users\YOUR_NAME\AppData\Local\Packages\Claude_pzs8sxrjxfjjc\LocalCache\Roaming\Claude\claude_desktop_config.json
Restart Claude Desktop completely. Note: In the latest versions of Claude Desktop, the plug (🔌) icon has been removed from the UI. MCP tools are simply loaded silently in the background. Just ask Claude to "Use your cp-mentor tools to solve X" and you will see an approval pop-up!
If you use the claude terminal tool, run:
claude mcp add cp-mentor uv --directory "/path/to/project" run app.py
claude # Start a sessionImportant for local models: If you are using
ollamawith the Claude CLI, make sure you launch it with a large enough model (e.g., 9B+ parameters) that supports tool calling. Small models (like 4B or 1B) will crash or fail to invoke MCP tools properly. Example:ollama launch claude --model qwen2.5:14b
- Open Cursor → Settings → Features → MCP
- Click + Add New MCP Server
- Set Name:
cp-mentor - Set Type:
command - Set Command:
uv --directory "/path/to/project" run app.py
User provides a problem (e.g., LeetCode / Codeforces)
│
▼
┌─────────────┐
│ Step 1 │ detect_patterns(problem)
│ Analyze │ extract_constraints(problem)
│ │ identify_topics(problem)
└──────┬──────┘
│
▼
┌─────────────┐
│ Step 2 │ suggest_algorithms(problem)
│ Plan │ compare_algorithms(problem, candidates)
│ │ choose_best_algorithm(problem, candidates)
└──────┬──────┘
│
▼
┌─────────────┐
│ Step 3 │ generate_solution(problem, approach, language)
│ Generate │ generate_pseudocode(problem)
└──────┬──────┘
│
▼
┌─────────────┐
│ Step 4 │ dry_run(problem, code)
│ Verify │ prove_correctness(problem)
│ │ analyze_complexity(problem, code)
└──────┬──────┘
│
▼
┌─────────────┐
│ Step 5 │ generate_testcases(problem)
│ Test │ generate_edge_cases(problem)
│ │ stress_testing(problem)
└──────┬──────┘
│
▼
┌─────────────┐
│ Step 6 │ review_solution(problem, code)
│ Review │ optimize_solution(problem, code)
└─────────────┘
When a client invokes detect_patterns(problem="..."), the following pipeline executes:
1. FastMCP receives JSON-RPC call via stdio transport
2. Tool function in tools/analysis/detect_patterns.py is invoked
3. CacheService.get("detect_patterns", {problem: "..."}) → checks memory, then disk
4. On MISS: PromptManager renders prompts/detect_patterns.md with Jinja2
5. ProviderFactory returns OpenAIProvider or OllamaProvider
6. Provider.structured_output(prompt, PatternResponse) calls the LLM API
7. Validator checks response against PatternResponse Pydantic schema
8. On validation failure: auto-correcting retry with error feedback prompt
9. CacheService.set() stores result in both memory and disk tiers
10. Formatter wraps response with _meta (tool name, model, cache status, latency)
11. Structured JSON returned to client
The validator implements a retry loop that feeds Pydantic validation errors back to the LLM:
# Simplified flow
for attempt in range(max_retries):
raw_json = await provider.structured_output(prompt, schema)
try:
return schema.model_validate_json(raw_json) # Success
except ValidationError as e:
prompt = f"Fix these errors: {e.errors()}\nOriginal: {raw_json}"
# Retry with corrective context ┌─────────────────┐
get(key) ────────►│ Memory (TTL) │──── HIT ────► return value
│ ~256 entries │
│ μs latency │
└───────┬─────────┘
│ MISS
┌───────▼─────────┐
│ Disk (SQLite) │──── HIT ────► promote to memory
│ Persistent │ + return value
│ ms latency │
└───────┬─────────┘
│ MISS
▼
Call LLM Provider
Cache keys are deterministic: f"{tool_name}:{sha256(sorted_json(inputs))[:16]}"
class BaseLLMProvider(ABC):
@abstractmethod
async def generate(self, prompt: str, system: str = "") -> str: ...
@abstractmethod
async def structured_output(self, prompt: str, response_model: type[T], system: str = "") -> T: ...
@property
@abstractmethod
def model_name(self) -> str: ...- OpenAIProvider: Uses
client.beta.chat.completions.parse()for native JSON schema generation - OllamaProvider: Uses
openai-compatible endpoint with regex JSON extraction fallback
# Run all tests
$env:PYTHONPATH="." # PowerShell
uv run pytest
# Run with verbose output
uv run pytest -v
# Test specific module
uv run pytest tests/test_parser.py
uv run pytest tests/test_cache.py
uv run pytest tests/test_validator.py| Module | Tests | What's Verified |
|---|---|---|
problem_parser |
2 | Constraint regex parsing (including 2 * 10^5 notation), default handling |
cache |
2 | Memory/disk hit/miss, tier promotion, Windows file lock cleanup |
validator |
2 | Schema compliance on first try, self-correcting retry on malformed JSON |
| Component | Technology | Purpose |
|---|---|---|
| MCP Framework | fastmcp >= 2.0 |
Server SDK, tool registration, stdio transport |
| LLM Client | openai >= 1.30 |
OpenAI + Ollama-compatible API calls |
| Validation | pydantic >= 2.7 |
Typed schemas for all 23 tool outputs |
| Configuration | pydantic-settings >= 2.3 |
.env → typed config singleton |
| Templating | jinja2 >= 3.1 |
Prompt template rendering |
| Memory Cache | cachetools >= 5.3 |
In-memory TTL cache |
| Disk Cache | diskcache >= 5.6 |
SQLite-backed persistent cache |
| Retry Logic | tenacity >= 8.3 |
Exponential backoff for LLM API calls |
| Testing | pytest + pytest-asyncio |
Async-compatible test framework |
This project is provided as-is for educational and competitive programming purposes.
Built with 🧠 by sami_codeai — Turning competitive programming into composable AI tools.