testmcpy 0.7.26__tar.gz → 0.9.0__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 (212) hide show
  1. {testmcpy-0.7.26/testmcpy.egg-info → testmcpy-0.9.0}/PKG-INFO +78 -14
  2. {testmcpy-0.7.26 → testmcpy-0.9.0}/README.md +69 -13
  3. {testmcpy-0.7.26 → testmcpy-0.9.0}/pyproject.toml +18 -1
  4. testmcpy-0.9.0/testmcpy/__init__.py +17 -0
  5. testmcpy-0.9.0/testmcpy/__main__.py +6 -0
  6. testmcpy-0.9.0/testmcpy/analytics.py +338 -0
  7. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/__init__.py +6 -0
  8. testmcpy-0.9.0/testmcpy/cli/commands/analytics.py +257 -0
  9. testmcpy-0.9.0/testmcpy/cli/commands/badge.py +137 -0
  10. testmcpy-0.9.0/testmcpy/cli/commands/bench.py +143 -0
  11. testmcpy-0.9.0/testmcpy/cli/commands/conformance.py +191 -0
  12. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/run.py +123 -12
  13. testmcpy-0.9.0/testmcpy/cli/commands/scan.py +195 -0
  14. testmcpy-0.9.0/testmcpy/cli/commands/score.py +190 -0
  15. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/wizard.py +20 -2
  16. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/db.py +30 -3
  17. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/evals/__init__.py +9 -0
  18. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/evals/auth_evaluators.py +333 -0
  19. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/evals/base_evaluators.py +10 -1
  20. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/evals/evaluator_packs.py +27 -0
  21. testmcpy-0.9.0/testmcpy/evals/security_evaluators.py +143 -0
  22. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/json_yaml.py +6 -2
  23. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/mcp_profiles.py +44 -0
  24. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/models.py +18 -0
  25. testmcpy-0.9.0/testmcpy/security/__init__.py +20 -0
  26. testmcpy-0.9.0/testmcpy/security/rules.py +184 -0
  27. testmcpy-0.9.0/testmcpy/security/scanner.py +433 -0
  28. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/api.py +66 -39
  29. testmcpy-0.9.0/testmcpy/server/routers/analytics.py +102 -0
  30. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/generation_logs.py +12 -3
  31. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/health.py +63 -5
  32. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/metrics.py +44 -1
  33. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/results.py +27 -71
  34. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/runs.py +36 -2
  35. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/security.py +5 -2
  36. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/tools.py +5 -1
  37. testmcpy-0.9.0/testmcpy/server/run_persistence.py +208 -0
  38. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/run_registry.py +65 -6
  39. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/websocket.py +133 -50
  40. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/ci_gate.py +39 -10
  41. testmcpy-0.9.0/testmcpy/src/emitters.py +153 -0
  42. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/llm_integration.py +1120 -260
  43. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/model_registry.py +181 -0
  44. testmcpy-0.9.0/testmcpy/src/usability_score.py +585 -0
  45. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/storage.py +216 -29
  46. testmcpy-0.9.0/testmcpy/ui/dist/assets/index-BXP9_Odn.js +324 -0
  47. testmcpy-0.9.0/testmcpy/ui/dist/assets/index-D35cfDhp.css +1 -0
  48. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/dist/index.html +2 -2
  49. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/package-lock.json +1510 -15
  50. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/package.json +9 -2
  51. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/App.jsx +23 -25
  52. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/BackgroundRunsIndicator.jsx +15 -4
  53. testmcpy-0.9.0/testmcpy/ui/src/components/Badge.jsx +33 -0
  54. testmcpy-0.9.0/testmcpy/ui/src/components/ConfirmDialog.jsx +66 -0
  55. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/EditorTabStrip.jsx +5 -4
  56. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/NotificationProvider.jsx +2 -1
  57. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/OptimizeDocsModal.jsx +3 -2
  58. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/TestGenerationModal.jsx +3 -2
  59. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/ToolComparison.jsx +85 -24
  60. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/ToolDebugModal.jsx +3 -2
  61. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/Wizard.jsx +2 -2
  62. testmcpy-0.9.0/testmcpy/ui/src/components/__tests__/OutputDiff.test.jsx +17 -0
  63. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/contexts/TestRunContext.jsx +195 -96
  64. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/index.css +4 -0
  65. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/pages/AuthDebugger.jsx +252 -11
  66. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/pages/ChatInterface.jsx +87 -50
  67. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/pages/Configuration.jsx +4 -4
  68. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/pages/GenerationHistory.jsx +38 -10
  69. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/pages/LLMProfiles.jsx +31 -81
  70. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/pages/MCPExplorer.jsx +180 -47
  71. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/pages/MCPProfiles.jsx +66 -113
  72. testmcpy-0.9.0/testmcpy/ui/src/pages/Performance.jsx +584 -0
  73. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/pages/Reports.jsx +350 -352
  74. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/pages/SecurityDashboard.jsx +101 -66
  75. testmcpy-0.9.0/testmcpy/ui/src/pages/Servers.jsx +601 -0
  76. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/pages/TestManager.jsx +522 -461
  77. testmcpy-0.9.0/testmcpy/ui/src/pages/__tests__/ChatInterface.test.jsx +70 -0
  78. testmcpy-0.9.0/testmcpy/ui/src/pages/__tests__/Performance.test.jsx +155 -0
  79. testmcpy-0.9.0/testmcpy/ui/src/test-setup.js +4 -0
  80. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/utils/__tests__/formatConverters.test.js +5 -3
  81. testmcpy-0.9.0/testmcpy/ui/src/utils/formatters.js +34 -0
  82. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/vite.config.js +5 -2
  83. testmcpy-0.9.0/testmcpy/ui/vitest.config.js +11 -0
  84. {testmcpy-0.7.26 → testmcpy-0.9.0/testmcpy.egg-info}/PKG-INFO +78 -14
  85. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy.egg-info/SOURCES.txt +28 -7
  86. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy.egg-info/requires.txt +9 -0
  87. testmcpy-0.7.26/testmcpy/__init__.py +0 -16
  88. testmcpy-0.7.26/testmcpy/server/api.py.bak +0 -3150
  89. testmcpy-0.7.26/testmcpy/ui/dist/assets/index-DN9kK9DT.js +0 -292
  90. testmcpy-0.7.26/testmcpy/ui/dist/assets/index-zH471udX.css +0 -1
  91. testmcpy-0.7.26/testmcpy/ui/src/pages/CompatibilityMatrix.jsx +0 -287
  92. testmcpy-0.7.26/testmcpy/ui/src/pages/MCPHealth.jsx +0 -227
  93. testmcpy-0.7.26/testmcpy/ui/src/pages/MetricsDashboard.jsx +0 -346
  94. testmcpy-0.7.26/testmcpy/ui/src/pages/RunComparison.jsx +0 -420
  95. {testmcpy-0.7.26 → testmcpy-0.9.0}/LICENSE +0 -0
  96. {testmcpy-0.7.26 → testmcpy-0.9.0}/MANIFEST.in +0 -0
  97. {testmcpy-0.7.26 → testmcpy-0.9.0}/NOTICE +0 -0
  98. {testmcpy-0.7.26 → testmcpy-0.9.0}/setup.cfg +0 -0
  99. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/agent/__init__.py +0 -0
  100. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/agent/hooks.py +0 -0
  101. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/agent/models.py +0 -0
  102. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/agent/orchestrator.py +0 -0
  103. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/agent/prompts.py +0 -0
  104. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/agent/tools.py +0 -0
  105. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/auth_debugger.py +0 -0
  106. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/auth_flow_recorder.py +0 -0
  107. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/app.py +0 -0
  108. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/__init__.py +0 -0
  109. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/agent.py +0 -0
  110. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/baseline.py +0 -0
  111. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/export_db.py +0 -0
  112. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/mcp.py +0 -0
  113. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/metamorphic.py +0 -0
  114. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/multi_env.py +0 -0
  115. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/mutate.py +0 -0
  116. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/push.py +0 -0
  117. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/server.py +0 -0
  118. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/tools.py +0 -0
  119. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/tui.py +0 -0
  120. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/config.py +0 -0
  121. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/core/__init__.py +0 -0
  122. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/core/chat_session.py +0 -0
  123. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/core/docs_optimizer.py +0 -0
  124. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/core/mcp_manager.py +0 -0
  125. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/core/tool_comparison.py +0 -0
  126. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/core/tool_discovery.py +0 -0
  127. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/error_handlers.py +0 -0
  128. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/__init__.py +0 -0
  129. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/base.py +0 -0
  130. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/curl.py +0 -0
  131. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/graphql.py +0 -0
  132. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/javascript_client.py +0 -0
  133. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/protobuf.py +0 -0
  134. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/python.py +0 -0
  135. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/python_client.py +0 -0
  136. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/thrift.py +0 -0
  137. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/typescript.py +0 -0
  138. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/typescript_client.py +0 -0
  139. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/llm_profiles.py +0 -0
  140. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/migrate_json.py +0 -0
  141. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/research/claude_sdk_detailed_exploration.py +0 -0
  142. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/research/claude_sdk_poc.py +0 -0
  143. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/research/claude_sdk_working_poc.py +0 -0
  144. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/research/test_ollama_tools.py +0 -0
  145. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/__init__.py +0 -0
  146. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/auth_middleware.py +0 -0
  147. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/helpers/__init__.py +0 -0
  148. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/helpers/mcp_config.py +0 -0
  149. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/models.py +0 -0
  150. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/__init__.py +0 -0
  151. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/agent.py +0 -0
  152. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/auth.py +0 -0
  153. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/compare.py +0 -0
  154. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/compatibility.py +0 -0
  155. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/llm.py +0 -0
  156. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/mcp_profiles.py +0 -0
  157. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/search.py +0 -0
  158. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/smoke_reports.py +0 -0
  159. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/test_profiles.py +0 -0
  160. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/tests.py +0 -0
  161. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/state.py +0 -0
  162. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/smoke_test.py +0 -0
  163. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/__init__.py +0 -0
  164. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/baseline.py +0 -0
  165. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/comparison_runner.py +0 -0
  166. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/coverage_analyzer.py +0 -0
  167. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/html_report.py +0 -0
  168. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/mcp_client.py +0 -0
  169. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/metamorphic.py +0 -0
  170. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/models.py +0 -0
  171. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/multi_env.py +0 -0
  172. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/oauth_flows.py +0 -0
  173. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/prompt_mutation.py +0 -0
  174. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/report_generator.py +0 -0
  175. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/runner_tools.py +0 -0
  176. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/schema_diff.py +0 -0
  177. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/test_runner.py +0 -0
  178. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/token_manager.py +0 -0
  179. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/test_profiles.py +0 -0
  180. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/README.md +0 -0
  181. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/index.html +0 -0
  182. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/postcss.config.js +0 -0
  183. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/CommandPalette.jsx +0 -0
  184. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/CompareToolsTab.jsx +0 -0
  185. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/EditorStatusBar.jsx +0 -0
  186. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/ErrorAlert.jsx +0 -0
  187. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/ErrorBoundary.jsx +0 -0
  188. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/LLMProfileSelector.jsx +0 -0
  189. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/LoadingSpinner.jsx +0 -0
  190. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/MCPProfileSelector.jsx +0 -0
  191. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/OutputDiff.jsx +0 -0
  192. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/ParameterCard.jsx +0 -0
  193. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/SchemaCodeViewer.jsx +0 -0
  194. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/SkeletonLoader.jsx +0 -0
  195. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/StreamingLogViewer.jsx +0 -0
  196. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/TestProfileSelector.jsx +0 -0
  197. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/TestResultPanel.jsx +0 -0
  198. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/TestStatusIndicator.jsx +0 -0
  199. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/ToolCallTimeline.jsx +0 -0
  200. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/TraceView.jsx +0 -0
  201. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/TypeBadge.jsx +0 -0
  202. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/contexts/ThemeContext.jsx +0 -0
  203. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/hooks/useEditorTheme.js +0 -0
  204. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/hooks/useKeyboardShortcuts.js +0 -0
  205. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/hooks/useSafeFetch.js +0 -0
  206. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/main.jsx +0 -0
  207. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/pages/ProfilesManager.jsx +0 -0
  208. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/utils/formatConverters.js +0 -0
  209. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/tailwind.config.js +0 -0
  210. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy.egg-info/dependency_links.txt +0 -0
  211. {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy.egg-info/entry_points.txt +0 -0
  212. {testmcpy-0.7.26 → testmcpy-0.9.0}/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.7.26
3
+ Version: 0.9.0
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
@@ -50,8 +50,12 @@ Requires-Dist: fastapi<1.0.0,>=0.104.0; extra == "server"
50
50
  Requires-Dist: uvicorn[standard]<1.0.0,>=0.24.0; extra == "server"
51
51
  Requires-Dist: websockets<17.0,>=14.0; extra == "server"
52
52
  Requires-Dist: claude-agent-sdk<1.0.0,>=0.2.0; extra == "server"
53
+ Requires-Dist: openai-agents<0.18,>=0.17; extra == "server"
54
+ Requires-Dist: google-adk<3.0.0,>=2.0.0; extra == "server"
53
55
  Provides-Extra: sdk
54
56
  Requires-Dist: claude-agent-sdk<1.0.0,>=0.2.0; extra == "sdk"
57
+ Requires-Dist: openai-agents<0.18,>=0.17; extra == "sdk"
58
+ Requires-Dist: google-adk<3.0.0,>=2.0.0; extra == "sdk"
55
59
  Provides-Extra: tui
56
60
  Requires-Dist: textual>=0.85.0; extra == "tui"
57
61
  Provides-Extra: e2e
@@ -61,11 +65,15 @@ Provides-Extra: export
61
65
  Requires-Dist: pandas<3.0.0,>=2.0.0; extra == "export"
62
66
  Provides-Extra: bedrock
63
67
  Requires-Dist: boto3>=1.28.0; extra == "bedrock"
68
+ Provides-Extra: postgres
69
+ Requires-Dist: psycopg[binary]<4.0,>=3.1; extra == "postgres"
64
70
  Provides-Extra: all
65
71
  Requires-Dist: fastapi<1.0.0,>=0.104.0; extra == "all"
66
72
  Requires-Dist: uvicorn[standard]<1.0.0,>=0.24.0; extra == "all"
67
73
  Requires-Dist: websockets<17.0,>=14.0; extra == "all"
68
74
  Requires-Dist: claude-agent-sdk<1.0.0,>=0.2.0; extra == "all"
75
+ Requires-Dist: openai-agents<0.18,>=0.17; extra == "all"
76
+ Requires-Dist: google-adk<3.0.0,>=2.0.0; extra == "all"
69
77
  Requires-Dist: textual>=0.85.0; extra == "all"
70
78
  Requires-Dist: boto3>=1.28.0; extra == "all"
71
79
  Dynamic: license-file
@@ -103,6 +111,20 @@ Dynamic: license-file
103
111
  - **Prevent regressions**: Catch breaking changes in your MCP service with CI/CD
104
112
  - **Optimize costs**: Track token usage and identify the most cost-effective models
105
113
 
114
+ ### How it compares
115
+
116
+ | | testmcpy | [MCP Inspector](https://github.com/modelcontextprotocol/inspector) | [MCPJam](https://github.com/MCPJam/inspector) | [promptfoo](https://github.com/promptfoo/promptfoo) |
117
+ |---|---|---|---|---|
118
+ | Automated LLM-driven evals of MCP servers | ✅ YAML suites, 40+ evaluators | ❌ manual testing | ✅ | ⚠️ generic LLM eval with an MCP provider |
119
+ | Multi-provider (Claude / GPT / Gemini / Ollama / Bedrock…) | ✅ 11 providers incl. agent SDKs | n/a | ✅ | ✅ |
120
+ | CI gate with exit codes + JUnit | ✅ `--gate`, `--junit-xml` | ❌ | ✅ | ✅ |
121
+ | Cost & token tracking per test/model | ✅ | ❌ | ⚠️ | ⚠️ |
122
+ | Multi-turn, mutation & metamorphic testing | ✅ | ❌ | ❌ | ⚠️ |
123
+ | Auth testing (JWT/OAuth/mTLS) + debugger | ✅ 7 auth types | ⚠️ OAuth only | ✅ OAuth debugger | ❌ |
124
+ | Python-native (`pip`/`uvx`, pytest-friendly) | ✅ | ❌ npm | ❌ npm | ❌ npm |
125
+
126
+ Use MCP Inspector for quick manual poking; reach for testmcpy when you want repeatable, scored, CI-gated evaluation of how real models use your server.
127
+
106
128
  ## Quick Start
107
129
 
108
130
  ```bash
@@ -124,14 +146,21 @@ That's it! No complex configuration needed to get started.
124
146
 
125
147
  ### Multi-Provider LLM Support
126
148
 
127
- Test with **Claude**, **GPT-4**, **Llama**, and other models. Works with both paid APIs and free local models via Ollama. Includes a Claude SDK provider for subprocess-based MCP support.
128
-
129
- | Provider | Models | Features |
130
- |----------|--------|----------|
131
- | Anthropic | claude-opus-4, claude-sonnet-4-5, claude-haiku-4-5 | Native MCP, extended thinking, vision, token caching |
132
- | OpenAI | gpt-4, gpt-4-turbo, gpt-4o | Function calling, vision, cost tracking |
133
- | Ollama | Llama, Mistral, etc. (local) | Free, local execution, no API costs |
134
- | Claude SDK | claude-cli, claude-code | Subprocess-based, full MCP support |
149
+ Test with **Claude**, **GPT**, **Gemini**, **Llama**, and other models. Works with both paid APIs and free local models via Ollama. Includes agent-SDK providers (Claude, Codex, Gemini) with native MCP support.
150
+
151
+ | Provider | Config name | Models | Features |
152
+ |----------|-------------|--------|----------|
153
+ | Anthropic | `anthropic` | claude-opus-4, claude-sonnet-4-5, claude-haiku-4-5 | Native MCP, extended thinking, vision, token caching |
154
+ | OpenAI | `openai` | gpt-4, gpt-4-turbo, gpt-4o | Function calling, vision, cost tracking |
155
+ | Ollama | `ollama` | Llama, Mistral, etc. (local) | Free, local execution, no API costs |
156
+ | Claude SDK | `claude-sdk` (aliases: `claude-cli`, `claude-code`) | claude-sonnet-4-5, claude-opus-4 | Claude Agent SDK, native MCP, CLI OAuth login |
157
+ | Codex SDK | `codex-sdk` (aliases: `codex-cli`, `codex`) | gpt-5-codex, o3, o4-mini | openai-agents SDK, native MCP, Codex CLI OAuth or API key |
158
+ | Gemini SDK | `gemini-sdk` | gemini-sdk-flash, gemini-sdk-pro | google-adk, native MCP |
159
+ | Google Gemini | `gemini` (alias: `google`) | gemini-2.5-flash, gemini-2.5-pro | Direct Gemini API, function calling |
160
+ | Gemini CLI | `gemini-cli` | gemini-2.5-flash, gemini-2.5-pro | Subprocess-based Gemini CLI |
161
+ | AWS Bedrock | `bedrock` (alias: `aws-bedrock`) | Claude models via AWS | IAM auth, no Anthropic key needed |
162
+ | xAI | `xai` (alias: `grok`) | grok models | Function calling |
163
+ | OpenRouter | `openrouter` | 100+ models with one API key | Function calling, cost tracking |
135
164
 
136
165
  ![LLM Profiles — manage Anthropic, OpenAI, Ollama and Claude SDK provider configurations](https://raw.githubusercontent.com/preset-io/testmcpy/main/docs/screenshots/llm-profiles.png)
137
166
 
@@ -519,16 +548,51 @@ openai:
519
548
  default: true
520
549
  ```
521
550
 
522
- ## CI/CD Integration
551
+ ## CI in 60 Seconds
552
+
553
+ Gate your MCP service on eval results in any CI system — no wrapper required:
554
+
555
+ ```yaml
556
+ # .github/workflows/mcp-tests.yml
557
+ jobs:
558
+ mcp-tests:
559
+ runs-on: ubuntu-latest
560
+ steps:
561
+ - uses: actions/checkout@v4
562
+ - uses: astral-sh/setup-uv@v5
563
+ - name: Run MCP eval suite
564
+ env:
565
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
566
+ run: |
567
+ uvx testmcpy run tests/ \
568
+ --mcp-url "$MCP_URL" \
569
+ --gate --min-pass-rate 85 \
570
+ --junit-xml junit.xml
571
+ ```
572
+
573
+ - **`--gate`** exits non-zero when the run fails your thresholds, so the build fails. Tune thresholds in `.testmcpy-gate.yaml`:
574
+
575
+ ```yaml
576
+ min_pass_rate: 85.0 # % of tests that must pass
577
+ max_failures: 3 # absolute failure budget
578
+ required_tests: # these must always pass
579
+ - critical_auth_flow
580
+ block_on_regression: true # fail on baseline regressions
581
+ ```
582
+
583
+ - **`--junit-xml`** emits JUnit XML for CI systems that ingest it natively (Jenkins, GitLab, CircleCI, Buildkite). On GitHub Actions, pair it with an action like `dorny/test-reporter` — or just rely on the next bullet.
584
+ - Inside GitHub Actions, the markdown eval report is **automatically appended to the job summary** — results render on the workflow run page with zero extra steps.
523
585
 
524
- testmcpy ships as a GitHub Action:
586
+ Or use the bundled reusable Action — adds a sticky PR comment, JUnit artifact upload, and structured outputs (`pass-rate`, `gate_passed`):
525
587
 
526
588
  ```yaml
527
589
  - uses: preset-io/testmcpy@v1
528
590
  with:
529
- test-path: tests/
530
- model: claude-haiku-4-5
531
- mcp-profile: prod
591
+ test_path: tests/
592
+ mcp_url: ${{ vars.MCP_URL }}
593
+ pass_threshold: '85'
594
+ pr_comment: 'true'
595
+ anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
532
596
  ```
533
597
 
534
598
  ## Custom Evaluators
@@ -31,6 +31,20 @@
31
31
  - **Prevent regressions**: Catch breaking changes in your MCP service with CI/CD
32
32
  - **Optimize costs**: Track token usage and identify the most cost-effective models
33
33
 
34
+ ### How it compares
35
+
36
+ | | testmcpy | [MCP Inspector](https://github.com/modelcontextprotocol/inspector) | [MCPJam](https://github.com/MCPJam/inspector) | [promptfoo](https://github.com/promptfoo/promptfoo) |
37
+ |---|---|---|---|---|
38
+ | Automated LLM-driven evals of MCP servers | ✅ YAML suites, 40+ evaluators | ❌ manual testing | ✅ | ⚠️ generic LLM eval with an MCP provider |
39
+ | Multi-provider (Claude / GPT / Gemini / Ollama / Bedrock…) | ✅ 11 providers incl. agent SDKs | n/a | ✅ | ✅ |
40
+ | CI gate with exit codes + JUnit | ✅ `--gate`, `--junit-xml` | ❌ | ✅ | ✅ |
41
+ | Cost & token tracking per test/model | ✅ | ❌ | ⚠️ | ⚠️ |
42
+ | Multi-turn, mutation & metamorphic testing | ✅ | ❌ | ❌ | ⚠️ |
43
+ | Auth testing (JWT/OAuth/mTLS) + debugger | ✅ 7 auth types | ⚠️ OAuth only | ✅ OAuth debugger | ❌ |
44
+ | Python-native (`pip`/`uvx`, pytest-friendly) | ✅ | ❌ npm | ❌ npm | ❌ npm |
45
+
46
+ Use MCP Inspector for quick manual poking; reach for testmcpy when you want repeatable, scored, CI-gated evaluation of how real models use your server.
47
+
34
48
  ## Quick Start
35
49
 
36
50
  ```bash
@@ -52,14 +66,21 @@ That's it! No complex configuration needed to get started.
52
66
 
53
67
  ### Multi-Provider LLM Support
54
68
 
55
- Test with **Claude**, **GPT-4**, **Llama**, and other models. Works with both paid APIs and free local models via Ollama. Includes a Claude SDK provider for subprocess-based MCP support.
56
-
57
- | Provider | Models | Features |
58
- |----------|--------|----------|
59
- | Anthropic | claude-opus-4, claude-sonnet-4-5, claude-haiku-4-5 | Native MCP, extended thinking, vision, token caching |
60
- | OpenAI | gpt-4, gpt-4-turbo, gpt-4o | Function calling, vision, cost tracking |
61
- | Ollama | Llama, Mistral, etc. (local) | Free, local execution, no API costs |
62
- | Claude SDK | claude-cli, claude-code | Subprocess-based, full MCP support |
69
+ Test with **Claude**, **GPT**, **Gemini**, **Llama**, and other models. Works with both paid APIs and free local models via Ollama. Includes agent-SDK providers (Claude, Codex, Gemini) with native MCP support.
70
+
71
+ | Provider | Config name | Models | Features |
72
+ |----------|-------------|--------|----------|
73
+ | Anthropic | `anthropic` | claude-opus-4, claude-sonnet-4-5, claude-haiku-4-5 | Native MCP, extended thinking, vision, token caching |
74
+ | OpenAI | `openai` | gpt-4, gpt-4-turbo, gpt-4o | Function calling, vision, cost tracking |
75
+ | Ollama | `ollama` | Llama, Mistral, etc. (local) | Free, local execution, no API costs |
76
+ | Claude SDK | `claude-sdk` (aliases: `claude-cli`, `claude-code`) | claude-sonnet-4-5, claude-opus-4 | Claude Agent SDK, native MCP, CLI OAuth login |
77
+ | Codex SDK | `codex-sdk` (aliases: `codex-cli`, `codex`) | gpt-5-codex, o3, o4-mini | openai-agents SDK, native MCP, Codex CLI OAuth or API key |
78
+ | Gemini SDK | `gemini-sdk` | gemini-sdk-flash, gemini-sdk-pro | google-adk, native MCP |
79
+ | Google Gemini | `gemini` (alias: `google`) | gemini-2.5-flash, gemini-2.5-pro | Direct Gemini API, function calling |
80
+ | Gemini CLI | `gemini-cli` | gemini-2.5-flash, gemini-2.5-pro | Subprocess-based Gemini CLI |
81
+ | AWS Bedrock | `bedrock` (alias: `aws-bedrock`) | Claude models via AWS | IAM auth, no Anthropic key needed |
82
+ | xAI | `xai` (alias: `grok`) | grok models | Function calling |
83
+ | OpenRouter | `openrouter` | 100+ models with one API key | Function calling, cost tracking |
63
84
 
64
85
  ![LLM Profiles — manage Anthropic, OpenAI, Ollama and Claude SDK provider configurations](https://raw.githubusercontent.com/preset-io/testmcpy/main/docs/screenshots/llm-profiles.png)
65
86
 
@@ -447,16 +468,51 @@ openai:
447
468
  default: true
448
469
  ```
449
470
 
450
- ## CI/CD Integration
471
+ ## CI in 60 Seconds
472
+
473
+ Gate your MCP service on eval results in any CI system — no wrapper required:
474
+
475
+ ```yaml
476
+ # .github/workflows/mcp-tests.yml
477
+ jobs:
478
+ mcp-tests:
479
+ runs-on: ubuntu-latest
480
+ steps:
481
+ - uses: actions/checkout@v4
482
+ - uses: astral-sh/setup-uv@v5
483
+ - name: Run MCP eval suite
484
+ env:
485
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
486
+ run: |
487
+ uvx testmcpy run tests/ \
488
+ --mcp-url "$MCP_URL" \
489
+ --gate --min-pass-rate 85 \
490
+ --junit-xml junit.xml
491
+ ```
492
+
493
+ - **`--gate`** exits non-zero when the run fails your thresholds, so the build fails. Tune thresholds in `.testmcpy-gate.yaml`:
494
+
495
+ ```yaml
496
+ min_pass_rate: 85.0 # % of tests that must pass
497
+ max_failures: 3 # absolute failure budget
498
+ required_tests: # these must always pass
499
+ - critical_auth_flow
500
+ block_on_regression: true # fail on baseline regressions
501
+ ```
502
+
503
+ - **`--junit-xml`** emits JUnit XML for CI systems that ingest it natively (Jenkins, GitLab, CircleCI, Buildkite). On GitHub Actions, pair it with an action like `dorny/test-reporter` — or just rely on the next bullet.
504
+ - Inside GitHub Actions, the markdown eval report is **automatically appended to the job summary** — results render on the workflow run page with zero extra steps.
451
505
 
452
- testmcpy ships as a GitHub Action:
506
+ Or use the bundled reusable Action — adds a sticky PR comment, JUnit artifact upload, and structured outputs (`pass-rate`, `gate_passed`):
453
507
 
454
508
  ```yaml
455
509
  - uses: preset-io/testmcpy@v1
456
510
  with:
457
- test-path: tests/
458
- model: claude-haiku-4-5
459
- mcp-profile: prod
511
+ test_path: tests/
512
+ mcp_url: ${{ vars.MCP_URL }}
513
+ pass_threshold: '85'
514
+ pr_comment: 'true'
515
+ anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
460
516
  ```
461
517
 
462
518
  ## Custom Evaluators
@@ -93,7 +93,7 @@ testmcpy = [
93
93
 
94
94
  [project]
95
95
  name = "testmcpy"
96
- version = "0.7.26"
96
+ version = "0.9.0"
97
97
  description = "A comprehensive testing framework for validating LLM tool calling capabilities with MCP services"
98
98
  authors = [{name = "Amin Ghadersohi"}]
99
99
  license = "Apache-2.0"
@@ -171,9 +171,19 @@ server = [
171
171
  # that bumps mcp out of fastmcp's range fails at install time
172
172
  # instead of at first agentic test run.
173
173
  "claude-agent-sdk>=0.2.0,<1.0.0",
174
+ # openai-agents provides CodexSDKProvider with native MCP support.
175
+ # Pinned to 0.17.x — the minor that introduced the agents.mcp /
176
+ # agents.models.openai_provider / agents.run_config import paths we rely on.
177
+ "openai-agents>=0.17,<0.18",
178
+ # google-adk provides GeminiSDKProvider with native MCP support via McpToolset.
179
+ # Pinned to 2.x — the version that introduced StreamableHTTPConnectionParams.
180
+ # Transitively pulls google-genai>=2.4,<3 and mcp>=1.24,<2 (already pinned).
181
+ "google-adk>=2.0.0,<3.0.0",
174
182
  ]
175
183
  sdk = [
176
184
  "claude-agent-sdk>=0.2.0,<1.0.0",
185
+ "openai-agents>=0.17,<0.18",
186
+ "google-adk>=2.0.0,<3.0.0",
177
187
  ]
178
188
  tui = [
179
189
  "textual>=0.85.0",
@@ -188,6 +198,11 @@ export = [
188
198
  bedrock = [
189
199
  "boto3>=1.28.0",
190
200
  ]
201
+ postgres = [
202
+ # Enables TESTMCPY_DB_URL=postgresql+psycopg://... for a shared
203
+ # results database. SQLite stays the default.
204
+ "psycopg[binary]>=3.1,<4.0",
205
+ ]
191
206
  all = [
192
207
  "fastapi>=0.104.0,<1.0.0",
193
208
  "uvicorn[standard]>=0.24.0,<1.0.0",
@@ -196,6 +211,8 @@ all = [
196
211
  # time uvicorn/fastmcp bumps a minor.
197
212
  "websockets>=14.0,<17.0",
198
213
  "claude-agent-sdk>=0.2.0,<1.0.0",
214
+ "openai-agents>=0.17,<0.18",
215
+ "google-adk>=2.0.0,<3.0.0",
199
216
  "textual>=0.85.0",
200
217
  "boto3>=1.28.0",
201
218
  ]
@@ -0,0 +1,17 @@
1
+ """
2
+ testmcpy - MCP Testing Framework
3
+
4
+ A comprehensive testing framework for validating LLM tool calling
5
+ capabilities with MCP (Model Context Protocol) services.
6
+ """
7
+
8
+ from importlib.metadata import PackageNotFoundError, version
9
+
10
+ try:
11
+ __version__ = version("testmcpy")
12
+ except PackageNotFoundError:
13
+ # Running from a bare checkout without an installed package — use an
14
+ # obviously-not-a-release marker instead of a stale hardcoded version.
15
+ __version__ = "0.0.0+unknown"
16
+
17
+ __author__ = "testmcpy Contributors"
@@ -0,0 +1,6 @@
1
+ """Allow `python -m testmcpy` to run the CLI (used by `testmcpy bench`)."""
2
+
3
+ from testmcpy.cli import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
@@ -0,0 +1,338 @@
1
+ """Aggregation core for test-performance analytics.
2
+
3
+ Answers "which tests perform better than others given the model /
4
+ routing config": per-question × per-config pass rates, scores, costs,
5
+ latency, flakiness, and day-bucketed trends.
6
+
7
+ Pure query/aggregation functions over a SQLAlchemy session, shared by
8
+ the /api/analytics router and the `testmcpy matrix | leaderboard |
9
+ flaky` CLI commands so both surfaces report identical numbers.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any
15
+
16
+ from sqlalchemy import case, func
17
+ from sqlalchemy.orm import Session
18
+
19
+ from testmcpy.models import QuestionResultModel, TestRunModel
20
+
21
+ # A cell needs at least this many runs before an intermittent result is
22
+ # called "flaky" rather than noise.
23
+ FLAKY_MIN_RUNS = 3
24
+
25
+ # Number of day buckets in a cell's trend sparkline.
26
+ TREND_BUCKETS = 7
27
+
28
+
29
+ def config_key(model: str, provider: str, mcp_profile_id: str | None = None) -> str:
30
+ """Canonical label for a model/provider/profile combination."""
31
+ key = f"{provider}/{model}"
32
+ if mcp_profile_id:
33
+ key = f"{key} @ {mcp_profile_id}"
34
+ return key
35
+
36
+
37
+ def _pass_rate_expr() -> Any:
38
+ """Portable AVG over a boolean column (0.0–1.0)."""
39
+ return func.avg(case((QuestionResultModel.passed.is_(True), 1.0), else_=0.0))
40
+
41
+
42
+ def _apply_run_filters(
43
+ query,
44
+ suite_id: str | None,
45
+ date_from: str | None,
46
+ date_to: str | None,
47
+ ):
48
+ query = query.filter(TestRunModel.status == "completed")
49
+ if suite_id:
50
+ query = query.filter(TestRunModel.suite_id == suite_id)
51
+ if date_from:
52
+ query = query.filter(TestRunModel.started_at >= date_from)
53
+ if date_to:
54
+ query = query.filter(TestRunModel.started_at <= date_to)
55
+ return query
56
+
57
+
58
+ def test_matrix(
59
+ session: Session,
60
+ suite_id: str | None = None,
61
+ date_from: str | None = None,
62
+ date_to: str | None = None,
63
+ min_runs: int = 1,
64
+ include_profile: bool = True,
65
+ trend_buckets: int = TREND_BUCKETS,
66
+ ) -> dict[str, Any]:
67
+ """Per-question × per-config aggregation.
68
+
69
+ Returns ``{"configs": [...], "rows": [...], "warnings": [...]}`` where
70
+ each row carries one cell per config with n, pass_rate, flaky flag,
71
+ avg score/cost/latency, and a day-bucketed pass-rate trend.
72
+ """
73
+ config_cols: list[Any] = [TestRunModel.model, TestRunModel.provider]
74
+ if include_profile:
75
+ config_cols.append(TestRunModel.mcp_profile_id)
76
+
77
+ def cell_key(row) -> str:
78
+ profile = row.mcp_profile_id if include_profile else None
79
+ return config_key(row.model, row.provider, profile)
80
+
81
+ group_cols = [QuestionResultModel.question_id, *config_cols]
82
+ cells_query = _apply_run_filters(
83
+ session.query(
84
+ *group_cols,
85
+ func.count(QuestionResultModel.id).label("n"),
86
+ _pass_rate_expr().label("pass_rate"),
87
+ func.avg(QuestionResultModel.score).label("avg_score"),
88
+ func.avg(func.coalesce(QuestionResultModel.cost_usd, 0.0)).label("avg_cost"),
89
+ func.avg(func.coalesce(QuestionResultModel.duration_ms, 0)).label("avg_duration_ms"),
90
+ func.max(TestRunModel.started_at).label("last_run_at"),
91
+ ).join(TestRunModel, TestRunModel.run_id == QuestionResultModel.run_id),
92
+ suite_id,
93
+ date_from,
94
+ date_to,
95
+ ).group_by(*group_cols)
96
+
97
+ # Day-bucketed pass rates for trend sparklines. started_at is an ISO
98
+ # string, so the first 10 chars are the date — portable across
99
+ # SQLite and Postgres.
100
+ day_expr = func.substr(TestRunModel.started_at, 1, 10)
101
+ trend_query = _apply_run_filters(
102
+ session.query(
103
+ *group_cols,
104
+ day_expr.label("day"),
105
+ _pass_rate_expr().label("pass_rate"),
106
+ ).join(TestRunModel, TestRunModel.run_id == QuestionResultModel.run_id),
107
+ suite_id,
108
+ date_from,
109
+ date_to,
110
+ ).group_by(*group_cols, day_expr)
111
+
112
+ trends: dict[tuple[str, str], list[tuple[str, float]]] = {}
113
+ for row in trend_query:
114
+ trends.setdefault((row.question_id, cell_key(row)), []).append(
115
+ (row.day, round(row.pass_rate or 0.0, 4))
116
+ )
117
+
118
+ rows: dict[str, dict[str, Any]] = {}
119
+ config_totals: dict[str, dict[str, Any]] = {}
120
+ single_run_cells = 0
121
+
122
+ for row in cells_query:
123
+ key = cell_key(row)
124
+ n = int(row.n or 0)
125
+ if n < min_runs:
126
+ continue
127
+ pass_rate = round(row.pass_rate or 0.0, 4)
128
+ flaky = 0.0 < pass_rate < 1.0 and n >= FLAKY_MIN_RUNS
129
+ if n == 1:
130
+ single_run_cells += 1
131
+
132
+ trend_points = sorted(trends.get((row.question_id, key), []))[-trend_buckets:]
133
+ rows.setdefault(row.question_id, {"question_id": row.question_id, "cells": {}})["cells"][
134
+ key
135
+ ] = {
136
+ "n": n,
137
+ "pass_rate": pass_rate,
138
+ "flaky": flaky,
139
+ "avg_score": round(row.avg_score or 0.0, 4),
140
+ "avg_cost": round(row.avg_cost or 0.0, 6),
141
+ "avg_duration_ms": round(row.avg_duration_ms or 0.0, 1),
142
+ "last_run_at": row.last_run_at,
143
+ "trend": [rate for _, rate in trend_points],
144
+ }
145
+
146
+ totals = config_totals.setdefault(
147
+ key,
148
+ {
149
+ "key": key,
150
+ "model": row.model,
151
+ "provider": row.provider,
152
+ "mcp_profile": row.mcp_profile_id if include_profile else None,
153
+ "n": 0,
154
+ "passed_weight": 0.0,
155
+ "total_cost": 0.0,
156
+ "flaky_cells": 0,
157
+ },
158
+ )
159
+ totals["n"] += n
160
+ totals["passed_weight"] += pass_rate * n
161
+ totals["total_cost"] += (row.avg_cost or 0.0) * n
162
+ totals["flaky_cells"] += 1 if flaky else 0
163
+
164
+ # Distinct completed-run counts per config (cells count question
165
+ # results; the leaderboard wants whole runs).
166
+ runs_query = _apply_run_filters(
167
+ session.query(
168
+ *config_cols,
169
+ func.count(TestRunModel.run_id).label("n_runs"),
170
+ ),
171
+ suite_id,
172
+ date_from,
173
+ date_to,
174
+ ).group_by(*config_cols)
175
+ run_counts = {cell_key(row): int(row.n_runs or 0) for row in runs_query}
176
+
177
+ configs = []
178
+ for key, totals in sorted(config_totals.items()):
179
+ n = totals["n"]
180
+ configs.append(
181
+ {
182
+ "key": key,
183
+ "model": totals["model"],
184
+ "provider": totals["provider"],
185
+ "mcp_profile": totals["mcp_profile"],
186
+ "n_runs": run_counts.get(key, 0),
187
+ "n_results": n,
188
+ "pass_rate": round(totals["passed_weight"] / n, 4) if n else 0.0,
189
+ "total_cost": round(totals["total_cost"], 6),
190
+ "flaky_cells": totals["flaky_cells"],
191
+ }
192
+ )
193
+
194
+ warnings = []
195
+ if single_run_cells:
196
+ warnings.append(
197
+ f"{single_run_cells} cell(s) have n=1 — single runs are noise, "
198
+ f"re-run with `testmcpy bench --repeat {FLAKY_MIN_RUNS}` for signal"
199
+ )
200
+
201
+ return {
202
+ "configs": configs,
203
+ "rows": sorted(rows.values(), key=lambda r: r["question_id"]),
204
+ "warnings": warnings,
205
+ }
206
+
207
+
208
+ def leaderboard(
209
+ session: Session,
210
+ suite_id: str | None = None,
211
+ date_from: str | None = None,
212
+ date_to: str | None = None,
213
+ include_profile: bool = True,
214
+ ) -> list[dict[str, Any]]:
215
+ """Configs ranked by pass rate, with cost-per-pass and latency."""
216
+ matrix = test_matrix(
217
+ session,
218
+ suite_id=suite_id,
219
+ date_from=date_from,
220
+ date_to=date_to,
221
+ include_profile=include_profile,
222
+ )
223
+
224
+ # Aggregate latency per config from the row cells.
225
+ latency: dict[str, list[tuple[float, int]]] = {}
226
+ passes: dict[str, float] = {}
227
+ for row in matrix["rows"]:
228
+ for key, cell in row["cells"].items():
229
+ latency.setdefault(key, []).append((cell["avg_duration_ms"], cell["n"]))
230
+ passes[key] = passes.get(key, 0.0) + cell["pass_rate"] * cell["n"]
231
+
232
+ ranked = []
233
+ for config in matrix["configs"]:
234
+ key = config["key"]
235
+ weighted = latency.get(key, [])
236
+ total_n = sum(n for _, n in weighted)
237
+ avg_duration = sum(duration * n for duration, n in weighted) / total_n if total_n else 0.0
238
+ passed = passes.get(key, 0.0)
239
+ ranked.append(
240
+ {
241
+ **config,
242
+ "avg_duration_ms": round(avg_duration, 1),
243
+ "cost_per_pass": round(config["total_cost"] / passed, 6) if passed else None,
244
+ }
245
+ )
246
+
247
+ ranked.sort(key=lambda c: (-c["pass_rate"], c["cost_per_pass"] or float("inf")))
248
+ return ranked
249
+
250
+
251
+ def question_history(
252
+ session: Session,
253
+ question_id: str,
254
+ suite_id: str | None = None,
255
+ model: str | None = None,
256
+ provider: str | None = None,
257
+ mcp_profile: str | None = None,
258
+ limit: int = 100,
259
+ ) -> list[dict[str, Any]]:
260
+ """Chronological per-run points for one question (drill-down view)."""
261
+ query = _apply_run_filters(
262
+ session.query(
263
+ QuestionResultModel.run_id,
264
+ QuestionResultModel.passed,
265
+ QuestionResultModel.score,
266
+ QuestionResultModel.cost_usd,
267
+ QuestionResultModel.duration_ms,
268
+ QuestionResultModel.error,
269
+ TestRunModel.model,
270
+ TestRunModel.provider,
271
+ TestRunModel.mcp_profile_id,
272
+ TestRunModel.started_at,
273
+ )
274
+ .join(TestRunModel, TestRunModel.run_id == QuestionResultModel.run_id)
275
+ .filter(QuestionResultModel.question_id == question_id),
276
+ suite_id,
277
+ None,
278
+ None,
279
+ )
280
+ if model:
281
+ query = query.filter(TestRunModel.model == model)
282
+ if provider:
283
+ query = query.filter(TestRunModel.provider == provider)
284
+ if mcp_profile:
285
+ query = query.filter(TestRunModel.mcp_profile_id == mcp_profile)
286
+
287
+ rows = query.order_by(TestRunModel.started_at.desc()).limit(limit).all()
288
+ points = [
289
+ {
290
+ "run_id": row.run_id,
291
+ "started_at": row.started_at,
292
+ "passed": bool(row.passed),
293
+ "score": round(row.score or 0.0, 4),
294
+ "cost_usd": round(row.cost_usd or 0.0, 6),
295
+ "duration_ms": row.duration_ms or 0,
296
+ "error": row.error,
297
+ "model": row.model,
298
+ "provider": row.provider,
299
+ "mcp_profile": row.mcp_profile_id,
300
+ "config": config_key(row.model, row.provider, row.mcp_profile_id),
301
+ }
302
+ for row in rows
303
+ ]
304
+ points.reverse() # chronological
305
+ return points
306
+
307
+
308
+ def flaky_tests(
309
+ session: Session,
310
+ suite_id: str | None = None,
311
+ date_from: str | None = None,
312
+ date_to: str | None = None,
313
+ min_runs: int = FLAKY_MIN_RUNS,
314
+ ) -> list[dict[str, Any]]:
315
+ """Flaky question × config cells, sorted by how intermittent they are."""
316
+ matrix = test_matrix(
317
+ session,
318
+ suite_id=suite_id,
319
+ date_from=date_from,
320
+ date_to=date_to,
321
+ min_runs=min_runs,
322
+ )
323
+ flaky = []
324
+ for row in matrix["rows"]:
325
+ for key, cell in row["cells"].items():
326
+ if cell["flaky"]:
327
+ flaky.append(
328
+ {
329
+ "question_id": row["question_id"],
330
+ "config": key,
331
+ "n": cell["n"],
332
+ "pass_rate": cell["pass_rate"],
333
+ "last_run_at": cell["last_run_at"],
334
+ }
335
+ )
336
+ # Most intermittent first: pass rates near 0.5 are the flakiest.
337
+ flaky.sort(key=lambda f: abs(f["pass_rate"] - 0.5))
338
+ return flaky