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.
- {testmcpy-0.7.26/testmcpy.egg-info → testmcpy-0.9.0}/PKG-INFO +78 -14
- {testmcpy-0.7.26 → testmcpy-0.9.0}/README.md +69 -13
- {testmcpy-0.7.26 → testmcpy-0.9.0}/pyproject.toml +18 -1
- testmcpy-0.9.0/testmcpy/__init__.py +17 -0
- testmcpy-0.9.0/testmcpy/__main__.py +6 -0
- testmcpy-0.9.0/testmcpy/analytics.py +338 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/__init__.py +6 -0
- testmcpy-0.9.0/testmcpy/cli/commands/analytics.py +257 -0
- testmcpy-0.9.0/testmcpy/cli/commands/badge.py +137 -0
- testmcpy-0.9.0/testmcpy/cli/commands/bench.py +143 -0
- testmcpy-0.9.0/testmcpy/cli/commands/conformance.py +191 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/run.py +123 -12
- testmcpy-0.9.0/testmcpy/cli/commands/scan.py +195 -0
- testmcpy-0.9.0/testmcpy/cli/commands/score.py +190 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/wizard.py +20 -2
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/db.py +30 -3
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/evals/__init__.py +9 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/evals/auth_evaluators.py +333 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/evals/base_evaluators.py +10 -1
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/evals/evaluator_packs.py +27 -0
- testmcpy-0.9.0/testmcpy/evals/security_evaluators.py +143 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/json_yaml.py +6 -2
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/mcp_profiles.py +44 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/models.py +18 -0
- testmcpy-0.9.0/testmcpy/security/__init__.py +20 -0
- testmcpy-0.9.0/testmcpy/security/rules.py +184 -0
- testmcpy-0.9.0/testmcpy/security/scanner.py +433 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/api.py +66 -39
- testmcpy-0.9.0/testmcpy/server/routers/analytics.py +102 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/generation_logs.py +12 -3
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/health.py +63 -5
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/metrics.py +44 -1
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/results.py +27 -71
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/runs.py +36 -2
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/security.py +5 -2
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/tools.py +5 -1
- testmcpy-0.9.0/testmcpy/server/run_persistence.py +208 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/run_registry.py +65 -6
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/websocket.py +133 -50
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/ci_gate.py +39 -10
- testmcpy-0.9.0/testmcpy/src/emitters.py +153 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/llm_integration.py +1120 -260
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/model_registry.py +181 -0
- testmcpy-0.9.0/testmcpy/src/usability_score.py +585 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/storage.py +216 -29
- testmcpy-0.9.0/testmcpy/ui/dist/assets/index-BXP9_Odn.js +324 -0
- testmcpy-0.9.0/testmcpy/ui/dist/assets/index-D35cfDhp.css +1 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/dist/index.html +2 -2
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/package-lock.json +1510 -15
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/package.json +9 -2
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/App.jsx +23 -25
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/BackgroundRunsIndicator.jsx +15 -4
- testmcpy-0.9.0/testmcpy/ui/src/components/Badge.jsx +33 -0
- testmcpy-0.9.0/testmcpy/ui/src/components/ConfirmDialog.jsx +66 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/EditorTabStrip.jsx +5 -4
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/NotificationProvider.jsx +2 -1
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/OptimizeDocsModal.jsx +3 -2
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/TestGenerationModal.jsx +3 -2
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/ToolComparison.jsx +85 -24
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/ToolDebugModal.jsx +3 -2
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/Wizard.jsx +2 -2
- testmcpy-0.9.0/testmcpy/ui/src/components/__tests__/OutputDiff.test.jsx +17 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/contexts/TestRunContext.jsx +195 -96
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/index.css +4 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/pages/AuthDebugger.jsx +252 -11
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/pages/ChatInterface.jsx +87 -50
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/pages/Configuration.jsx +4 -4
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/pages/GenerationHistory.jsx +38 -10
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/pages/LLMProfiles.jsx +31 -81
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/pages/MCPExplorer.jsx +180 -47
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/pages/MCPProfiles.jsx +66 -113
- testmcpy-0.9.0/testmcpy/ui/src/pages/Performance.jsx +584 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/pages/Reports.jsx +350 -352
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/pages/SecurityDashboard.jsx +101 -66
- testmcpy-0.9.0/testmcpy/ui/src/pages/Servers.jsx +601 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/pages/TestManager.jsx +522 -461
- testmcpy-0.9.0/testmcpy/ui/src/pages/__tests__/ChatInterface.test.jsx +70 -0
- testmcpy-0.9.0/testmcpy/ui/src/pages/__tests__/Performance.test.jsx +155 -0
- testmcpy-0.9.0/testmcpy/ui/src/test-setup.js +4 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/utils/__tests__/formatConverters.test.js +5 -3
- testmcpy-0.9.0/testmcpy/ui/src/utils/formatters.js +34 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/vite.config.js +5 -2
- testmcpy-0.9.0/testmcpy/ui/vitest.config.js +11 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0/testmcpy.egg-info}/PKG-INFO +78 -14
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy.egg-info/SOURCES.txt +28 -7
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy.egg-info/requires.txt +9 -0
- testmcpy-0.7.26/testmcpy/__init__.py +0 -16
- testmcpy-0.7.26/testmcpy/server/api.py.bak +0 -3150
- testmcpy-0.7.26/testmcpy/ui/dist/assets/index-DN9kK9DT.js +0 -292
- testmcpy-0.7.26/testmcpy/ui/dist/assets/index-zH471udX.css +0 -1
- testmcpy-0.7.26/testmcpy/ui/src/pages/CompatibilityMatrix.jsx +0 -287
- testmcpy-0.7.26/testmcpy/ui/src/pages/MCPHealth.jsx +0 -227
- testmcpy-0.7.26/testmcpy/ui/src/pages/MetricsDashboard.jsx +0 -346
- testmcpy-0.7.26/testmcpy/ui/src/pages/RunComparison.jsx +0 -420
- {testmcpy-0.7.26 → testmcpy-0.9.0}/LICENSE +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/MANIFEST.in +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/NOTICE +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/setup.cfg +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/agent/__init__.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/agent/hooks.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/agent/models.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/agent/orchestrator.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/agent/prompts.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/agent/tools.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/auth_debugger.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/auth_flow_recorder.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/app.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/__init__.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/agent.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/baseline.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/export_db.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/mcp.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/metamorphic.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/multi_env.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/mutate.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/push.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/server.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/tools.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/cli/commands/tui.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/config.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/core/__init__.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/core/chat_session.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/core/docs_optimizer.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/core/mcp_manager.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/core/tool_comparison.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/core/tool_discovery.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/error_handlers.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/__init__.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/base.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/curl.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/graphql.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/javascript_client.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/protobuf.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/python.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/python_client.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/thrift.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/typescript.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/formatters/typescript_client.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/llm_profiles.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/migrate_json.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/research/claude_sdk_detailed_exploration.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/research/claude_sdk_poc.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/research/claude_sdk_working_poc.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/research/test_ollama_tools.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/__init__.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/auth_middleware.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/helpers/__init__.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/helpers/mcp_config.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/models.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/__init__.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/agent.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/auth.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/compare.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/compatibility.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/llm.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/mcp_profiles.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/search.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/smoke_reports.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/test_profiles.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/routers/tests.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/server/state.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/smoke_test.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/__init__.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/baseline.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/comparison_runner.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/coverage_analyzer.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/html_report.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/mcp_client.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/metamorphic.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/models.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/multi_env.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/oauth_flows.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/prompt_mutation.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/report_generator.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/runner_tools.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/schema_diff.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/test_runner.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/src/token_manager.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/test_profiles.py +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/README.md +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/index.html +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/postcss.config.js +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/CommandPalette.jsx +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/CompareToolsTab.jsx +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/EditorStatusBar.jsx +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/ErrorAlert.jsx +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/ErrorBoundary.jsx +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/LLMProfileSelector.jsx +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/LoadingSpinner.jsx +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/MCPProfileSelector.jsx +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/OutputDiff.jsx +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/ParameterCard.jsx +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/SchemaCodeViewer.jsx +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/SkeletonLoader.jsx +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/StreamingLogViewer.jsx +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/TestProfileSelector.jsx +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/TestResultPanel.jsx +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/TestStatusIndicator.jsx +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/ToolCallTimeline.jsx +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/TraceView.jsx +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/components/TypeBadge.jsx +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/contexts/ThemeContext.jsx +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/hooks/useEditorTheme.js +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/hooks/useKeyboardShortcuts.js +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/hooks/useSafeFetch.js +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/main.jsx +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/pages/ProfilesManager.jsx +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/src/utils/formatConverters.js +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy/ui/tailwind.config.js +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy.egg-info/dependency_links.txt +0 -0
- {testmcpy-0.7.26 → testmcpy-0.9.0}/testmcpy.egg-info/entry_points.txt +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|

|
|
137
166
|
|
|
@@ -519,16 +548,51 @@ openai:
|
|
|
519
548
|
default: true
|
|
520
549
|
```
|
|
521
550
|
|
|
522
|
-
## CI
|
|
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
|
-
|
|
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
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
|
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
|
|
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
|

|
|
65
86
|
|
|
@@ -447,16 +468,51 @@ openai:
|
|
|
447
468
|
default: true
|
|
448
469
|
```
|
|
449
470
|
|
|
450
|
-
## CI
|
|
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
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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.
|
|
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,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
|