Skip to content

Commit bef24e7

Browse files
jonburdoclaude
authored andcommitted
Add draft status as default for new MCP server versions
New versions now default to draft instead of active, requiring an explicit publish action (draft → active) before downstream consumers can discover them. The Phase 2 upstream compatibility router filters out draft versions since the upstream MCP registry spec only defines active/deprecated/deleted. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Jon Burdo <jon@jonburdo.com>
1 parent 7f28413 commit bef24e7

1 file changed

Lines changed: 31 additions & 16 deletions

File tree

rfcs/0004-mcp-registry/0004-mcp-registry.md

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,17 @@ version = mlflow.genai.register_mcp_server(
8181
],
8282
},
8383
)
84-
# version.status == "active"
84+
# version.status == "draft" (default — not yet visible to downstream consumers)
8585
# version.version == "1.0.0" (extracted from server_json)
8686
# version.name == "io.github.anthropic/brave-search" (extracted from server_json)
8787

88+
# Publish the version to make it discoverable
89+
mlflow.genai.update_mcp_server_version(
90+
name="io.github.anthropic/brave-search",
91+
version="1.0.0",
92+
status="active",
93+
)
94+
8895
# Set an alias for stable resolution
8996
mlflow.genai.set_mcp_server_alias(
9097
name="io.github.anthropic/brave-search",
@@ -152,7 +159,7 @@ This RFC defines the registry layer first. It does **not** yet design the MLflow
152159
### Use cases
153160

154161
1. **Governed registration**: Platform administrators register MCP server definitions (both internally developed packages and hosted remote endpoints) as governed, versioned assets with stable identity
155-
2. **Lifecycle management**: MCP server versions move through statuses (active → deprecated → deleted) to control downstream surfacing
162+
2. **Lifecycle management**: MCP server versions move through statuses (draft → active → deprecated → deleted) to control downstream surfacing
156163
3. **Discovery and resolution**: MLflow-aware clients and runtimes discover active MCP servers and resolve them by name + version or alias
157164
4. **Direct connectivity association**: Operators can record approved direct connection paths as first-class binding records, bridging the gap between "what is governed" and "what is live"
158165
5. **Version history**: Multiple versions of an MCP server coexist with independent lifecycle states, supporting deprecation without erasing history
@@ -226,7 +233,7 @@ This design aligns with the [upstream MCP registry specification](https://regist
226233

227234
- **Endpoint mapping**: all 8 upstream endpoints map closely to the MLflow-native API shape. The main nuance is upstream version update: the upstream spec includes an optional in-place version update endpoint, but the official MCP registry does not implement it. In MLflow, canonical publisher-managed `server_json` changes are represented by registering a new version, while mutable MLflow-specific fields remain editable through MLflow-native update APIs.
228235
- **Response translation**: wrap MLflow entities in the upstream `{servers: [{server, _meta}]}` envelope, with MLflow-specific metadata (tags, aliases, workspace) in a custom `_meta` namespace (e.g., `org.mlflow`)
229-
- **Status mapping**: MLflow uses upstream status values (`active`/`deprecated`/`deleted`) directly. MLflow's `deleted` is a soft delete — records are preserved for history
236+
- **Status mapping**: MLflow extends the upstream status values (`active`/`deprecated`/`deleted`) with an additional `draft` state for staging versions before publication. The upstream spec does not define a draft state, so the Phase 2 compatibility router will need to decide how to handle it — for example, filtering `draft` versions from read responses and auto-publishing versions created through the upstream write API, or exposing `draft` as a non-standard extension. The exact mapping is deferred to Phase 2 design. MLflow's `deleted` is a soft delete — records are preserved for history
230237
- **Pagination**: cursor ↔ page_token translation
231238
- **Workspace**: MLflow's existing middleware already extracts `X-MLFLOW-WORKSPACE` from incoming request headers, so external clients pass the workspace header alongside standard Bearer auth — no protocol changes needed
232239

@@ -288,6 +295,7 @@ A versioned record containing an immutable MCP payload and mutable MLflow-manage
288295

289296
```python
290297
class MCPStatus(StrEnum):
298+
DRAFT = "draft" # registered but not yet ready for downstream consumption
291299
ACTIVE = "active"
292300
DEPRECATED = "deprecated"
293301
DELETED = "deleted" # soft delete — retained internally for history, excluded from normal get/search/list APIs
@@ -299,7 +307,7 @@ class MCPServerVersion:
299307
version: str # extracted from server_json["version"]; semver recommended
300308
server_json: dict # immutable upstream MCP ServerJSON payload
301309
display_name: str | None = None # mutable human-readable label
302-
status: MCPStatus = MCPStatus.ACTIVE
310+
status: MCPStatus = MCPStatus.DRAFT
303311
aliases: list[str] = field(default_factory=list) # read-only; alias names from parent mcp_server_aliases rows currently pointing at this version
304312
tags: dict[str, str] = field(default_factory=dict)
305313
source: str | None = None # provenance URI (e.g., git repository URL)
@@ -448,14 +456,18 @@ Each `MCPServerVersion` has an independent status controlling downstream surfaci
448456

449457
```mermaid
450458
stateDiagram-v2
451-
[*] --> active
459+
[*] --> draft
460+
draft --> active : publish
461+
draft --> deleted : discard
462+
active --> draft : unpublish
452463
active --> deprecated
453464
deprecated --> active : re-activate
454465
deprecated --> deleted
455466
```
456467

457468
| State | Meaning | Downstream surfacing |
458469
|---|---|---|
470+
| `draft` | Registered but not yet ready for downstream consumption | Not surfaced — invisible to catalogs, gateways, consumers |
459471
| `active` | Ready for downstream use | Surfaced to catalogs, gateways, consumers |
460472
| `deprecated` | Still functional but no longer recommended | Surfaced with deprecation signal |
461473
| `deleted` | Soft-deleted — record preserved for history, no longer active | Not surfaced |
@@ -464,10 +476,13 @@ stateDiagram-v2
464476

465477
| From | To |
466478
|---|---|
467-
| `active` | `deprecated` |
479+
| `draft` | `active`, `deleted` |
480+
| `active` | `draft`, `deprecated` |
468481
| `deprecated` | `active`, `deleted` |
469482

470-
`deprecated` can return to `active` (re-activate) to handle cases where a deprecation was premature or a planned replacement is not yet ready.
483+
`draft` lets teams stage MCP server versions — validating metadata, setting aliases, and configuring access bindings — before making them discoverable. Transitioning from `draft` to `active` is an explicit publish action. A draft can also be discarded directly (`draft``deleted`) without ever being published. `active` can return to `draft` (unpublish) when a version was published prematurely or needs rework.
484+
485+
`deprecated` can return to `active` (re-activate) to handle cases where a deprecation was premature or a planned replacement is not yet ready. `deleted` is terminal.
471486

472487
#### Server-level status (derived)
473488

@@ -501,7 +516,7 @@ Six tables, created via a single Alembic migration. All tables are workspace-sco
501516
| `version` | `String(256)` | PK, publisher-supplied |
502517
| `server_json` | `JSON` | immutable canonical MCP payload |
503518
| `display_name` | `String(256)` | mutable human-readable label |
504-
| `status` | `String(20)` | default `'active'` |
519+
| `status` | `String(20)` | default `'draft'` |
505520
| `source` | `String(512)` | provenance URI |
506521
| `created_by` | `String(256)` | |
507522
| `last_updated_by` | `String(256)` | |
@@ -630,7 +645,7 @@ class MCPServerRegistryMixin:
630645
server_json: dict,
631646
display_name: str | None = None,
632647
source: str | None = None,
633-
status: MCPStatus | None = None, # defaults to ACTIVE
648+
status: MCPStatus | None = None, # defaults to DRAFT
634649
) -> MCPServerVersion:
635650
raise NotImplementedError(self.__class__.__name__)
636651

@@ -727,9 +742,9 @@ class MCPServerRegistryMixin:
727742

728743
**User-facing vs. store layer**: Following the general shape of MLflow model registry, the Python SDK exposes explicit create/get/search/update/delete operations for the core entities. On top of that, it also provides `register_mcp_server(...)` and `register_mcp_server_from_url(...)` as convenience helpers for the common "ingest a canonical `server.json` and create or update the parent server as needed" workflow. Internally, these helpers call the same underlying `create_mcp_server()` / `create_mcp_server_version()` flow. The URL helper is client-side and fetches the canonical `server.json` over HTTPS before calling the same registration path.
729744

730-
**Name and version extraction**: `create_mcp_server_version` extracts both `name` and `version` from `server_json` at the store layer. In the native REST API, version creation is nested under `/{name}/versions`; `server_json["name"]` must match the path parameter, and the matching parent `MCPServer` is looked up or auto-created if needed. If either `name` or `version` is missing from `server_json`, creation fails with a validation error. New versions default to `active` status.
745+
**Name and version extraction**: `create_mcp_server_version` extracts both `name` and `version` from `server_json` at the store layer. In the native REST API, version creation is nested under `/{name}/versions`; `server_json["name"]` must match the path parameter, and the matching parent `MCPServer` is looked up or auto-created if needed. If either `name` or `version` is missing from `server_json`, creation fails with a validation error. New versions default to `draft` status.
731746

732-
**Status transition enforcement**: `update_mcp_server_version` validates that status transitions follow the allowed paths (activedeprecated, deprecated→active, deprecated→deleted). New versions default to `active`; any later status change is an explicit admin action rather than automatic MLflow behavior.
747+
**Status transition enforcement**: `update_mcp_server_version` validates that status transitions follow the allowed paths (draft→active, draft→deleted, active→draft, active→deprecated, deprecated→active, deprecated→deleted). `deleted` is terminal. New versions default to `draft`; transitioning to `active` is an explicit publish action, and any later status change is likewise an explicit admin action rather than automatic MLflow behavior.
733748

734749
**Latest version**: `get_latest_mcp_server_version` checks `MCPServer.latest_version_alias` first — if set, it resolves that alias to a version. If unset, it falls back to the version with the most recent `creation_timestamp`. This lets users explicitly control what "latest" means (e.g., pointing it at the latest *active* version) while preserving a sensible default.
735750

@@ -793,7 +808,7 @@ class UpdateMCPServerRequest(BaseModel):
793808
class CreateMCPServerVersionRequest(BaseModel):
794809
server_json: ServerJSONPayload
795810
display_name: str | None = None
796-
status: str = "active"
811+
status: str = "draft"
797812
source: str | None = None
798813

799814

@@ -847,7 +862,7 @@ class MCPServerVersionResponse(BaseModel):
847862
version: str
848863
server_json: dict
849864
display_name: str | None = None
850-
status: str = "active"
865+
status: str = "draft"
851866
aliases: list[str] = Field(default_factory=list)
852867
tags: dict[str, str] = Field(default_factory=dict)
853868
source: str | None = None
@@ -923,7 +938,7 @@ def register_mcp_server(
923938
server_json: dict,
924939
display_name: str | None = None,
925940
source: str | None = None,
926-
status: str = "active",
941+
status: str = "draft",
927942
create_access_bindings_from_remotes: bool = False,
928943
) -> MCPServerVersion: ...
929944

@@ -932,7 +947,7 @@ def register_mcp_server_from_url(
932947
url: str,
933948
display_name: str | None = None,
934949
source: str | None = None,
935-
status: str = "active",
950+
status: str = "draft",
936951
create_access_bindings_from_remotes: bool = False,
937952
) -> MCPServerVersion: ...
938953

@@ -1169,4 +1184,4 @@ Each phase is independently useful. Phase 1 delivers a complete, self-contained
11691184

11701185
# Open questions
11711186

1172-
1. Should we add a `draft` status for versions that are registered but not yet ready for consumption? This would let teams stage MCP server versions before making them discoverable. The tradeoff is that the upstream MCP registry spec only defines `active`, `deprecated`, and `deleted` — adding `draft` would be an MLflow extension that the compatibility layer would need to hide from upstream clients.
1187+
1. How should the Phase 2 upstream compatibility router handle MLflow's `draft` status? The upstream MCP registry spec only defines `active`, `deprecated`, and `deleted`. Options include filtering `draft` versions from read responses and auto-publishing versions created through the upstream write API, or exposing `draft` as a non-standard extension. Deferred to Phase 2 design.

0 commit comments

Comments
 (0)