boost-skill-cli 1.0.93__tar.gz → 1.0.95__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 (244) hide show
  1. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/PKG-INFO +1 -1
  2. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/_version.py +2 -2
  3. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/commands/configuration.py +10 -186
  4. boost_skill_cli-1.0.95/boost_cli/core/mcp.py +165 -0
  5. boost_skill_cli-1.0.95/boost_cli/core/serve.py +151 -0
  6. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_skill_cli.egg-info/PKG-INFO +1 -1
  7. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_skill_cli.egg-info/SOURCES.txt +2 -0
  8. boost_skill_cli-1.0.95/docs/roadmap/items/extract-mcp-http-servers-out-of-configuration-py.md +26 -0
  9. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap.html +23 -12
  10. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/functional/test_cli_configuration.py +3 -3
  11. boost_skill_cli-1.0.95/tests/unit/test_mcp.py +241 -0
  12. boost_skill_cli-1.0.95/tests/unit/test_serve.py +137 -0
  13. boost_skill_cli-1.0.93/boost_cli/core/mcp.py +0 -70
  14. boost_skill_cli-1.0.93/docs/roadmap/items/extract-mcp-http-servers-out-of-configuration-py.md +0 -16
  15. boost_skill_cli-1.0.93/tests/unit/test_mcp.py +0 -83
  16. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/.gitattributes +0 -0
  17. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/.gitignore +0 -0
  18. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/CLAUDE.md +0 -0
  19. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/CONTRIBUTING.md +0 -0
  20. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/LICENSE +0 -0
  21. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/MANIFEST.in +0 -0
  22. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/Makefile +0 -0
  23. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/README.md +0 -0
  24. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/SECURITY.md +0 -0
  25. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost +0 -0
  26. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/__init__.py +0 -0
  27. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/__main__.py +0 -0
  28. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/cli.py +0 -0
  29. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/cliparse.py +0 -0
  30. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/commands/__init__.py +0 -0
  31. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/commands/bmad.py +0 -0
  32. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/commands/discovery.py +0 -0
  33. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/commands/hooks.py +0 -0
  34. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/commands/info.py +0 -0
  35. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/commands/intelligence.py +0 -0
  36. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/commands/pkg.py +0 -0
  37. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/commands/quality.py +0 -0
  38. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/commands/taps.py +0 -0
  39. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/commands/team.py +0 -0
  40. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/__init__.py +0 -0
  41. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/agents.py +0 -0
  42. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/ai.py +0 -0
  43. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/catalog.py +0 -0
  44. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/claude_settings.py +0 -0
  45. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/config.py +0 -0
  46. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/dense.py +0 -0
  47. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/embed.py +0 -0
  48. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/frontmatter.py +0 -0
  49. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/gitutil.py +0 -0
  50. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/journal.py +0 -0
  51. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/lockfile.py +0 -0
  52. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/logs.py +0 -0
  53. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/output.py +0 -0
  54. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/paths.py +0 -0
  55. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/policy.py +0 -0
  56. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/rag.py +0 -0
  57. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/registry.py +0 -0
  58. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/staleness.py +0 -0
  59. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/store.py +0 -0
  60. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/typosquat.py +0 -0
  61. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/util.py +0 -0
  62. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/data/registries.json +0 -0
  63. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/errors.py +0 -0
  64. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/spin.py +0 -0
  65. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_skill_cli.egg-info/dependency_links.txt +0 -0
  66. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_skill_cli.egg-info/entry_points.txt +0 -0
  67. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_skill_cli.egg-info/requires.txt +0 -0
  68. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_skill_cli.egg-info/top_level.txt +0 -0
  69. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/.claude/settings.local.json +0 -0
  70. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/DEBUGGING.md +0 -0
  71. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/demo.gif +0 -0
  72. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/demo.tape +0 -0
  73. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/design-roadmap.html +0 -0
  74. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/index.html +0 -0
  75. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/mcp-hub.html +0 -0
  76. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/rag-architecture.md +0 -0
  77. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D01.md +0 -0
  78. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D02.md +0 -0
  79. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D03.md +0 -0
  80. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D04.md +0 -0
  81. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D05.md +0 -0
  82. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D06.md +0 -0
  83. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D07.md +0 -0
  84. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D08.md +0 -0
  85. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D09.md +0 -0
  86. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D10.md +0 -0
  87. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D11.md +0 -0
  88. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D12.md +0 -0
  89. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D13.md +0 -0
  90. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D14.md +0 -0
  91. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D15.md +0 -0
  92. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D16.md +0 -0
  93. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D17.md +0 -0
  94. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D18.md +0 -0
  95. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D19.md +0 -0
  96. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D20.md +0 -0
  97. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D21.md +0 -0
  98. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D22.md +0 -0
  99. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D23.md +0 -0
  100. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D24.md +0 -0
  101. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D25.md +0 -0
  102. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D26.md +0 -0
  103. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/accessibility-audit-pa11y-ci-axe-core.md +0 -0
  104. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/atomic-corruption-safe-lock-file-writes.md +0 -0
  105. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/atomic-skill-install-temp-dir-swap.md +0 -0
  106. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/autonomous-ship-workflow-and-isolated-worktree.md +0 -0
  107. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/bring-commands-under-mutation-testing.md +0 -0
  108. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/broken-link-and-anchor-checking-lychee.md +0 -0
  109. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/build-provenance-slsa-attestations.md +0 -0
  110. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/cache-the-catalog-entry-set-across-rag-queries.md +0 -0
  111. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/capability-manifest-and-least-privilege-policy.md +0 -0
  112. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/clean-env-install-smoke-pip-and-pipx.md +0 -0
  113. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/command-reference-docs-site.md +0 -0
  114. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/complexity-and-dead-code-radar-xenon-vulture.md +0 -0
  115. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/consolidate-skill-staleness-drift-logic-into-cor.md +0 -0
  116. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/coverage-dashboard-codecov-free-for-oss.md +0 -0
  117. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/coverage-guided-fuzzing-atheris-oss-fuzz.md +0 -0
  118. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/crash-recorder-error-paths-core-logs-py.md +0 -0
  119. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/dependency-cve-gate-pip-audit.md +0 -0
  120. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/docstring-coverage-interrogate.md +0 -0
  121. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/end-of-options-guard-on-git-commands.md +0 -0
  122. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/extension-free-tests-core-dense-py.md +0 -0
  123. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/finish-mutation-hardening-across-core.md +0 -0
  124. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/fix-self-update-version-detection-dead-branch.md +0 -0
  125. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/fork-safe-network-proxy-handler.md +0 -0
  126. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/frontmatter-scalar-over-coercion.md +0 -0
  127. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/github-pages-deploy-is-broken-every-push.md +0 -0
  128. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/hash-pinned-reproducible-toolchain-uv-lock.md +0 -0
  129. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/html-validation-html-validate.md +0 -0
  130. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/integrity-verification-boost-verify.md +0 -0
  131. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/invocation-pid-logging-for-crash-correlation.md +0 -0
  132. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/layering-guard-import-linter.md +0 -0
  133. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/lighthouse-ci-on-the-pages-site.md +0 -0
  134. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/localize-the-stored-bm25-snippet.md +0 -0
  135. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/lockfile-enforcement-and-commit-pinning.md +0 -0
  136. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/log-timestamps-mislabeled-utc.md +0 -0
  137. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/lowest-version-resolution-uv-resolution-lowest-d.md +0 -0
  138. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/make-the-roadmaps-discoverable.md +0 -0
  139. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/markdown-consistency-markdownlint-cli2.md +0 -0
  140. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/mcp-launch-objc-fork-safety.md +0 -0
  141. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/memoize-config-load-in-process.md +0 -0
  142. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/modernization-smells-refurb-pyupgrade.md +0 -0
  143. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/mutation-hardening-core-frontmatter-py.md +0 -0
  144. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/mutation-hardening-core-gitutil-py.md +0 -0
  145. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/mutation-hardening-core-store-py.md +0 -0
  146. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/one-command-every-env-nox.md +0 -0
  147. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/one-shared-atomic-write-helper.md +0 -0
  148. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/package-metadata-validation-twine-check-friends.md +0 -0
  149. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/patch-coverage-gate-diff-cover.md +0 -0
  150. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/performance-regression-gate-pytest-benchmark.md +0 -0
  151. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/post-deploy-smoke-headless-load-check.md +0 -0
  152. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/pre-release-python-canary-3-14t-free-threaded.md +0 -0
  153. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/promote-nav-footer-into-the-shared-style-system.md +0 -0
  154. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/prompt-injection-scanning-of-skill-markdown.md +0 -0
  155. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/property-based-tests-hypothesis-on-the-parsers.md +0 -0
  156. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/prose-and-terminology-linting-vale.md +0 -0
  157. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/prune-ignored-dirs-during-scan-dir-walk.md +0 -0
  158. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/quality-dashboard-sonarcloud-free-for-oss.md +0 -0
  159. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/reconcile-the-theme-drift.md +0 -0
  160. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/refresh-the-marketing-surface.md +0 -0
  161. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/reuse-helpers-kill-minor-dead-work.md +0 -0
  162. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/robust-tag-argument-parsing.md +0 -0
  163. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/rule-install-native-materialization.md +0 -0
  164. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/runner-egress-monitoring-stepsecurity-harden-run.md +0 -0
  165. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/sbom-aware-scanning-osv-scanner.md +0 -0
  166. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/sbom-on-every-release-cyclonedx-syft.md +0 -0
  167. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/second-type-checker-pyright.md +0 -0
  168. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/secret-and-pii-scanning-of-installed-skills.md +0 -0
  169. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/secret-scanning-gitleaks-push-protection.md +0 -0
  170. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/security-linting-bandit-via-ruff-s-rules.md +0 -0
  171. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/shift-left-gate-pre-commit-pre-commit-ci.md +0 -0
  172. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/single-imperative-rule-extractor.md +0 -0
  173. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/single-tech-stack-prober.md +0 -0
  174. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/split-oversized-command-modules.md +0 -0
  175. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/startup-and-import-time-budget-x-importtime.md +0 -0
  176. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/stop-re-serializing-entry-meta-on-every-search.md +0 -0
  177. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/supply-chain-posture-openssf-scorecard.md +0 -0
  178. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/tap-signing-and-provenance-sigstore-minisign.md +0 -0
  179. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/theme-asset-linting-stylelint-eslint.md +0 -0
  180. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/typo-detection-codespell.md +0 -0
  181. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/typosquat-and-name-confusion-detection.md +0 -0
  182. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/unify-tilde-two-copies-have-a-boundary-bug.md +0 -0
  183. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/untrack-generated-build-noise.md +0 -0
  184. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/update-diff-before-apply.md +0 -0
  185. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/visual-regression-pass-on-the-guide.md +0 -0
  186. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/widen-the-ruff-rule-surface-b-sim-c4-perf-ruf.md +0 -0
  187. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/windows-in-the-ci-matrix.md +0 -0
  188. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/workflow-linting-actionlint.md +0 -0
  189. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/workflow-sast-zizmor.md +0 -0
  190. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/workspace-scope-install.md +0 -0
  191. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/index.html +0 -0
  192. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/pyproject.toml +0 -0
  193. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/scripts/build_registries.py +0 -0
  194. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/scripts/build_roadmap.py +0 -0
  195. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/scripts/mutation_gate.py +0 -0
  196. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/setup.cfg +0 -0
  197. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/style/README.md +0 -0
  198. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/style/boost.css +0 -0
  199. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/style/boost.js +0 -0
  200. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/style/demo.html +0 -0
  201. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/conftest.py +0 -0
  202. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/functional/test_cli_bmad.py +0 -0
  203. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/functional/test_cli_discovery.py +0 -0
  204. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/functional/test_cli_hooks.py +0 -0
  205. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/functional/test_cli_info.py +0 -0
  206. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/functional/test_cli_intelligence.py +0 -0
  207. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/functional/test_cli_pkg.py +0 -0
  208. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/functional/test_cli_quality.py +0 -0
  209. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/functional/test_cli_taps.py +0 -0
  210. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/functional/test_cli_team.py +0 -0
  211. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/functional/test_everyday_loop.py +0 -0
  212. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/make_fixture.py +0 -0
  213. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/smoke.sh +0 -0
  214. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_agents.py +0 -0
  215. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_ai.py +0 -0
  216. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_catalog.py +0 -0
  217. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_claude_settings.py +0 -0
  218. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_cliparse.py +0 -0
  219. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_config.py +0 -0
  220. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_dense.py +0 -0
  221. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_dense_fallback.py +0 -0
  222. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_embed.py +0 -0
  223. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_errors_and_cli_table.py +0 -0
  224. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_frontmatter.py +0 -0
  225. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_gitutil.py +0 -0
  226. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_journal.py +0 -0
  227. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_lockfile.py +0 -0
  228. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_logs.py +0 -0
  229. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_mutation_hardening.py +0 -0
  230. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_output.py +0 -0
  231. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_paths.py +0 -0
  232. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_pkg_confusions.py +0 -0
  233. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_policy.py +0 -0
  234. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_rag.py +0 -0
  235. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_registries_fresh.py +0 -0
  236. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_registry.py +0 -0
  237. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_roadmap_fresh.py +0 -0
  238. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_spin.py +0 -0
  239. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_staleness.py +0 -0
  240. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_store.py +0 -0
  241. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_token_parity.py +0 -0
  242. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_typosquat.py +0 -0
  243. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_util.py +0 -0
  244. {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boost-skill-cli
3
- Version: 1.0.93
3
+ Version: 1.0.95
4
4
  Summary: boost — Homebrew for AI coding skills
5
5
  Author: Jonathan Reyes
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.0.93'
22
- __version_tuple__ = version_tuple = (1, 0, 93)
21
+ __version__ = version = '1.0.95'
22
+ __version_tuple__ = version_tuple = (1, 0, 95)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -2,9 +2,7 @@
2
2
  completions, schedule, serve, mcp, self-update."""
3
3
  from __future__ import annotations
4
4
 
5
- import errno
6
5
  import getpass
7
- import html
8
6
  import json
9
7
  import os
10
8
  import re
@@ -12,15 +10,13 @@ import shutil
12
10
  import subprocess
13
11
  import sys
14
12
  import time
15
- import urllib.parse
16
13
  from datetime import datetime, timedelta
17
- from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
18
14
  from pathlib import Path
19
15
 
20
16
  from .. import cliparse
21
17
  from .. import __version__
22
18
  from ..core import agents, catalog, config, frontmatter, gitutil, journal
23
- from ..core import lockfile, mcp, paths, policy, rag, registry, store, util
19
+ from ..core import lockfile, mcp, paths, policy, rag, registry, serve, store, util
24
20
  from ..core import output as out
25
21
  from ..errors import BoostError
26
22
 
@@ -730,106 +726,8 @@ def cmd_schedule(argv) -> int:
730
726
 
731
727
  # ---------------------------------------------------------------- serve
732
728
 
733
- _PAGE_CSS = ("body{background:#111;color:#ddd;font-family:ui-monospace,SFMono-Regular,"
734
- "Menlo,monospace;max-width:720px;margin:2rem auto;padding:0 1rem}"
735
- "h1{color:#fff;font-size:1.3rem}a{color:#7dc4ff;text-decoration:none}"
736
- "a:hover{text-decoration:underline}table{border-collapse:collapse;width:100%}"
737
- "th,td{text-align:left;padding:.3rem .8rem .3rem 0;"
738
- "border-bottom:1px solid #2a2a2a}.dim{color:#777}")
739
-
740
- _SKILL_NAME_RE = re.compile(r"^[A-Za-z0-9._-]+$")
741
-
742
-
743
- def _serve_page() -> str:
744
- installed = lockfile.installed()
745
- entries = catalog.all_entries()
746
- taps = registry.list_taps()
747
- rows = "".join(
748
- '<tr><td><a href="/skill/%s">%s</a></td><td>%s</td><td>%s</td></tr>'
749
- % (urllib.parse.quote(n), html.escape(n),
750
- html.escape(str(e.get("version", "?"))),
751
- html.escape(str(e.get("tap", "?"))))
752
- for n, e in sorted(installed.items()))
753
- return ("<!doctype html><html><head><meta charset='utf-8'>"
754
- "<title>boost — skill catalog</title><style>%s</style></head><body>"
755
- "<h1>⚡ boost <span class='dim'>v%s</span></h1>"
756
- "<p class='dim'>%d installed · %d available across %d taps</p>"
757
- "<table><tr><th>skill</th><th>version</th><th>tap</th></tr>%s</table>"
758
- "<p><a href='/installed.json'>installed.json</a> · "
759
- "<a href='/catalog.json'>catalog.json</a></p></body></html>"
760
- % (_PAGE_CSS, __version__, len(installed), len(entries), len(taps),
761
- rows or "<tr><td colspan='3' class='dim'>nothing installed</td></tr>"))
762
-
763
-
764
- def _skill_text(name: str):
765
- """SKILL.md text for an installed skill, else from a tap. None if unknown."""
766
- if not _SKILL_NAME_RE.fullmatch(name):
767
- return None
768
- fp = store.skill_store_dir(name) / "SKILL.md"
769
- if fp.is_file():
770
- return fp.read_text(encoding="utf-8", errors="replace")
771
- for e in catalog.find(name):
772
- try:
773
- fp = registry.get(e["tap"]).path / e["skill_md"]
774
- except BoostError:
775
- continue
776
- if fp.is_file():
777
- return fp.read_text(encoding="utf-8", errors="replace")
778
- return None
779
-
780
-
781
- class _CatalogHandler(BaseHTTPRequestHandler):
782
- server_version = "boost/" + __version__
783
-
784
- def log_message(self, fmt, *args): # logged in _send instead
785
- pass
786
-
787
- def _send(self, status: int, ctype: str, body: bytes) -> None:
788
- self.send_response(status)
789
- self.send_header("Content-Type", ctype)
790
- self.send_header("Content-Length", str(len(body)))
791
- self.end_headers()
792
- self.wfile.write(body)
793
- out.dim(" %s %s → %d" % (self.command, self.path, status))
794
-
795
- def do_GET(self):
796
- path = urllib.parse.unquote(self.path.split("?", 1)[0])
797
- try:
798
- if path in ("/", "/index.html"):
799
- self._send(200, "text/html; charset=utf-8", _serve_page().encode())
800
- elif path == "/catalog.json":
801
- self._send(200, "application/json",
802
- json.dumps(catalog.all_entries(), indent=2).encode())
803
- elif path == "/installed.json":
804
- self._send(200, "application/json",
805
- json.dumps(lockfile.read(), indent=2).encode())
806
- elif path.startswith("/skill/"):
807
- name = path[len("/skill/"):].strip("/")
808
- if not _SKILL_NAME_RE.fullmatch(name):
809
- self._send(404, "application/json",
810
- json.dumps({"error": "no skill named %r" % name}).encode())
811
- return
812
- text = _skill_text(name)
813
- if text is None:
814
- self._send(404, "application/json",
815
- json.dumps({"error": "no skill named %r" % name}).encode())
816
- else:
817
- self._send(200, "text/plain; charset=utf-8", text.encode())
818
- else:
819
- self._send(404, "application/json",
820
- json.dumps({"error": "not found"}).encode())
821
- except BrokenPipeError:
822
- pass
823
- except Exception as e: # noqa: BLE001 — keep the server alive
824
- try:
825
- self._send(500, "application/json",
826
- json.dumps({"error": str(e)}).encode())
827
- except Exception:
828
- pass
829
-
830
-
831
729
  def cmd_serve(argv) -> int:
832
- """boost serve [--port N] [--host H]"""
730
+ """boost serve [--port N] [--host H] — thin wrapper over core.serve."""
833
731
  p = cliparse.parser(
834
732
  prog="boost serve",
835
733
  description="Serve the skill catalog over HTTP (port 8787)")
@@ -839,24 +737,7 @@ def cmd_serve(argv) -> int:
839
737
  p.add_argument("--host", default="127.0.0.1",
840
738
  help="address to bind (default: 127.0.0.1)")
841
739
  args = p.parse_args(argv)
842
-
843
- try:
844
- httpd = ThreadingHTTPServer((args.host, args.port), _CatalogHandler)
845
- except OSError as e:
846
- if e.errno == errno.EADDRINUSE:
847
- raise BoostError("port %d is already in use" % args.port,
848
- hint="pick another with --port")
849
- raise BoostError("cannot bind %s:%d — %s" % (args.host, args.port, e),
850
- hint="check --host and --port")
851
- out.info("⚡ serving skill catalog on http://%s:%d %s"
852
- % (args.host, args.port, out.c("(ctrl-c to stop)", out.DIM)))
853
- try:
854
- httpd.serve_forever()
855
- except KeyboardInterrupt:
856
- print()
857
- out.ok("server stopped")
858
- finally:
859
- httpd.server_close()
740
+ serve.serve_http(args.host, args.port)
860
741
  return 0
861
742
 
862
743
 
@@ -1021,70 +902,13 @@ _MCP_TOOLS = REGISTRY.specs()
1021
902
 
1022
903
 
1023
904
  def _mcp_tool(tool: str, args: dict):
1024
- """Run one MCP tool -> (text, is_error). (None, _) for unknown tools."""
1025
- return REGISTRY.call(tool, args)
905
+ """Run one MCP tool -> (text, is_error). (None, _) for unknown tools.
1026
906
 
1027
-
1028
- def _mcp_send(msg: dict) -> bool:
1029
- try:
1030
- sys.stdout.write(json.dumps(msg) + "\n")
1031
- sys.stdout.flush()
1032
- return True
1033
- except (BrokenPipeError, OSError):
1034
- return False
1035
-
1036
-
1037
- def _mcp_serve_stdio() -> int:
1038
- """Newline-delimited JSON-RPC 2.0 MCP server on stdin/stdout."""
1039
- while True:
1040
- try:
1041
- line = sys.stdin.readline()
1042
- except (KeyboardInterrupt, OSError):
1043
- return 0
1044
- if not line: # EOF
1045
- return 0
1046
- line = line.strip()
1047
- if not line:
1048
- continue
1049
- try:
1050
- req = json.loads(line)
1051
- except json.JSONDecodeError:
1052
- if not _mcp_send({"jsonrpc": "2.0", "id": None,
1053
- "error": {"code": -32700, "message": "parse error"}}):
1054
- return 0
1055
- continue
1056
- method = str(req.get("method", ""))
1057
- if "id" not in req: # notification (e.g. notifications/initialized)
1058
- continue
1059
- resp = {"jsonrpc": "2.0", "id": req.get("id")}
1060
- if method == "initialize":
1061
- resp["result"] = {"protocolVersion": "2024-11-05",
1062
- "capabilities": {"tools": {}},
1063
- "serverInfo": {"name": "boost", "version": __version__}}
1064
- elif method == "ping":
1065
- resp["result"] = {}
1066
- elif method == "tools/list":
1067
- resp["result"] = {"tools": _MCP_TOOLS}
1068
- elif method == "tools/call":
1069
- params = req.get("params") or {}
1070
- tool = str(params.get("name", ""))
1071
- try:
1072
- text, is_err = _mcp_tool(tool, params.get("arguments") or {})
1073
- except BoostError as e:
1074
- text = "Error: %s" % e.message + ("\nhint: %s" % e.hint if e.hint else "")
1075
- is_err = True
1076
- except Exception as e: # noqa: BLE001 — server must not die
1077
- text, is_err = "Error: %s" % e, True
1078
- if text is None:
1079
- resp["error"] = {"code": -32602, "message": "unknown tool %r" % tool}
1080
- else:
1081
- resp["result"] = {"content": [{"type": "text", "text": text}]}
1082
- if is_err:
1083
- resp["result"]["isError"] = True
1084
- else:
1085
- resp["error"] = {"code": -32601, "message": "method not found: %s" % method}
1086
- if not _mcp_send(resp):
1087
- return 0
907
+ Thin shim over ``REGISTRY.call`` kept for the functional tests; the JSON-RPC
908
+ protocol itself now lives in ``core/mcp.py`` (``handle_request`` /
909
+ ``serve_stdio``).
910
+ """
911
+ return REGISTRY.call(tool, args)
1088
912
 
1089
913
 
1090
914
  def cmd_mcp(argv) -> int:
@@ -1100,7 +924,7 @@ def cmd_mcp(argv) -> int:
1100
924
  args = p.parse_args(argv)
1101
925
 
1102
926
  if args.stdio:
1103
- return _mcp_serve_stdio()
927
+ return mcp.serve_stdio(REGISTRY, version=__version__)
1104
928
 
1105
929
  shim = paths.launcher()
1106
930
  if args.action == "register":
@@ -0,0 +1,165 @@
1
+ """Extensible MCP tool registry — the Phase-3 "MCP as a hub" seam.
2
+
3
+ An MCP tool is a *handler* — ``fn(args: dict) -> (text, is_error)`` — paired with
4
+ its JSON spec (``name`` / ``description`` / ``inputSchema``). Instead of a flat
5
+ ``if/elif`` dispatcher, tools self-register on a :class:`Registry`; the JSON-RPC
6
+ server iterates it for both ``tools/list`` and ``tools/call``. Adding a
7
+ capability (a GitHub reach-out, a database query, some future dependency) is one
8
+ ``register()`` call — no dispatcher edits, no server changes.
9
+
10
+ Handlers return ``(text, is_error)``. A handler that returns ``text is None``
11
+ signals "I did not produce a result"; :meth:`Registry.call` also returns
12
+ ``(None, False)`` for an unknown tool, so the server can answer with a JSON-RPC
13
+ ``unknown tool`` error in exactly one place. Reach-out handlers degrade the way
14
+ ``core/ai.py`` does: probe for the dependency, and when it is absent return a
15
+ short helpful message rather than raising.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import sys
21
+ from typing import Callable, Dict, List, Optional, Tuple
22
+
23
+ from ..errors import BoostError
24
+
25
+ # A handler maps parsed arguments to (text, is_error). ``text is None`` means the
26
+ # handler produced no result (treated by the server as an unknown/aborted tool).
27
+ Handler = Callable[[dict], Tuple[Optional[str], bool]]
28
+
29
+
30
+ class Registry:
31
+ """An ordered name -> (spec, handler) map for MCP tools."""
32
+
33
+ def __init__(self) -> None:
34
+ self._order: List[str] = []
35
+ self._specs: Dict[str, dict] = {}
36
+ self._handlers: Dict[str, Handler] = {}
37
+
38
+ def register(self, name: str, description: str, input_schema: dict,
39
+ handler: Handler) -> None:
40
+ """Register one tool. Raises on an empty or duplicate name."""
41
+ if not name:
42
+ raise ValueError("MCP tool name must be non-empty")
43
+ if name in self._handlers:
44
+ raise ValueError("duplicate MCP tool %r" % name)
45
+ self._order.append(name)
46
+ self._specs[name] = {"name": name, "description": description,
47
+ "inputSchema": input_schema}
48
+ self._handlers[name] = handler
49
+
50
+ def tool(self, name: str, description: str,
51
+ input_schema: dict) -> Callable[[Handler], Handler]:
52
+ """Decorator form of :meth:`register`; returns the handler unchanged."""
53
+ def deco(fn: Handler) -> Handler:
54
+ self.register(name, description, input_schema, fn)
55
+ return fn
56
+ return deco
57
+
58
+ def specs(self) -> List[dict]:
59
+ """The ``tools/list`` payload — specs in registration order."""
60
+ return [self._specs[name] for name in self._order]
61
+
62
+ def names(self) -> List[str]:
63
+ """Registered tool names, in registration order."""
64
+ return list(self._order)
65
+
66
+ def has(self, name: str) -> bool:
67
+ return name in self._handlers
68
+
69
+ def call(self, name: str, args: dict) -> Tuple[Optional[str], bool]:
70
+ """Dispatch to a handler. ``(None, False)`` for an unknown tool."""
71
+ handler = self._handlers.get(name)
72
+ if handler is None:
73
+ return None, False
74
+ return handler(args)
75
+
76
+
77
+ # ── JSON-RPC 2.0 protocol ─────────────────────────────────────────────────
78
+ # The wire protocol Claude Code speaks to `boost mcp --stdio`. handle_request is
79
+ # a *pure* request→response mapping (no I/O), so the whole protocol is unit
80
+ # testable; serve_stdio only adds the newline-delimited stdin/stdout loop.
81
+ PROTOCOL_VERSION = "2024-11-05"
82
+
83
+
84
+ def handle_request(req: dict, *, version: str,
85
+ registry: "Registry") -> Optional[dict]:
86
+ """Map one parsed JSON-RPC request to its response dict.
87
+
88
+ Returns ``None`` for a notification (a request with no ``id``) — the caller
89
+ sends nothing back. Handlers that raise during ``tools/call`` are turned into
90
+ an error *result* (``isError``) rather than a protocol error, so a failing
91
+ tool never kills the session.
92
+ """
93
+ method = str(req.get("method", ""))
94
+ if "id" not in req: # notification (e.g. notifications/initialized)
95
+ return None
96
+ resp: dict = {"jsonrpc": "2.0", "id": req.get("id")}
97
+ if method == "initialize":
98
+ resp["result"] = {"protocolVersion": PROTOCOL_VERSION,
99
+ "capabilities": {"tools": {}},
100
+ "serverInfo": {"name": "boost", "version": version}}
101
+ elif method == "ping":
102
+ resp["result"] = {}
103
+ elif method == "tools/list":
104
+ resp["result"] = {"tools": registry.specs()}
105
+ elif method == "tools/call":
106
+ params = req.get("params") or {}
107
+ tool = str(params.get("name", ""))
108
+ try:
109
+ text, is_err = registry.call(tool, params.get("arguments") or {})
110
+ except BoostError as e:
111
+ text = "Error: %s" % e.message + ("\nhint: %s" % e.hint if e.hint else "")
112
+ is_err = True
113
+ except Exception as e: # noqa: BLE001 — server must not die
114
+ text, is_err = "Error: %s" % e, True
115
+ if text is None:
116
+ resp["error"] = {"code": -32602, "message": "unknown tool %r" % tool}
117
+ else:
118
+ resp["result"] = {"content": [{"type": "text", "text": text}]}
119
+ if is_err:
120
+ resp["result"]["isError"] = True
121
+ else:
122
+ resp["error"] = {"code": -32601, "message": "method not found: %s" % method}
123
+ return resp
124
+
125
+
126
+ def serve_stdio(registry: "Registry", *, version: str,
127
+ stdin=None, stdout=None) -> int:
128
+ """Newline-delimited JSON-RPC 2.0 MCP server on stdin/stdout.
129
+
130
+ ``stdin``/``stdout`` default to the process streams but can be injected
131
+ (e.g. ``io.StringIO``) so the loop is testable end to end.
132
+ """
133
+ stdin = stdin if stdin is not None else sys.stdin
134
+ stdout = stdout if stdout is not None else sys.stdout
135
+
136
+ def send(msg: dict) -> bool:
137
+ try:
138
+ stdout.write(json.dumps(msg) + "\n")
139
+ stdout.flush()
140
+ return True
141
+ except (BrokenPipeError, OSError):
142
+ return False
143
+
144
+ while True:
145
+ try:
146
+ line = stdin.readline()
147
+ except (KeyboardInterrupt, OSError):
148
+ return 0
149
+ if not line: # EOF
150
+ return 0
151
+ line = line.strip()
152
+ if not line:
153
+ continue
154
+ try:
155
+ req = json.loads(line)
156
+ except json.JSONDecodeError:
157
+ if not send({"jsonrpc": "2.0", "id": None,
158
+ "error": {"code": -32700, "message": "parse error"}}):
159
+ return 0
160
+ continue
161
+ resp = handle_request(req, version=version, registry=registry)
162
+ if resp is None: # notification — nothing to send
163
+ continue
164
+ if not send(resp):
165
+ return 0
@@ -0,0 +1,151 @@
1
+ """The `boost serve` HTTP catalog server — a small read-only view of the
2
+ installed skills plus JSON endpoints, extracted from the command layer.
3
+
4
+ The routing is a pure function (:func:`route`) that maps a request path to a
5
+ ``(status, content_type, body)`` triple with no socket bound, so the whole
6
+ endpoint surface is unit-testable. :func:`serve_http` wires it into a threaded
7
+ stdlib HTTP server for the CLI.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import errno
12
+ import html
13
+ import json
14
+ import re
15
+ import urllib.parse
16
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
17
+ from typing import Optional, Tuple
18
+
19
+ from .. import __version__
20
+ from ..errors import BoostError
21
+ from . import catalog, lockfile, registry, store
22
+ from . import output as out
23
+
24
+ SKILL_NAME_RE = re.compile(r"^[A-Za-z0-9._-]+$")
25
+
26
+ _PAGE_CSS = ("body{background:#111;color:#ddd;font-family:ui-monospace,SFMono-Regular,"
27
+ "Menlo,monospace;max-width:720px;margin:2rem auto;padding:0 1rem}"
28
+ "h1{color:#fff;font-size:1.3rem}a{color:#7dc4ff;text-decoration:none}"
29
+ "a:hover{text-decoration:underline}table{border-collapse:collapse;width:100%}"
30
+ "th,td{text-align:left;padding:.3rem .8rem .3rem 0;"
31
+ "border-bottom:1px solid #2a2a2a}.dim{color:#777}")
32
+
33
+
34
+ def serve_page() -> str:
35
+ """The HTML index: a table of installed skills with JSON-endpoint links."""
36
+ installed = lockfile.installed()
37
+ entries = catalog.all_entries()
38
+ taps = registry.list_taps()
39
+ rows = "".join(
40
+ '<tr><td><a href="/skill/%s">%s</a></td><td>%s</td><td>%s</td></tr>'
41
+ % (urllib.parse.quote(n), html.escape(n),
42
+ html.escape(str(e.get("version", "?"))),
43
+ html.escape(str(e.get("tap", "?"))))
44
+ for n, e in sorted(installed.items()))
45
+ return ("<!doctype html><html><head><meta charset='utf-8'>"
46
+ "<title>boost — skill catalog</title><style>%s</style></head><body>"
47
+ "<h1>⚡ boost <span class='dim'>v%s</span></h1>"
48
+ "<p class='dim'>%d installed · %d available across %d taps</p>"
49
+ "<table><tr><th>skill</th><th>version</th><th>tap</th></tr>%s</table>"
50
+ "<p><a href='/installed.json'>installed.json</a> · "
51
+ "<a href='/catalog.json'>catalog.json</a></p></body></html>"
52
+ % (_PAGE_CSS, __version__, len(installed), len(entries), len(taps),
53
+ rows or "<tr><td colspan='3' class='dim'>nothing installed</td></tr>"))
54
+
55
+
56
+ def skill_text(name: str) -> Optional[str]:
57
+ """SKILL.md text for an installed skill, else from a tap. None if unknown."""
58
+ if not SKILL_NAME_RE.fullmatch(name):
59
+ return None
60
+ fp = store.skill_store_dir(name) / "SKILL.md"
61
+ if fp.is_file():
62
+ return fp.read_text(encoding="utf-8", errors="replace")
63
+ for e in catalog.find(name):
64
+ try:
65
+ fp = registry.get(e["tap"]).path / e["skill_md"]
66
+ except BoostError:
67
+ continue
68
+ if fp.is_file():
69
+ return fp.read_text(encoding="utf-8", errors="replace")
70
+ return None
71
+
72
+
73
+ def _json_body(obj) -> bytes:
74
+ return json.dumps(obj, indent=2).encode()
75
+
76
+
77
+ def route(path: str) -> Tuple[int, str, bytes]:
78
+ """Map a GET path to ``(status, content_type, body)``. Pure — no socket.
79
+
80
+ Mirrors the historical dispatch exactly: ``/`` and ``/index.html`` serve the
81
+ HTML page; ``/catalog.json`` and ``/installed.json`` the JSON views; a
82
+ ``/skill/<name>`` path the raw SKILL.md (404 for an invalid or unknown name);
83
+ anything else a JSON ``not found``.
84
+ """
85
+ path = urllib.parse.unquote(path.split("?", 1)[0])
86
+ if path in ("/", "/index.html"):
87
+ return 200, "text/html; charset=utf-8", serve_page().encode()
88
+ if path == "/catalog.json":
89
+ return 200, "application/json", _json_body(catalog.all_entries())
90
+ if path == "/installed.json":
91
+ return 200, "application/json", _json_body(lockfile.read())
92
+ if path.startswith("/skill/"):
93
+ name = path[len("/skill/"):].strip("/")
94
+ if not SKILL_NAME_RE.fullmatch(name):
95
+ return (404, "application/json",
96
+ json.dumps({"error": "no skill named %r" % name}).encode())
97
+ text = skill_text(name)
98
+ if text is None:
99
+ return (404, "application/json",
100
+ json.dumps({"error": "no skill named %r" % name}).encode())
101
+ return 200, "text/plain; charset=utf-8", text.encode()
102
+ return 404, "application/json", json.dumps({"error": "not found"}).encode()
103
+
104
+
105
+ class _CatalogHandler(BaseHTTPRequestHandler):
106
+ server_version = "boost/" + __version__
107
+
108
+ def log_message(self, fmt, *args): # logged in _send instead
109
+ pass
110
+
111
+ def _send(self, status: int, ctype: str, body: bytes) -> None:
112
+ self.send_response(status)
113
+ self.send_header("Content-Type", ctype)
114
+ self.send_header("Content-Length", str(len(body)))
115
+ self.end_headers()
116
+ self.wfile.write(body)
117
+ out.dim(" %s %s → %d" % (self.command, self.path, status))
118
+
119
+ def do_GET(self):
120
+ try:
121
+ status, ctype, body = route(self.path)
122
+ self._send(status, ctype, body)
123
+ except BrokenPipeError:
124
+ pass
125
+ except Exception as e: # noqa: BLE001 — keep the server alive
126
+ try:
127
+ self._send(500, "application/json",
128
+ json.dumps({"error": str(e)}).encode())
129
+ except Exception:
130
+ pass
131
+
132
+
133
+ def serve_http(host: str, port: int) -> None:
134
+ """Bind and run the catalog server until interrupted (blocks)."""
135
+ try:
136
+ httpd = ThreadingHTTPServer((host, port), _CatalogHandler)
137
+ except OSError as e:
138
+ if e.errno == errno.EADDRINUSE:
139
+ raise BoostError("port %d is already in use" % port,
140
+ hint="pick another with --port")
141
+ raise BoostError("cannot bind %s:%d — %s" % (host, port, e),
142
+ hint="check --host and --port")
143
+ out.info("⚡ serving skill catalog on http://%s:%d %s"
144
+ % (host, port, out.c("(ctrl-c to stop)", out.DIM)))
145
+ try:
146
+ httpd.serve_forever()
147
+ except KeyboardInterrupt:
148
+ print()
149
+ out.ok("server stopped")
150
+ finally:
151
+ httpd.server_close()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boost-skill-cli
3
- Version: 1.0.93
3
+ Version: 1.0.95
4
4
  Summary: boost — Homebrew for AI coding skills
5
5
  Author: Jonathan Reyes
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -48,6 +48,7 @@ boost_cli/core/paths.py
48
48
  boost_cli/core/policy.py
49
49
  boost_cli/core/rag.py
50
50
  boost_cli/core/registry.py
51
+ boost_cli/core/serve.py
51
52
  boost_cli/core/staleness.py
52
53
  boost_cli/core/store.py
53
54
  boost_cli/core/typosquat.py
@@ -229,6 +230,7 @@ tests/unit/test_rag.py
229
230
  tests/unit/test_registries_fresh.py
230
231
  tests/unit/test_registry.py
231
232
  tests/unit/test_roadmap_fresh.py
233
+ tests/unit/test_serve.py
232
234
  tests/unit/test_spin.py
233
235
  tests/unit/test_staleness.py
234
236
  tests/unit/test_store.py
@@ -0,0 +1,26 @@
1
+ ---
2
+ id: extract-mcp-http-servers-out-of-configuration-py
3
+ board: code
4
+ section: shipped
5
+ status: shipped
6
+ category: Tech-debt
7
+ complexity: L
8
+ impact: High
9
+ wow: 4
10
+ note: two servers now testable in core
11
+ order: 5
12
+ owner: loop/extract-serve-mcp
13
+ pr: 106
14
+ title: Extract MCP + HTTP servers out of <code>configuration.py</code>
15
+ ---
16
+ ~200 lines implementing a full HTTP catalog server <b>and</b> a JSON-RPC 2.0 MCP
17
+ server lived inside the "configuration" command module — nearly untestable in
18
+ place. The JSON-RPC protocol now lives in <code>core/mcp.py</code> as a <em>pure</em>
19
+ <code>handle_request(req, *, version, registry)</code> plus a
20
+ <code>serve_stdio()</code> loop with injectable stdin/stdout; the HTTP server
21
+ moved to a new <code>core/serve.py</code> with a pure <code>route()</code>
22
+ function, <code>serve_page()</code>/<code>skill_text()</code>, and
23
+ <code>serve_http()</code>. <code>cmd_serve</code> and <code>cmd_mcp --stdio</code>
24
+ became thin wrappers. Behavior is preserved exactly (the full-server and
25
+ stdio-protocol functional tests pass unchanged) and the extracted core is pinned
26
+ by new unit tests covering every branch.