Google OR-Tools server
by Jacck
- mcp-ortools
- src
- mcp_ortools
import asyncio
import logging
from typing import List, Optional, Any
from mcp.server import Server, NotificationOptions
from mcp.server.models import InitializationOptions
import mcp.server.stdio
import mcp.types as types
from mcp.shared.exceptions import McpError
from .solver_manager import SolverManager, SolverError
logger = logging.getLogger(__name__)
async def serve() -> None:
"""Main server function that handles the MCP protocol"""
logger.info("Starting OR-Tools MCP server")
server = Server("ortools")
solver_mgr = SolverManager()
@server.list_tools()
async def list_tools() -> List[types.Tool]:
return [
types.Tool(
name="submit_model",
description="Submit an optimization model in JSON format",
inputSchema={
"type": "object",
"properties": {
"model": {
"type": "string",
"description": "Model specification in JSON format"
}
},
"required": ["model"]
}
),
types.Tool(
name="solve_model",
description="Solve the current optimization model",
inputSchema={
"type": "object",
"properties": {
"timeout": {
"type": ["number", "null"],
"description": "Optional solve timeout in seconds"
}
}
}
),
types.Tool(
name="get_solution",
description="Get the current solution if available",
inputSchema={
"type": "object",
"properties": {}
}
)
]
@server.call_tool()
async def handle_call_tool(name: str, arguments: dict) -> List[types.TextContent]:
"""Handle tool calls and map them to solver operations"""
logger.debug(f"Tool call: {name} with arguments {arguments}")
try:
match name:
case "submit_model":
model_str = arguments.get("model")
if not model_str:
raise McpError("Model required", "model parameter is required")
valid, message = solver_mgr.parse_model(model_str)
if not valid:
raise McpError("Invalid model", message)
logger.info("Model submitted successfully")
return [types.TextContent(type="text", text="Model submitted successfully")]
case "solve_model":
try:
timeout = arguments.get("timeout")
result = solver_mgr.solve(timeout)
logger.info(f"Solve completed with status {result.get('status')}")
return [types.TextContent(type="text", text=str(result))]
except SolverError as e:
raise McpError("Solver error", str(e))
case "get_solution":
solution = solver_mgr.get_current_solution()
if solution is None:
raise McpError("No solution", "No solution is available")
return [types.TextContent(type="text", text=str(solution))]
case _:
raise McpError("Unknown tool", f"Tool {name} not found")
except McpError:
raise
except Exception as e:
logger.exception(f"Error in {name}")
raise McpError("Tool execution failed", str(e))
logger.info("Starting STDIO server")
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
logger.info("STDIO server started")
await server.run(
read_stream,
write_stream,
InitializationOptions(
server_name="ortools",
server_version="0.1.0",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
),
)
def main() -> int:
"""Main entry point"""
try:
asyncio.run(serve())
return 0
except KeyboardInterrupt:
logger.info("Server stopped by user")
return 0
except Exception as e:
logger.exception("Server error")
return 1
if __name__ == "__main__":
main()