ai-coding-gym-mcp 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,254 @@
1
+ Metadata-Version: 2.4
2
+ Name: ai-coding-gym-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for AI Coding Gym - fetch and submit coding challenges
5
+ Home-page: https://github.com/yourusername/ai-coding-gym-mcp
6
+ Author: AICodingGym Team
7
+ Author-email: Your Name <your.email@example.com>
8
+ License: MIT
9
+ Project-URL: Homepage, https://github.com/yourusername/ai-coding-gym-mcp
10
+ Project-URL: Issues, https://github.com/yourusername/ai-coding-gym-mcp/issues
11
+ Keywords: mcp,model-context-protocol,coding-gym,ai
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: mcp>=0.9.0
22
+ Requires-Dist: requests>=2.31.0
23
+ Dynamic: author
24
+ Dynamic: home-page
25
+ Dynamic: requires-python
26
+
27
+ # AI Coding Gym - MCP Server
28
+
29
+ Local MCP server for interacting with the AI Coding Gym platform. Provides tools to fetch coding problems and submit solutions.
30
+
31
+ ## Features
32
+
33
+ - **`/fetch`**: Fetch a coding problem and clone the repository to your local machine
34
+ - **`/submit`**: Submit your solution by committing and pushing changes
35
+
36
+ ## Quick Start
37
+
38
+ ### Installation
39
+
40
+ **Option 1: Install from PyPI**
41
+ ```bash
42
+ pip install ai-coding-gym-mcp
43
+ ```
44
+
45
+ **Option 2: Install from GitHub**
46
+ ```bash
47
+ pip install git+https://github.com/yourusername/ai-coding-gym-mcp.git
48
+ ```
49
+
50
+ **Option 3: Install from source**
51
+ ```bash
52
+ git clone https://github.com/yourusername/ai-coding-gym-mcp.git
53
+ cd ai-coding-gym-mcp
54
+ pip install -e .
55
+ ```
56
+
57
+ ### Configure Claude Desktop
58
+
59
+ Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on Mac):
60
+
61
+ ```json
62
+ {
63
+ "mcpServers": {
64
+ "ai-coding-gym": {
65
+ "command": "python",
66
+ "args": ["-m", "server"],
67
+ "env": {
68
+ "AI_CODING_GYM_SERVER": "https://your-server-url.com"
69
+ }
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+ See [INSTALLATION.md](INSTALLATION.md) for detailed setup instructions for Claude Desktop and VS Code.
76
+
77
+ ## Usage
78
+
79
+ ### Running the MCP Server
80
+
81
+ The server uses stdio for communication with MCP clients:
82
+
83
+ ```bash
84
+ python server.py
85
+ ```
86
+
87
+ Or configure it in your MCP client settings (e.g., Claude Desktop).
88
+
89
+ ### Tool: `/fetch`
90
+
91
+ Fetches a problem from the backend and clones the repository locally.
92
+
93
+ **Parameters:**
94
+ - `problem_id` (required): Problem identifier (e.g., `"django__django-10097"`)
95
+ - `user_id` (required): Your user ID for authentication
96
+ - `server_url` (optional): Backend server URL (default: `"https://api.example.com"`)
97
+ - `workspace_dir` (optional): Local workspace directory (default: `"./workspace"`)
98
+
99
+ **Example:**
100
+ ```json
101
+ {
102
+ "problem_id": "django__django-10097",
103
+ "user_id": "user_123",
104
+ "server_url": "https://ai-coding-gym.example.com"
105
+ }
106
+ ```
107
+
108
+ **What it does:**
109
+ 1. Calls backend API to get repository URL and deployment key
110
+ 2. Clones the repository to `workspace/{problem_id}/`
111
+ 3. Checks out the problem-specific branch
112
+ 4. Saves the problem statement to `PROBLEM_STATEMENT.md`
113
+
114
+ ### Tool: `/submit`
115
+
116
+ Submits your solution by committing changes and pushing to the remote repository.
117
+
118
+ **Parameters:**
119
+ - `problem_id` (required): Problem identifier
120
+ - `user_id` (required): Your user ID for authentication
121
+ - `server_url` (optional): Backend server URL (default: `"https://api.example.com"`)
122
+ - `commit_message` (optional): Custom commit message
123
+
124
+ **Example:**
125
+ ```json
126
+ {
127
+ "problem_id": "django__django-10097",
128
+ "user_id": "user_123",
129
+ "commit_message": "Fixed the authentication bug"
130
+ }
131
+ ```
132
+
133
+ **What it does:**
134
+ 1. Stages all changes in the working directory (`git add -A`)
135
+ 2. Commits with the provided or auto-generated message
136
+ 3. Pushes to the remote branch using deployment key
137
+ 4. Notifies the backend server about the submission
138
+
139
+ ## Backend API Endpoints
140
+
141
+ The MCP server expects the following backend API endpoints:
142
+
143
+ ### POST `/api/fetch-problem`
144
+
145
+ **Request:**
146
+ ```json
147
+ {
148
+ "problem_id": "django__django-10097",
149
+ "user_id": "user_123"
150
+ }
151
+ ```
152
+
153
+ **Response:**
154
+ ```json
155
+ {
156
+ "repo_url": "git@github.com:org/repo.git",
157
+ "branch": "django__django-10097-user_123",
158
+ "deploy_key": "-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----",
159
+ "problem_statement": "# Problem Description\n\n..."
160
+ }
161
+ ```
162
+
163
+ ### POST `/api/submit`
164
+
165
+ **Request:**
166
+ ```json
167
+ {
168
+ "problem_id": "django__django-10097",
169
+ "user_id": "user_123",
170
+ "commit_hash": "abc123def456...",
171
+ "branch": "django__django-10097-user_123",
172
+ "timestamp": "2026-02-03T10:30:00"
173
+ }
174
+ ```
175
+
176
+ **Response:**
177
+ ```json
178
+ {
179
+ "status": "success",
180
+ "message": "Submission received"
181
+ }
182
+ ```
183
+
184
+ ## Security
185
+
186
+ - Deployment keys are stored in `~/.mcp-keys/` with 600 permissions
187
+ - Keys are scoped per problem and managed by the backend
188
+ - SSH host key checking is disabled for convenience (consider enabling in production)
189
+ - Credentials are cached in memory during the MCP server session
190
+
191
+ ## Configuration
192
+
193
+ Default server URL is `https://api.example.com`. You can override it by passing `server_url` parameter to each tool call, or set it via environment variable:
194
+
195
+ ```bash
196
+ export AI_CODING_GYM_SERVER="https://your-server.com"
197
+ ```
198
+
199
+ ## Troubleshooting
200
+
201
+ **"No credentials found for problem_id"**
202
+ - Run `/fetch` first to download the problem and credentials
203
+
204
+ **"Git clone/push failed"**
205
+ - Check network connectivity
206
+ - Verify deployment key is valid
207
+ - Ensure SSH agent isn't interfering
208
+
209
+ **"Directory already exists"**
210
+ - Remove the existing directory or use a different workspace location
211
+
212
+ ## Publishing
213
+
214
+ See [PUBLISHING.md](PUBLISHING.md) for instructions on:
215
+ - Publishing to PyPI
216
+ - Publishing to GitHub
217
+ - Version management
218
+ - Release workflow
219
+
220
+ ## Development
221
+
222
+ The server uses:
223
+ - **mcp**: Model Context Protocol SDK
224
+ - **requests**: HTTP client for backend API calls
225
+ - **subprocess**: Git command execution with SSH key management
226
+
227
+ ### Local Development
228
+
229
+ ```bash
230
+ # Clone the repository
231
+ git clone https://github.com/yourusername/ai-coding-gym-mcp.git
232
+ cd ai-coding-gym-mcp
233
+
234
+ # Install in development mode
235
+ pip install -e .
236
+
237
+ # Run tests (if available)
238
+ pytest
239
+
240
+ # Test the server locally
241
+ python server.py
242
+ ```
243
+
244
+ ## Contributing
245
+
246
+ Contributions welcome! Please:
247
+ 1. Fork the repository
248
+ 2. Create a feature branch
249
+ 3. Make your changes
250
+ 4. Submit a pull request
251
+
252
+ ## License
253
+
254
+ MIT
@@ -0,0 +1,6 @@
1
+ server.py,sha256=4MZVe8BZFE-qSua-fzBD8hHNwEur7nPqklcmfIwR15I,18691
2
+ ai_coding_gym_mcp-0.1.0.dist-info/METADATA,sha256=bLTu2JVY10OYjrfPl3EzuZ06nvxVGhdIytgOexsap20,6377
3
+ ai_coding_gym_mcp-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
4
+ ai_coding_gym_mcp-0.1.0.dist-info/entry_points.txt,sha256=O_Ya91fge6V-bwpf4oace2eFB2asJVEE4Oe3fWcL6dY,50
5
+ ai_coding_gym_mcp-0.1.0.dist-info/top_level.txt,sha256=StKOSmRhvWS5IPcvhsDRbtxUTEofJgYFGOu5AAJdSWo,7
6
+ ai_coding_gym_mcp-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ai-coding-gym-mcp = server:main
@@ -0,0 +1 @@
1
+ server
server.py ADDED
@@ -0,0 +1,532 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MCP Server for AI Coding Gym
4
+ Provides /fetch and /submit tools for local problem solving workflow
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import os
10
+ import subprocess
11
+ import tempfile
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+ from typing import Any, Dict, Optional
15
+ import requests
16
+
17
+ from mcp.server import Server
18
+ from mcp.types import Tool, TextContent, ErrorData
19
+ import mcp.server.stdio
20
+
21
+
22
+ # Global state
23
+ app = Server("ai-coding-gym")
24
+ credentials_store: Dict[str, Dict[str, Any]] = {} # problem_id -> {deploy_key, repo_url, branch, etc}
25
+ config_store: Dict[str, str] = {} # Global config: user_id, server_url, api_key
26
+
27
+ # Default server URL
28
+ DEFAULT_SERVER_URL = "http://ai-coding-gym.cis240515.projects.jetstream-cloud.org:4000"
29
+
30
+
31
+ # Tool definitions
32
+ CONFIGURE_TOOL = Tool(
33
+ name="configure",
34
+ description="Configure global settings for the MCP server. Generates an SSH key pair locally, "
35
+ "sends the public key to the server, and receives your repository name. "
36
+ "Set your user_id, server_url, and optional api_key once. "
37
+ "These values will be used as defaults for all fetch and submit operations.",
38
+ inputSchema={
39
+ "type": "object",
40
+ "properties": {
41
+ "user_id": {
42
+ "type": "string",
43
+ "description": "Your user ID for authentication with the backend server"
44
+ },
45
+ "server_url": {
46
+ "type": "string",
47
+ "description": "Backend server URL (e.g., 'https://api.example.com')"
48
+ },
49
+ "api_key": {
50
+ "type": "string",
51
+ "description": "API key for backend authentication (optional)"
52
+ },
53
+ "workspace_dir": {
54
+ "type": "string",
55
+ "description": "Default workspace directory for cloning repositories (optional)"
56
+ }
57
+ },
58
+ "required": ["user_id", "server_url"]
59
+ }
60
+ )
61
+
62
+ FETCH_TOOL = Tool(
63
+ name="fetch",
64
+ description="Fetch a coding problem from the server and clone the repository locally. "
65
+ "Uses your SSH key from /configure to access the repository.",
66
+ inputSchema={
67
+ "type": "object",
68
+ "properties": {
69
+ "problem_id": {
70
+ "type": "string",
71
+ "description": "The unique identifier for the problem (e.g., 'django__django-10097')"
72
+ },
73
+ "user_id": {
74
+ "type": "string",
75
+ "description": "Your user ID (optional if configured globally via /configure)"
76
+ },
77
+ "server_url": {
78
+ "type": "string",
79
+ "description": "Backend server URL (optional if configured globally)"
80
+ },
81
+ "workspace_dir": {
82
+ "type": "string",
83
+ "description": "Local directory to clone the repository into (optional)"
84
+ }
85
+ },
86
+ "required": ["problem_id"]
87
+ }
88
+ )
89
+
90
+ SUBMIT_TOOL = Tool(
91
+ name="submit",
92
+ description="Submit your solution by committing all changes and pushing to the remote repository. "
93
+ "Notifies the backend server that a submission has been made.",
94
+ inputSchema={
95
+ "type": "object",
96
+ "properties": {
97
+ "problem_id": {
98
+ "type": "string",
99
+ "description": "The unique identifier for the problem"
100
+ },
101
+ "user_id": {
102
+ "type": "string",
103
+ "description": "Your user ID (optional if configured globally via /configure)"
104
+ },
105
+ "server_url": {
106
+ "type": "string",
107
+ "description": "Backend server URL (optional if configured globally)"
108
+ },
109
+ "commit_message": {
110
+ "type": "string",
111
+ "description": "Custom commit message (optional)"
112
+ }
113
+ },
114
+ "required": ["problem_id"]
115
+ }
116
+ )
117
+
118
+
119
+ # Helper functions for Git SSH key management
120
+ def infer_user_id_from_keys() -> Optional[str]:
121
+ """
122
+ Infer user_id by scanning for SSH key files in ~/.mcp-keys/.
123
+ Returns the user_id if found, None otherwise.
124
+ """
125
+ key_dir = Path.home() / ".mcp-keys"
126
+ if not key_dir.exists():
127
+ return None
128
+
129
+ # Look for *_id_rsa files
130
+ key_files = list(key_dir.glob("*_id_rsa"))
131
+ if not key_files:
132
+ return None
133
+
134
+ # Extract user_id from the first key file found
135
+ key_file = key_files[0]
136
+ user_id = key_file.stem.replace("_id_rsa", "")
137
+ return user_id
138
+
139
+
140
+ def generate_ssh_key_pair(user_id: str) -> tuple[Path, str]:
141
+ """
142
+ Generate an SSH key pair for the user.
143
+ Returns tuple of (private_key_path, public_key_content)
144
+ """
145
+ key_dir = Path.home() / ".mcp-keys"
146
+ key_dir.mkdir(mode=0o700, exist_ok=True)
147
+
148
+ key_path = key_dir / f"{user_id}_id_rsa"
149
+
150
+ # Generate SSH key pair if it doesn't exist
151
+ if not key_path.exists():
152
+ result = subprocess.run(
153
+ ["ssh-keygen", "-t", "rsa", "-b", "4096", "-f", str(key_path), "-N", "", "-C", f"mcp-{user_id}"],
154
+ capture_output=True,
155
+ text=True
156
+ )
157
+ if result.returncode != 0:
158
+ raise RuntimeError(f"Failed to generate SSH key: {result.stderr}")
159
+
160
+ # Read public key
161
+ pub_key_path = Path(str(key_path) + ".pub")
162
+ public_key = pub_key_path.read_text().strip()
163
+
164
+ return key_path, public_key
165
+
166
+
167
+ def setup_deploy_key(problem_id: str, deploy_key: str) -> Path:
168
+ """
169
+ Write deployment key to a temporary file with secure permissions.
170
+ Returns the path to the key file.
171
+ """
172
+ key_dir = Path.home() / ".mcp-keys"
173
+ key_dir.mkdir(mode=0o700, exist_ok=True)
174
+
175
+ key_path = key_dir / f"{problem_id}_deploy_key"
176
+ key_path.write_text(deploy_key)
177
+ key_path.chmod(0o600)
178
+
179
+ return key_path
180
+
181
+
182
+ def cleanup_deploy_key(problem_id: str) -> None:
183
+ """Remove deployment key file for a problem."""
184
+ key_path = Path.home() / ".mcp-keys" / f"{problem_id}_deploy_key"
185
+ if key_path.exists():
186
+ key_path.unlink()
187
+
188
+
189
+ def run_git_command(cmd: str, cwd: str, key_path: Optional[Path] = None) -> subprocess.CompletedProcess:
190
+ """
191
+ Execute a git command with optional SSH key configuration.
192
+ """
193
+ env = os.environ.copy()
194
+ if key_path:
195
+ env["GIT_SSH_COMMAND"] = f"ssh -i {key_path} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
196
+
197
+ result = subprocess.run(
198
+ cmd,
199
+ shell=True,
200
+ cwd=cwd,
201
+ capture_output=True,
202
+ text=True,
203
+ env=env
204
+ )
205
+ return result
206
+
207
+
208
+ # Tool implementations
209
+ async def configure(
210
+ user_id: str,
211
+ server_url: str,
212
+ api_key: Optional[str] = None,
213
+ workspace_dir: Optional[str] = None
214
+ ) -> str:
215
+ """Configure global settings for the MCP server."""
216
+ try:
217
+ # Generate SSH key pair
218
+ private_key_path, public_key = generate_ssh_key_pair(user_id)
219
+
220
+ # Register public key with server
221
+ api_endpoint = f"{server_url}/api/configure"
222
+ payload = {
223
+ "user_id": user_id,
224
+ "public_key": public_key
225
+ }
226
+
227
+ if api_key:
228
+ payload["api_key"] = api_key
229
+
230
+ response = requests.post(api_endpoint, json=payload, timeout=30)
231
+ response.raise_for_status()
232
+ data = response.json()
233
+
234
+ repo_name = data.get("repo_name")
235
+ if not repo_name:
236
+ return "Error: Server did not return a repository name"
237
+
238
+ # Store configuration
239
+ config_store["user_id"] = user_id
240
+ config_store["server_url"] = server_url
241
+ config_store["repo_name"] = repo_name
242
+ config_store["private_key_path"] = str(private_key_path)
243
+
244
+ if api_key:
245
+ config_store["api_key"] = api_key
246
+
247
+ if workspace_dir:
248
+ config_store["workspace_dir"] = workspace_dir
249
+
250
+ return f"""Configuration saved successfully!
251
+
252
+ User ID: {user_id}
253
+ Server URL: {server_url}
254
+ Repository: {repo_name}
255
+ API Key: {'Set' if api_key else 'Not set'}
256
+ Workspace: {workspace_dir or 'Default (./workspace)'}
257
+ SSH Key: {private_key_path}
258
+
259
+ Your public key has been registered with the server.
260
+ You can now use /fetch and /submit without passing these parameters.
261
+ """
262
+
263
+ except requests.RequestException as e:
264
+ return f"Error: Failed to register with server: {str(e)}"
265
+ except Exception as e:
266
+ return f"Error: Configuration failed: {str(e)}"
267
+
268
+
269
+ async def fetch_problem(
270
+ problem_id: str,
271
+ user_id: Optional[str] = None,
272
+ server_url: Optional[str] = None,
273
+ workspace_dir: Optional[str] = None
274
+ ) -> str:
275
+ """
276
+ Fetch problem by cloning only the specific branch from the forked repository.
277
+ """
278
+ try:
279
+ # Use configured values if not provided
280
+ user_id = user_id or config_store.get("user_id") or infer_user_id_from_keys()
281
+ server_url = server_url or config_store.get("server_url") or DEFAULT_SERVER_URL
282
+ workspace_dir = workspace_dir or config_store.get("workspace_dir", "./workspace")
283
+ repo_name = config_store.get("repo_name")
284
+
285
+ if not user_id:
286
+ return "Error: Could not determine user_id. Please run /configure first to set up your credentials."
287
+
288
+ if not repo_name:
289
+ return "Error: Repository not configured. Please run /configure first."
290
+
291
+ # Get user's private key path
292
+ private_key_path = config_store.get("private_key_path")
293
+ if not private_key_path:
294
+ # Try to infer from user_id
295
+ key_path = Path.home() / ".mcp-keys" / f"{user_id}_id_rsa"
296
+ if not key_path.exists():
297
+ return "Error: SSH key not found. Please run /configure first."
298
+ private_key_path = str(key_path)
299
+
300
+ # Construct repo URL from config
301
+ # Assuming GITHUB_OWNER from server is "AICodingGym" or can be inferred
302
+ github_owner = os.environ.get("GITHUB_OWNER", "AICodingGym")
303
+ repo_url = f"git@github.com:{github_owner}/{repo_name}.git"
304
+
305
+ # The branch name is the problem_id itself in the forked repo
306
+ branch = problem_id
307
+
308
+ # Store credentials for later use
309
+ credentials_store[problem_id] = {
310
+ "repo_url": repo_url,
311
+ "branch": branch,
312
+ "user_id": user_id,
313
+ "server_url": server_url,
314
+ "private_key_path": private_key_path
315
+ }
316
+
317
+ # Use user's own SSH key
318
+ key_path = Path(private_key_path)
319
+
320
+ # Create workspace directory
321
+ workspace_path = Path(workspace_dir).resolve()
322
+ workspace_path.mkdir(parents=True, exist_ok=True)
323
+
324
+ problem_dir = workspace_path / problem_id
325
+ if problem_dir.exists():
326
+ # Check if it's already on the right branch
327
+ check_branch = f"cd {problem_dir} && git rev-parse --abbrev-ref HEAD"
328
+ result = subprocess.run(check_branch, shell=True, capture_output=True, text=True)
329
+ if result.returncode == 0 and result.stdout.strip() == branch:
330
+ # Pull latest changes
331
+ pull_cmd = f"git pull origin {branch}"
332
+ result = run_git_command(pull_cmd, str(problem_dir), key_path)
333
+ if result.returncode != 0:
334
+ return f"Error: Git pull failed:\n{result.stderr}"
335
+ return f"""Problem {problem_id} already exists. Updated to latest version.
336
+
337
+ Repository: {problem_dir}
338
+ Branch: {branch}
339
+
340
+ You can continue working on your solution!
341
+ """
342
+ else:
343
+ return f"Error: Directory {problem_dir} already exists with different content. Please remove it first or use a different workspace."
344
+
345
+ # Clone only the specific branch (shallow clone for efficiency)
346
+ clone_cmd = f"git clone --single-branch --branch {branch} --depth 1 {repo_url} {problem_id}"
347
+ result = run_git_command(clone_cmd, str(workspace_path), key_path)
348
+
349
+ if result.returncode != 0:
350
+ return f"Error: Git clone failed:\n{result.stderr}\n\nMake sure the branch '{branch}' exists in the repository."
351
+
352
+ return f"""Successfully fetched problem: {problem_id}
353
+
354
+ Repository cloned to: {problem_dir}
355
+ Branch: {branch}
356
+
357
+ The repository contains only the problem branch (shallow clone to save space).
358
+ You can now start working on the solution!
359
+ """
360
+
361
+ except Exception as e:
362
+ return f"Error: Unexpected error: {str(e)}"
363
+
364
+
365
+ async def submit_solution(
366
+ problem_id: str,
367
+ user_id: Optional[str] = None,
368
+ server_url: Optional[str] = None,
369
+ commit_message: Optional[str] = None
370
+ ) -> str:
371
+ """
372
+ Submit solution by committing and pushing changes, then notify backend.
373
+ """
374
+ try:
375
+ # Use configured values if not provided
376
+ user_id = user_id or config_store.get("user_id") or infer_user_id_from_keys()
377
+ server_url = server_url or config_store.get("server_url") or DEFAULT_SERVER_URL
378
+ commit_message = commit_message or ""
379
+
380
+ if not user_id:
381
+ return "Error: Could not determine user_id. Please run /configure first to set up your credentials."
382
+
383
+ if not server_url:
384
+ return "Error: server_url is required. Please run /configure first or pass it as a parameter."
385
+
386
+ # Check if we have cached credentials
387
+ if problem_id not in credentials_store:
388
+ return f"Error: No credentials found for {problem_id}. Please run /fetch first."
389
+
390
+ creds = credentials_store[problem_id]
391
+
392
+ # Verify user_id matches
393
+ if creds["user_id"] != user_id:
394
+ return f"Error: User ID mismatch. Problem was fetched by {creds['user_id']}, not {user_id}."
395
+
396
+ # Find problem directory
397
+ workspace_dir = Path("./workspace").resolve()
398
+ problem_dir = workspace_dir / problem_id
399
+
400
+ if not problem_dir.exists():
401
+ return f"Error: Problem directory not found at {problem_dir}"
402
+
403
+ # Use user's SSH key
404
+ private_key_path = creds.get("private_key_path") or config_store.get("private_key_path")
405
+ if not private_key_path:
406
+ return "Error: SSH key path not found. Please run /fetch again."
407
+
408
+ key_path = Path(private_key_path)
409
+
410
+ # Stage all changes
411
+ result = run_git_command("git add -A", str(problem_dir))
412
+ if result.returncode != 0:
413
+ return f"Error: Git add failed:\n{result.stderr}"
414
+
415
+ # Check if there are changes to commit
416
+ status_result = run_git_command("git status --porcelain", str(problem_dir))
417
+ if not status_result.stdout.strip():
418
+ return "Warning: No changes to commit. Your working directory is clean."
419
+
420
+ # Commit changes
421
+ if not commit_message:
422
+ commit_message = f"Solution submission for {problem_id} at {datetime.now().isoformat()}"
423
+
424
+ commit_cmd = f'git commit -m "{commit_message}"'
425
+ result = run_git_command(commit_cmd, str(problem_dir))
426
+ if result.returncode != 0:
427
+ return f"Error: Git commit failed:\n{result.stderr}"
428
+
429
+ # Get commit hash
430
+ hash_result = run_git_command("git rev-parse HEAD", str(problem_dir))
431
+ commit_hash = hash_result.stdout.strip()
432
+
433
+ # Push to remote
434
+ branch = creds["branch"]
435
+ push_cmd = f"git push origin {branch}"
436
+ result = run_git_command(push_cmd, str(problem_dir), key_path)
437
+
438
+ if result.returncode != 0:
439
+ return f"Error: Git push failed:\n{result.stderr}"
440
+
441
+ # Notify backend server
442
+ api_endpoint = f"{server_url}/api/submit"
443
+ payload = {
444
+ "problem_id": problem_id,
445
+ "user_id": user_id,
446
+ "commit_hash": commit_hash,
447
+ "branch": branch,
448
+ "timestamp": datetime.now().isoformat()
449
+ }
450
+
451
+ # Add API key if configured
452
+ if config_store.get("api_key"):
453
+ payload["api_key"] = config_store["api_key"]
454
+
455
+ response = requests.post(api_endpoint, json=payload, timeout=30)
456
+ response.raise_for_status()
457
+
458
+ return f"""Successfully submitted solution for {problem_id}
459
+
460
+ Commit: {commit_hash[:8]}
461
+ Branch: {branch}
462
+ Pushed to remote
463
+ Backend notified
464
+
465
+ Your solution has been submitted for evaluation!
466
+ """
467
+
468
+ except requests.RequestException as e:
469
+ return f"Warning: Changes committed and pushed, but failed to notify backend:\n{str(e)}"
470
+ except Exception as e:
471
+ return f"Error: Unexpected error: {str(e)}"
472
+
473
+
474
+ # MCP Server handlers
475
+ @app.list_tools()
476
+ async def list_tools() -> list[Tool]:
477
+ """List available tools."""
478
+ return [CONFIGURE_TOOL, FETCH_TOOL, SUBMIT_TOOL]
479
+
480
+
481
+ @app.call_tool()
482
+ async def call_tool(name: str, arguments: Any) -> list[TextContent]:
483
+ """Handle tool calls."""
484
+ try:
485
+ if name == "configure":
486
+ result = await configure(
487
+ user_id=arguments["user_id"],
488
+ server_url=arguments["server_url"],
489
+ api_key=arguments.get("api_key"),
490
+ workspace_dir=arguments.get("workspace_dir")
491
+ )
492
+ return [TextContent(type="text", text=result)]
493
+
494
+ elif name == "fetch":
495
+ result = await fetch_problem(
496
+ problem_id=arguments["problem_id"],
497
+ user_id=arguments.get("user_id"),
498
+ server_url=arguments.get("server_url"),
499
+ workspace_dir=arguments.get("workspace_dir")
500
+ )
501
+ return [TextContent(type="text", text=result)]
502
+
503
+ elif name == "submit":
504
+ result = await submit_solution(
505
+ problem_id=arguments["problem_id"],
506
+ user_id=arguments.get("user_id"),
507
+ server_url=arguments.get("server_url"),
508
+ commit_message=arguments.get("commit_message")
509
+ )
510
+ return [TextContent(type="text", text=result)]
511
+
512
+ else:
513
+ raise ValueError(f"Unknown tool: {name}")
514
+
515
+ except KeyError as e:
516
+ return [TextContent(type="text", text=f"Error: Missing required parameter: {str(e)}")]
517
+ except Exception as e:
518
+ return [TextContent(type="text", text=f"Error: Error executing tool: {str(e)}")]
519
+
520
+
521
+ async def main():
522
+ """Run the MCP server."""
523
+ async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
524
+ await app.run(
525
+ read_stream,
526
+ write_stream,
527
+ app.create_initialization_options()
528
+ )
529
+
530
+
531
+ if __name__ == "__main__":
532
+ asyncio.run(main())