codeforces-mcp-avinash 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codeforces_mcp_avinash-0.1.0/PKG-INFO +47 -0
- codeforces_mcp_avinash-0.1.0/README.md +27 -0
- codeforces_mcp_avinash-0.1.0/pyproject.toml +38 -0
- codeforces_mcp_avinash-0.1.0/src/codeforces_mcp/__init__.py +0 -0
- codeforces_mcp_avinash-0.1.0/src/codeforces_mcp/client.py +64 -0
- codeforces_mcp_avinash-0.1.0/src/codeforces_mcp/sandbox.py +116 -0
- codeforces_mcp_avinash-0.1.0/src/codeforces_mcp/scraper.py +69 -0
- codeforces_mcp_avinash-0.1.0/src/codeforces_mcp/server.py +114 -0
- codeforces_mcp_avinash-0.1.0/tests/test_server.py +0 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: codeforces-mcp-avinash
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Codeforces MCP server for competitive programming, upsolving, and automated stress testing
|
|
5
|
+
Project-URL: Repository, https://github.com/avinash111921/codeforces-mcp
|
|
6
|
+
Author-email: Avinash Verma <avinash1921avi@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Requires-Dist: beautifulsoup4>=4.12.3
|
|
10
|
+
Requires-Dist: curl-cffi>=0.9.0
|
|
11
|
+
Requires-Dist: diskcache>=5.6.3
|
|
12
|
+
Requires-Dist: httpx>=0.28.0
|
|
13
|
+
Requires-Dist: mcp[cli]>=1.4.0
|
|
14
|
+
Requires-Dist: pydantic>=2.10.0
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest-asyncio>=0.25.0; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest>=8.3.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: respx>=0.22.0; extra == 'dev'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Codeforces MCP Server
|
|
22
|
+
|
|
23
|
+
A Model Context Protocol (MCP) server for Codeforces competitive programming. This server enables AI coding assistants (like Claude, Cursor, and Roo Code / Cline) to autonomously fetch problem statements, analyze your submission history, recommend upsolving targets, and run automated local stress tests.
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
- **Profile & Stats**: Fetch live user ratings, ranks, and recent verdict breakdowns.
|
|
27
|
+
- **Problem Scraping**: Extracts full problem descriptions and exact sample test cases.
|
|
28
|
+
- **Local Sandbox**: Compiles and executes C++ or Python solutions locally against official sample inputs.
|
|
29
|
+
- **Automated Stress Testing**: Isolates minimal failing test cases by running your optimized code against a brute-force script and a random generator.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
Add this to your `cline_mcp_settings.json` or Cursor MCP settings:
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"mcpServers": {
|
|
36
|
+
"codeforces-mcp": {
|
|
37
|
+
"command": "uvx",
|
|
38
|
+
"args": ["codeforces-mcp"]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
MIT
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
mcp-name: io.github.avinash111921/codeforces-mcp
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Codeforces MCP Server
|
|
2
|
+
|
|
3
|
+
A Model Context Protocol (MCP) server for Codeforces competitive programming. This server enables AI coding assistants (like Claude, Cursor, and Roo Code / Cline) to autonomously fetch problem statements, analyze your submission history, recommend upsolving targets, and run automated local stress tests.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
- **Profile & Stats**: Fetch live user ratings, ranks, and recent verdict breakdowns.
|
|
7
|
+
- **Problem Scraping**: Extracts full problem descriptions and exact sample test cases.
|
|
8
|
+
- **Local Sandbox**: Compiles and executes C++ or Python solutions locally against official sample inputs.
|
|
9
|
+
- **Automated Stress Testing**: Isolates minimal failing test cases by running your optimized code against a brute-force script and a random generator.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
Add this to your `cline_mcp_settings.json` or Cursor MCP settings:
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"mcpServers": {
|
|
16
|
+
"codeforces-mcp": {
|
|
17
|
+
"command": "uvx",
|
|
18
|
+
"args": ["codeforces-mcp"]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
## License
|
|
24
|
+
MIT
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
mcp-name: io.github.avinash111921/codeforces-mcp
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "codeforces-mcp-avinash"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Codeforces MCP server for competitive programming, upsolving, and automated stress testing"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
authors = [{ name = "Avinash Verma", email = "avinash1921avi@gmail.com" }]
|
|
12
|
+
urls = { Repository = "https://github.com/avinash111921/codeforces-mcp" }
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
dependencies = [
|
|
15
|
+
"mcp[cli]>=1.4.0",
|
|
16
|
+
"httpx>=0.28.0",
|
|
17
|
+
"curl-cffi>=0.9.0",
|
|
18
|
+
"beautifulsoup4>=4.12.3",
|
|
19
|
+
"pydantic>=2.10.0",
|
|
20
|
+
"diskcache>=5.6.3",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = [
|
|
25
|
+
"pytest>=8.3.0",
|
|
26
|
+
"pytest-asyncio>=0.25.0",
|
|
27
|
+
"respx>=0.22.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
codeforces-mcp = "codeforces_mcp.server:main"
|
|
32
|
+
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
asyncio_mode = "auto"
|
|
35
|
+
testpaths = ["tests"]
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/codeforces_mcp"]
|
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
import httpx
|
|
6
|
+
from diskcache import Cache
|
|
7
|
+
|
|
8
|
+
CACHE_DIR = Path.home() / ".cache" / "codeforces-mcp"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CodeforcesClient:
|
|
12
|
+
BASE_URL = "https://codeforces.com/api"
|
|
13
|
+
|
|
14
|
+
def __init__(self, rate_limit_delay: float = 2.1) -> None:
|
|
15
|
+
self.rate_limit_delay = rate_limit_delay
|
|
16
|
+
self._lock = asyncio.Lock()
|
|
17
|
+
self._last_call_time = 0.0
|
|
18
|
+
self.cache = Cache(str(CACHE_DIR))
|
|
19
|
+
self._http = httpx.AsyncClient(timeout=15.0)
|
|
20
|
+
|
|
21
|
+
async def _request(self, endpoint: str, params: dict[str, Any] | None = None) -> Any:
|
|
22
|
+
async with self._lock:
|
|
23
|
+
elapsed = time.monotonic() - self._last_call_time
|
|
24
|
+
if elapsed < self.rate_limit_delay:
|
|
25
|
+
await asyncio.sleep(self.rate_limit_delay - elapsed)
|
|
26
|
+
|
|
27
|
+
url = f"{self.BASE_URL}/{endpoint}"
|
|
28
|
+
response = await self._http.get(url, params=params)
|
|
29
|
+
self._last_call_time = time.monotonic()
|
|
30
|
+
|
|
31
|
+
response.raise_for_status()
|
|
32
|
+
data = response.json()
|
|
33
|
+
if data.get("status") != "OK":
|
|
34
|
+
raise RuntimeError(data.get("comment", "Unknown Codeforces API error"))
|
|
35
|
+
return data["result"]
|
|
36
|
+
|
|
37
|
+
async def get_user_info(self, handle: str) -> dict[str, Any]:
|
|
38
|
+
result = await self._request("user.info", {"handles": handle})
|
|
39
|
+
return result[0]
|
|
40
|
+
|
|
41
|
+
async def get_user_submissions(self, handle: str, count: int = 50) -> list[dict[str, Any]]:
|
|
42
|
+
return await self._request("user.status", {"handle": handle, "from": 1, "count": count})
|
|
43
|
+
|
|
44
|
+
async def get_problemset(self) -> dict[str, Any]:
|
|
45
|
+
cached = self.cache.get("problemset_all")
|
|
46
|
+
if cached is not None:
|
|
47
|
+
return cached
|
|
48
|
+
result = await self._request("problemset.problems")
|
|
49
|
+
# Cache the 10,000+ problem catalog for 1 hour
|
|
50
|
+
self.cache.set("problemset_all", result, expire=3600)
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
async def get_upcoming_contests(self) -> list[dict[str, Any]]:
|
|
54
|
+
cached = self.cache.get("upcoming_contests")
|
|
55
|
+
if cached is not None:
|
|
56
|
+
return cached
|
|
57
|
+
contests = await self._request("contest.list", {"gym": "false"})
|
|
58
|
+
upcoming = [c for c in contests if c.get("phase") == "BEFORE"]
|
|
59
|
+
upcoming.sort(key=lambda x: x.get("startTimeSeconds", 0))
|
|
60
|
+
self.cache.set("upcoming_contests", upcoming, expire=900)
|
|
61
|
+
return upcoming
|
|
62
|
+
|
|
63
|
+
async def close(self) -> None:
|
|
64
|
+
await self._http.aclose()
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import tempfile
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def compile_if_cpp(source_path: Path, work_dir: Path) -> list[str]:
|
|
9
|
+
if source_path.suffix == ".cpp":
|
|
10
|
+
exe_path = work_dir / ("sol.exe" if Path.cwd().drive else "sol.out")
|
|
11
|
+
cmd = ["g++", "-O2", "-std=c++20", str(source_path), "-o", str(exe_path)]
|
|
12
|
+
res = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
|
|
13
|
+
if res.returncode != 0:
|
|
14
|
+
raise RuntimeError(f"Compilation Error in {source_path.name}:\n{res.stderr}")
|
|
15
|
+
return [str(exe_path)]
|
|
16
|
+
elif source_path.suffix == ".py":
|
|
17
|
+
return ["python", str(source_path)]
|
|
18
|
+
raise ValueError(f"Unsupported file extension: {source_path.suffix}. Use .cpp or .py")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def execute_cmd(cmd: list[str], stdin_data: str, timeout_sec: float = 2.0) -> dict[str, Any]:
|
|
22
|
+
start = time.perf_counter()
|
|
23
|
+
try:
|
|
24
|
+
proc = subprocess.run(
|
|
25
|
+
cmd,
|
|
26
|
+
input=stdin_data,
|
|
27
|
+
capture_output=True,
|
|
28
|
+
text=True,
|
|
29
|
+
timeout=timeout_sec,
|
|
30
|
+
)
|
|
31
|
+
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
|
|
32
|
+
return {
|
|
33
|
+
"verdict": "OK" if proc.returncode == 0 else "RTE",
|
|
34
|
+
"stdout": proc.stdout.strip(),
|
|
35
|
+
"stderr": proc.stderr.strip(),
|
|
36
|
+
"time_ms": elapsed_ms,
|
|
37
|
+
}
|
|
38
|
+
except subprocess.TimeoutExpired:
|
|
39
|
+
return {"verdict": "TLE", "stdout": "", "stderr": "Time Limit Exceeded", "time_ms": timeout_sec * 1000}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def run_samples(source_file: str, samples: list[dict[str, str]]) -> dict[str, Any]:
|
|
43
|
+
src = Path(source_file).resolve()
|
|
44
|
+
if not src.exists():
|
|
45
|
+
raise FileNotFoundError(f"File not found: {src}")
|
|
46
|
+
|
|
47
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
48
|
+
cmd = compile_if_cpp(src, Path(tmp))
|
|
49
|
+
results = []
|
|
50
|
+
all_passed = True
|
|
51
|
+
|
|
52
|
+
for idx, sample in enumerate(samples, start=1):
|
|
53
|
+
run_res = execute_cmd(cmd, sample["input"])
|
|
54
|
+
expected = sample["output"].strip()
|
|
55
|
+
actual = run_res["stdout"]
|
|
56
|
+
passed = (run_res["verdict"] == "OK") and (actual == expected)
|
|
57
|
+
if not passed:
|
|
58
|
+
all_passed = False
|
|
59
|
+
|
|
60
|
+
results.append({
|
|
61
|
+
"test_case": idx,
|
|
62
|
+
"status": "PASSED" if passed else (run_res["verdict"] if run_res["verdict"] != "OK" else "WA"),
|
|
63
|
+
"time_ms": run_res["time_ms"],
|
|
64
|
+
"input": sample["input"],
|
|
65
|
+
"expected_output": expected,
|
|
66
|
+
"actual_output": actual,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
return {"all_passed": all_passed, "results": results}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def run_stress_test(
|
|
73
|
+
optimal_file: str,
|
|
74
|
+
brute_file: str,
|
|
75
|
+
generator_file: str,
|
|
76
|
+
iterations: int = 100,
|
|
77
|
+
) -> dict[str, Any]:
|
|
78
|
+
opt_path = Path(optimal_file).resolve()
|
|
79
|
+
brute_path = Path(brute_file).resolve()
|
|
80
|
+
gen_path = Path(generator_file).resolve()
|
|
81
|
+
|
|
82
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
83
|
+
tmp_dir = Path(tmp)
|
|
84
|
+
opt_cmd = compile_if_cpp(opt_path, tmp_dir / "opt") if opt_path.suffix == ".cpp" else ["python", str(opt_path)]
|
|
85
|
+
if opt_path.suffix == ".cpp":
|
|
86
|
+
(tmp_dir / "opt").mkdir(exist_ok=True)
|
|
87
|
+
opt_cmd = compile_if_cpp(opt_path, tmp_dir / "opt")
|
|
88
|
+
|
|
89
|
+
if brute_path.suffix == ".cpp":
|
|
90
|
+
(tmp_dir / "brute").mkdir(exist_ok=True)
|
|
91
|
+
brute_cmd = compile_if_cpp(brute_path, tmp_dir / "brute")
|
|
92
|
+
else:
|
|
93
|
+
brute_cmd = ["python", str(brute_path)]
|
|
94
|
+
|
|
95
|
+
gen_cmd = ["python", str(gen_path)]
|
|
96
|
+
|
|
97
|
+
for i in range(1, min(iterations, 500) + 1):
|
|
98
|
+
gen_res = execute_cmd(gen_cmd, str(i))
|
|
99
|
+
if gen_res["verdict"] != "OK":
|
|
100
|
+
raise RuntimeError(f"Generator crashed on iteration {i}: {gen_res['stderr']}")
|
|
101
|
+
|
|
102
|
+
test_input = gen_res["stdout"]
|
|
103
|
+
opt_res = execute_cmd(opt_cmd, test_input)
|
|
104
|
+
brute_res = execute_cmd(brute_cmd, test_input)
|
|
105
|
+
|
|
106
|
+
if opt_res["verdict"] != "OK" or opt_res["stdout"] != brute_res["stdout"]:
|
|
107
|
+
return {
|
|
108
|
+
"found_counterexample": True,
|
|
109
|
+
"iteration": i,
|
|
110
|
+
"failing_input": test_input,
|
|
111
|
+
"expected_brute_output": brute_res["stdout"],
|
|
112
|
+
"actual_optimal_output": opt_res["stdout"],
|
|
113
|
+
"optimal_verdict": opt_res["verdict"],
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {"found_counterexample": False, "iterations_tested": iterations}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Any
|
|
3
|
+
from bs4 import BeautifulSoup
|
|
4
|
+
from curl_cffi.requests import AsyncSession
|
|
5
|
+
from diskcache import Cache
|
|
6
|
+
from .client import CACHE_DIR
|
|
7
|
+
|
|
8
|
+
scraper_cache = Cache(str(CACHE_DIR / "problems"))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _parse_problem_html(html: str, contest_id: int, index: str) -> dict[str, Any]:
|
|
12
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
13
|
+
statement = soup.select_one("div.problem-statement")
|
|
14
|
+
if not statement:
|
|
15
|
+
raise ValueError(f"Could not locate problem statement for {contest_id}{index}.")
|
|
16
|
+
|
|
17
|
+
header = statement.select_one("div.header")
|
|
18
|
+
title = header.select_one("div.title").get_text(strip=True) if header else f"{contest_id}{index}"
|
|
19
|
+
time_limit = (
|
|
20
|
+
header.select_one("div.time-limit").get_text(separator=" ", strip=True)
|
|
21
|
+
if header else "Unknown"
|
|
22
|
+
)
|
|
23
|
+
memory_limit = (
|
|
24
|
+
header.select_one("div.memory-limit").get_text(separator=" ", strip=True)
|
|
25
|
+
if header else "Unknown"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Extract sample test cases preserving exact newlines
|
|
29
|
+
samples: list[dict[str, str]] = []
|
|
30
|
+
inputs = statement.select("div.sample-test div.input pre")
|
|
31
|
+
outputs = statement.select("div.sample-test div.output pre")
|
|
32
|
+
|
|
33
|
+
for inp, out in zip(inputs, outputs):
|
|
34
|
+
in_lines = [div.get_text() for div in inp.select("div.test-example-line")]
|
|
35
|
+
in_text = "\n".join(in_lines) if in_lines else inp.get_text("\n", strip=True)
|
|
36
|
+
out_text = out.get_text("\n", strip=True)
|
|
37
|
+
samples.append({"input": in_text.strip(), "output": out_text.strip()})
|
|
38
|
+
|
|
39
|
+
# Extract body paragraphs
|
|
40
|
+
body_parts = []
|
|
41
|
+
for div in statement.find_all("div", recursive=False):
|
|
42
|
+
classes = div.get("class") or []
|
|
43
|
+
if "header" not in classes and "sample-test" not in classes:
|
|
44
|
+
body_parts.append(div.get_text("\n", strip=True))
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
"problem_id": f"{contest_id}{index.upper()}",
|
|
48
|
+
"title": title,
|
|
49
|
+
"time_limit": time_limit,
|
|
50
|
+
"memory_limit": memory_limit,
|
|
51
|
+
"description": "\n\n".join(body_parts),
|
|
52
|
+
"samples": samples,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def fetch_problem_statement(contest_id: int, index: str) -> dict[str, Any]:
|
|
57
|
+
key = f"prob_{contest_id}_{index.upper()}"
|
|
58
|
+
cached = scraper_cache.get(key)
|
|
59
|
+
if cached is not None:
|
|
60
|
+
return cached
|
|
61
|
+
|
|
62
|
+
url = f"https://codeforces.com/contest/{contest_id}/problem/{index.upper()}"
|
|
63
|
+
async with AsyncSession(impersonate="chrome124", timeout=15) as session:
|
|
64
|
+
response = await session.get(url)
|
|
65
|
+
if response.status_code != 200:
|
|
66
|
+
raise RuntimeError(f"Failed to fetch problem page (HTTP {response.status_code})")
|
|
67
|
+
parsed = await asyncio.to_thread(_parse_problem_html, response.text, contest_id, index)
|
|
68
|
+
scraper_cache.set(key, parsed)
|
|
69
|
+
return parsed
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from mcp.server.mcpserver import MCPServer
|
|
3
|
+
from mcp.types import ToolAnnotations
|
|
4
|
+
from .client import CodeforcesClient
|
|
5
|
+
from .scraper import fetch_problem_statement
|
|
6
|
+
from .sandbox import run_samples, run_stress_test
|
|
7
|
+
|
|
8
|
+
mcp = MCPServer("Codeforces-MCP")
|
|
9
|
+
cf_client = CodeforcesClient()
|
|
10
|
+
|
|
11
|
+
READ_ONLY = ToolAnnotations(readOnlyHint=True, destructiveHint=False, idempotentHint=True)
|
|
12
|
+
LOCAL_EXEC = ToolAnnotations(readOnlyHint=False, destructiveHint=False)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@mcp.tool(annotations=READ_ONLY)
|
|
16
|
+
async def get_user_profile(handle: str) -> dict[str, Any]:
|
|
17
|
+
"""Fetch a Codeforces user's current rating, max rating, rank, and recent verdict breakdown."""
|
|
18
|
+
info = await cf_client.get_user_info(handle)
|
|
19
|
+
submissions = await cf_client.get_user_submissions(handle, count=100)
|
|
20
|
+
|
|
21
|
+
verdicts: dict[str, int] = {}
|
|
22
|
+
weak_tags: dict[str, int] = {}
|
|
23
|
+
|
|
24
|
+
for sub in submissions:
|
|
25
|
+
v = sub.get("verdict", "UNKNOWN")
|
|
26
|
+
verdicts[v] = verdicts.get(v, 0) + 1
|
|
27
|
+
if v != "OK":
|
|
28
|
+
for tag in sub.get("problem", {}).get("tags", []):
|
|
29
|
+
weak_tags[tag] = weak_tags.get(tag, 0) + 1
|
|
30
|
+
|
|
31
|
+
top_weak_tags = sorted(weak_tags.items(), key=lambda x: x[1], reverse=True)[:5]
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
"handle": info.get("handle"),
|
|
35
|
+
"rating": info.get("rating", "Unrated"),
|
|
36
|
+
"maxRating": info.get("maxRating", "Unrated"),
|
|
37
|
+
"rank": info.get("rank", "Unrated"),
|
|
38
|
+
"recent_100_verdicts": verdicts,
|
|
39
|
+
"most_failed_tags_recently": [t[0] for t in top_weak_tags],
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@mcp.tool(annotations=READ_ONLY)
|
|
44
|
+
async def get_problem(contest_id: int, index: str) -> dict[str, Any]:
|
|
45
|
+
"""Fetch the full problem statement, constraints, and sample test cases for a Codeforces problem."""
|
|
46
|
+
return await fetch_problem_statement(contest_id, index)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@mcp.tool(annotations=READ_ONLY)
|
|
50
|
+
async def recommend_upsolve_problems(
|
|
51
|
+
handle: str,
|
|
52
|
+
tag: str | None = None,
|
|
53
|
+
min_rating: int = 1600,
|
|
54
|
+
max_rating: int = 1900,
|
|
55
|
+
limit: int = 5,
|
|
56
|
+
) -> list[dict[str, Any]]:
|
|
57
|
+
"""Recommend unsolved Codeforces problems filtered by rating range and optional topic tag."""
|
|
58
|
+
subs = await cf_client.get_user_submissions(handle, count=500)
|
|
59
|
+
solved = {
|
|
60
|
+
f"{s['problem'].get('contestId')}{s['problem'].get('index')}"
|
|
61
|
+
for s in subs
|
|
62
|
+
if s.get("verdict") == "OK" and "problem" in s
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
catalog = await cf_client.get_problemset()
|
|
66
|
+
recommendations = []
|
|
67
|
+
|
|
68
|
+
for prob in catalog.get("problems", []):
|
|
69
|
+
pid = f"{prob.get('contestId')}{prob.get('index')}"
|
|
70
|
+
rating = prob.get("rating", 0)
|
|
71
|
+
tags = prob.get("tags", [])
|
|
72
|
+
|
|
73
|
+
if pid in solved or not (min_rating <= rating <= max_rating):
|
|
74
|
+
continue
|
|
75
|
+
if tag and tag.lower() not in [t.lower() for t in tags]:
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
recommendations.append({
|
|
79
|
+
"problem_id": pid,
|
|
80
|
+
"name": prob.get("name"),
|
|
81
|
+
"rating": rating,
|
|
82
|
+
"tags": tags,
|
|
83
|
+
"url": f"https://codeforces.com/contest/{prob.get('contestId')}/problem/{prob.get('index')}",
|
|
84
|
+
})
|
|
85
|
+
if len(recommendations) >= limit:
|
|
86
|
+
break
|
|
87
|
+
|
|
88
|
+
return recommendations
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@mcp.tool(annotations=LOCAL_EXEC)
|
|
92
|
+
async def test_local_solution(source_file: str, contest_id: int, index: str) -> dict[str, Any]:
|
|
93
|
+
"""Compile and run a local .cpp or .py file against official Codeforces sample test cases."""
|
|
94
|
+
problem = await fetch_problem_statement(contest_id, index)
|
|
95
|
+
return run_samples(source_file, problem["samples"])
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@mcp.tool(annotations=LOCAL_EXEC)
|
|
99
|
+
async def find_counterexample(
|
|
100
|
+
optimal_file: str,
|
|
101
|
+
brute_file: str,
|
|
102
|
+
generator_file: str,
|
|
103
|
+
iterations: int = 100,
|
|
104
|
+
) -> dict[str, Any]:
|
|
105
|
+
"""Stress test an optimal solution against a brute-force solution using a generator script."""
|
|
106
|
+
return run_stress_test(optimal_file, brute_file, generator_file, iterations)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def main() -> None:
|
|
110
|
+
mcp.run()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
main()
|
|
File without changes
|