testmcpy 0.2.9__tar.gz → 0.2.11__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. {testmcpy-0.2.9/testmcpy.egg-info → testmcpy-0.2.11}/PKG-INFO +1 -1
  2. {testmcpy-0.2.9 → testmcpy-0.2.11}/pyproject.toml +1 -1
  3. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/__init__.py +8 -1
  4. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/mcp_profiles.py +46 -20
  5. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/server/api.py +61 -67
  6. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/server/routers/mcp_profiles.py +3 -2
  7. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/App.jsx +13 -1
  8. {testmcpy-0.2.9 → testmcpy-0.2.11/testmcpy.egg-info}/PKG-INFO +1 -1
  9. {testmcpy-0.2.9 → testmcpy-0.2.11}/LICENSE +0 -0
  10. {testmcpy-0.2.9 → testmcpy-0.2.11}/MANIFEST.in +0 -0
  11. {testmcpy-0.2.9 → testmcpy-0.2.11}/NOTICE +0 -0
  12. {testmcpy-0.2.9 → testmcpy-0.2.11}/README.md +0 -0
  13. {testmcpy-0.2.9 → testmcpy-0.2.11}/setup.cfg +0 -0
  14. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/auth_debugger.py +0 -0
  15. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/auth_flow_recorder.py +0 -0
  16. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/cli/__init__.py +0 -0
  17. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/cli/app.py +0 -0
  18. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/cli/commands/__init__.py +0 -0
  19. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/cli/commands/mcp.py +0 -0
  20. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/cli/commands/run.py +0 -0
  21. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/cli/commands/server.py +0 -0
  22. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/cli/commands/tools.py +0 -0
  23. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/cli/commands/tui.py +0 -0
  24. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/config.py +0 -0
  25. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/core/__init__.py +0 -0
  26. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/core/chat_session.py +0 -0
  27. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/core/docs_optimizer.py +0 -0
  28. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/core/mcp_manager.py +0 -0
  29. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/core/tool_comparison.py +0 -0
  30. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/core/tool_discovery.py +0 -0
  31. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/error_handlers.py +0 -0
  32. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/evals/__init__.py +0 -0
  33. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/evals/auth_evaluators.py +0 -0
  34. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/evals/base_evaluators.py +0 -0
  35. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/formatters/__init__.py +0 -0
  36. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/formatters/base.py +0 -0
  37. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/formatters/curl.py +0 -0
  38. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/formatters/graphql.py +0 -0
  39. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/formatters/javascript_client.py +0 -0
  40. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/formatters/json_yaml.py +0 -0
  41. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/formatters/protobuf.py +0 -0
  42. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/formatters/python.py +0 -0
  43. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/formatters/python_client.py +0 -0
  44. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/formatters/thrift.py +0 -0
  45. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/formatters/typescript.py +0 -0
  46. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/formatters/typescript_client.py +0 -0
  47. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/llm_profiles.py +0 -0
  48. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/research/claude_sdk_detailed_exploration.py +0 -0
  49. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/research/claude_sdk_poc.py +0 -0
  50. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/research/claude_sdk_working_poc.py +0 -0
  51. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/research/test_ollama_tools.py +0 -0
  52. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/server/__init__.py +0 -0
  53. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/server/api.py.bak +0 -0
  54. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/server/helpers/__init__.py +0 -0
  55. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/server/helpers/mcp_config.py +0 -0
  56. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/server/models.py +0 -0
  57. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/server/routers/__init__.py +0 -0
  58. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/server/routers/auth.py +0 -0
  59. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/server/routers/generation_logs.py +0 -0
  60. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/server/routers/llm.py +0 -0
  61. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/server/routers/results.py +0 -0
  62. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/server/routers/smoke_reports.py +0 -0
  63. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/server/routers/test_profiles.py +0 -0
  64. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/server/routers/tests.py +0 -0
  65. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/server/routers/tools.py +0 -0
  66. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/server/state.py +0 -0
  67. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/server/tool_compare_endpoint.py +0 -0
  68. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/server/websocket.py +0 -0
  69. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/smoke_test.py +0 -0
  70. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/src/__init__.py +0 -0
  71. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/src/llm_integration.py +0 -0
  72. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/src/mcp_client.py +0 -0
  73. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/src/model_registry.py +0 -0
  74. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/src/models.py +0 -0
  75. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/src/runner_tools.py +0 -0
  76. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/src/test_runner.py +0 -0
  77. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/storage.py +0 -0
  78. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/test_profiles.py +0 -0
  79. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/README.md +0 -0
  80. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/dist/assets/index-CaEBvXci.css +0 -0
  81. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/dist/assets/index-DbhZ0GnU.js +0 -0
  82. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/dist/index.html +0 -0
  83. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/index.html +0 -0
  84. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/package-lock.json +0 -0
  85. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/package.json +0 -0
  86. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/postcss.config.js +0 -0
  87. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/components/CompareToolsTab.jsx +0 -0
  88. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/components/ErrorAlert.jsx +0 -0
  89. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/components/ErrorBoundary.jsx +0 -0
  90. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/components/LLMProfileSelector.jsx +0 -0
  91. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/components/LoadingSpinner.jsx +0 -0
  92. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/components/MCPProfileSelector.jsx +0 -0
  93. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/components/OptimizeDocsModal.jsx +0 -0
  94. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/components/ParameterCard.jsx +0 -0
  95. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/components/SchemaCodeViewer.jsx +0 -0
  96. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/components/SkeletonLoader.jsx +0 -0
  97. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/components/TestGenerationModal.jsx +0 -0
  98. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/components/TestProfileSelector.jsx +0 -0
  99. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/components/TestResultPanel.jsx +0 -0
  100. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/components/TestStatusIndicator.jsx +0 -0
  101. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/components/ToolComparison.jsx +0 -0
  102. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/components/ToolDebugModal.jsx +0 -0
  103. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/components/TypeBadge.jsx +0 -0
  104. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/contexts/TestRunContext.jsx +0 -0
  105. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/hooks/useKeyboardShortcuts.js +0 -0
  106. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/hooks/useSafeFetch.js +0 -0
  107. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/index.css +0 -0
  108. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/main.jsx +0 -0
  109. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/pages/AuthDebugger.jsx +0 -0
  110. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/pages/ChatInterface.jsx +0 -0
  111. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/pages/Configuration.jsx +0 -0
  112. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/pages/GenerationHistory.jsx +0 -0
  113. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/pages/LLMProfiles.jsx +0 -0
  114. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/pages/MCPExplorer.jsx +0 -0
  115. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/pages/MCPProfiles.jsx +0 -0
  116. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/pages/ProfilesManager.jsx +0 -0
  117. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/pages/Reports.jsx +0 -0
  118. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/pages/TestManager.jsx +0 -0
  119. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/utils/__tests__/formatConverters.test.js +0 -0
  120. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/src/utils/formatConverters.js +0 -0
  121. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/tailwind.config.js +0 -0
  122. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy/ui/vite.config.js +0 -0
  123. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy.egg-info/SOURCES.txt +0 -0
  124. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy.egg-info/dependency_links.txt +0 -0
  125. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy.egg-info/entry_points.txt +0 -0
  126. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy.egg-info/requires.txt +0 -0
  127. {testmcpy-0.2.9 → testmcpy-0.2.11}/testmcpy.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: testmcpy
3
- Version: 0.2.9
3
+ Version: 0.2.11
4
4
  Summary: A comprehensive testing framework for validating LLM tool calling capabilities with MCP services
5
5
  Author: Amin Ghadersohi
6
6
  License-Expression: Apache-2.0
@@ -88,7 +88,7 @@ testmcpy = [
88
88
 
89
89
  [project]
90
90
  name = "testmcpy"
91
- version = "0.2.9"
91
+ version = "0.2.11"
92
92
  description = "A comprehensive testing framework for validating LLM tool calling capabilities with MCP services"
93
93
  authors = [{name = "Amin Ghadersohi"}]
94
94
  license = "Apache-2.0"
@@ -5,5 +5,12 @@ A comprehensive testing framework for validating LLM tool calling
5
5
  capabilities with MCP (Model Context Protocol) services.
6
6
  """
7
7
 
8
- __version__ = "0.2.0"
8
+ try:
9
+ from importlib.metadata import version
10
+
11
+ __version__ = version("testmcpy")
12
+ except Exception:
13
+ # Fallback for development or when package not installed
14
+ __version__ = "0.2.11"
15
+
9
16
  __author__ = "testmcpy Contributors"
@@ -120,12 +120,7 @@ class MCPProfileConfig:
120
120
  """
121
121
  Find MCP services configuration file.
122
122
 
123
- Searches in order:
124
- 1. Provided config_path
125
- 2. .mcp_services.yaml in current directory
126
- 3. .mcp_services.yaml in parent directories (up to 5 levels)
127
-
128
- Note: User home directory (~/.mcp_services.yaml) is NOT searched.
123
+ Looks for .mcp_services.yaml in the current working directory.
129
124
  """
130
125
  if config_path:
131
126
  path = Path(config_path)
@@ -133,15 +128,10 @@ class MCPProfileConfig:
133
128
  return path
134
129
  return None
135
130
 
136
- # Check current directory and parents
137
- current = Path.cwd()
138
- for _ in range(5):
139
- config_file = current / ".mcp_services.yaml"
140
- if config_file.exists():
141
- return config_file
142
- if current.parent == current:
143
- break
144
- current = current.parent
131
+ # Check current directory only (same as llm_profiles)
132
+ config_file = Path.cwd() / ".mcp_services.yaml"
133
+ if config_file.exists():
134
+ return config_file
145
135
 
146
136
  return None
147
137
 
@@ -196,14 +186,33 @@ class MCPProfileConfig:
196
186
 
197
187
  # Load profiles
198
188
  profiles_config = config.get("profiles", {})
199
- for profile_id, profile_data in profiles_config.items():
200
- self.profiles[profile_id] = self._parse_profile(profile_id, profile_data)
189
+ if not isinstance(profiles_config, dict):
190
+ print(
191
+ f"Warning: 'profiles' in config should be a dict, got {type(profiles_config).__name__}"
192
+ )
193
+ return
201
194
 
195
+ for profile_id, profile_data in profiles_config.items():
196
+ if not isinstance(profile_data, dict):
197
+ print(
198
+ f"Warning: Skipping profile '{profile_id}' - invalid format (expected dict)"
199
+ )
200
+ continue
201
+ try:
202
+ self.profiles[profile_id] = self._parse_profile(profile_id, profile_data)
203
+ except Exception as e:
204
+ print(f"Warning: Failed to parse profile '{profile_id}': {e}")
205
+ continue
206
+
207
+ except yaml.YAMLError as e:
208
+ print(f"Warning: Invalid YAML in {self.config_path}: {e}")
202
209
  except Exception as e:
203
210
  print(f"Warning: Failed to load MCP profile config from {self.config_path}: {e}")
204
211
 
205
- def _parse_auth(self, auth_data: dict[str, Any]) -> AuthConfig:
212
+ def _parse_auth(self, auth_data: dict[str, Any] | None) -> AuthConfig:
206
213
  """Parse auth configuration."""
214
+ if not auth_data or not isinstance(auth_data, dict):
215
+ return AuthConfig(auth_type="none")
207
216
  auth_type = auth_data.get("type", "none")
208
217
 
209
218
  return AuthConfig(
@@ -252,10 +261,27 @@ class MCPProfileConfig:
252
261
  mcps.append(mcp_server)
253
262
  else:
254
263
  # New format: multiple MCPs per profile
255
- for mcp_data in mcps_data:
264
+ if not isinstance(mcps_data, list):
265
+ print(
266
+ f"Warning: 'mcps' in profile '{profile_id}' should be a list, got {type(mcps_data).__name__}"
267
+ )
268
+ mcps_data = []
269
+
270
+ for idx, mcp_data in enumerate(mcps_data):
271
+ if not isinstance(mcp_data, dict):
272
+ print(
273
+ f"Warning: Skipping MCP entry {idx} in profile '{profile_id}' - invalid format"
274
+ )
275
+ continue
276
+ mcp_url = mcp_data.get("mcp_url")
277
+ if not mcp_url:
278
+ print(
279
+ f"Warning: Skipping MCP entry {idx} in profile '{profile_id}' - missing 'mcp_url'"
280
+ )
281
+ continue
256
282
  mcp_server = MCPServer(
257
283
  name=mcp_data.get("name", "Unnamed MCP"),
258
- mcp_url=mcp_data["mcp_url"],
284
+ mcp_url=mcp_url,
259
285
  auth=self._parse_auth(mcp_data.get("auth", {})),
260
286
  timeout=mcp_data.get("timeout", timeout),
261
287
  rate_limit_rpm=mcp_data.get("rate_limit_rpm", rate_limit_rpm),
@@ -373,6 +373,14 @@ async def health_check():
373
373
  }
374
374
 
375
375
 
376
+ @app.get("/api/version")
377
+ async def get_version():
378
+ """Get the testmcpy version."""
379
+ from testmcpy import __version__
380
+
381
+ return {"version": __version__}
382
+
383
+
376
384
  @app.get("/api/config")
377
385
  async def get_configuration():
378
386
  """Get current configuration."""
@@ -521,17 +529,15 @@ async def list_mcp_tools(profiles: list[str] = Query(default=None)):
521
529
  @app.get("/api/mcp/resources")
522
530
  async def list_mcp_resources(profiles: list[str] = Query(default=None)):
523
531
  """List all MCP resources. Supports optional ?profiles=xxx&profiles=yyy parameters."""
524
- accessed_servers = [] # Track servers accessed for cache invalidation on error
525
- try:
526
- all_resources = []
527
-
528
- if profiles:
529
- # Parse server IDs in format "profileId:mcpName"
530
- for server_id in profiles:
531
- if ":" in server_id:
532
- # New format: specific server selection
533
- profile_id, mcp_name = server_id.split(":", 1)
534
- accessed_servers.append(f"{profile_id}:{mcp_name}")
532
+ all_resources = []
533
+
534
+ if profiles:
535
+ # Parse server IDs in format "profileId:mcpName"
536
+ for server_id in profiles:
537
+ if ":" in server_id:
538
+ # New format: specific server selection
539
+ profile_id, mcp_name = server_id.split(":", 1)
540
+ try:
535
541
  client = await get_mcp_client_for_server(profile_id, mcp_name)
536
542
  if client:
537
543
  resources = await client.list_resources()
@@ -539,47 +545,40 @@ async def list_mcp_resources(profiles: list[str] = Query(default=None)):
539
545
  if isinstance(resource, dict):
540
546
  resource["mcp_source"] = mcp_name
541
547
  all_resources.append(resource)
542
- else:
543
- # Legacy format: entire profile
548
+ except Exception as e:
549
+ # Server doesn't support resources or connection failed - skip silently
550
+ print(f"Warning: Could not list resources from {mcp_name}: {e}")
551
+ else:
552
+ # Legacy format: entire profile
553
+ try:
544
554
  clients = await get_mcp_clients_for_profile(server_id)
545
555
  for mcp_name, client in clients:
546
- accessed_servers.append(f"{server_id}:{mcp_name}")
547
- resources = await client.list_resources()
548
- for resource in resources:
549
- if isinstance(resource, dict):
550
- resource["mcp_source"] = mcp_name
551
- all_resources.append(resource)
556
+ try:
557
+ resources = await client.list_resources()
558
+ for resource in resources:
559
+ if isinstance(resource, dict):
560
+ resource["mcp_source"] = mcp_name
561
+ all_resources.append(resource)
562
+ except Exception as e:
563
+ print(f"Warning: Could not list resources from {mcp_name}: {e}")
564
+ except Exception as e:
565
+ print(f"Warning: Could not get clients for profile {server_id}: {e}")
552
566
 
553
- return all_resources
554
- except HTTPException:
555
- raise
556
- except Exception as e:
557
- error_msg = str(e)
558
- if is_connection_error(error_msg):
559
- # Clear stale cached clients so retry can get fresh connection
560
- for cache_key in accessed_servers:
561
- await clear_cached_client(cache_key)
562
- raise HTTPException(
563
- status_code=503,
564
- detail=f"Service unavailable: Unable to connect to MCP server. {error_msg}",
565
- )
566
- raise HTTPException(status_code=500, detail=error_msg)
567
+ return all_resources
567
568
 
568
569
 
569
570
  @app.get("/api/mcp/prompts")
570
571
  async def list_mcp_prompts(profiles: list[str] = Query(default=None)):
571
572
  """List all MCP prompts. Supports optional ?profiles=xxx&profiles=yyy parameters."""
572
- accessed_servers = [] # Track servers accessed for cache invalidation on error
573
- try:
574
- all_prompts = []
575
-
576
- if profiles:
577
- # Parse server IDs in format "profileId:mcpName"
578
- for server_id in profiles:
579
- if ":" in server_id:
580
- # New format: specific server selection
581
- profile_id, mcp_name = server_id.split(":", 1)
582
- accessed_servers.append(f"{profile_id}:{mcp_name}")
573
+ all_prompts = []
574
+
575
+ if profiles:
576
+ # Parse server IDs in format "profileId:mcpName"
577
+ for server_id in profiles:
578
+ if ":" in server_id:
579
+ # New format: specific server selection
580
+ profile_id, mcp_name = server_id.split(":", 1)
581
+ try:
583
582
  client = await get_mcp_client_for_server(profile_id, mcp_name)
584
583
  if client:
585
584
  prompts = await client.list_prompts()
@@ -587,31 +586,26 @@ async def list_mcp_prompts(profiles: list[str] = Query(default=None)):
587
586
  if isinstance(prompt, dict):
588
587
  prompt["mcp_source"] = mcp_name
589
588
  all_prompts.append(prompt)
590
- else:
591
- # Legacy format: entire profile
589
+ except Exception as e:
590
+ # Server doesn't support prompts or connection failed - skip silently
591
+ print(f"Warning: Could not list prompts from {mcp_name}: {e}")
592
+ else:
593
+ # Legacy format: entire profile
594
+ try:
592
595
  clients = await get_mcp_clients_for_profile(server_id)
593
596
  for mcp_name, client in clients:
594
- accessed_servers.append(f"{server_id}:{mcp_name}")
595
- prompts = await client.list_prompts()
596
- for prompt in prompts:
597
- if isinstance(prompt, dict):
598
- prompt["mcp_source"] = mcp_name
599
- all_prompts.append(prompt)
600
-
601
- return all_prompts
602
- except HTTPException:
603
- raise
604
- except Exception as e:
605
- error_msg = str(e)
606
- if is_connection_error(error_msg):
607
- # Clear stale cached clients so retry can get fresh connection
608
- for cache_key in accessed_servers:
609
- await clear_cached_client(cache_key)
610
- raise HTTPException(
611
- status_code=503,
612
- detail=f"Service unavailable: Unable to connect to MCP server. {error_msg}",
613
- )
614
- raise HTTPException(status_code=500, detail=error_msg)
597
+ try:
598
+ prompts = await client.list_prompts()
599
+ for prompt in prompts:
600
+ if isinstance(prompt, dict):
601
+ prompt["mcp_source"] = mcp_name
602
+ all_prompts.append(prompt)
603
+ except Exception as e:
604
+ print(f"Warning: Could not list prompts from {mcp_name}: {e}")
605
+ except Exception as e:
606
+ print(f"Warning: Could not get clients for profile {server_id}: {e}")
607
+
608
+ return all_prompts
615
609
 
616
610
 
617
611
  # Chat endpoint
@@ -166,10 +166,11 @@ global:
166
166
  @router.get("/profiles")
167
167
  async def list_mcp_profiles():
168
168
  """List available MCP profiles from .mcp_services.yaml."""
169
- from testmcpy.mcp_profiles import get_profile_config
169
+ from testmcpy.mcp_profiles import reload_profile_config
170
170
 
171
171
  try:
172
- profile_config = get_profile_config()
172
+ # Always reload to pick up file changes
173
+ profile_config = reload_profile_config()
173
174
  if not profile_config.has_profiles():
174
175
  return {
175
176
  "profiles": [],
@@ -36,6 +36,7 @@ function AppContent() {
36
36
  const [selectedLlmProfile, setSelectedLlmProfile] = useState(null)
37
37
  const [apiReady, setApiReady] = useState(false)
38
38
  const [healthCheckAttempts, setHealthCheckAttempts] = useState(0)
39
+ const [appVersion, setAppVersion] = useState('v0.0.0')
39
40
  const navigate = useNavigate()
40
41
  const location = useLocation()
41
42
 
@@ -48,6 +49,7 @@ function AppContent() {
48
49
  loadConfig()
49
50
  loadProfiles()
50
51
  loadLlmProfiles()
52
+ loadVersion()
51
53
  }
52
54
  }, [apiReady])
53
55
 
@@ -84,6 +86,16 @@ function AppContent() {
84
86
  setApiReady(true)
85
87
  }
86
88
 
89
+ const loadVersion = async () => {
90
+ try {
91
+ const res = await fetch('/api/version')
92
+ const data = await res.json()
93
+ setAppVersion(`v${data.version}`)
94
+ } catch (error) {
95
+ console.error('Failed to load version:', error)
96
+ }
97
+ }
98
+
87
99
  const loadConfig = async () => {
88
100
  try {
89
101
  const res = await fetch('/api/config')
@@ -419,7 +431,7 @@ function AppContent() {
419
431
  {sidebarOpen && (
420
432
  <div className="text-xs text-text-tertiary space-y-0.5">
421
433
  <div className="font-medium">MCP Testing Framework</div>
422
- <div className="text-text-disabled">v1.0.0</div>
434
+ <div className="text-text-disabled">{appVersion}</div>
423
435
  </div>
424
436
  )}
425
437
  </div>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: testmcpy
3
- Version: 0.2.9
3
+ Version: 0.2.11
4
4
  Summary: A comprehensive testing framework for validating LLM tool calling capabilities with MCP services
5
5
  Author: Amin Ghadersohi
6
6
  License-Expression: Apache-2.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes