blq-cli 1.1.0__tar.gz → 1.2.1__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.
- {blq_cli-1.1.0 → blq_cli-1.2.1}/CHANGELOG.md +54 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/PKG-INFO +2 -1
- {blq_cli-1.1.0 → blq_cli-1.2.1}/pyproject.toml +4 -1
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/serve.py +147 -14
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_mcp_ci_tools.py +12 -1
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_mcp_server.py +12 -1
- blq_cli-1.2.1/tests/test_output_schemas.py +90 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/.claude/hooks/blq-suggest.sh +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/.github/workflows/ci.yml +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/.github/workflows/docs.yml +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/.github/workflows/publish.yml +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/.gitignore +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/.mcp.json +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/.readthedocs.yml +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/AGENT.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/AGENTS.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/CLAUDE.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/CONTRIBUTING.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/README.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/SKILL.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/api-reference.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/capture.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/ci.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/completions.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/errors.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/exec.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/filter.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/index.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/init.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/maintenance.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/query.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/registry.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/report.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/run.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/sql.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/status.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/sync.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/watch.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-bird-migration.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-commands.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-config-command.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-extensions.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-git-integration.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-hooks-v2.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-live-inspection.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-parameterized-commands.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-run-args.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-sandbox-specs.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-sync.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-track-save.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/duck-hunt-v2-migration.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/duck-hunt-v3-migration.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/proposal-bird-v5.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/skill-inspect-enrichment.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/getting-started.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/index.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/integration.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/mcp.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/plans/explore-bird-sqlite-backend.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/plans/explore-nsjail-python-wrapper.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/plans/explore-nsjail-spack-package.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/plans/patterns-integration-prompt.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/plans/roadmap-to-1.0.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/plugin-guide.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/python-api.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/query-guide.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/requirements.txt +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/sandbox.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/schema-stability.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/schema.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/superpowers/plans/2026-03-22-extension-system.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/superpowers/plans/2026-03-28-bwrap-engine.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/superpowers/plans/2026-03-28-command-locks.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/superpowers/plans/2026-03-29-annotators-and-tighten.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/superpowers/plans/2026-03-29-service-layer-phase1.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/superpowers/plans/2026-03-29-strace-profiling.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/superpowers/specs/2026-03-22-extension-system-design.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/superpowers/specs/2026-03-29-unified-service-layer-design.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/experiments/agent-build-test.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/mkdocs.yml +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/__init__.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/__main__.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/bird.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/bird_schema.sql +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/cli.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/README.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/__init__.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/ci_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/clean_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/config_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/core.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/events.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/execution.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/hooks_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/hooks_gen.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/init_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/management.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/management_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/mcp_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/migrate.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/query_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/record_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/registry.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/report_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/sandbox_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/serve_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/sync_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/watch_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/config_format.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/ext/__init__.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/ext/annotator.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/ext/discovery.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/ext/local_executor.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/ext/pipeline.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/git.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/github.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/locks.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/output.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/query.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/runtime.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/schema.sql +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/services/__init__.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/services/execution.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/services/inspect.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/services/query.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/services/refs.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/storage.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/templates/drone.yml.j2 +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/templates/git_hook.sh.j2 +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/templates/github_workflow.yml.j2 +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/templates/gitlab_ci.yml.j2 +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/templates/hook_script.sh.j2 +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/user_config.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq_sandbox/__init__.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq_sandbox/engines.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq_sandbox/profile.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq_sandbox/source_annotator.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq_sandbox/spec.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq_sandbox/strace_parser.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq_sandbox/tighten.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq_sandbox/violations.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq_sandbox_bwrap/__init__.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq_sandbox_bwrap/args.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq_sandbox_systemd/__init__.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/__init__.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/conftest.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/sql/test_duck_hunt_v2_migration.sql +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_annotator.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_attempts_outcomes.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_auto_init.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_autoprune.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_bird.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_bwrap_args.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_bwrap_engine.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_ci.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_ci_generators.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_command_args.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_command_lock_field.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_config_consumption.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_core.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_execution_locks.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_exit_code_reason.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_ext_discovery.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_ext_integration.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_ext_local_executor.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_ext_pipeline.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_ext_types.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_git.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_hooks.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_hooks_gen.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_import_capture_storage.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_inspect.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_json_null_filter.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_locks.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_log_content_persistence.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_mcp_merge.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_phase1_structured_output.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_phase2_command_registry.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_query_api.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_query_filter.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_record_invocation.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_report.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_runtime_config.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_sandbox.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_sandbox_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_sandbox_events.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_sandbox_ext.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_sandbox_profile.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_sandbox_register.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_sandbox_systemd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_sandbox_tighten.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_sandbox_violations.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_schema_repair.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_security_containment.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_services_execution.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_services_inspect.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_services_query.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_services_refs.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_source_annotator.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_storage.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_storage_prune.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_strace_parser.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_user_config.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_watch.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.1}/uv.lock +0 -0
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.2.1
|
|
4
|
+
|
|
5
|
+
### Fixed — 1.2.0 was broken on Python 3.10 and 3.11
|
|
6
|
+
|
|
7
|
+
The TypedDicts added in 1.2.0 were imported from `typing`. pydantic refuses to
|
|
8
|
+
build a schema from `typing.TypedDict` below 3.12 ("Please use
|
|
9
|
+
`typing_extensions.TypedDict` instead"), so importing `blq.serve` raised on
|
|
10
|
+
every supported Python under 3.12 — which is to say the MCP server did not
|
|
11
|
+
start at all there.
|
|
12
|
+
|
|
13
|
+
`requires-python` is `>=3.10`, so 1.2.0 should not have shipped. It passed
|
|
14
|
+
local checks because the development interpreter was 3.14; CI caught it on the
|
|
15
|
+
3.10 matrix entry, but the publish workflow triggers on release rather than on
|
|
16
|
+
a green CI run, so the package reached PyPI anyway.
|
|
17
|
+
|
|
18
|
+
Now imported from `typing_extensions` below 3.12, with the backport declared as
|
|
19
|
+
a conditional dependency. Verified against a real 3.11 interpreter: 1382 tests
|
|
20
|
+
pass, where 1.2.0 could not import.
|
|
21
|
+
|
|
22
|
+
**If you installed 1.2.0 on Python 3.10 or 3.11, upgrade.** On 3.12+ it was
|
|
23
|
+
unaffected.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## v1.2.0
|
|
27
|
+
|
|
28
|
+
### Added — MCP tools publish the names of the keys they return (#47)
|
|
29
|
+
|
|
30
|
+
fastmcp derives `outputSchema` from a tool's return annotation, so
|
|
31
|
+
`dict[str, Any]` published `{"type": "object", "additionalProperties": true}`
|
|
32
|
+
— an object with no key names. A programmatic caller had to guess them, and a
|
|
33
|
+
wrong guess produces a program that validates, runs, and answers incorrectly.
|
|
34
|
+
Measured across four local models driven through lackpy: **0/24 correct while
|
|
35
|
+
17/24 called the right tool**, with `len()` on `events`' dict returning its key
|
|
36
|
+
count rather than its event count.
|
|
37
|
+
|
|
38
|
+
14 tools now return TypedDicts whose keys come from the implementations' own
|
|
39
|
+
return statements. Declaring the shapes immediately caught three places where
|
|
40
|
+
the code did not match a reasonable reading of it:
|
|
41
|
+
|
|
42
|
+
- `output.streams` is a list of stream names, not a mapping
|
|
43
|
+
- `query.rows` is positional value lists paired with `columns`, not per-row objects
|
|
44
|
+
- `ci_generate.available` is the registered command names, not a boolean
|
|
45
|
+
|
|
46
|
+
No data is dropped — keys a TypedDict does not declare still reach the client
|
|
47
|
+
in `structuredContent`.
|
|
48
|
+
|
|
49
|
+
**One behavioural note for Python callers:** with an `outputSchema` present,
|
|
50
|
+
fastmcp's own client deserializes `.data` into a generated model rather than a
|
|
51
|
+
dict, so `result.data["key"]` no longer works. Read `structured_content`, which
|
|
52
|
+
is the JSON a real MCP client receives over the wire. Consumers speaking
|
|
53
|
+
JSON-RPC (Claude Code, lackpy, any stdio client) are unaffected and strictly
|
|
54
|
+
better off.
|
|
55
|
+
|
|
56
|
+
|
|
3
57
|
## v1.1.0
|
|
4
58
|
|
|
5
59
|
Three fixes with one theme: blq reported success while losing the thing it was
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: blq-cli
|
|
3
|
-
Version: 1.1
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary: Build Log Query - capture and query build/test logs with DuckDB
|
|
5
5
|
Project-URL: Homepage, https://blq-cli.readthedocs.io/
|
|
6
6
|
Project-URL: Repository, https://github.com/teaguesterling/blq-cli
|
|
@@ -27,6 +27,7 @@ Requires-Dist: pyyaml>=6.0
|
|
|
27
27
|
Requires-Dist: tabulate>=0.9.0
|
|
28
28
|
Requires-Dist: tomli-w>=1.0.0
|
|
29
29
|
Requires-Dist: tomli>=2.0.0; python_version < '3.11'
|
|
30
|
+
Requires-Dist: typing-extensions>=4.0; python_version < '3.12'
|
|
30
31
|
Requires-Dist: watchdog>=3.0.0
|
|
31
32
|
Provides-Extra: dev
|
|
32
33
|
Requires-Dist: fastmcp>=2.0.0; extra == 'dev'
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "blq-cli"
|
|
7
|
-
version = "1.1
|
|
7
|
+
version = "1.2.1"
|
|
8
8
|
description = "Build Log Query - capture and query build/test logs with DuckDB"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -33,6 +33,9 @@ dependencies = [
|
|
|
33
33
|
"pyyaml>=6.0",
|
|
34
34
|
"tabulate>=0.9.0",
|
|
35
35
|
"tomli>=2.0.0;python_version<'3.11'",
|
|
36
|
+
# pydantic will not build a schema from `typing.TypedDict` below 3.12, so
|
|
37
|
+
# the MCP tools' output schemas need the backport there.
|
|
38
|
+
"typing_extensions>=4.0;python_version<'3.12'",
|
|
36
39
|
"tomli-w>=1.0.0",
|
|
37
40
|
"watchdog>=3.0.0",
|
|
38
41
|
]
|
|
@@ -36,6 +36,16 @@ import subprocess
|
|
|
36
36
|
import sys
|
|
37
37
|
from typing import Any
|
|
38
38
|
|
|
39
|
+
# pydantic rejects `typing.TypedDict` below 3.12 and refuses to build a schema
|
|
40
|
+
# from it ("Please use typing_extensions.TypedDict instead"). This package
|
|
41
|
+
# supports 3.10+, so the stdlib import breaks every MCP tool on 3.10/3.11 —
|
|
42
|
+
# which is exactly how it shipped in 1.2.0, because the dev interpreter was
|
|
43
|
+
# 3.14 and only CI runs the older matrix.
|
|
44
|
+
if sys.version_info >= (3, 12):
|
|
45
|
+
from typing import TypedDict
|
|
46
|
+
else:
|
|
47
|
+
from typing_extensions import TypedDict
|
|
48
|
+
|
|
39
49
|
import pandas as pd # type: ignore[import-untyped]
|
|
40
50
|
from fastmcp import FastMCP
|
|
41
51
|
|
|
@@ -214,6 +224,129 @@ def _get_storage() -> BlqStorage:
|
|
|
214
224
|
return BlqStorage.open()
|
|
215
225
|
|
|
216
226
|
|
|
227
|
+
# ── Published result shapes ──────────────────────────────────────────
|
|
228
|
+
#
|
|
229
|
+
# fastmcp derives each tool's `outputSchema` from its return annotation. A bare
|
|
230
|
+
# `dict[str, Any]` becomes {"type": "object", "additionalProperties": true} —
|
|
231
|
+
# an object with no key names, which tells a programmatic caller nothing it can
|
|
232
|
+
# write code against. Measured cost, from four local models driven against this
|
|
233
|
+
# suite: 0/24 correct while 17/24 called the correct tool, because the models
|
|
234
|
+
# picked the right tool and guessed the return shape wrong. `len()` on `events`'
|
|
235
|
+
# dict returns its key count, not its event count.
|
|
236
|
+
#
|
|
237
|
+
# These TypedDicts exist to put the key names on the wire. Keys are taken from
|
|
238
|
+
# the implementations' own `return {...}` statements. `total=False` throughout:
|
|
239
|
+
# most of these tools return an `error` key on failure paths and omit it
|
|
240
|
+
# otherwise, and several have distinct success shapes (`events` returns batch
|
|
241
|
+
# keys under `run_ids`), so declaring everything required would publish a
|
|
242
|
+
# contract the code does not keep.
|
|
243
|
+
|
|
244
|
+
class EventsResult(TypedDict, total=False):
|
|
245
|
+
"""`events` — single-run keys, plus the batch keys used with `run_ids`."""
|
|
246
|
+
events: list[dict[str, Any]]
|
|
247
|
+
total_count: int
|
|
248
|
+
runs: list[dict[str, Any]]
|
|
249
|
+
run_count: int
|
|
250
|
+
total_events: int
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class StatusResult(TypedDict, total=False):
|
|
254
|
+
sources: list[dict[str, Any]]
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class HistoryResult(TypedDict, total=False):
|
|
258
|
+
runs: list[dict[str, Any]]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class QueryResult(TypedDict, total=False):
|
|
262
|
+
# Positional value lists paired with `columns`, not per-row objects.
|
|
263
|
+
rows: list[list[Any]]
|
|
264
|
+
columns: list[str]
|
|
265
|
+
row_count: int
|
|
266
|
+
error: str
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class InspectResult(TypedDict, total=False):
|
|
270
|
+
events: list[dict[str, Any]]
|
|
271
|
+
found: int
|
|
272
|
+
total: int
|
|
273
|
+
error: str
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class OutputResult(TypedDict, total=False):
|
|
277
|
+
# A LIST of stream names, not a mapping — declaring it as an object made
|
|
278
|
+
# fastmcp reject every call with "[] is not of type 'object'". Worth noting
|
|
279
|
+
# that the schema caught the wrong assumption immediately, which is most of
|
|
280
|
+
# the argument for declaring these at all.
|
|
281
|
+
streams: list[str]
|
|
282
|
+
ref: str
|
|
283
|
+
run_id: int
|
|
284
|
+
error: str
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class DiffResult(TypedDict, total=False):
|
|
288
|
+
new: list[dict[str, Any]]
|
|
289
|
+
fixed: list[dict[str, Any]]
|
|
290
|
+
summary: dict[str, Any]
|
|
291
|
+
error: str
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class CommandsResult(TypedDict, total=False):
|
|
295
|
+
commands: list[dict[str, Any]]
|
|
296
|
+
error: str
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class RunResult(TypedDict, total=False):
|
|
300
|
+
# Nullable, not merely optional: the failure paths return these keys set to
|
|
301
|
+
# None rather than omitting them, and `total=False` only makes a key
|
|
302
|
+
# absent-able.
|
|
303
|
+
run_ref: str | None
|
|
304
|
+
status: str | None
|
|
305
|
+
exit_code: int | None
|
|
306
|
+
summary: dict[str, Any]
|
|
307
|
+
results: list[dict[str, Any]]
|
|
308
|
+
commands_requested: int
|
|
309
|
+
commands_run: int
|
|
310
|
+
error: str
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class ExecResult(TypedDict, total=False):
|
|
314
|
+
run_ref: str | None
|
|
315
|
+
status: str | None
|
|
316
|
+
exit_code: int | None
|
|
317
|
+
summary: dict[str, Any]
|
|
318
|
+
errors: list[dict[str, Any]]
|
|
319
|
+
error: str
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class CiCheckResult(TypedDict, total=False):
|
|
323
|
+
status: str | None
|
|
324
|
+
mode: str | None
|
|
325
|
+
has_errors: bool
|
|
326
|
+
current_errors: int
|
|
327
|
+
current_run_id: int
|
|
328
|
+
error: str
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class CiGenerateResult(TypedDict, total=False):
|
|
332
|
+
# The names of the registered commands, not a yes/no — read from the
|
|
333
|
+
# implementation rather than inferred from the key name, which is how the
|
|
334
|
+
# first three drafts of these annotations got it wrong.
|
|
335
|
+
available: list[str]
|
|
336
|
+
count: int
|
|
337
|
+
scripts: list[dict[str, Any]]
|
|
338
|
+
shell: str | None
|
|
339
|
+
error: str
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class CommandMutationResult(TypedDict, total=False):
|
|
343
|
+
"""Shared by `register_command` / `unregister_command`."""
|
|
344
|
+
success: bool
|
|
345
|
+
message: str
|
|
346
|
+
existing_name: str
|
|
347
|
+
error: str
|
|
348
|
+
|
|
349
|
+
|
|
217
350
|
def _get_suppressed_list(include_suppressed: bool = False) -> list[str] | None:
|
|
218
351
|
"""Get list of suppressed fingerprints, or None if no filtering."""
|
|
219
352
|
if include_suppressed:
|
|
@@ -2282,7 +2415,7 @@ def run(
|
|
|
2282
2415
|
# Batch mode parameters
|
|
2283
2416
|
commands: list[str] | None = None,
|
|
2284
2417
|
stop_on_failure: bool = True,
|
|
2285
|
-
) ->
|
|
2418
|
+
) -> RunResult:
|
|
2286
2419
|
"""Run a registered command and capture its output.
|
|
2287
2420
|
|
|
2288
2421
|
Can run a single command or multiple commands in sequence (batch mode).
|
|
@@ -2340,7 +2473,7 @@ def exec(
|
|
|
2340
2473
|
timeout: int | None = None,
|
|
2341
2474
|
shell: bool = False,
|
|
2342
2475
|
lines: str | None = None,
|
|
2343
|
-
) ->
|
|
2476
|
+
) -> ExecResult:
|
|
2344
2477
|
"""Execute an ad-hoc shell command and capture its output.
|
|
2345
2478
|
|
|
2346
2479
|
IMPORTANT: Do NOT use shell pipes, redirects, or command chains
|
|
@@ -2379,7 +2512,7 @@ def query(
|
|
|
2379
2512
|
sql: str | None = None,
|
|
2380
2513
|
filter: str | None = None,
|
|
2381
2514
|
limit: int = 100,
|
|
2382
|
-
) ->
|
|
2515
|
+
) -> QueryResult:
|
|
2383
2516
|
"""Query stored log events with SQL or simple filter expressions.
|
|
2384
2517
|
|
|
2385
2518
|
Use either `sql` for raw SQL queries or `filter` for simple expressions.
|
|
@@ -2421,7 +2554,7 @@ def events(
|
|
|
2421
2554
|
# Batch mode
|
|
2422
2555
|
run_ids: list[int] | None = None,
|
|
2423
2556
|
limit_per_run: int = 10,
|
|
2424
|
-
) ->
|
|
2557
|
+
) -> EventsResult:
|
|
2425
2558
|
"""Get events with optional severity filter.
|
|
2426
2559
|
|
|
2427
2560
|
For errors only: use severity="error"
|
|
@@ -2493,7 +2626,7 @@ def inspect(
|
|
|
2493
2626
|
include_fingerprint_history: bool = False,
|
|
2494
2627
|
# Batch mode
|
|
2495
2628
|
refs: list[str] | None = None,
|
|
2496
|
-
) ->
|
|
2629
|
+
) -> InspectResult:
|
|
2497
2630
|
"""Get comprehensive event details with context and enrichment.
|
|
2498
2631
|
|
|
2499
2632
|
Returns event details with optional context and enrichment:
|
|
@@ -2572,7 +2705,7 @@ def output(
|
|
|
2572
2705
|
context: int = 0,
|
|
2573
2706
|
lines: str | None = None,
|
|
2574
2707
|
debug_formats: bool = False,
|
|
2575
|
-
) ->
|
|
2708
|
+
) -> OutputResult:
|
|
2576
2709
|
"""Get raw output for a run with optional search and filtering.
|
|
2577
2710
|
|
|
2578
2711
|
Retrieves the captured stdout/stderr from a command execution.
|
|
@@ -2606,7 +2739,7 @@ def output(
|
|
|
2606
2739
|
|
|
2607
2740
|
|
|
2608
2741
|
@mcp.tool()
|
|
2609
|
-
def status() ->
|
|
2742
|
+
def status() -> StatusResult:
|
|
2610
2743
|
"""Get current status summary of all sources.
|
|
2611
2744
|
|
|
2612
2745
|
Returns:
|
|
@@ -2975,7 +3108,7 @@ def _last_impl(
|
|
|
2975
3108
|
@mcp.tool()
|
|
2976
3109
|
def history(
|
|
2977
3110
|
limit: int | None = None, source: str | None = None, status: str | None = None
|
|
2978
|
-
) ->
|
|
3111
|
+
) -> HistoryResult:
|
|
2979
3112
|
"""Get run history.
|
|
2980
3113
|
|
|
2981
3114
|
Args:
|
|
@@ -2995,7 +3128,7 @@ def history(
|
|
|
2995
3128
|
|
|
2996
3129
|
|
|
2997
3130
|
@mcp.tool()
|
|
2998
|
-
def diff(run1: int, run2: int) ->
|
|
3131
|
+
def diff(run1: int, run2: int) -> DiffResult:
|
|
2999
3132
|
"""Compare errors between two runs.
|
|
3000
3133
|
|
|
3001
3134
|
Args:
|
|
@@ -3023,7 +3156,7 @@ def register_command(
|
|
|
3023
3156
|
lines: str | None = None,
|
|
3024
3157
|
sandbox: str | dict[str, Any] | None = None,
|
|
3025
3158
|
lock: str | None = None,
|
|
3026
|
-
) ->
|
|
3159
|
+
) -> CommandMutationResult:
|
|
3027
3160
|
"""Register a new command.
|
|
3028
3161
|
|
|
3029
3162
|
If a command with the same name or same command string already exists,
|
|
@@ -3075,7 +3208,7 @@ def register_command(
|
|
|
3075
3208
|
|
|
3076
3209
|
|
|
3077
3210
|
@mcp.tool()
|
|
3078
|
-
def unregister_command(name: str) ->
|
|
3211
|
+
def unregister_command(name: str) -> CommandMutationResult:
|
|
3079
3212
|
"""Remove a registered command.
|
|
3080
3213
|
|
|
3081
3214
|
Note: This tool can be disabled via mcp.disabled_tools config.
|
|
@@ -3091,7 +3224,7 @@ def unregister_command(name: str) -> dict[str, Any]:
|
|
|
3091
3224
|
|
|
3092
3225
|
|
|
3093
3226
|
@mcp.tool()
|
|
3094
|
-
def commands() ->
|
|
3227
|
+
def commands() -> CommandsResult:
|
|
3095
3228
|
"""List all registered commands.
|
|
3096
3229
|
|
|
3097
3230
|
Returns:
|
|
@@ -3592,7 +3725,7 @@ def ci_check(
|
|
|
3592
3725
|
baseline: str | None = None,
|
|
3593
3726
|
fail_on_any: bool = False,
|
|
3594
3727
|
run_id: int | None = None,
|
|
3595
|
-
) ->
|
|
3728
|
+
) -> CiCheckResult:
|
|
3596
3729
|
"""Check for regressions compared to a baseline run.
|
|
3597
3730
|
|
|
3598
3731
|
Compares error fingerprints between the current run and a baseline to
|
|
@@ -3619,7 +3752,7 @@ def ci_check(
|
|
|
3619
3752
|
def ci_generate(
|
|
3620
3753
|
commands: list[str] | None = None,
|
|
3621
3754
|
shell: str = "bash",
|
|
3622
|
-
) ->
|
|
3755
|
+
) -> CiGenerateResult:
|
|
3623
3756
|
"""Generate standalone shell scripts from registered commands.
|
|
3624
3757
|
|
|
3625
3758
|
Creates shell script content for CI environments where blq may not be
|
|
@@ -16,7 +16,18 @@ if fastmcp:
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def get_data(result):
|
|
19
|
-
"""Extract data from CallToolResult.
|
|
19
|
+
"""Extract data from CallToolResult as a plain dict.
|
|
20
|
+
|
|
21
|
+
Prefer `structured_content`: once a tool declares an outputSchema (which
|
|
22
|
+
they now do, so callers can see the key names — see #47), fastmcp
|
|
23
|
+
deserializes `.data` into a generated model rather than a dict, and a model
|
|
24
|
+
is not subscriptable. `structured_content` is the JSON payload a real MCP
|
|
25
|
+
client receives over the wire, including any keys the schema does not
|
|
26
|
+
declare, so asserting against it tests what a consumer actually gets.
|
|
27
|
+
"""
|
|
28
|
+
sc = getattr(result, "structured_content", None)
|
|
29
|
+
if sc is not None:
|
|
30
|
+
return sc
|
|
20
31
|
if hasattr(result, "data"):
|
|
21
32
|
return result.data
|
|
22
33
|
return result
|
|
@@ -16,7 +16,18 @@ if fastmcp:
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def get_data(result):
|
|
19
|
-
"""Extract data from CallToolResult.
|
|
19
|
+
"""Extract data from CallToolResult as a plain dict.
|
|
20
|
+
|
|
21
|
+
Prefer `structured_content`: once a tool declares an outputSchema (which
|
|
22
|
+
they now do, so callers can see the key names — see #47), fastmcp
|
|
23
|
+
deserializes `.data` into a generated model rather than a dict, and a model
|
|
24
|
+
is not subscriptable. `structured_content` is the JSON payload a real MCP
|
|
25
|
+
client receives over the wire, including any keys the schema does not
|
|
26
|
+
declare, so asserting against it tests what a consumer actually gets.
|
|
27
|
+
"""
|
|
28
|
+
sc = getattr(result, "structured_content", None)
|
|
29
|
+
if sc is not None:
|
|
30
|
+
return sc
|
|
20
31
|
if hasattr(result, "data"):
|
|
21
32
|
return result.data
|
|
22
33
|
return result
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""MCP tools must publish the NAMES of the keys they return.
|
|
2
|
+
|
|
3
|
+
fastmcp derives `outputSchema` from a tool's return annotation. A bare
|
|
4
|
+
`dict[str, Any]` becomes:
|
|
5
|
+
|
|
6
|
+
{"type": "object", "additionalProperties": true}
|
|
7
|
+
|
|
8
|
+
which tells a caller it will receive *an object* and nothing more. A
|
|
9
|
+
programmatic consumer then has to guess the keys, and a wrong guess produces
|
|
10
|
+
a program that validates, runs, and answers incorrectly.
|
|
11
|
+
|
|
12
|
+
Measured by a session driving four local models against this suite through
|
|
13
|
+
lackpy, which generates one restricted-Python program per intent:
|
|
14
|
+
|
|
15
|
+
0/24 correct while 17/24 called the correct tool
|
|
16
|
+
|
|
17
|
+
`len()` on `events`' returned dict gives 2 — its key count — where the answer
|
|
18
|
+
was 1. Annotating the return types is the whole fix: the wire format already
|
|
19
|
+
carries the property names, nothing else changes, and it helps every
|
|
20
|
+
programmatic caller rather than one harness.
|
|
21
|
+
|
|
22
|
+
See #47.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import pytest
|
|
26
|
+
|
|
27
|
+
fastmcp = pytest.importorskip("fastmcp")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def _schema_for(tool_name):
|
|
31
|
+
from fastmcp import Client
|
|
32
|
+
from blq.serve import mcp
|
|
33
|
+
|
|
34
|
+
async with Client(mcp) as client:
|
|
35
|
+
for tool in await client.list_tools():
|
|
36
|
+
if tool.name == tool_name:
|
|
37
|
+
return tool.outputSchema
|
|
38
|
+
raise AssertionError(f"tool {tool_name!r} not published")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _properties(schema):
|
|
42
|
+
"""The named properties a caller can rely on.
|
|
43
|
+
|
|
44
|
+
fastmcp wraps a non-object return in {"result": ...}; for a TypedDict it
|
|
45
|
+
emits the properties directly. Accept either, and look through the wrapper
|
|
46
|
+
so the assertion is about the tool's shape, not fastmcp's packaging.
|
|
47
|
+
"""
|
|
48
|
+
assert schema is not None, "tool publishes no outputSchema at all"
|
|
49
|
+
props = schema.get("properties") or {}
|
|
50
|
+
if set(props) == {"result"}:
|
|
51
|
+
inner = props["result"]
|
|
52
|
+
props = inner.get("properties") or {}
|
|
53
|
+
return props
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Keys taken from the impl's own `return {...}` statements, not from docs.
|
|
57
|
+
EXPECTED = {
|
|
58
|
+
"events": {"events", "total_count"},
|
|
59
|
+
"status": {"sources"},
|
|
60
|
+
"history": {"runs"},
|
|
61
|
+
"query": {"rows", "columns", "row_count"},
|
|
62
|
+
"inspect": {"events", "found", "total"},
|
|
63
|
+
"output": {"streams", "ref"},
|
|
64
|
+
"diff": {"new", "fixed", "summary"},
|
|
65
|
+
"commands": {"commands"},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.mark.asyncio
|
|
70
|
+
@pytest.mark.parametrize("tool_name,expected", sorted(EXPECTED.items()))
|
|
71
|
+
async def test_tool_publishes_named_properties(tool_name, expected):
|
|
72
|
+
schema = await _schema_for(tool_name)
|
|
73
|
+
props = _properties(schema)
|
|
74
|
+
missing = expected - set(props)
|
|
75
|
+
assert not missing, (
|
|
76
|
+
f"{tool_name} publishes no property names for {sorted(missing)} — "
|
|
77
|
+
f"got {sorted(props) or 'nothing'}. A caller cannot write code "
|
|
78
|
+
f"against this."
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.mark.asyncio
|
|
83
|
+
@pytest.mark.parametrize("tool_name", sorted(EXPECTED))
|
|
84
|
+
async def test_schema_is_not_an_open_object(tool_name):
|
|
85
|
+
"""`additionalProperties: true` with no properties is the defect."""
|
|
86
|
+
schema = await _schema_for(tool_name)
|
|
87
|
+
props = _properties(schema)
|
|
88
|
+
assert props, (
|
|
89
|
+
f"{tool_name}'s outputSchema names no properties at all"
|
|
90
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|