ortools-mcp
by iam-aditya
README.md
# ortools-mcp
An MCP server that exposes [Google OR-Tools CP-SAT](https://developers.google.com/optimization/reference/python/sat/python/cp_model) as tools for constraint-based value correction. Given a set of equations and observed variable values, it finds the minimal corrections needed to make everything consistent.
**Use case**: you have values extracted from a document (OCR, form parsing, etc.) that should satisfy known equations (e.g. `total = quantity × rate + tax`), but one or more values were misread. The solver finds which values to adjust and by how much, weighted by how much you trust each observation.
---
## Tools
### `solve`
Runs CP-SAT. Returns corrected values for free (non-fixed) variables that best satisfy the given equations.
**Input**
```json
{
"variables": {
"gross_amount": {
"obs": 500,
"confidence": 0.3,
"mult_factor": 100,
"min": 0,
"max": 100000,
"fixed": false
},
"quantity": {
"obs": 10,
"mult_factor": 1,
"fixed": true
},
"rate_per_pc": {
"obs": 52,
"mult_factor": 100,
"fixed": true
}
},
"equations": [
{
"lhs": "gross_amount",
"rhs": "quantity * rate_per_pc",
"relation": "==",
"weight": 1000,
"tolerance": 0
}
],
"timeout_seconds": 5,
"num_workers": 8
}
```
**Output**
```json
{
"status": "OPTIMAL",
"corrected": {
"gross_amount": 520.0
}
}
```
Only free (non-fixed) variables appear in `corrected`. Status values: `OPTIMAL`, `FEASIBLE`, `INFEASIBLE`, `UNKNOWN`, `INVALID_EQUATION`.
**Variable spec fields**
| Field | Type | Default | Description |
|---|---|---|---|
| `obs` | number | — | Observed (extracted) value in real-world units |
| `confidence` | 0–1 | `0.0` | How much to trust `obs`. Higher → stronger pull toward the observed value |
| `mult_factor` | int | `1` | Scales float values to integers for the solver (`obs × mult_factor`), result is divided back on return |
| `min` / `max` | number | `0` / `2e9` | Domain bounds in real-world units |
| `fixed` | bool | `false` | Pin this variable to `obs` — it won't appear in `corrected` |
**Equation fields**
| Field | Type | Default | Description |
|---|---|---|---|
| `lhs` | string | required | Left-hand side expression |
| `rhs` | string | required | Right-hand side expression |
| `relation` | `==` \| `<=` \| `>=` | required | Relation between lhs and rhs |
| `weight` | int | `1000` | Penalty per unit of violation — higher means the solver tries harder to satisfy this equation |
| `tolerance` | int | `0` | Allowed slack (in scaled integer units) before penalty kicks in |
**Solver params**
| Field | Default | Description |
|---|---|---|
| `timeout_seconds` | `5` | Wall-clock limit for the solver |
| `num_workers` | `8` | Parallel search workers |
---
### `validate_equations`
Pure AST check — no solver invoked. Call this before `solve` to catch unsupported constructs early.
**Input**
```json
{
"equations": [
{ "lhs": "total", "rhs": "taxable + tax" },
{ "lhs": "discount", "rhs": "-base * 0.1" }
]
}
```
**Output**
```json
[
{ "lhs": "total", "rhs": "taxable + tax", "errors": [] },
{
"lhs": "discount",
"rhs": "-base * 0.1",
"errors": [
"Unary operator 'USub' not supported in '-base * 0.1'. Rewrite e.g. '-x' as '(0 - x)'."
]
}
]
```
Empty `errors` list means the equation is valid. Catches:
- Unary negation (`-x`) and unary plus (`+x`) — rewrite as `(0 - x)`
- Non-constant or zero/negative exponents in `**` — only `x ** 2`, `x ** 3`, etc. are supported
- Syntax errors
---
### `evaluate_equations`
Evaluates equations against known values and returns pass/fail per equation. No OR-Tools involved — useful to check whether values already satisfy the system before deciding to call `solve`.
**Input**
```json
{
"equations": [
{ "lhs": "gross_amount", "rhs": "quantity * rate_per_pc", "relation": "==", "tolerance": 0.5 }
],
"values": {
"gross_amount": 520,
"quantity": 10,
"rate_per_pc": 52
}
}
```
**Output**
```json
[
{
"lhs": "gross_amount",
"rhs": "quantity * rate_per_pc",
"status": "passed",
"actual": 520,
"computed": 520,
"error": 0.0
}
]
```
Status values: `passed`, `failed`, `missing_values` (variable not in `values`), `error` (evaluation exception).
---
### `optimize`
Maximize or minimize an objective expression subject to constraints. Unlike `solve`, there are no observed values — this is pure optimization: find the best possible assignment of variables within their domains.
**Example use cases:**
- Maximize margin (`revenue - cost`) subject to budget and inventory limits
- Minimize waste subject to production constraints
- Find the best allocation of a fixed total across items
**Input**
```json
{
"variables": {
"units": { "min": 0, "max": 1000, "mult_factor": 1 },
"price": { "min": 0, "max": 500, "mult_factor": 100 }
},
"objective": {
"expr": "units * price",
"direction": "maximize"
},
"hard_constraints": [
{ "lhs": "units + price", "rhs": "120", "relation": "<=" }
],
"soft_constraints": [
{ "lhs": "price", "rhs": "50", "relation": "<=", "weight": 500, "tolerance": 0 }
],
"timeout_seconds": 5,
"num_workers": 8
}
```
**Output**
```json
{
"status": "OPTIMAL",
"values": { "units": 100, "price": 20.0 },
"objective_value": 2000.0
}
```
- `values` contains all variables (real-world units, divided by `mult_factor`)
- `objective_value` is computed from the real-world values after solving
- Status values: `OPTIMAL`, `FEASIBLE`, `INFEASIBLE`, `UNKNOWN`, `INVALID_EQUATION`
**Constraint types**
| Type | Behaviour |
|---|---|
| `hard_constraints` | Must be satisfied. If any are contradictory the solver returns `INFEASIBLE`. |
| `soft_constraints` | Violations are penalised in the objective (for maximize: subtracted; for minimize: added). Same `weight`/`tolerance` fields as `solve`. |
**Variable spec** (simpler than `solve` — no `obs`, `confidence`, or `fixed`):
| Field | Default | Note |
|---|---|---|
| `min` | `0` | Domain lower bound (real-world units) |
| `max` | `2e9 // mult_factor` | Domain upper bound (real-world units) |
| `mult_factor` | `1` | Same integer-scaling as `solve` |
---
## Supported expression syntax
Both `lhs` and `rhs` accept arithmetic expressions over variable names and numeric constants:
| Operator | Example | Notes |
|---|---|---|
| `+` | `a + b` | |
| `-` | `a - b` | Binary only — `-a` (unary) is not supported |
| `*` | `qty * rate` | |
| `/` | `total / qty` | RHS divisions are automatically moved to LHS to avoid integer-division loss |
| `%` | `a % b` | Modulo |
| `**` | `x ** 2` | Exponent must be a constant positive integer |
| Parentheses | `(a + b) * c` | |
---
## `mult_factor` and integer scaling
OR-Tools CP-SAT works only with integers. `mult_factor` scales real-world float values to integers before solving and divides back on return.
- **Amount fields** (prices, totals): `mult_factor: 100` — values like `52.30` become `5230` internally
- **Quantity fields**: `mult_factor: 1` — must stay at 1 for multiplicative equations
**Why quantities must use `mult_factor: 1`**: for `gross = qty × rate`, scaling all three by 100 gives `gross×100 = (qty×100) × (rate×100)`, which is off by 100×. With qty at ×1: `gross×100 = (qty×1) × (rate×100)` — correct.
`tolerance` is expressed in scaled integer units (after mult_factor is applied). For `mult_factor: 100`, a tolerance of `200` means ±2.00 in real units.
---
## Setup
```bash
pip install -r requirements.txt
```
**Add to Claude Code** (`~/.claude/settings.json`):
```json
{
"mcpServers": {
"ortools-solver": {
"command": "python3",
"args": ["/path/to/ortools-mcp/server.py"]
}
}
}
```
---
## Running tests
```bash
pip install pytest
pytest test_solver.py test_server.py -v
```
`test_server.py` mocks the `mcp` package so it runs without needing `mcp` installed. 50 tests total covering all three tools, expression validation, scaling, confidence weighting, tolerance, and inequality constraints.
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues