Skip to content

[FEA] Decouple cuopt_mcp from the cuopt package so it ships as a pure-Python wheel #1755

Description

@ramakrishnap-nv

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

  1. MPS/LP/QPS parsing (Read). Options: (a) drop problem_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.
  2. Package name. Dropping the dependency makes cuopt-mcp (unsuffixed)
    correct, via disable-cuda = true. Verified: that flag alone un-suffixes the
    name and the dependency, which is only safe once the dependency is gone.
  3. Does cuopt_mcp stay 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 with verify_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.data is "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

Metadata

Metadata

Assignees

Labels

awaiting responseThis expects a response from maintainer or contributor depending on who requested in last comment.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions