blq-cli 1.1.0__tar.gz → 1.2.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 (205) hide show
  1. {blq_cli-1.1.0 → blq_cli-1.2.0}/CHANGELOG.md +31 -0
  2. {blq_cli-1.1.0 → blq_cli-1.2.0}/PKG-INFO +1 -1
  3. {blq_cli-1.1.0 → blq_cli-1.2.0}/pyproject.toml +1 -1
  4. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/serve.py +138 -15
  5. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_mcp_ci_tools.py +12 -1
  6. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_mcp_server.py +12 -1
  7. blq_cli-1.2.0/tests/test_output_schemas.py +90 -0
  8. {blq_cli-1.1.0 → blq_cli-1.2.0}/.claude/hooks/blq-suggest.sh +0 -0
  9. {blq_cli-1.1.0 → blq_cli-1.2.0}/.github/workflows/ci.yml +0 -0
  10. {blq_cli-1.1.0 → blq_cli-1.2.0}/.github/workflows/docs.yml +0 -0
  11. {blq_cli-1.1.0 → blq_cli-1.2.0}/.github/workflows/publish.yml +0 -0
  12. {blq_cli-1.1.0 → blq_cli-1.2.0}/.gitignore +0 -0
  13. {blq_cli-1.1.0 → blq_cli-1.2.0}/.mcp.json +0 -0
  14. {blq_cli-1.1.0 → blq_cli-1.2.0}/.readthedocs.yml +0 -0
  15. {blq_cli-1.1.0 → blq_cli-1.2.0}/AGENT.md +0 -0
  16. {blq_cli-1.1.0 → blq_cli-1.2.0}/AGENTS.md +0 -0
  17. {blq_cli-1.1.0 → blq_cli-1.2.0}/CLAUDE.md +0 -0
  18. {blq_cli-1.1.0 → blq_cli-1.2.0}/CONTRIBUTING.md +0 -0
  19. {blq_cli-1.1.0 → blq_cli-1.2.0}/README.md +0 -0
  20. {blq_cli-1.1.0 → blq_cli-1.2.0}/SKILL.md +0 -0
  21. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/api-reference.md +0 -0
  22. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/capture.md +0 -0
  23. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/ci.md +0 -0
  24. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/completions.md +0 -0
  25. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/errors.md +0 -0
  26. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/exec.md +0 -0
  27. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/filter.md +0 -0
  28. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/index.md +0 -0
  29. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/init.md +0 -0
  30. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/maintenance.md +0 -0
  31. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/query.md +0 -0
  32. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/registry.md +0 -0
  33. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/report.md +0 -0
  34. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/run.md +0 -0
  35. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/sql.md +0 -0
  36. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/status.md +0 -0
  37. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/sync.md +0 -0
  38. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/watch.md +0 -0
  39. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-bird-migration.md +0 -0
  40. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-commands.md +0 -0
  41. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-config-command.md +0 -0
  42. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-extensions.md +0 -0
  43. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-git-integration.md +0 -0
  44. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-hooks-v2.md +0 -0
  45. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-live-inspection.md +0 -0
  46. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-parameterized-commands.md +0 -0
  47. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-run-args.md +0 -0
  48. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-sandbox-specs.md +0 -0
  49. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-sync.md +0 -0
  50. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-track-save.md +0 -0
  51. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/duck-hunt-v2-migration.md +0 -0
  52. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/duck-hunt-v3-migration.md +0 -0
  53. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/proposal-bird-v5.md +0 -0
  54. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/skill-inspect-enrichment.md +0 -0
  55. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/getting-started.md +0 -0
  56. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/index.md +0 -0
  57. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/integration.md +0 -0
  58. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/mcp.md +0 -0
  59. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/plans/explore-bird-sqlite-backend.md +0 -0
  60. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/plans/explore-nsjail-python-wrapper.md +0 -0
  61. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/plans/explore-nsjail-spack-package.md +0 -0
  62. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/plans/patterns-integration-prompt.md +0 -0
  63. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/plans/roadmap-to-1.0.md +0 -0
  64. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/plugin-guide.md +0 -0
  65. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/python-api.md +0 -0
  66. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/query-guide.md +0 -0
  67. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/requirements.txt +0 -0
  68. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/sandbox.md +0 -0
  69. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/schema-stability.md +0 -0
  70. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/schema.md +0 -0
  71. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-22-extension-system.md +0 -0
  72. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-28-bwrap-engine.md +0 -0
  73. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-28-command-locks.md +0 -0
  74. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-29-annotators-and-tighten.md +0 -0
  75. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-29-service-layer-phase1.md +0 -0
  76. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-29-strace-profiling.md +0 -0
  77. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/superpowers/specs/2026-03-22-extension-system-design.md +0 -0
  78. {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/superpowers/specs/2026-03-29-unified-service-layer-design.md +0 -0
  79. {blq_cli-1.1.0 → blq_cli-1.2.0}/experiments/agent-build-test.md +0 -0
  80. {blq_cli-1.1.0 → blq_cli-1.2.0}/mkdocs.yml +0 -0
  81. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/__init__.py +0 -0
  82. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/__main__.py +0 -0
  83. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/bird.py +0 -0
  84. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/bird_schema.sql +0 -0
  85. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/cli.py +0 -0
  86. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/README.md +0 -0
  87. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/__init__.py +0 -0
  88. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/ci_cmd.py +0 -0
  89. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/clean_cmd.py +0 -0
  90. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/config_cmd.py +0 -0
  91. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/core.py +0 -0
  92. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/events.py +0 -0
  93. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/execution.py +0 -0
  94. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/hooks_cmd.py +0 -0
  95. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/hooks_gen.py +0 -0
  96. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/init_cmd.py +0 -0
  97. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/management.py +0 -0
  98. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/management_cmd.py +0 -0
  99. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/mcp_cmd.py +0 -0
  100. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/migrate.py +0 -0
  101. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/query_cmd.py +0 -0
  102. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/record_cmd.py +0 -0
  103. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/registry.py +0 -0
  104. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/report_cmd.py +0 -0
  105. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/sandbox_cmd.py +0 -0
  106. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/serve_cmd.py +0 -0
  107. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/sync_cmd.py +0 -0
  108. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/watch_cmd.py +0 -0
  109. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/config_format.py +0 -0
  110. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/ext/__init__.py +0 -0
  111. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/ext/annotator.py +0 -0
  112. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/ext/discovery.py +0 -0
  113. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/ext/local_executor.py +0 -0
  114. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/ext/pipeline.py +0 -0
  115. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/git.py +0 -0
  116. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/github.py +0 -0
  117. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/locks.py +0 -0
  118. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/output.py +0 -0
  119. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/query.py +0 -0
  120. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/runtime.py +0 -0
  121. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/schema.sql +0 -0
  122. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/services/__init__.py +0 -0
  123. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/services/execution.py +0 -0
  124. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/services/inspect.py +0 -0
  125. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/services/query.py +0 -0
  126. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/services/refs.py +0 -0
  127. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/storage.py +0 -0
  128. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/templates/drone.yml.j2 +0 -0
  129. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/templates/git_hook.sh.j2 +0 -0
  130. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/templates/github_workflow.yml.j2 +0 -0
  131. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/templates/gitlab_ci.yml.j2 +0 -0
  132. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/templates/hook_script.sh.j2 +0 -0
  133. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/user_config.py +0 -0
  134. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq_sandbox/__init__.py +0 -0
  135. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq_sandbox/engines.py +0 -0
  136. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq_sandbox/profile.py +0 -0
  137. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq_sandbox/source_annotator.py +0 -0
  138. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq_sandbox/spec.py +0 -0
  139. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq_sandbox/strace_parser.py +0 -0
  140. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq_sandbox/tighten.py +0 -0
  141. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq_sandbox/violations.py +0 -0
  142. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq_sandbox_bwrap/__init__.py +0 -0
  143. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq_sandbox_bwrap/args.py +0 -0
  144. {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq_sandbox_systemd/__init__.py +0 -0
  145. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/__init__.py +0 -0
  146. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/conftest.py +0 -0
  147. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/sql/test_duck_hunt_v2_migration.sql +0 -0
  148. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_annotator.py +0 -0
  149. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_attempts_outcomes.py +0 -0
  150. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_auto_init.py +0 -0
  151. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_autoprune.py +0 -0
  152. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_bird.py +0 -0
  153. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_bwrap_args.py +0 -0
  154. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_bwrap_engine.py +0 -0
  155. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_ci.py +0 -0
  156. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_ci_generators.py +0 -0
  157. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_command_args.py +0 -0
  158. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_command_lock_field.py +0 -0
  159. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_config_consumption.py +0 -0
  160. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_core.py +0 -0
  161. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_execution_locks.py +0 -0
  162. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_exit_code_reason.py +0 -0
  163. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_ext_discovery.py +0 -0
  164. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_ext_integration.py +0 -0
  165. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_ext_local_executor.py +0 -0
  166. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_ext_pipeline.py +0 -0
  167. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_ext_types.py +0 -0
  168. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_git.py +0 -0
  169. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_hooks.py +0 -0
  170. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_hooks_gen.py +0 -0
  171. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_import_capture_storage.py +0 -0
  172. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_inspect.py +0 -0
  173. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_json_null_filter.py +0 -0
  174. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_locks.py +0 -0
  175. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_log_content_persistence.py +0 -0
  176. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_mcp_merge.py +0 -0
  177. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_phase1_structured_output.py +0 -0
  178. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_phase2_command_registry.py +0 -0
  179. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_query_api.py +0 -0
  180. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_query_filter.py +0 -0
  181. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_record_invocation.py +0 -0
  182. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_report.py +0 -0
  183. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_runtime_config.py +0 -0
  184. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_sandbox.py +0 -0
  185. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_sandbox_cmd.py +0 -0
  186. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_sandbox_events.py +0 -0
  187. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_sandbox_ext.py +0 -0
  188. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_sandbox_profile.py +0 -0
  189. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_sandbox_register.py +0 -0
  190. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_sandbox_systemd.py +0 -0
  191. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_sandbox_tighten.py +0 -0
  192. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_sandbox_violations.py +0 -0
  193. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_schema_repair.py +0 -0
  194. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_security_containment.py +0 -0
  195. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_services_execution.py +0 -0
  196. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_services_inspect.py +0 -0
  197. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_services_query.py +0 -0
  198. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_services_refs.py +0 -0
  199. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_source_annotator.py +0 -0
  200. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_storage.py +0 -0
  201. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_storage_prune.py +0 -0
  202. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_strace_parser.py +0 -0
  203. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_user_config.py +0 -0
  204. {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_watch.py +0 -0
  205. {blq_cli-1.1.0 → blq_cli-1.2.0}/uv.lock +0 -0
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.2.0
4
+
5
+ ### Added — MCP tools publish the names of the keys they return (#47)
6
+
7
+ fastmcp derives `outputSchema` from a tool's return annotation, so
8
+ `dict[str, Any]` published `{"type": "object", "additionalProperties": true}`
9
+ — an object with no key names. A programmatic caller had to guess them, and a
10
+ wrong guess produces a program that validates, runs, and answers incorrectly.
11
+ Measured across four local models driven through lackpy: **0/24 correct while
12
+ 17/24 called the right tool**, with `len()` on `events`' dict returning its key
13
+ count rather than its event count.
14
+
15
+ 14 tools now return TypedDicts whose keys come from the implementations' own
16
+ return statements. Declaring the shapes immediately caught three places where
17
+ the code did not match a reasonable reading of it:
18
+
19
+ - `output.streams` is a list of stream names, not a mapping
20
+ - `query.rows` is positional value lists paired with `columns`, not per-row objects
21
+ - `ci_generate.available` is the registered command names, not a boolean
22
+
23
+ No data is dropped — keys a TypedDict does not declare still reach the client
24
+ in `structuredContent`.
25
+
26
+ **One behavioural note for Python callers:** with an `outputSchema` present,
27
+ fastmcp's own client deserializes `.data` into a generated model rather than a
28
+ dict, so `result.data["key"]` no longer works. Read `structured_content`, which
29
+ is the JSON a real MCP client receives over the wire. Consumers speaking
30
+ JSON-RPC (Claude Code, lackpy, any stdio client) are unaffected and strictly
31
+ better off.
32
+
33
+
3
34
  ## v1.1.0
4
35
 
5
36
  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.0
3
+ Version: 1.2.0
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "blq-cli"
7
- version = "1.1.0"
7
+ version = "1.2.0"
8
8
  description = "Build Log Query - capture and query build/test logs with DuckDB"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -34,7 +34,7 @@ import re as _re
34
34
  import shlex
35
35
  import subprocess
36
36
  import sys
37
- from typing import Any
37
+ from typing import Any, TypedDict
38
38
 
39
39
  import pandas as pd # type: ignore[import-untyped]
40
40
  from fastmcp import FastMCP
@@ -214,6 +214,129 @@ def _get_storage() -> BlqStorage:
214
214
  return BlqStorage.open()
215
215
 
216
216
 
217
+ # ── Published result shapes ──────────────────────────────────────────
218
+ #
219
+ # fastmcp derives each tool's `outputSchema` from its return annotation. A bare
220
+ # `dict[str, Any]` becomes {"type": "object", "additionalProperties": true} —
221
+ # an object with no key names, which tells a programmatic caller nothing it can
222
+ # write code against. Measured cost, from four local models driven against this
223
+ # suite: 0/24 correct while 17/24 called the correct tool, because the models
224
+ # picked the right tool and guessed the return shape wrong. `len()` on `events`'
225
+ # dict returns its key count, not its event count.
226
+ #
227
+ # These TypedDicts exist to put the key names on the wire. Keys are taken from
228
+ # the implementations' own `return {...}` statements. `total=False` throughout:
229
+ # most of these tools return an `error` key on failure paths and omit it
230
+ # otherwise, and several have distinct success shapes (`events` returns batch
231
+ # keys under `run_ids`), so declaring everything required would publish a
232
+ # contract the code does not keep.
233
+
234
+ class EventsResult(TypedDict, total=False):
235
+ """`events` — single-run keys, plus the batch keys used with `run_ids`."""
236
+ events: list[dict[str, Any]]
237
+ total_count: int
238
+ runs: list[dict[str, Any]]
239
+ run_count: int
240
+ total_events: int
241
+
242
+
243
+ class StatusResult(TypedDict, total=False):
244
+ sources: list[dict[str, Any]]
245
+
246
+
247
+ class HistoryResult(TypedDict, total=False):
248
+ runs: list[dict[str, Any]]
249
+
250
+
251
+ class QueryResult(TypedDict, total=False):
252
+ # Positional value lists paired with `columns`, not per-row objects.
253
+ rows: list[list[Any]]
254
+ columns: list[str]
255
+ row_count: int
256
+ error: str
257
+
258
+
259
+ class InspectResult(TypedDict, total=False):
260
+ events: list[dict[str, Any]]
261
+ found: int
262
+ total: int
263
+ error: str
264
+
265
+
266
+ class OutputResult(TypedDict, total=False):
267
+ # A LIST of stream names, not a mapping — declaring it as an object made
268
+ # fastmcp reject every call with "[] is not of type 'object'". Worth noting
269
+ # that the schema caught the wrong assumption immediately, which is most of
270
+ # the argument for declaring these at all.
271
+ streams: list[str]
272
+ ref: str
273
+ run_id: int
274
+ error: str
275
+
276
+
277
+ class DiffResult(TypedDict, total=False):
278
+ new: list[dict[str, Any]]
279
+ fixed: list[dict[str, Any]]
280
+ summary: dict[str, Any]
281
+ error: str
282
+
283
+
284
+ class CommandsResult(TypedDict, total=False):
285
+ commands: list[dict[str, Any]]
286
+ error: str
287
+
288
+
289
+ class RunResult(TypedDict, total=False):
290
+ # Nullable, not merely optional: the failure paths return these keys set to
291
+ # None rather than omitting them, and `total=False` only makes a key
292
+ # absent-able.
293
+ run_ref: str | None
294
+ status: str | None
295
+ exit_code: int | None
296
+ summary: dict[str, Any]
297
+ results: list[dict[str, Any]]
298
+ commands_requested: int
299
+ commands_run: int
300
+ error: str
301
+
302
+
303
+ class ExecResult(TypedDict, total=False):
304
+ run_ref: str | None
305
+ status: str | None
306
+ exit_code: int | None
307
+ summary: dict[str, Any]
308
+ errors: list[dict[str, Any]]
309
+ error: str
310
+
311
+
312
+ class CiCheckResult(TypedDict, total=False):
313
+ status: str | None
314
+ mode: str | None
315
+ has_errors: bool
316
+ current_errors: int
317
+ current_run_id: int
318
+ error: str
319
+
320
+
321
+ class CiGenerateResult(TypedDict, total=False):
322
+ # The names of the registered commands, not a yes/no — read from the
323
+ # implementation rather than inferred from the key name, which is how the
324
+ # first three drafts of these annotations got it wrong.
325
+ available: list[str]
326
+ count: int
327
+ scripts: list[dict[str, Any]]
328
+ shell: str | None
329
+ error: str
330
+
331
+
332
+ class CommandMutationResult(TypedDict, total=False):
333
+ """Shared by `register_command` / `unregister_command`."""
334
+ success: bool
335
+ message: str
336
+ existing_name: str
337
+ error: str
338
+
339
+
217
340
  def _get_suppressed_list(include_suppressed: bool = False) -> list[str] | None:
218
341
  """Get list of suppressed fingerprints, or None if no filtering."""
219
342
  if include_suppressed:
@@ -2282,7 +2405,7 @@ def run(
2282
2405
  # Batch mode parameters
2283
2406
  commands: list[str] | None = None,
2284
2407
  stop_on_failure: bool = True,
2285
- ) -> dict[str, Any]:
2408
+ ) -> RunResult:
2286
2409
  """Run a registered command and capture its output.
2287
2410
 
2288
2411
  Can run a single command or multiple commands in sequence (batch mode).
@@ -2340,7 +2463,7 @@ def exec(
2340
2463
  timeout: int | None = None,
2341
2464
  shell: bool = False,
2342
2465
  lines: str | None = None,
2343
- ) -> dict[str, Any]:
2466
+ ) -> ExecResult:
2344
2467
  """Execute an ad-hoc shell command and capture its output.
2345
2468
 
2346
2469
  IMPORTANT: Do NOT use shell pipes, redirects, or command chains
@@ -2379,7 +2502,7 @@ def query(
2379
2502
  sql: str | None = None,
2380
2503
  filter: str | None = None,
2381
2504
  limit: int = 100,
2382
- ) -> dict[str, Any]:
2505
+ ) -> QueryResult:
2383
2506
  """Query stored log events with SQL or simple filter expressions.
2384
2507
 
2385
2508
  Use either `sql` for raw SQL queries or `filter` for simple expressions.
@@ -2421,7 +2544,7 @@ def events(
2421
2544
  # Batch mode
2422
2545
  run_ids: list[int] | None = None,
2423
2546
  limit_per_run: int = 10,
2424
- ) -> dict[str, Any]:
2547
+ ) -> EventsResult:
2425
2548
  """Get events with optional severity filter.
2426
2549
 
2427
2550
  For errors only: use severity="error"
@@ -2493,7 +2616,7 @@ def inspect(
2493
2616
  include_fingerprint_history: bool = False,
2494
2617
  # Batch mode
2495
2618
  refs: list[str] | None = None,
2496
- ) -> dict[str, Any]:
2619
+ ) -> InspectResult:
2497
2620
  """Get comprehensive event details with context and enrichment.
2498
2621
 
2499
2622
  Returns event details with optional context and enrichment:
@@ -2572,7 +2695,7 @@ def output(
2572
2695
  context: int = 0,
2573
2696
  lines: str | None = None,
2574
2697
  debug_formats: bool = False,
2575
- ) -> dict[str, Any]:
2698
+ ) -> OutputResult:
2576
2699
  """Get raw output for a run with optional search and filtering.
2577
2700
 
2578
2701
  Retrieves the captured stdout/stderr from a command execution.
@@ -2606,7 +2729,7 @@ def output(
2606
2729
 
2607
2730
 
2608
2731
  @mcp.tool()
2609
- def status() -> dict[str, Any]:
2732
+ def status() -> StatusResult:
2610
2733
  """Get current status summary of all sources.
2611
2734
 
2612
2735
  Returns:
@@ -2975,7 +3098,7 @@ def _last_impl(
2975
3098
  @mcp.tool()
2976
3099
  def history(
2977
3100
  limit: int | None = None, source: str | None = None, status: str | None = None
2978
- ) -> dict[str, Any]:
3101
+ ) -> HistoryResult:
2979
3102
  """Get run history.
2980
3103
 
2981
3104
  Args:
@@ -2995,7 +3118,7 @@ def history(
2995
3118
 
2996
3119
 
2997
3120
  @mcp.tool()
2998
- def diff(run1: int, run2: int) -> dict[str, Any]:
3121
+ def diff(run1: int, run2: int) -> DiffResult:
2999
3122
  """Compare errors between two runs.
3000
3123
 
3001
3124
  Args:
@@ -3023,7 +3146,7 @@ def register_command(
3023
3146
  lines: str | None = None,
3024
3147
  sandbox: str | dict[str, Any] | None = None,
3025
3148
  lock: str | None = None,
3026
- ) -> dict[str, Any]:
3149
+ ) -> CommandMutationResult:
3027
3150
  """Register a new command.
3028
3151
 
3029
3152
  If a command with the same name or same command string already exists,
@@ -3075,7 +3198,7 @@ def register_command(
3075
3198
 
3076
3199
 
3077
3200
  @mcp.tool()
3078
- def unregister_command(name: str) -> dict[str, Any]:
3201
+ def unregister_command(name: str) -> CommandMutationResult:
3079
3202
  """Remove a registered command.
3080
3203
 
3081
3204
  Note: This tool can be disabled via mcp.disabled_tools config.
@@ -3091,7 +3214,7 @@ def unregister_command(name: str) -> dict[str, Any]:
3091
3214
 
3092
3215
 
3093
3216
  @mcp.tool()
3094
- def commands() -> dict[str, Any]:
3217
+ def commands() -> CommandsResult:
3095
3218
  """List all registered commands.
3096
3219
 
3097
3220
  Returns:
@@ -3592,7 +3715,7 @@ def ci_check(
3592
3715
  baseline: str | None = None,
3593
3716
  fail_on_any: bool = False,
3594
3717
  run_id: int | None = None,
3595
- ) -> dict[str, Any]:
3718
+ ) -> CiCheckResult:
3596
3719
  """Check for regressions compared to a baseline run.
3597
3720
 
3598
3721
  Compares error fingerprints between the current run and a baseline to
@@ -3619,7 +3742,7 @@ def ci_check(
3619
3742
  def ci_generate(
3620
3743
  commands: list[str] | None = None,
3621
3744
  shell: str = "bash",
3622
- ) -> dict[str, Any]:
3745
+ ) -> CiGenerateResult:
3623
3746
  """Generate standalone shell scripts from registered commands.
3624
3747
 
3625
3748
  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