[FEA] Decouple cuopt_mcp from the cuopt package so it ships as a pure-Python wheel
@Kh4ster is already working on this.
Since Aug 20, 2026.
- Dominant language
- Cuda
- Stars
- 1k
- Forks
- 233
- Avg merge
- 4d 4h
- Merged PRs (30d)
- 95
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 |
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.