Skip to content

Commit 0a6f7e0

Browse files
fix(mcp): Handle missing config file in MCP client availability detection (#12172)
* Handle missing config file in MCP client availability detection * code improvements * [autofix.ci] apply automated fixes * code improvements review * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 4e53cc5 commit 0a6f7e0

2 files changed

Lines changed: 219 additions & 0 deletions

File tree

src/backend/base/langflow/api/v1/mcp_projects.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,13 @@ async def check_installed_mcp_servers(
10131013
project_sse_url,
10141014
list(config_data.get("mcpServers", {}).keys()),
10151015
)
1016+
except FileNotFoundError:
1017+
await logger.adebug(
1018+
"%s config file not found at %s (directory exists, app installed but not configured)",
1019+
client_name,
1020+
config_path,
1021+
)
1022+
# available stays True, installed stays False — app is installed but not yet configured
10161023
except json.JSONDecodeError:
10171024
await logger.awarning("Failed to parse %s config JSON at: %s", client_name, config_path)
10181025
# available is True but installed remains False due to parse error

src/backend/tests/unit/api/v1/test_mcp_projects.py

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,3 +977,215 @@ async def test_mcp_longterm_token_fails_without_superuser():
977977
async with session_scope() as session:
978978
with pytest.raises(HTTPException, match="Auto login required to create a long-term token"):
979979
await create_user_longterm_token(session)
980+
981+
982+
def _prepare_installed_check_env(monkeypatch, tmp_path):
983+
"""Set up environment for check_installed_mcp_servers tests.
984+
985+
Creates per-client config directories under tmp_path so that
986+
``get_config_path`` returns paths whose *parent* directories exist
987+
but whose config *files* may or may not exist.
988+
"""
989+
client_paths = {
990+
"cursor": tmp_path / "cursor" / "mcp.json",
991+
"windsurf": tmp_path / "windsurf" / "mcp_config.json",
992+
"claude": tmp_path / "claude" / "claude_desktop_config.json",
993+
}
994+
# Create parent directories (simulating installed applications)
995+
for path in client_paths.values():
996+
path.parent.mkdir(parents=True, exist_ok=True)
997+
998+
async def fake_get_config_path(client_name):
999+
return client_paths[client_name]
1000+
1001+
monkeypatch.setattr("langflow.api.v1.mcp_projects.get_config_path", fake_get_config_path)
1002+
monkeypatch.setattr("langflow.api.v1.mcp_projects.should_use_mcp_composer", lambda project: False) # noqa: ARG005
1003+
1004+
async def fake_streamable(project_id):
1005+
return f"https://langflow.local/api/v1/mcp/project/{project_id}/streamable"
1006+
1007+
async def fake_sse(project_id):
1008+
return f"https://langflow.local/api/v1/mcp/project/{project_id}/sse"
1009+
1010+
monkeypatch.setattr("langflow.api.v1.mcp_projects.get_project_streamable_http_url", fake_streamable)
1011+
monkeypatch.setattr("langflow.api.v1.mcp_projects.get_project_sse_url", fake_sse)
1012+
1013+
return client_paths
1014+
1015+
1016+
async def test_should_report_available_true_when_app_directory_exists_but_config_file_missing(
1017+
client: AsyncClient,
1018+
user_test_project,
1019+
logged_in_headers,
1020+
tmp_path,
1021+
monkeypatch,
1022+
):
1023+
"""Bug: FileNotFoundError when config file is missing marks client as unavailable.
1024+
1025+
GIVEN: App directories exist (e.g. ~/.cursor/) but config files don't exist yet
1026+
WHEN: GET /mcp/project/{id}/installed is called
1027+
THEN: Each client should have available=True (app is installed) and installed=False (not configured)
1028+
"""
1029+
_prepare_installed_check_env(monkeypatch, tmp_path)
1030+
1031+
response = await client.get(
1032+
f"/api/v1/mcp/project/{user_test_project.id}/installed",
1033+
headers=logged_in_headers,
1034+
)
1035+
1036+
assert response.status_code == 200
1037+
results = response.json()
1038+
1039+
# All three clients should be reported
1040+
assert len(results) == 3
1041+
1042+
for entry in results:
1043+
assert entry["available"] is True, (
1044+
f"{entry['name']} should be available (directory exists) even when config file is missing"
1045+
)
1046+
assert entry["installed"] is False, f"{entry['name']} should not be installed (config file doesn't exist)"
1047+
1048+
1049+
async def test_should_report_installed_true_when_config_file_contains_matching_url(
1050+
client: AsyncClient,
1051+
user_test_project,
1052+
logged_in_headers,
1053+
tmp_path,
1054+
monkeypatch,
1055+
):
1056+
"""Config with matching URL marks client as installed.
1057+
1058+
GIVEN: Config files exist with a matching project URL in mcpServers args
1059+
WHEN: GET /mcp/project/{id}/installed is called
1060+
THEN: Each client should have available=True AND installed=True
1061+
"""
1062+
client_paths = _prepare_installed_check_env(monkeypatch, tmp_path)
1063+
1064+
# Write config files with matching URLs for all clients
1065+
project_id = user_test_project.id
1066+
for path in client_paths.values():
1067+
config = {"mcpServers": {"lf-test": {"args": [f"https://langflow.local/api/v1/mcp/project/{project_id}/sse"]}}}
1068+
path.write_text(json.dumps(config))
1069+
1070+
response = await client.get(
1071+
f"/api/v1/mcp/project/{project_id}/installed",
1072+
headers=logged_in_headers,
1073+
)
1074+
1075+
assert response.status_code == 200
1076+
results = response.json()
1077+
1078+
for entry in results:
1079+
assert entry["available"] is True, f"{entry['name']} should be available"
1080+
assert entry["installed"] is True, f"{entry['name']} should be installed (config has matching URL)"
1081+
1082+
1083+
async def test_should_report_installed_false_when_config_file_has_no_matching_url(
1084+
client: AsyncClient,
1085+
user_test_project,
1086+
logged_in_headers,
1087+
tmp_path,
1088+
monkeypatch,
1089+
):
1090+
"""Config with non-matching URL reports installed=False.
1091+
1092+
GIVEN: Config files exist but with a DIFFERENT project URL
1093+
WHEN: GET /mcp/project/{id}/installed is called
1094+
THEN: available=True (file exists) but installed=False (URL doesn't match)
1095+
"""
1096+
client_paths = _prepare_installed_check_env(monkeypatch, tmp_path)
1097+
1098+
for path in client_paths.values():
1099+
config = {"mcpServers": {"other-server": {"args": ["https://other-server.example.com/sse"]}}}
1100+
path.write_text(json.dumps(config))
1101+
1102+
response = await client.get(
1103+
f"/api/v1/mcp/project/{user_test_project.id}/installed",
1104+
headers=logged_in_headers,
1105+
)
1106+
1107+
assert response.status_code == 200
1108+
results = response.json()
1109+
1110+
for entry in results:
1111+
assert entry["available"] is True, f"{entry['name']} should be available"
1112+
assert entry["installed"] is False, f"{entry['name']} should not be installed (URL doesn't match)"
1113+
1114+
1115+
async def test_should_report_available_false_when_app_directory_does_not_exist(
1116+
client: AsyncClient,
1117+
user_test_project,
1118+
logged_in_headers,
1119+
tmp_path,
1120+
monkeypatch,
1121+
):
1122+
"""Missing app directory reports available=False.
1123+
1124+
GIVEN: App directories do NOT exist (applications not installed)
1125+
WHEN: GET /mcp/project/{id}/installed is called
1126+
THEN: available=False and installed=False for all clients
1127+
"""
1128+
# Point to paths whose parent directories do NOT exist
1129+
nonexistent_paths = {
1130+
"cursor": tmp_path / "nonexistent_cursor" / "mcp.json",
1131+
"windsurf": tmp_path / "nonexistent_windsurf" / "mcp_config.json",
1132+
"claude": tmp_path / "nonexistent_claude" / "claude_desktop_config.json",
1133+
}
1134+
1135+
async def fake_get_config_path(client_name):
1136+
return nonexistent_paths[client_name]
1137+
1138+
monkeypatch.setattr("langflow.api.v1.mcp_projects.get_config_path", fake_get_config_path)
1139+
monkeypatch.setattr("langflow.api.v1.mcp_projects.should_use_mcp_composer", lambda project: False) # noqa: ARG005
1140+
1141+
async def fake_streamable(project_id):
1142+
return f"https://langflow.local/api/v1/mcp/project/{project_id}/streamable"
1143+
1144+
async def fake_sse(project_id):
1145+
return f"https://langflow.local/api/v1/mcp/project/{project_id}/sse"
1146+
1147+
monkeypatch.setattr("langflow.api.v1.mcp_projects.get_project_streamable_http_url", fake_streamable)
1148+
monkeypatch.setattr("langflow.api.v1.mcp_projects.get_project_sse_url", fake_sse)
1149+
1150+
response = await client.get(
1151+
f"/api/v1/mcp/project/{user_test_project.id}/installed",
1152+
headers=logged_in_headers,
1153+
)
1154+
1155+
assert response.status_code == 200
1156+
results = response.json()
1157+
1158+
for entry in results:
1159+
assert entry["available"] is False, f"{entry['name']} should not be available (directory doesn't exist)"
1160+
assert entry["installed"] is False
1161+
1162+
1163+
async def test_should_report_available_true_when_config_file_has_corrupt_json(
1164+
client: AsyncClient,
1165+
user_test_project,
1166+
logged_in_headers,
1167+
tmp_path,
1168+
monkeypatch,
1169+
):
1170+
"""Corrupt JSON config reports available=True but installed=False.
1171+
1172+
GIVEN: Config files exist but contain invalid/corrupt JSON
1173+
WHEN: GET /mcp/project/{id}/installed is called
1174+
THEN: available=True (directory exists) but installed=False (can't parse config)
1175+
"""
1176+
client_paths = _prepare_installed_check_env(monkeypatch, tmp_path)
1177+
1178+
for path in client_paths.values():
1179+
path.write_text("{corrupt json content!!! not valid")
1180+
1181+
response = await client.get(
1182+
f"/api/v1/mcp/project/{user_test_project.id}/installed",
1183+
headers=logged_in_headers,
1184+
)
1185+
1186+
assert response.status_code == 200
1187+
results = response.json()
1188+
1189+
for entry in results:
1190+
assert entry["available"] is True, f"{entry['name']} should be available (directory exists)"
1191+
assert entry["installed"] is False, f"{entry['name']} should not be installed (JSON is corrupt)"

0 commit comments

Comments
 (0)