@@ -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