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
447 changes: 447 additions & 0 deletions cpp/src/grpc/codegen/field_registry.yaml

Large diffs are not rendered by default.

132 changes: 132 additions & 0 deletions cpp/src/grpc/codegen/generate_conversions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"""

import argparse
import json
import os
import re
import sys
Expand Down Expand Up @@ -334,6 +335,131 @@ def _array_wire_type_comment(f):
return f"raw bytes ({size} B/elem)"


_JSON_SCHEMA_TYPES = {
"double": "number",
"float": "number",
"int32": "integer",
"int64": "integer",
"uint32": "integer",
"uint64": "integer",
"bool": "boolean",
"string": "string",
}


def _json_schema_property(registry, f):
"""Render one settings field as a JSON Schema property.

Used by the MCP tool-input schema (see generate_mcp_schema). Enums
become string enums keyed by their proto value names so a model emits
`"Stable3"` rather than a magic integer.
"""
ftype = f.get("type", "double")
prop = {}
edef = _lookup_enum(registry, ftype)
if edef is not None and "values" in edef:
# Proto value names, not C++ names, so the schema and the wire agree
# on the spelling a client sends.
prefix = edef.get("proto_prefix", "")
named = {
_proto_enum_value_name(cpp_name, prefix): num
for cpp_name, num in parse_enum_values(edef["values"])
}
prop["type"] = "string"
prop["enum"] = list(named)
# cuOpt's string parameter interface takes the integer for an enum
# setting, not its name. Callers show the name to a user and send
# the number; emitting the mapping keeps that translation derived
# from the registry instead of hand-written in each client.
prop["x-enum-values"] = named
else:
prop["type"] = _JSON_SCHEMA_TYPES.get(ftype, "string")

description = f.get("description")
if description:
prop["description"] = " ".join(str(description).split())
default = f.get("default")
if default is not None:
# Rendered into the description rather than JSON Schema `default`:
# the registry stores prose ("-1 (automatic)", "no limit (INT_MAX)"),
# not a typed value, and a wrong-typed `default` misleads a model
# more than no `default` does.
note = f"Default: {default}."
prop["description"] = (
f"{prop['description']} {note}" if description else note
)
# The registry field name is the proto field name, which is not always
# the CUOPT_* string parameter a client passes to set_parameter (MIP
# diverges heavily: relative_mip_gap vs mip_relative_gap, mir_cuts vs
# mip_mixed_integer_rounding_cuts). Carry the real name so no client has
# to rediscover the mapping.
param_name = f.get("param_name")
if param_name:
prop["x-parameter-name"] = param_name
if f.get("sentinel"):
# The wire encoding (e.g. max() <=> -1) is an implementation detail.
# A model must express "no limit" by omitting the field, never by
# sending the reserved value.
prop["description"] = (
prop.get("description", "") + " Omit for the default limit."
).strip()
return prop


def generate_mcp_schema(registry):
"""Build the MCP tool-input JSON Schema for the solver settings.

Emitted as a generated artifact so an MCP server never hand-maintains a
second copy of the settings surface: a field added to the registry
reaches the schema in the same commit as the proto, and
ci/verify_grpc_codegen.sh guards the pair.

Only fields carrying a `field_num` are included — those are exactly the
settings that cross the gRPC wire, which is exactly what a remote MCP
server can set.
"""
schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$comment": (
"AUTO-GENERATED by src/grpc/codegen/generate_conversions.py "
"from field_registry.yaml. DO NOT EDIT — regenerate with "
"./build.sh codegen."
),
"settings": {},
}
for section, title in (
("pdlp_settings", "PDLPSolverSettings"),
("mip_settings", "MIPSolverSettings"),
):
properties = {}
for f in parse_settings_fields(registry[section].get("fields", [])):
if f.get("field_num") is None:
continue
name = f["name"]
# An explicit `param_name: null` marks a field with no CUOPT_*
# string parameter — settable over the wire but not through the
# parameter API an MCP client uses, so advertising it would
# produce calls that can only fail.
if "param_name" in f and f["param_name"] is None:
continue
assert name not in properties, (
f"duplicate settings field {section}.{name} — JSON Schema "
"properties must be unique"
)
properties[name] = _json_schema_property(registry, f)
schema["settings"][section] = {
"title": title,
"type": "object",
"description": (
"Solver settings. Every field is optional; omit a field to "
"keep the cuOpt default."
),
"properties": properties,
"additionalProperties": False,
}
return json.dumps(schema, indent=2, sort_keys=False) + "\n"


# ============================================================================
# Enum helpers — convention-based derivation
# ============================================================================
Expand Down Expand Up @@ -3828,6 +3954,12 @@ def main():
os.path.join(outdir, "generated_array_field_element_size.inc"),
HEADER + generate_array_field_element_size_inc(registry) + "\n",
)
# JSON has no comment syntax, so the provenance banner every other
# artifact carries in HEADER lives in the schema's own $comment.
write_file(
os.path.join(outdir, "cuopt_mcp_schema.json"),
generate_mcp_schema(registry),
)

print(f"\nDone! Generated {len(os.listdir(outdir))} files in: {outdir}")

Expand Down
Loading
Loading