Skip to content
Draft
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
31 changes: 21 additions & 10 deletions python/cuopt_mcp/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# cuopt_mcp — MCP server for NVIDIA cuOpt

Exposes cuOpt LP and MILP solving to MCP clients (Claude Code, Cursor, Codex)
over the cuOpt gRPC backend.
Exposes cuOpt LP, MILP, and vehicle routing (VRP/PDP) solving to MCP clients
(Claude Code, Cursor, Codex) over the cuOpt gRPC backend.

```text
MCP client ──stdio (JSON-RPC)──> cuopt-mcp ──gRPC──> cuopt_grpc_server (GPU)
Expand Down Expand Up @@ -55,13 +55,15 @@ needed) whenever `gpu-host` isn't a trusted local network.
| `cuopt_health` | Report the configured gRPC target and whether it answers |
| `cuopt_solve_lp` | Submit an LP; returns a `job_id` immediately |
| `cuopt_solve_milp` | Submit a MILP; returns a `job_id` immediately |
| `cuopt_status` | Poll job state |
| `cuopt_result` | Fetch the solution, shaped to stay readable |
| `cuopt_solve_vrp` | Submit a vehicle routing problem; returns a `job_id` immediately |
| `cuopt_status` | Poll job state (LP, MILP, or VRP) |
| `cuopt_result` | Fetch an LP/MILP solution, shaped to stay readable |
| `cuopt_vrp_result` | Fetch a VRP solution (route stops), shaped to stay readable |
| `cuopt_incumbents` | Watch a MILP's objective improve (needs `track_incumbents=true` at submit) |
| `cuopt_logs` | Solver log lines for a finished job (no live tail yet) |
| `cuopt_cancel` | Stop a running job |
| `cuopt_delete` | Release a job's server-side state once its result is no longer needed |
| `cuopt_list_settings` | Discover solver parameters |
| `cuopt_list_settings` | Discover LP/MILP solver parameters |

Solves are asynchronous by design. A blocking call would exceed the MCP
client timeout on any realistic MILP and would make cancellation impossible.
Expand Down Expand Up @@ -149,8 +151,17 @@ limit on a tool result is the model's context window, not the transport. So
`cuopt_result` returns a summary plus narrow accessors (`variables`,
`nonzero_only`), writing the full vector to a file past `limit`.

**Settings catalogue is generated.** `_generated/cuopt_mcp_schema.json` is
emitted from `cpp/src/grpc/codegen/field_registry.yaml` by
`./build.sh codegen`, the same source of truth that drives the proto and the
C++ conversion code. A new solver parameter reaches this server with no
MCP-specific work.
**Settings catalogue is generated (LP/MILP only).** `_generated/
cuopt_mcp_schema.json` is emitted from `cpp/src/grpc/codegen/
field_registry.yaml` by `./build.sh codegen`, the same source of truth
that drives the proto and the C++ conversion code. A new LP/MILP solver
parameter reaches this server with no MCP-specific work. VRP settings
aren't in this registry (only `time_limit`/`verbose_mode` (or `verbose`)/
`error_logging` reach the server; see `cuopt_solve_vrp`'s docstring) so there's no
equivalent `cuopt_list_settings` coverage for VRP.

**VRP submission has no host-CUDA dependency at record time.**
`cuopt.routing.DataModel` records setter calls (numpy arrays) and never
builds a device model on this host -- it only serializes the recorded
calls onto the wire, the same way `cuopt_solve_lp`/`cuopt_solve_milp`'s
JSON path never runs a solve locally.
32 changes: 32 additions & 0 deletions python/cuopt_mcp/cuopt_mcp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

if TYPE_CHECKING:
from cuopt.grpc.linear_programming import Client
from cuopt.grpc.routing import RoutingClient

DEFAULT_HOST = "localhost"
# Matches cuopt_default_grpc_port (cpp/src/grpc/cuopt_default_grpc_port.h) --
Expand All @@ -36,6 +37,7 @@ def redact_paths(text: str) -> str:

_lock = threading.Lock()
_client = None
_routing_client = None


def endpoint() -> tuple:
Expand Down Expand Up @@ -120,6 +122,36 @@ def reset_client() -> None:
_client = None


def get_routing_client() -> "RoutingClient":
"""Return a process-wide VRP gRPC client, connecting on first use.

Separate from :func:`get_client`: the LP/MIP and VRP services are
distinct proto services with distinct compiled client classes, even
though both point at the same ``cuopt_grpc_server`` target.

Returns
-------
The cached ``cuopt.grpc.routing.RoutingClient``, creating it
against the current ``CUOPT_REMOTE_HOST``/``CUOPT_REMOTE_PORT`` /
TLS environment on first call.
"""
global _routing_client
with _lock:
if _routing_client is None:
from cuopt.grpc.routing import RoutingClient

host, port = endpoint()
_routing_client = RoutingClient(host, port, tls=_tls_config())
return _routing_client


def reset_routing_client() -> None:
"""Drop the cached VRP client. Used by tests and after a channel error."""
global _routing_client
with _lock:
_routing_client = None


class CuOptMCPError(RuntimeError):
"""Raised with text meant for the model, not a stack trace."""

Expand Down
Loading
Loading