Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
API_KEY=sk-xxxxxxxxxxxxxx
54 changes: 54 additions & 0 deletions DOCKER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Nomos Solver MCP - Docker

## Build
```bash
docker build -t nomos-solver-mcp .
```

## Run
```bash
docker run -p 9000:9000 nomos-solver-mcp
```

## With custom config
```bash
docker run -p 9000:9000 -v $(pwd)/config.toml:/app/config.toml nomos-solver-mcp
```

## Environment variables
Set `API_KEY` if needed:
```bash
docker run -p 9000:9000 -e API_KEY=your-key-here nomos-solver-mcp
```

---

## Docker Compose

### Quick start
```bash
docker-compose up -d
```

### Build and run
```bash
docker-compose up --build
```

### Stop
```bash
docker-compose down
```

### View logs
```bash
docker-compose logs -f nomos-solver-mcp
```

### Environment variables
Create a `.env` file:
```env
API_KEY=your-key-here
```

**Note**: Adjust `base_url` in `config.toml` to point to your model server (use host IP instead of localhost when running in Docker).
17 changes: 17 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM python:3.11-slim

WORKDIR /app

# Copy requirements and install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application files
COPY mcp.py solve_agent.py config.toml ./
COPY prompts/ ./prompts/

# Expose MCP server port
EXPOSE 9000

# Run the MCP server
CMD ["fastmcp", "run", "mcp.py", "--transport", "http", "--port", "9000", "--host", "0.0.0.0"]
9 changes: 9 additions & 0 deletions config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[models]
base_url = "http://localhost:30000/v1"
judge_model = "nomos-1"
solve_model = "nomos-1"

[agent]
target_perfect_score=4
max_concurrent=32
time_limit_hours=3.0
19 changes: 19 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: '3.8'

services:
nomos-solver-mcp:
build: .
container_name: nomos-solver-mcp
ports:
- "9000:9000"
volumes:
- ./config.toml:/app/config.toml
environment:
- API_KEY=sk-xxxxxxxxxxxxx
restart: unless-stopped
networks:
- nomos-network

networks:
nomos-network:
driver: bridge
59 changes: 59 additions & 0 deletions mcp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from fastmcp import FastMCP
from solve_agent import SolveAgent, ProblemState
import toml
import uuid
import asyncio
import time


with open("config.toml", "r") as f:
config = toml.load(f)

mcp = FastMCP("Nomos1 Solver")

@mcp.tool
async def solve(problem: str) -> str:
"""Solve a complex mathematical or logical problem using Nomos1 Solver."""
agent = SolveAgent(
problems_dir=None,
base_url=config["models"]["base_url"],
model=config["models"]["solve_model"],
judge_model=config["models"]["judge_model"],
target_perfect_scores=config["agent"]["target_perfect_score"],
max_concurrent=int(config["agent"]["max_concurrent"]),
time_limit_hours=float(config["agent"]["time_limit_hours"])
)

suid = str(uuid.uuid4())

problem = ProblemState(
problem_id=suid,
problem_text=problem
)

problems = {
suid: problem
}

agent.problems = problems
agent.start_time = time.time() # Initialize start_time before solving

# Start watchdog in background (safe mode = doesn't kill process)
watchdog = asyncio.create_task(agent._deadline_watchdog(safe=True))

try:
await agent.run_solving_phase()
await agent.run_finalization_phase()
finally:
watchdog.cancel()

solution = agent.get_problem_solution(suid)

return solution if solution else "No solution generated"



if __name__ == "__main__":
print("MCP Server sould be ran using :")
print("fastmcp run ./mcp.py --transport http --port 9000")
print("Then connect to it using any MCP client (tested on Insomnia).")
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ fire
tenacity
httpx
rich
fastmcp
toml
36 changes: 30 additions & 6 deletions solve_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ class SolveAgent:

def __init__(
self,
problems_dir: str,
problems_dir: Optional[str] = None,
judge_prompt: str = "prompts/score.md",
submissions_dir: Optional[str] = None,
solve_prompt: Optional[str] = None,
Expand All @@ -140,11 +140,12 @@ def __init__(
judge_model: str = "nomos-1",
base_url: str = "http://localhost:30000/v1",
):
self.problems_dir = Path(problems_dir)
if submissions_dir is None:
self.solution = None
self.problems_dir = Path(problems_dir) if problems_dir else None
if submissions_dir is None and problems_dir is not None:
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
submissions_dir = f"submissions/{self.problems_dir.name}-{timestamp}"
self.submissions_dir = Path(submissions_dir)
self.submissions_dir = Path(submissions_dir) if submissions_dir else None
self.time_limit_seconds = time_limit_hours * 3600
# Stop solving 15 min before end, but ensure at least 50% of time for solving
self.early_stop_seconds = max(
Expand All @@ -169,6 +170,7 @@ def __init__(
self.client = AsyncOpenAI(
base_url=base_url,
timeout=httpx.Timeout(99999, connect=60.0),
api_key=os.getenv("API_KEY")
)
self.start_time: float = 0
self.stopping = False
Expand Down Expand Up @@ -629,6 +631,8 @@ async def run_finalization_phase(self):

def write_submissions(self):
"""Write final submissions to disk."""
if self.submissions_dir is None:
return
self.submissions_dir.mkdir(parents=True, exist_ok=True)

for problem in self.problems.values():
Expand All @@ -652,8 +656,25 @@ def write_submissions(self):

filepath.write_text(output, encoding="utf-8")
safeprint(f"[green]Wrote {filepath}[/green]")

def get_problem_solution(self, problem_id: str) -> Optional[str]:
"""Get the final solution content for a given problem ID."""
problem = self.problems.get(problem_id)

safeprint(f"[bold]Retrieving solution for problem {problem.problem_id}[/bold]")

if problem is None:
return None

content = problem.final_submission.content if problem.final_submission else ""

async def _deadline_watchdog(self):
output = f"# {problem.problem_id}\n\n"
output += f"## Problem\n\n{problem.problem_text}\n\n"
output += f"## Submission\n\n{content}\n"

return output

async def _deadline_watchdog(self, safe=False):
"""Background task that enforces hard deadline by killing the process."""
# Trigger 1 min before deadline to allow time for writing submissions
deadline = self.time_limit_seconds - 60
Expand All @@ -666,7 +687,10 @@ async def _deadline_watchdog(self):
self.write_submissions()
elapsed = time.time() - self.start_time
safeprint(f"\n[bold green]Done in {elapsed/60:.1f} minutes[/bold green]")
os._exit(0)
if not safe:
os._exit(0)
else :
break

async def run(self):
"""Main entry point."""
Expand Down