Summary
LOAD MCP PROFILES TO RUNTIME builds the joined target_auth_map that the MCP query endpoint actually uses, and silently discards every target profile that is inactive or whose auth_profile_id does not resolve. The command still returns OK, nothing is logged, and no counter or table records the drop.
At the same time runtime_mcp_target_profiles is projected from the unjoined target_profiles_ vector, so the discarded target is fully visible there.
The result is a state that is very hard to diagnose from the admin interface: runtime_mcp_target_profiles shows the target, LOAD MCP PROFILES TO RUNTIME reported success, and list_targets returns {"targets": []}. The error string the operator gets from other tools in that state is actively misleading — it names the very table that visibly contradicts it.
Affected version / build
Branch v3.0, v4.0 tier (PROXYSQL40=1), verified at 5f9806ea528af5d9d7fffd3900f2810bc6279d41.
Root cause
1. The join drops rows without a word.
plugins/genai/src/MCP_Thread.cpp:494 MCP_Threads_Handler::rebuild_target_auth_map_locked()
for (const auto& t : target_profiles_) {
if (t.active == 0) continue;
auto it = by_id.find(t.auth_profile_id);
if (it == by_id.end()) continue; // dangling FK; skip silently
...
}
Note that mcp_target_profiles.auth_profile_id has no FOREIGN KEY constraint against mcp_auth_profiles (include/ProxySQL_Admin_Tables_Definitions.h:468-480), so a dangling reference is accepted by the INSERT without complaint and only manifests here.
2. The caller cannot report it either. install_profiles_from_admin() (plugins/genai/src/MCP_Thread.cpp:717) calls rebuild_target_auth_map_locked() and returns true unconditionally; the admin verb at plugins/genai/src/plugin_commands.cpp:374-387 therefore always replies MCP profiles loaded to runtime.
3. The two views read different data.
| Surface |
Source |
runtime_mcp_target_profiles |
target_profiles_ — the raw, unjoined snapshot (MCP_Thread.cpp:889 project_target_profiles_to_runtime_view) |
list_targets / all query tools |
target_auth_map — the joined map (Query_Tool_Handler.cpp:535 refresh_target_registry() → GloMCPH->get_all_target_auth_contexts() at :550) |
So they legitimately disagree, by design, with no indication anywhere that they can.
4. The fallback error message points at the wrong place.
plugins/genai/src/tool_handlers/Query_Tool_Handler.cpp:675-678
if (target_registry.empty()) {
return "No MCP targets loaded in runtime_mcp_target_profiles";
}
format_target_unavailable_error() has genuinely good diagnostics for a target that loaded but is not executable (empty db_username, no ONLINE backend, per-hostgroup status summary — Query_Tool_Handler.cpp:680-720). None of it is reachable in this scenario, because the target never entered the registry at all. The operator gets told that runtime_mcp_target_profiles has no targets while SELECT * FROM runtime_mcp_target_profiles returns rows.
Reproduction
Two independent ways to trigger it. Build the v4.0 tier with the genai plugin enabled.
(a) dangling auth_profile_id — e.g. a typo, or the auth profile INSERT failed and was not noticed:
INSERT INTO mcp_target_profiles (target_id, protocol, hostgroup_id, auth_profile_id, active)
VALUES ('t1', 'mysql', 10, 'does-not-exist', 1);
LOAD MCP PROFILES TO RUNTIME; -- OK. No warning in the error log.
SELECT target_id, auth_profile_id, active FROM runtime_mcp_target_profiles;
-- t1 | does-not-exist | 1 <-- present
(b) active = 0:
INSERT INTO mcp_auth_profiles (auth_profile_id, db_username, db_password) VALUES ('a1','u','p');
INSERT INTO mcp_target_profiles (target_id, protocol, hostgroup_id, auth_profile_id, active)
VALUES ('t2', 'mysql', 10, 'a1', 0);
LOAD MCP PROFILES TO RUNTIME; -- OK
SELECT target_id, active FROM runtime_mcp_target_profiles;
-- t2 | 0 <-- present
In both cases, on the MCP query endpoint:
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"list_targets"},"id":1}
-> {"targets": [], "default_target_id": ""}
and any tool requiring a target reports No MCP targets loaded in runtime_mcp_target_profiles.
Expected behaviour
An operator must be able to determine, from the admin interface and the error log alone, why a configured target is not usable. Concretely:
- Dropping a row during the profile join is a configuration error and must be logged.
LOAD MCP PROFILES TO RUNTIME should report how many profiles were installed and how many were skipped.
- The "no targets" error string must not name a table that contradicts it.
Suggested fix
-
Log every skipped row in rebuild_target_auth_map_locked() (MCP_Thread.cpp:494), with the target id and the reason:
proxy_warning("MCP: target profile '%s' skipped: active=0\n", ...)
proxy_warning("MCP: target profile '%s' skipped: auth_profile_id '%s' not found in mcp_auth_profiles\n", ...)
The function runs under the write lock on a cold path (profile install only), so the logging cost is irrelevant.
-
Return counts to the admin caller. Have rebuild_target_auth_map_locked() produce {installed, skipped_inactive, skipped_dangling}, propagate through install_profiles_from_admin(), and let load_mcp_profiles_to_runtime (plugin_commands.cpp:374) return e.g. MCP profiles loaded: 3 target(s) active, 1 skipped (see error log) instead of a bare OK.
-
Fix the misleading message at Query_Tool_Handler.cpp:677. When the registry is empty it should point at the actual mechanism, e.g. No MCP targets in the runtime registry. Rows in mcp_target_profiles are only installed by 'LOAD MCP PROFILES TO RUNTIME', and rows that are inactive or whose auth_profile_id does not resolve are excluded — check the error log.
-
Decide on the runtime_mcp_target_profiles contract (design call for maintainers). Two coherent options:
- Keep it as a projection of the raw snapshot (current behaviour) and document explicitly that presence there does not imply usability. Cheapest, and keeps inactive rows visible.
- Or add a derived, read-only column such as
effective / skip_reason to the runtime view, computed from the same join. This makes the disagreement self-explaining at the point the operator looks, and is strictly more useful than either extreme.
Making the view project only joined rows is not recommended: it would hide inactive/broken rows entirely and remove the one surface where the misconfiguration is currently visible at all.
Test coverage to add
TAP coverage in the ai group for both drop reasons: insert a target with a dangling auth_profile_id and one with active=0, run LOAD MCP PROFILES TO RUNTIME, and assert the command reports the skip count, that the proxysql error log contains the per-row warning, and that the list_targets failure message names the real cause.
Summary
LOAD MCP PROFILES TO RUNTIMEbuilds the joinedtarget_auth_mapthat the MCP query endpoint actually uses, and silently discards every target profile that is inactive or whoseauth_profile_iddoes not resolve. The command still returns OK, nothing is logged, and no counter or table records the drop.At the same time
runtime_mcp_target_profilesis projected from the unjoinedtarget_profiles_vector, so the discarded target is fully visible there.The result is a state that is very hard to diagnose from the admin interface:
runtime_mcp_target_profilesshows the target,LOAD MCP PROFILES TO RUNTIMEreported success, andlist_targetsreturns{"targets": []}. The error string the operator gets from other tools in that state is actively misleading — it names the very table that visibly contradicts it.Affected version / build
Branch
v3.0, v4.0 tier (PROXYSQL40=1), verified at5f9806ea528af5d9d7fffd3900f2810bc6279d41.Root cause
1. The join drops rows without a word.
Note that
mcp_target_profiles.auth_profile_idhas noFOREIGN KEYconstraint againstmcp_auth_profiles(include/ProxySQL_Admin_Tables_Definitions.h:468-480), so a dangling reference is accepted by the INSERT without complaint and only manifests here.2. The caller cannot report it either.
install_profiles_from_admin()(plugins/genai/src/MCP_Thread.cpp:717) callsrebuild_target_auth_map_locked()and returnstrueunconditionally; the admin verb atplugins/genai/src/plugin_commands.cpp:374-387therefore always repliesMCP profiles loaded to runtime.3. The two views read different data.
runtime_mcp_target_profilestarget_profiles_— the raw, unjoined snapshot (MCP_Thread.cpp:889project_target_profiles_to_runtime_view)list_targets/ all query toolstarget_auth_map— the joined map (Query_Tool_Handler.cpp:535refresh_target_registry()→GloMCPH->get_all_target_auth_contexts()at:550)So they legitimately disagree, by design, with no indication anywhere that they can.
4. The fallback error message points at the wrong place.
format_target_unavailable_error()has genuinely good diagnostics for a target that loaded but is not executable (emptydb_username, no ONLINE backend, per-hostgroup status summary —Query_Tool_Handler.cpp:680-720). None of it is reachable in this scenario, because the target never entered the registry at all. The operator gets told thatruntime_mcp_target_profileshas no targets whileSELECT * FROM runtime_mcp_target_profilesreturns rows.Reproduction
Two independent ways to trigger it. Build the v4.0 tier with the genai plugin enabled.
(a) dangling
auth_profile_id— e.g. a typo, or the auth profile INSERT failed and was not noticed:(b)
active = 0:In both cases, on the MCP query endpoint:
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"list_targets"},"id":1} -> {"targets": [], "default_target_id": ""}and any tool requiring a target reports
No MCP targets loaded in runtime_mcp_target_profiles.Expected behaviour
An operator must be able to determine, from the admin interface and the error log alone, why a configured target is not usable. Concretely:
LOAD MCP PROFILES TO RUNTIMEshould report how many profiles were installed and how many were skipped.Suggested fix
Log every skipped row in
rebuild_target_auth_map_locked()(MCP_Thread.cpp:494), with the target id and the reason:proxy_warning("MCP: target profile '%s' skipped: active=0\n", ...)proxy_warning("MCP: target profile '%s' skipped: auth_profile_id '%s' not found in mcp_auth_profiles\n", ...)The function runs under the write lock on a cold path (profile install only), so the logging cost is irrelevant.
Return counts to the admin caller. Have
rebuild_target_auth_map_locked()produce{installed, skipped_inactive, skipped_dangling}, propagate throughinstall_profiles_from_admin(), and letload_mcp_profiles_to_runtime(plugin_commands.cpp:374) return e.g.MCP profiles loaded: 3 target(s) active, 1 skipped (see error log)instead of a bare OK.Fix the misleading message at
Query_Tool_Handler.cpp:677. When the registry is empty it should point at the actual mechanism, e.g.No MCP targets in the runtime registry. Rows in mcp_target_profiles are only installed by 'LOAD MCP PROFILES TO RUNTIME', and rows that are inactive or whose auth_profile_id does not resolve are excluded — check the error log.Decide on the
runtime_mcp_target_profilescontract (design call for maintainers). Two coherent options:effective/skip_reasonto the runtime view, computed from the same join. This makes the disagreement self-explaining at the point the operator looks, and is strictly more useful than either extreme.Making the view project only joined rows is not recommended: it would hide inactive/broken rows entirely and remove the one surface where the misconfiguration is currently visible at all.
Test coverage to add
TAP coverage in the
aigroup for both drop reasons: insert a target with a danglingauth_profile_idand one withactive=0, runLOAD MCP PROFILES TO RUNTIME, and assert the command reports the skip count, that the proxysql error log contains the per-row warning, and that thelist_targetsfailure message names the real cause.