feat(core): fail-loud when platform concierge declared management MCP is not loaded #3101

Merged
devops-engineer merged 5 commits from core-3082-concierge-mcp-fail-loud into main 2026-06-20 23:05:18 +00:00
4 changed files with 447 additions and 0 deletions
@@ -0,0 +1,300 @@
package handlers
import (
"bytes"
"errors"
"net/http"
"net/http/httptest"
"testing"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/models"
"github.com/DATA-DOG/go-sqlmock"
"github.com/gin-gonic/gin"
)
// TestHeartbeatHandler_PlatformManagementMCPMissing_FlipsOnlineToDegraded
// verifies core#3082 (CR2 #12653 fix): a platform concierge that reports
// loaded_mcp_tools but does NOT include the literal required tool identifier
// `mcp__molecule-platform__create_workspace` is marked degraded. The old
// check compared the loaded tools against the plugin NAME
// (`molecule-ai-plugin-molecule-platform-mcp`) which never matches the
// namespaced tool ids Claude Code dispatches — that was the false-green.
func TestHeartbeatHandler_PlatformManagementMCPMissing_FlipsOnlineToDegraded(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
broadcaster := newTestBroadcaster()
handler := NewRegistryHandler(broadcaster)
// Initial heartbeat UPDATE.
mock.ExpectQuery("SELECT COALESCE\\(current_task").
WithArgs("ws-mcp-missing").
WillReturnRows(sqlmock.NewRows([]string{"current_task", "monthly_spend", "status"}).AddRow("", 0, "online"))
mock.ExpectExec("UPDATE workspaces SET").
WithArgs("ws-mcp-missing", 0.0, "", 0, 60, "").
WillReturnResult(sqlmock.NewResult(0, 1))
// evaluateStatus: currentStatus=online, kind=platform.
mock.ExpectQuery("SELECT status, kind, last_register_failure_at FROM workspaces WHERE id =").
WithArgs("ws-mcp-missing").
WillReturnRows(sqlmock.NewRows([]string{"status", "kind", "last_register_failure_at"}).AddRow("online", "platform", nil))
// platformAgentHasModelSecret: model secret exists.
mock.ExpectQuery("SELECT EXISTS").
WithArgs("ws-mcp-missing").
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
// platformAgentManagementMCPLoaded: listDeclaredPlugins returns management MCP.
mock.ExpectQuery("SELECT plugin_name, source_raw FROM workspace_declared_plugins").
WithArgs("ws-mcp-missing").
WillReturnRows(sqlmock.NewRows([]string{"plugin_name", "source_raw"}).
AddRow(conciergePlatformMCPName, "gitea://molecule-ai/molecule-ai-plugin-molecule-platform-mcp#main"))
// Degraded UPDATE — required tool absent.
mock.ExpectExec("UPDATE workspaces SET status =.*status = 'online'").
WithArgs(models.StatusDegraded, "platform agent management MCP declared but not loaded; marking degraded (core#3082)", "ws-mcp-missing").
WillReturnResult(sqlmock.NewResult(0, 1))
// WORKSPACE_DEGRADED broadcast.
mock.ExpectExec("INSERT INTO structure_events").
WillReturnResult(sqlmock.NewResult(0, 1))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
// loaded_mcp_tools has plenty of tools but NOT the literal required one.
body := `{"workspace_id":"ws-mcp-missing","error_rate":0.0,"sample_error":"","active_tasks":0,"uptime_seconds":60,"mcp_server_present":true,"loaded_mcp_tools":["a2a","mcp__other-server__other-tool"]}`
c.Request = httptest.NewRequest("POST", "/registry/heartbeat", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
handler.Heartbeat(c)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations: %v", err)
}
}
// TestHeartbeatHandler_PlatformManagementMCPLoaded_StaysOnline verifies that
// a platform concierge reporting the literal required create_workspace tool
// in loaded_mcp_tools stays online. (The previous test loaded the plugin
// NAME as a fake tool — that was a no-op false-green; this test pins the
// real contract.)
func TestHeartbeatHandler_PlatformManagementMCPLoaded_StaysOnline(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
broadcaster := newTestBroadcaster()
handler := NewRegistryHandler(broadcaster)
mock.ExpectQuery("SELECT COALESCE\\(current_task").
WithArgs("ws-mcp-ok").
WillReturnRows(sqlmock.NewRows([]string{"current_task", "monthly_spend", "status"}).AddRow("", 0, "online"))
mock.ExpectExec("UPDATE workspaces SET").
WithArgs("ws-mcp-ok", 0.0, "", 0, 60, "").
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectQuery("SELECT status, kind, last_register_failure_at FROM workspaces WHERE id =").
WithArgs("ws-mcp-ok").
WillReturnRows(sqlmock.NewRows([]string{"status", "kind", "last_register_failure_at"}).AddRow("online", "platform", nil))
mock.ExpectQuery("SELECT EXISTS").
WithArgs("ws-mcp-ok").
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
mock.ExpectQuery("SELECT plugin_name, source_raw FROM workspace_declared_plugins").
WithArgs("ws-mcp-ok").
WillReturnRows(sqlmock.NewRows([]string{"plugin_name", "source_raw"}).
AddRow(conciergePlatformMCPName, "gitea://molecule-ai/molecule-ai-plugin-molecule-platform-mcp#main"))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
// loaded_mcp_tools carries the literal required tool identifier.
body := `{"workspace_id":"ws-mcp-ok","error_rate":0.0,"sample_error":"","active_tasks":0,"uptime_seconds":60,"mcp_server_present":true,"loaded_mcp_tools":["a2a","` + conciergePlatformMCPCreateWorkspaceTool + `"]}`
c.Request = httptest.NewRequest("POST", "/registry/heartbeat", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
handler.Heartbeat(c)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations: %v", err)
}
}
// TestHeartbeatHandler_RuntimeEmitsServerPresentButNoLoadedTools_Degraded
// pins the CR2+Researcher fail-loud behavior: a runtime that speaks the
// #147 contract (mcp_server_present=true) but does NOT report the new
// loaded_mcp_tools producer cannot prove the management MCP is actually
// loaded — flip to degraded instead of silent-skip. The previous
// "old-runtime stays online" test was the false-green #3082 exists to
// catch; the new contract says: if you can prove server-up, prove tools
// too, or fail loud.
func TestHeartbeatHandler_RuntimeEmitsServerPresentButNoLoadedTools_Degraded(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
broadcaster := newTestBroadcaster()
handler := NewRegistryHandler(broadcaster)
mock.ExpectQuery("SELECT COALESCE\\(current_task").
WithArgs("ws-server-present-no-tools").
WillReturnRows(sqlmock.NewRows([]string{"current_task", "monthly_spend", "status"}).AddRow("", 0, "online"))
mock.ExpectExec("UPDATE workspaces SET").
WithArgs("ws-server-present-no-tools", 0.0, "", 0, 60, "").
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectQuery("SELECT status, kind, last_register_failure_at FROM workspaces WHERE id =").
WithArgs("ws-server-present-no-tools").
WillReturnRows(sqlmock.NewRows([]string{"status", "kind", "last_register_failure_at"}).AddRow("online", "platform", nil))
mock.ExpectQuery("SELECT EXISTS").
WithArgs("ws-server-present-no-tools").
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
// Degraded UPDATE — runtime spoke server-present but omitted loaded_mcp_tools.
mock.ExpectExec("UPDATE workspaces SET status =.*status = 'online'").
WithArgs(models.StatusDegraded, "platform agent runtime did not report loaded_mcp_tools on a mcp_server_present=true heartbeat; cannot verify create_workspace tool is loaded — marking degraded (core#3082)", "ws-server-present-no-tools").
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec("INSERT INTO structure_events").
WillReturnResult(sqlmock.NewResult(0, 1))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
// mcp_server_present=true but loaded_mcp_tools absent — runtime needs a
// loaded_mcp_tools producer. Until it does, every platform concierge
// will be flagged degraded (which is the honest signal).
body := `{"workspace_id":"ws-server-present-no-tools","error_rate":0.0,"sample_error":"","active_tasks":0,"uptime_seconds":60,"mcp_server_present":true}`
c.Request = httptest.NewRequest("POST", "/registry/heartbeat", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
handler.Heartbeat(c)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations: %v", err)
}
}
// TestHeartbeatHandler_Pre147RuntimeNoMCPServerPresent_StaysOnline pins the
// backward-compat path: a runtime that predates the #147 contract (neither
// mcp_server_present nor loaded_mcp_tools) does NOT trigger the #3082 gate.
// The earlier platformAgentMCPServerPresent nil-tolerance keeps legacy
// runtimes serving until the runtime-side loaded_mcp_tools producer lands.
func TestHeartbeatHandler_Pre147RuntimeNoMCPServerPresent_StaysOnline(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
broadcaster := newTestBroadcaster()
handler := NewRegistryHandler(broadcaster)
mock.ExpectQuery("SELECT COALESCE\\(current_task").
WithArgs("ws-pre-147").
WillReturnRows(sqlmock.NewRows([]string{"current_task", "monthly_spend", "status"}).AddRow("", 0, "online"))
mock.ExpectExec("UPDATE workspaces SET").
WithArgs("ws-pre-147", 0.0, "", 0, 60, "").
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectQuery("SELECT status, kind, last_register_failure_at FROM workspaces WHERE id =").
WithArgs("ws-pre-147").
WillReturnRows(sqlmock.NewRows([]string{"status", "kind", "last_register_failure_at"}).AddRow("online", "platform", nil))
mock.ExpectQuery("SELECT EXISTS").
WithArgs("ws-pre-147").
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
// No listDeclaredPlugins query — the #3082 gate is skipped entirely for
// pre-#147 runtimes (mcp_server_present nil ⇒ platformAgentMCPServerPresent
// returns true under nil-tolerance; the new gate requires
// mcp_server_present != nil && *mcp_server_present).
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"workspace_id":"ws-pre-147","error_rate":0.0,"sample_error":"","active_tasks":0,"uptime_seconds":60}`
c.Request = httptest.NewRequest("POST", "/registry/heartbeat", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
handler.Heartbeat(c)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations: %v", err)
}
}
// TestHeartbeatHandler_PlatformManagementMCPLookupError_FlipsOnlineToDegraded
// verifies that a failure to read workspace_declared_plugins is fail-loud:
// the workspace is marked degraded rather than staying online with an
// unverified management MCP. This closes the false-green path where a broken
// lookup silently looked healthy (CR2 #12653 follow-up).
func TestHeartbeatHandler_PlatformManagementMCPLookupError_FlipsOnlineToDegraded(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
broadcaster := newTestBroadcaster()
handler := NewRegistryHandler(broadcaster)
// Initial heartbeat UPDATE.
mock.ExpectQuery("SELECT COALESCE\\(current_task").
WithArgs("ws-mcp-lookup-err").
WillReturnRows(sqlmock.NewRows([]string{"current_task", "monthly_spend", "status"}).AddRow("", 0, "online"))
mock.ExpectExec("UPDATE workspaces SET").
WithArgs("ws-mcp-lookup-err", 0.0, "", 0, 60, "").
WillReturnResult(sqlmock.NewResult(0, 1))
// evaluateStatus: currentStatus=online, kind=platform.
mock.ExpectQuery("SELECT status, kind, last_register_failure_at FROM workspaces WHERE id =").
WithArgs("ws-mcp-lookup-err").
WillReturnRows(sqlmock.NewRows([]string{"status", "kind", "last_register_failure_at"}).AddRow("online", "platform", nil))
// platformAgentHasModelSecret: model secret exists.
mock.ExpectQuery("SELECT EXISTS").
WithArgs("ws-mcp-lookup-err").
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
// platformAgentManagementMCPLoaded: listDeclaredPlugins fails.
mock.ExpectQuery("SELECT plugin_name, source_raw FROM workspace_declared_plugins").
WithArgs("ws-mcp-lookup-err").
WillReturnError(errors.New("connection refused"))
// Degraded UPDATE — lookup failure must not silently look healthy.
// Use AnyArg for the human-readable message so the test is not brittle
// against the exact wrapped error string produced by listDeclaredPlugins.
mock.ExpectExec("UPDATE workspaces SET status =.*status = 'online'").
WithArgs(models.StatusDegraded, sqlmock.AnyArg(), "ws-mcp-lookup-err").
WillReturnResult(sqlmock.NewResult(0, 1))
// WORKSPACE_DEGRADED broadcast.
mock.ExpectExec("INSERT INTO structure_events").
WillReturnResult(sqlmock.NewResult(0, 1))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
// Even though loaded_mcp_tools contains the required tool, the lookup error
// takes precedence and the workspace must degrade.
body := `{"workspace_id":"ws-mcp-lookup-err","error_rate":0.0,"sample_error":"","active_tasks":0,"uptime_seconds":60,"mcp_server_present":true,"loaded_mcp_tools":["` + conciergePlatformMCPCreateWorkspaceTool + `"]}`
c.Request = httptest.NewRequest("POST", "/registry/heartbeat", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
handler.Heartbeat(c)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations: %v", err)
}
}
@@ -456,6 +456,28 @@ const conciergePlatformMCPSource = "gitea://molecule-ai/molecule-ai-plugin-molec
// derivation, not the human label "molecule-platform-mcp".
const conciergePlatformMCPName = "molecule-ai-plugin-molecule-platform-mcp"
// conciergePlatformMCPCreateWorkspaceTool is the literal MCP tool identifier
// the platform concierge must surface for the post-online fail-loud gate
// (core#3082) to consider the management MCP actually loaded. The Claude
// Code dispatcher formats every MCP tool as `mcp__<server>__<tool>`; the
// platform MCP server's install name derives to "molecule-platform" via
// PluginNameFromSource(conciergePlatformMCPSource), so the create_workspace
// tool's namespaced identifier is `mcp__molecule-platform__create_workspace`.
// This is the SAME literal the staging concierge E2E (tests/e2e/test_staging_
// concierge_creates_workspace_e2e.sh:4.5/6) probes for; pinning it as a
// constant keeps the runtime gate and the E2E in lock-step so a drift in
// one breaks both with the same signal.
//
// Why we don't match the server/plugin NAME here: the heartbeat's
// loaded_mcp_tools list carries TOOL identifiers (mcp__<server>__<tool>),
// not plugin names. The previous check (loadedSet[conciergePlatformMCPName])
// was a no-op false-green — it matched the plugin NAME against a list of
// TOOL strings, which would always be empty for the management MCP (the
// plugin is named "molecule-ai-plugin-molecule-platform-mcp" while its
// tools are namespaced "mcp__molecule-platform__*"). The literal-tool
// match below is the actual contract the runtime must satisfy.
const conciergePlatformMCPCreateWorkspaceTool = "mcp__molecule-platform__create_workspace"
// ensureConciergeProvider pins the concierge's LLM provider to `platform` (core
// companion to ensureConciergeModel). It guarantees the env-level provider pin
// that the runtime needs, independent of the template config.yaml (which is NOT
@@ -413,6 +413,53 @@ func (h *RegistryHandler) platformAgentMCPServerPresent(present *bool) bool {
return present == nil || *present
}
// platformAgentManagementMCPLoaded reports whether the concierge's declared
// management MCP is actually loaded into the LLM's runtime tool list. It
// returns true (caller marks degraded) only when:
// - the workspace has the management plugin declared in
// workspace_declared_plugins (the install NAME conciergePlatformMCPName),
// AND
// - the reported loaded tool list does NOT contain the literal required
// tool identifier (conciergePlatformMCPCreateWorkspaceTool).
//
// Why this checks the TOOL identifier and not the plugin name: the heartbeat's
// loaded_mcp_tools carries namespaced tool ids (`mcp__<server>__<tool>`), not
// plugin names. The management MCP's server is "molecule-platform" (the
// PluginNameFromSource derivation), so its create_workspace tool is
// "mcp__molecule-platform__create_workspace" — a different value from the
// plugin name "molecule-ai-plugin-molecule-platform-mcp". Comparing the
// plugin NAME against TOOL ids was a no-op false-green (CR2 #12653).
//
// If the management plugin is not declared (non-platform workspace, or a
// platform concierge before plugin reconciliation), it returns false (NOT
// missing) so we don't false-alarm on workspaces that legitimately don't
// declare it. Errors are returned to the caller and MUST be treated as
// fail-loud/degraded — a failed lookup must not silently look healthy.
func (h *RegistryHandler) platformAgentManagementMCPLoaded(ctx context.Context, workspaceID string, loaded []string) (bool, error) {
declared, err := listDeclaredPlugins(ctx, workspaceID)
if err != nil {
return false, fmt.Errorf("declared-plugin lookup: %w", err)
}
hasDeclaredManagement := false
for _, d := range declared {
if d.PluginName == conciergePlatformMCPName {
hasDeclaredManagement = true
break
}
}
if !hasDeclaredManagement {
return false, nil
}
for _, t := range loaded {
if t == conciergePlatformMCPCreateWorkspaceTool {
return false, nil
}
}
return true, nil
}
// markWorkspaceFailed updates a workspace row to status='failed' and broadcasts
// WORKSPACE_PROVISION_FAILED. It is a RegistryHandler-local fallback for the
// fail-closed platform-agent identity gate; the WorkspaceHandler's
@@ -1277,6 +1324,67 @@ func (h *RegistryHandler) evaluateStatus(c *gin.Context, payload models.Heartbea
h.markWorkspaceFailed(ctx, payload.WorkspaceID, msg, reason)
return
}
// core#3082: post-online fail-loud for a missing declared management MCP.
//
// Triggered when the runtime AFFIRMATIVELY reports mcp_server_present=true
// (the #147 contract). For pre-#147 runtimes where the field is nil,
// platformAgentMCPServerPresent above already returned true under
// backward-compat — we DO NOT run the #3082 check in that case so
// legacy runtimes don't flip to degraded before the runtime-side
// loaded_mcp_tools producer lands.
//
// Once triggered, the gate has two fail-loud paths:
// - loaded_mcp_tools present but missing the required tool
// (mcp__molecule-platform__create_workspace) → degraded.
// - loaded_mcp_tools ABSENT (runtime says server is up but won't
// report the tools list) → degraded. This is the fail-loud
// behavior CR2+Researcher asked for: silent-skip when the runtime
// doesn't speak the new contract is exactly the false-green
// #3082 exists to catch. Runtime needs a loaded_mcp_tools
// producer (tracked separately — see PR #3101 PM flag).
if payload.MCPServerPresent != nil && *payload.MCPServerPresent {
loaded := payload.LoadedMCPTools
var (
managementMissing bool
mErr error
absentToolsList bool
)
if loaded == nil {
// Runtime speaks #147 (server_present=true) but omits the new
// loaded_mcp_tools producer → we cannot verify the specific
// required tool is loaded. Fail-loud.
managementMissing = true
absentToolsList = true
} else {
managementMissing, mErr = h.platformAgentManagementMCPLoaded(ctx, payload.WorkspaceID, loaded)
}
if mErr != nil {
msg := fmt.Sprintf("platform agent declared management MCP lookup failed: %v; marking degraded (core#3082)", mErr)
log.Printf("Heartbeat: %s (workspace=%s)", msg, payload.WorkspaceID)
if _, err := db.DB.ExecContext(ctx, `UPDATE workspaces SET status = $1, last_sample_error = $2, updated_at = now() WHERE id = $3 AND status = 'online'`, models.StatusDegraded, msg, payload.WorkspaceID); err != nil {
log.Printf("Heartbeat: failed to mark %s degraded (management MCP lookup error): %v", payload.WorkspaceID, err)
}
h.broadcaster.RecordAndBroadcast(ctx, string(events.EventWorkspaceDegraded), payload.WorkspaceID, map[string]interface{}{
"management_mcp_lookup_failed": true,
"sample_error": msg,
})
} else if managementMissing {
msg := "platform agent management MCP declared but not loaded; marking degraded (core#3082)"
if absentToolsList {
msg = "platform agent runtime did not report loaded_mcp_tools on a mcp_server_present=true heartbeat; cannot verify create_workspace tool is loaded — marking degraded (core#3082)"
}
log.Printf("Heartbeat: %s (workspace=%s)", msg, payload.WorkspaceID)
if _, err := db.DB.ExecContext(ctx, `UPDATE workspaces SET status = $1, last_sample_error = $2, updated_at = now() WHERE id = $3 AND status = 'online'`, models.StatusDegraded, msg, payload.WorkspaceID); err != nil {
log.Printf("Heartbeat: failed to mark %s degraded (management MCP missing): %v", payload.WorkspaceID, err)
}
h.broadcaster.RecordAndBroadcast(ctx, string(events.EventWorkspaceDegraded), payload.WorkspaceID, map[string]interface{}{
"management_mcp_missing": true,
"loaded_mcp_tools_absent": absentToolsList,
"sample_error": msg,
})
}
}
}
// Self-reported runtime wedge: takes precedence over the error_rate
@@ -158,6 +158,23 @@ type HeartbeatPayload struct {
// so the fail-closed platform-agent gate can block recovery paths that
// would otherwise resurrect an mcp-less concierge (RCA #2970).
MCPServerPresent *bool `json:"mcp_server_present,omitempty"`
// LoadedMCPTools is the list of namespaced MCP tool identifiers the
// runtime reports as actually loaded for this workspace. For platform
// concierges, core cross-checks this against the declared management
// MCP so a missing plugin is surfaced as degraded instead of silent
// (core#3082, CR2 #12653 fix). Each entry is a Claude Code dispatcher
// id of the form `mcp__<server>__<tool>`; the platform MCP's required
// tool is `mcp__molecule-platform__create_workspace` (see
// conciergePlatformMCPCreateWorkspaceTool).
//
// On a heartbeat where mcp_server_present=true and LoadedMCPTools is
// nil/omitted, the #3082 gate fails loud (degraded) — the runtime
// spoke the #147 contract but omitted the new loaded_mcp_tools
// producer, so we cannot verify the specific required tool is loaded.
// Runtime needs a loaded_mcp_tools producer to make the deployed path
// healthy (tracked separately — see PR #3101 PM flag).
LoadedMCPTools []string `json:"loaded_mcp_tools,omitempty"`
}
// RuntimeMetadata is the adapter-declared capability + override block