Repository navigation
Description
Scope: decouple cuopt_mcp from the cuopt package
Goal
Make cuopt_mcp a pure-Python, CUDA-free wheel so an MCP client can install it
on a laptop (uvx cuopt-mcp) and talk to a remote cuopt_grpc_server. This is
what server.py already claims — "the host needs no GPU" — and what today's
packaging contradicts, since installing it pulls cuopt-cu13 and the CUDA
runtime onto a machine whose only job is to open a socket.
Secondary payoff: the CUDA-suffixed wheel matrix, the conda recipe and the
per-platform CI jobs all collapse to one py3-none-any artifact. That matters
now because none of that wiring exists yet — this decides what gets built.
What already exists (do not rebuild)
| Asset | Where | Why it matters |
|---|---|---|
| Full wire contract | cuopt_remote_service.proto, codegen/generated/cuopt_remote_data.proto |
"The protos and this document together fully specify the wire contract; any gRPC-capable language can implement a client. The cuOpt project does not consider this protocol private." — GRPC_INTERFACE.md |
| Codegen that already emits a Python-facing artifact | generate_conversions.py:409 generate_mcp_schema() → cuopt_mcp_schema.json |
The "generate it, don't hand-maintain it" pattern is already implemented and shipped |
| Loader for that artifact | cuopt_mcp/schema.py |
Already falls back between packaged _generated/ and the codegen output dir |
| Field ids for every array | field_registry.yaml (array_id: 0,1,2…) |
These are exactly the ArrayChunk.field_id values on the wire |
schema.py's docstring already states the principle: "Nothing here
hand-maintains a second copy of the settings surface." This work extends that
from settings to problem and solution arrays.
The five couplings to remove
cuopt_mcp imports exactly five symbols from cuopt, all lazily:
| Symbol | Site | Replacement |
|---|---|---|
grpc…Client |
client.py:64 |
generated cuopt_remote_service_pb2_grpc stub over grpcio |
grpc…TlsConfig |
client.py:45 |
grpc.ssl_channel_credentials |
DataModel |
tools.py:169 |
populate ChunkedProblemHeader + array payloads directly |
SolverSettings |
tools.py:267 |
populate PDLPSolverSettings / MIPSolverSettings |
Read (MPS/LP/QPS) |
tools.py:61 |
open decision — see below |
DataModel is the one worth calling out: today the MCP converts JSON arrays →
DataModel → the C++ client takes it apart again into arrays on the wire. The
MCP's JSON input is already the wire shape. Removing the middle step removes a
CUDA-linked C++ type that exists only to be disassembled.
Design
Three layers, smallest first.
1. Transport. grpcio + stubs generated by grpcio-tools from the two
existing protos. Generated at build time and committed alongside the other
codegen output, so ci/verify_grpc_codegen.sh covers drift.
2. Mapping. JSON arrays ↔ proto messages. Emitted from field_registry.yaml
by a new generate_python_mapping() in generate_conversions.py, sitting
beside the existing generate_mcp_schema(). A field added to the registry then
appears on the C++ side, the proto, the MCP schema and the Python mapper — or
CI fails. There is no second interface to drift because there is no
hand-written interface.
3. Tools. tools.py / client.py swap their imports. The tool surface,
JSON contract and tests are unchanged — the existing 63 tests are the
regression suite for this refactor.
Phasing
Phase 1 — unary only. GRPC_INTERFACE.md explicitly offers this: "Stick to
unary SubmitJob/GetResult and raise max_message_bytes on both sides… a custom
client can send any problem up to ~2 GiB or so as a single round-trip and skip
the chunked machinery entirely." This removes the largest risk from the
critical path. Ships a working CUDA-free MCP.
Phase 2 — chunked path, only if needed. StartChunkedUpload /
SendArrayChunk / FinishChunkedUpload and the download mirror. Needed only
for problems past ~2 GiB, or for throughput (the doc measures ~580 MB/s chunked
vs ~280 MB/s unary on a 706 MB problem). Defer until a real workload asks.
Open decisions
- MPS/LP/QPS parsing (
Read). Options: (a) dropproblem_path, JSON-only;
(b) add an RPC that ships file bytes and parses server-side; (c) pure-Python
parser. Recommend (b) — it keeps the feature, puts parsing where the C++
parser already lives, and lets a remote user submit an MPS file with no local
cuOpt. Costs a proto addition, so it wants Trevor's view. - Package name. Dropping the dependency makes
cuopt-mcp(unsuffixed)
correct, viadisable-cuda = true. Verified: that flag alone un-suffixes the
name and the dependency, which is only safe once the dependency is gone. - Does
cuopt_mcpstay in this repo? In-repo keeps it next to
field_registry.yaml, which is what makes the generated mapper trustworthy.
Recommend staying.
Non-goals
- Replacing the Cython client. It stays the client for in-process/GPU users.
- Routing/VRP over MCP.
- Chunking in phase 1.
Risks
- Two implementations of the wire protocol. Mitigated by generating the
mapper from the registry and covering it withverify_grpc_codegen.sh; not
eliminated. The chunking layer in phase 2 would be genuinely hand-written and
is the real drift risk — another argument for deferring it. - Endianness.
ArrayChunk.datais "raw native-endian element bytes". A
Python client must match;numpy.tobytes()is native-endian, so this is fine
in practice but needs an explicit test. - Scope creep into a general Python client. The deliverable is an MCP
server, not a supported public Python gRPC client. Keep the generated mapper
private to the package until someone asks otherwise.
Rough effort
| Stub generation + build wiring | small |
generate_python_mapping() emitter |
medium — the registry is already parsed; this is a new emitter beside generate_mcp_schema() |
tools.py / client.py swap |
small — 5 call sites, tests already exist |
| Server-side MPS RPC (if chosen) | medium, and needs proto review |
| Phase 2 chunking | medium-large, deferred |
Activity
github-actions commentedon Sep 11, 2026
🔔 Hi @anandhkb @Kh4ster, this issue has had no activity for 21 days. Please update or let us know if it can be closed. Thank you!
If this is an "epic" issue, then please add the "epic" label to this issue.
If it is a PR and not ready for review, then please convert this to draft.
If you just want to switch off this notification, then use the "skip inactivity reminder" label.
github-actions commentedon Oct 2, 2026
🔔 Hi @anandhkb @Kh4ster, this issue has had no activity for 21 days. Please update or let us know if it can be closed. Thank you!
If this is an "epic" issue, then please add the "epic" label to this issue.
If it is a PR and not ready for review, then please convert this to draft.
If you just want to switch off this notification, then use the "skip inactivity reminder" label.