Skip to content

Commit 1c7636d

Browse files
committed
First stab - MCP server that uses GCF as a code interpreter
This is just a first stab. It assumes the user has `gcloud` user set up in the proper project, etc. It requires some manual setup, use of environment variable for the GCF URL, etc. But... it works
0 parents  commit 1c7636d

23 files changed

Lines changed: 2159 additions & 0 deletions

.github/workflows/test.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ["3.11", "3.12"]
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Set up Python ${{ matrix.python-version }}
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: ${{ matrix.python-version }}
23+
24+
- name: Install dependencies
25+
run: |
26+
python -m pip install --upgrade pip
27+
pip install -e .
28+
29+
- name: Run tests
30+
run: |
31+
pytest -v
32+
33+
- name: Run linting
34+
run: |
35+
pip install ruff
36+
ruff check src/ tests/
37+
ruff format --check src/ tests/

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Python-generated files
2+
__pycache__/
3+
*.py[oc]
4+
build/
5+
dist/
6+
wheels/
7+
*.egg-info
8+
9+
# Virtual environments
10+
.venv

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.11

README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Code MCP Server
2+
3+
An MCP (Model Context Protocol) server that provides code interpretation capabilities via Google Cloud Functions.
4+
5+
## Features
6+
7+
- Execute Python, JavaScript, and Bash code in a sandboxed environment
8+
- Automatic deployment to Google Cloud Functions
9+
- STDIO-based MCP server implementation
10+
11+
## Prerequisites
12+
13+
- Python 3.11+
14+
- Google Cloud SDK (`gcloud`) installed and configured
15+
- A Google Cloud Project with Cloud Functions API enabled
16+
17+
## Installation
18+
19+
```bash
20+
pip install -e ".[dev]"
21+
```
22+
23+
## Usage
24+
25+
### As an MCP Server
26+
27+
```bash
28+
python main.py
29+
```
30+
31+
### Running Tests
32+
33+
```bash
34+
pytest
35+
```
36+
37+
### Testing with the MCP Inspector
38+
You can use the CLI feature with
39+
40+
```console
41+
$ GCF_URL=$MY_COOL_GCF_URL \
42+
npx @modelcontextprotocol/inspector@0.11.0 \
43+
--cli uv run python main.py \
44+
--method tools/call \
45+
--tool-name run_code \
46+
--tool-arg "code=print(1+1)" \
47+
--tool-arg language=python \
48+
| jq
49+
```
50+
51+
## Configuration
52+
53+
Set the `GCF_URL` environment variable to use an existing Cloud Function, otherwise the server will attempt to deploy one automatically.
54+
55+
```bash
56+
export GCF_URL="https://region-project.cloudfunctions.net/code-interpreter"
57+
```
58+
59+
## Architecture
60+
61+
- **MCP Server**: Handles tool requests from AI agents
62+
- **Google Cloud Function**: Executes code in an isolated environment
63+
- **Supported Languages**: Python, JavaScript (Node.js), Bash

deploy_gcf.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env python
2+
"""
3+
Script to deploy the Google Cloud Function for code execution.
4+
5+
Usage: python deploy_gcf.py [--project PROJECT_ID]
6+
"""
7+
8+
import argparse
9+
import sys
10+
from src.code_mcp.gcf_deployer import GCFDeployer
11+
12+
13+
def main():
14+
parser = argparse.ArgumentParser(description="Deploy Code Interpreter Google Cloud Function")
15+
parser.add_argument(
16+
"--project",
17+
help="Google Cloud Project ID (defaults to current gcloud config)",
18+
default=None
19+
)
20+
21+
args = parser.parse_args()
22+
23+
deployer = GCFDeployer(project_id=args.project)
24+
25+
print("🚀 Deploying Code Interpreter Cloud Function...")
26+
27+
result = deployer.deploy()
28+
29+
if result["success"]:
30+
print(f"✅ Successfully deployed!")
31+
print(f"📍 Function URL: {result['function_url']}")
32+
print(f"🔧 Project ID: {result['project_id']}")
33+
print(f"\nTo use with the MCP server, set the environment variable:")
34+
print(f"export GCF_URL=\"{result['function_url']}\"")
35+
else:
36+
print(f"❌ Deployment failed: {result.get('error', 'Unknown error')}")
37+
sys.exit(1)
38+
39+
40+
if __name__ == "__main__":
41+
main()

dev.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
name: code-mcp
2+
3+
type: python
4+
5+
up:
6+
- uv
7+
8+
commands:
9+
setup: python deploy_gcf.py
10+
server: python main.py
11+
test: pytest . -v

example_usage.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/usr/bin/env python
2+
"""
3+
Example demonstrating how to use the Code MCP server programmatically.
4+
5+
This shows how an AI agent would interact with the MCP server to execute code.
6+
"""
7+
8+
import asyncio
9+
import json
10+
from mcp.client import ClientSession
11+
from mcp.client.stdio import stdio_client
12+
13+
14+
async def main():
15+
# Connect to the MCP server via stdio
16+
async with stdio_client() as (read_stream, write_stream):
17+
async with ClientSession(read_stream, write_stream) as session:
18+
# Initialize the session
19+
await session.initialize()
20+
21+
# List available tools
22+
tools = await session.list_tools()
23+
print("Available tools:")
24+
for tool in tools:
25+
print(f" - {tool.name}: {tool.description}")
26+
27+
# Example 1: Execute Python code
28+
print("\n--- Python Example ---")
29+
result = await session.call_tool(
30+
"run_code",
31+
arguments={
32+
"code": "print('Hello from Python!')\nprint(2 + 2)",
33+
"language": "python"
34+
}
35+
)
36+
print(f"Result: {result[0].text}")
37+
38+
# Example 2: Execute JavaScript code
39+
print("\n--- JavaScript Example ---")
40+
result = await session.call_tool(
41+
"run_code",
42+
arguments={
43+
"code": "console.log('Hello from JavaScript!');\nconsole.log(6 * 7);",
44+
"language": "javascript"
45+
}
46+
)
47+
print(f"Result: {result[0].text}")
48+
49+
# Example 3: Execute Bash code
50+
print("\n--- Bash Example ---")
51+
result = await session.call_tool(
52+
"run_code",
53+
arguments={
54+
"code": "echo 'Hello from Bash!'\necho $((3 + 4))",
55+
"language": "bash"
56+
}
57+
)
58+
print(f"Result: {result[0].text}")
59+
60+
# Example 4: Handle errors
61+
print("\n--- Error Handling Example ---")
62+
result = await session.call_tool(
63+
"run_code",
64+
arguments={
65+
"code": "print('Unclosed string",
66+
"language": "python"
67+
}
68+
)
69+
print(f"Result: {result[0].text}")
70+
71+
72+
if __name__ == "__main__":
73+
asyncio.run(main())

gcf/__init__.py

Whitespace-only changes.

gcf/main.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import subprocess
2+
import json
3+
import logging
4+
from flask import jsonify
5+
6+
logging.basicConfig(level=logging.INFO)
7+
logger = logging.getLogger(__name__)
8+
9+
TIMEOUT = 30
10+
SUPPORTED_LANGUAGES = {
11+
"python": ["python", "-c"],
12+
"javascript": ["node", "-e"],
13+
"bash": ["bash", "-c"]
14+
}
15+
16+
17+
def execute_code(request):
18+
try:
19+
request_json = request.get_json()
20+
21+
if not request_json:
22+
return jsonify({"error": "Invalid request body"}), 400
23+
24+
code = request_json.get("code")
25+
language = request_json.get("language")
26+
27+
if not code:
28+
return jsonify({"error": "Missing required field: code"}), 400
29+
30+
if not language:
31+
return jsonify({"error": "Missing required field: language"}), 400
32+
33+
if language not in SUPPORTED_LANGUAGES:
34+
return jsonify({"error": f"Unsupported language: {language}"}), 400
35+
36+
cmd = SUPPORTED_LANGUAGES[language] + [code]
37+
38+
logger.info(f"Executing {language} code")
39+
40+
try:
41+
result = subprocess.run(
42+
cmd,
43+
capture_output=True,
44+
text=True,
45+
timeout=TIMEOUT
46+
)
47+
48+
return jsonify({
49+
"stdout": result.stdout,
50+
"stderr": result.stderr,
51+
"exitCode": result.returncode
52+
}), 200
53+
54+
except subprocess.TimeoutExpired:
55+
return jsonify({
56+
"stdout": "",
57+
"stderr": f"Code execution timed out after {TIMEOUT} seconds",
58+
"exitCode": -1
59+
}), 200
60+
61+
except Exception as e:
62+
logger.error(f"Execution error: {str(e)}")
63+
return jsonify({
64+
"stdout": "",
65+
"stderr": f"Execution error: {str(e)}",
66+
"exitCode": -1
67+
}), 200
68+
69+
except Exception as e:
70+
logger.error(f"Request handling error: {str(e)}")
71+
return jsonify({"error": f"Server error: {str(e)}"}), 500

gcf/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
functions-framework==3.*
2+
flask==3.*

0 commit comments

Comments
 (0)