diff --git a/workspace-server/internal/handlers/mcp_plugin_delivery_contract.go b/workspace-server/internal/handlers/mcp_plugin_delivery_contract.go index dd050d7e0..842e88302 100644 --- a/workspace-server/internal/handlers/mcp_plugin_delivery_contract.go +++ b/workspace-server/internal/handlers/mcp_plugin_delivery_contract.go @@ -24,6 +24,13 @@ type MCPPluginDeliveryContract struct { EntryShape string `json:"entry_shape"` Producer string `json:"producer"` Consumer string `json:"consumer"` + // MCPServerName is the SSOT for the platform MCP server NAME — the mcpServers + // key the runtime registers the server under, which Claude Code turns into + // the tool-id prefix `mcp____`. The platform-agent + // template's mcp_servers.yaml and the online/degraded gate's expected tool id + // (conciergePlatformMCPCreateWorkspaceTool) both derive from this value — see + // TestSSOT_DegradeGateToolDerivesFromContract. + MCPServerName string `json:"mcp_server_name"` } // LoadMCPPluginDeliveryContract loads the contract from the repo root. diff --git a/workspace-server/internal/handlers/mcp_plugin_delivery_contract_test.go b/workspace-server/internal/handlers/mcp_plugin_delivery_contract_test.go index cf54ca5ae..33ca38ee8 100644 --- a/workspace-server/internal/handlers/mcp_plugin_delivery_contract_test.go +++ b/workspace-server/internal/handlers/mcp_plugin_delivery_contract_test.go @@ -35,6 +35,32 @@ func TestMCPPluginDeliveryContract_MatchesSSOT(t *testing.T) { if c.Consumer != "claude_sdk_executor._load_settings_mcp" { t.Errorf("consumer = %q, want claude_sdk_executor._load_settings_mcp", c.Consumer) } + if c.MCPServerName != "molecule-platform" { + t.Errorf("mcp_server_name = %q, want molecule-platform", c.MCPServerName) + } +} + +// TestSSOT_DegradeGateToolDerivesFromContract enforces that the online/degraded +// gate's expected platform tool id is DERIVED from the contract's +// mcp_server_name (the SSOT), not an independent hardcode. If the contract's +// server name changes (or the constant drifts), this fails — preventing the +// class of bug where the gate looked for mcp__molecule-platform__create_workspace +// while the runtime (mcp_servers.yaml name: platform) emitted +// mcp__platform__create_workspace, marking every concierge degraded. +func TestSSOT_DegradeGateToolDerivesFromContract(t *testing.T) { + c, err := LoadMCPPluginDeliveryContract() + if err != nil { + t.Fatalf("load contract: %v", err) + } + if c.MCPServerName == "" { + t.Fatal("contract mcp_server_name is empty — SSOT for the platform MCP tool prefix") + } + want := "mcp__" + c.MCPServerName + "__create_workspace" + if conciergePlatformMCPCreateWorkspaceTool != want { + t.Errorf("SSOT drift: conciergePlatformMCPCreateWorkspaceTool = %q, but contract mcp_server_name = %q implies %q.\n"+ + "The degraded gate must look for the tool id the runtime actually emits (mcp____create_workspace).", + conciergePlatformMCPCreateWorkspaceTool, c.MCPServerName, want) + } } // TestMCPPluginDeliveryContract_LoadableFromRepoRoot guards against a moved