tool-compass 2.4.0__tar.gz → 2.5.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 (49) hide show
  1. {tool_compass-2.4.0 → tool_compass-2.5.0}/.env.example +9 -8
  2. {tool_compass-2.4.0 → tool_compass-2.5.0}/.gitignore +8 -0
  3. {tool_compass-2.4.0 → tool_compass-2.5.0}/CHANGELOG.md +80 -0
  4. {tool_compass-2.4.0 → tool_compass-2.5.0}/Dockerfile +21 -7
  5. {tool_compass-2.4.0 → tool_compass-2.5.0}/PKG-INFO +31 -21
  6. {tool_compass-2.4.0 → tool_compass-2.5.0}/README.md +30 -20
  7. {tool_compass-2.4.0 → tool_compass-2.5.0}/analytics.py +138 -20
  8. {tool_compass-2.4.0 → tool_compass-2.5.0}/backend_client_simple.py +499 -7
  9. {tool_compass-2.4.0 → tool_compass-2.5.0}/cli.py +302 -15
  10. {tool_compass-2.4.0 → tool_compass-2.5.0}/config.py +229 -0
  11. tool_compass-2.5.0/docs/index.md +28 -0
  12. {tool_compass-2.4.0 → tool_compass-2.5.0}/fly.toml +6 -0
  13. {tool_compass-2.4.0 → tool_compass-2.5.0}/gateway.py +692 -13
  14. {tool_compass-2.4.0 → tool_compass-2.5.0}/indexer.py +138 -18
  15. {tool_compass-2.4.0 → tool_compass-2.5.0}/llms.txt +5 -3
  16. {tool_compass-2.4.0 → tool_compass-2.5.0}/pyproject.toml +1 -1
  17. {tool_compass-2.4.0 → tool_compass-2.5.0}/requirements.txt +6 -0
  18. tool_compass-2.5.0/scripts/sync-version.mjs +136 -0
  19. {tool_compass-2.4.0 → tool_compass-2.5.0}/sync_manager.py +175 -82
  20. {tool_compass-2.4.0 → tool_compass-2.5.0}/tool_manifest.py +7 -0
  21. {tool_compass-2.4.0 → tool_compass-2.5.0}/ui.py +66 -37
  22. tool_compass-2.4.0/docs/index.md +0 -52
  23. {tool_compass-2.4.0 → tool_compass-2.5.0}/.dockerignore +0 -0
  24. {tool_compass-2.4.0 → tool_compass-2.5.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  25. {tool_compass-2.4.0 → tool_compass-2.5.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  26. {tool_compass-2.4.0 → tool_compass-2.5.0}/.github/dependabot.yml +0 -0
  27. {tool_compass-2.4.0 → tool_compass-2.5.0}/.github/workflows/ci.yml +0 -0
  28. {tool_compass-2.4.0 → tool_compass-2.5.0}/.github/workflows/publish.yml +0 -0
  29. {tool_compass-2.4.0 → tool_compass-2.5.0}/.github/workflows/release-binaries.yml +0 -0
  30. {tool_compass-2.4.0 → tool_compass-2.5.0}/.github/workflows/release.yml +0 -0
  31. {tool_compass-2.4.0 → tool_compass-2.5.0}/.pre-commit-config.yaml +0 -0
  32. {tool_compass-2.4.0 → tool_compass-2.5.0}/CODE_OF_CONDUCT.md +0 -0
  33. {tool_compass-2.4.0 → tool_compass-2.5.0}/CONTRIBUTING.md +0 -0
  34. {tool_compass-2.4.0 → tool_compass-2.5.0}/LICENSE +0 -0
  35. {tool_compass-2.4.0 → tool_compass-2.5.0}/Makefile +0 -0
  36. {tool_compass-2.4.0 → tool_compass-2.5.0}/SCORECARD.md +0 -0
  37. {tool_compass-2.4.0 → tool_compass-2.5.0}/SECURITY.md +0 -0
  38. {tool_compass-2.4.0 → tool_compass-2.5.0}/SHIP_GATE.md +0 -0
  39. {tool_compass-2.4.0 → tool_compass-2.5.0}/_version.py +0 -0
  40. {tool_compass-2.4.0 → tool_compass-2.5.0}/backend_client_mcp.py +0 -0
  41. {tool_compass-2.4.0 → tool_compass-2.5.0}/bootstrap.py +0 -0
  42. {tool_compass-2.4.0 → tool_compass-2.5.0}/chain_indexer.py +0 -0
  43. {tool_compass-2.4.0 → tool_compass-2.5.0}/compass_config.example.json +0 -0
  44. {tool_compass-2.4.0 → tool_compass-2.5.0}/docker-compose.yml +0 -0
  45. {tool_compass-2.4.0 → tool_compass-2.5.0}/embedder.py +0 -0
  46. {tool_compass-2.4.0 → tool_compass-2.5.0}/requirements-dev.txt +0 -0
  47. {tool_compass-2.4.0 → tool_compass-2.5.0}/scripts/check-org-urls.sh +0 -0
  48. {tool_compass-2.4.0 → tool_compass-2.5.0}/scripts/regenerate-scorecard.sh +0 -0
  49. {tool_compass-2.4.0 → tool_compass-2.5.0}/scripts/verify-metrics.sh +0 -0
@@ -35,14 +35,15 @@ LOG_LEVEL=INFO
35
35
  # Gradio UI Settings
36
36
  # =============================================================================
37
37
 
38
- # Server bind address (0.0.0.0 for Docker, 127.0.0.1 for local)
39
- GRADIO_SERVER_NAME=127.0.0.1
40
-
41
- # Server port
42
- GRADIO_SERVER_PORT=7860
43
-
44
- # Enable public sharing link (set to "true" for Gradio share URL)
45
- # GRADIO_SHARE=false
38
+ # Host / port are set on the CLI, not via env vars — `ui.py` passes them
39
+ # explicitly to Gradio's launch(), which overrides GRADIO_SERVER_NAME/PORT:
40
+ # tool-compass ui --host 0.0.0.0 --port 7860
41
+ # (defaults: 127.0.0.1:7860; the Docker image already binds 0.0.0.0.)
42
+
43
+ # Public sharing: pass `tool-compass ui --share` for a public Gradio URL.
44
+ # --share is REFUSED unless GRADIO_AUTH is set, so the link is never wide
45
+ # open. Format is "user:pass" (both sides non-empty).
46
+ # GRADIO_AUTH=user:pass
46
47
 
47
48
  # =============================================================================
48
49
  # Docker-Specific
@@ -68,6 +68,11 @@ db/tools.db
68
68
  db/compass_analytics.db
69
69
  db/chains.db
70
70
 
71
+ # SQLite transient sidecar files (WAL/SHM/journal — rewritten on every open/close)
72
+ db/*.db-shm
73
+ db/*.db-wal
74
+ db/*.db-journal
75
+
71
76
  # Logs
72
77
  *.log
73
78
  logs/
@@ -121,3 +126,6 @@ npm/README.pt-BR.md
121
126
  npm/node_modules/
122
127
  npm/package-lock.json
123
128
  npm/*.tgz
129
+
130
+ # Dogfood swarm working artifacts (manifest, wave outputs) — not shipped
131
+ swarms/
@@ -7,6 +7,86 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.5.0] - 2026-07-04
11
+
12
+ Dogfood swarm v4 — a composed re-audit health pass (Stage A bug/security →
13
+ B/C/D proactive/humanization/visual) followed by a 12-feature pass, each
14
+ feature closed by an adversarial cross-wave re-audit. Tests 1148 → 1337
15
+ (88.6% coverage); ruff clean. No breaking API changes — every new config
16
+ field defaults to today's behavior and all existing tool/CLI responses keep
17
+ their shape. Two behavior changes are called out under **Changed**.
18
+
19
+ ### Added
20
+ - **HTTP / streamable-http backend transport.** Backends of type `http` now
21
+ connect at runtime via the MCP SDK's Streamable HTTP client — tool-compass
22
+ can front remote / SaaS MCP servers, not just local stdio subprocesses.
23
+ `url` + `headers` (headers redacted in `doctor` output) as before.
24
+ - **Optional bearer-token gateway auth** (`gateway_auth_token`, or the
25
+ `TOOL_COMPASS_GATEWAY_AUTH_TOKEN` env var). When set, the HTTP transport
26
+ requires `Authorization: Bearer <token>` (constant-time compare, `/health`
27
+ and `/ready` stay open). Opt-in and fail-closed only when enabled; the stdio
28
+ transport is never affected.
29
+ - **Hybrid search + exact-name boost.** `compass()` now fuses the semantic
30
+ (HNSW) and lexical signals with Reciprocal Rank Fusion on the healthy path
31
+ (not only as an Ollama-outage fallback), and an exact tool-name paste
32
+ (`fs:read_file`, or the bare `read_file`) ranks #1. Toggles: `hybrid_search`,
33
+ `exact_name_boost`, `exact_match_confidence`.
34
+ - **Full-schema `describe()`.** The complete JSON `inputSchema`
35
+ (required fields, per-parameter descriptions, enums, defaults) is now
36
+ preserved through sync and returned by `describe()` alongside the collapsed
37
+ `parameters` — so an agent can build a correct `execute()` call in one hop.
38
+ - **Per-backend / per-tool timeouts** (`default_timeout`, `tool_timeouts`) —
39
+ override the 15 s default so slow image/video/LLM backends aren't cut off.
40
+ - **`tool-compass execute <tool> '<json-args>'`** — run a proxied tool call
41
+ from the terminal to smoke-test a backend. `--timeout`, `--json`.
42
+ - **Per-backend tool allow/deny** (`allow_tools` / `deny_tools` glob lists) —
43
+ expose a safe subset of a broad backend. Enforced at index time and at the
44
+ `execute()` boundary (defense-in-depth).
45
+ - **Active liveness probe** on `compass_status(active=True)` — surfaces
46
+ hung-but-connected backends a passive connection check would miss.
47
+ - **Analytics retention** (`analytics_retention_days`, default 30, `0` =
48
+ forever) with an automatic prune on startup sync, so the analytics DB no
49
+ longer grows without bound.
50
+ - **Auto-refresh polling** — `sync_polling_interval` is now honored at runtime
51
+ (the gateway starts a background re-sync loop when it is `> 0`).
52
+ - **Schema-aware change detection** — a backend that revises a tool's
53
+ description or parameters (same name) is now re-synced and re-embedded
54
+ (previously only name changes were detected).
55
+
56
+ ### Fixed
57
+ - **Multi-backend index data loss (HIGH).** Orphan-vector compaction rebuilt
58
+ the index from only the changed-backend subset, silently deleting every
59
+ other backend's tools after ~20 churn cycles. Compaction now rebuilds from
60
+ the full connected catalog.
61
+ - **Analytics time-window skew (HIGH).** Timeframe queries compared naive
62
+ local time against UTC-stored timestamps, shifting every windowed summary by
63
+ the host's UTC offset. All analytics windows are now computed in UTC.
64
+ - **Docker UI unreachable.** The published image ran the Gradio UI bound to
65
+ `127.0.0.1` (loopback) despite the port mapping — `docker-compose up` served
66
+ nothing on `localhost:7860`. The image now binds `0.0.0.0`.
67
+ - `compass()` no longer raises an unhandled `IndexError` on an empty-match +
68
+ zero-confidence-chain query; `compass_chains(create/detect)` returns the
69
+ structured service-unavailable envelope when Ollama is down instead of a raw
70
+ exception; `compass_chains(detect)` now actually indexes detected chains
71
+ (was reporting success without indexing them).
72
+ - The embedding-cache write no longer commits inside `build_index`'s atomic
73
+ rebuild transaction (a failed HNSW save could leave DB/HNSW divergence).
74
+ - CLI `search` now catches the real Ollama-down exception types and falls back
75
+ to keyword search (matching the UI/gateway), so `--help`'s promise holds.
76
+ - `requirements.txt` now includes `rich` (the Docker image was missing it);
77
+ `.env.example` / `llms.txt` corrected to the env vars the code actually reads.
78
+ - Several test-integrity fixes (coroutine leak, tautological/mis-named tests).
79
+
80
+ ### Changed
81
+ - **Hybrid search is on by default** (`hybrid_search=True`). This changes the
82
+ default result ranking (for the better); set `hybrid_search=false` to restore
83
+ pure-semantic ordering. The response envelope, `min_confidence`, and `top_k`
84
+ contracts are unchanged.
85
+ - **Backend names containing `:` are now rejected at config load** with a clear
86
+ error — a colon is the reserved `backend:tool` separator and made routing and
87
+ the allow/deny policy ambiguous. Rename any such backend (this only rejects
88
+ configs that were already mis-routing).
89
+
10
90
  ## [2.4.0] - 2026-06-20
11
91
 
12
92
  Dogfood swarm v3 — full health pass (bug/security → proactive → humanization
@@ -72,10 +72,14 @@ ENV PYTHONUNBUFFERED=1 \
72
72
  PYTHONPATH=/app \
73
73
  # Tool Compass settings
74
74
  TOOL_COMPASS_BASE_PATH=/app \
75
- OLLAMA_URL=http://host.docker.internal:11434 \
76
- # Gradio settings
77
- GRADIO_SERVER_NAME=0.0.0.0 \
78
- GRADIO_SERVER_PORT=7860
75
+ OLLAMA_URL=http://host.docker.internal:11434
76
+ # UI-DOCKER-BIND-001: the container UI bind is set explicitly on the CMD below
77
+ # (--host 0.0.0.0), NOT via GRADIO_SERVER_NAME. ui.py calls
78
+ # demo.launch(server_name=args.host, ...) with a truthy default, which
79
+ # overrides Gradio's native GRADIO_SERVER_NAME env support — so setting that env
80
+ # here was a silent no-op and left the containerized UI bound to loopback
81
+ # (unreachable through the published port). The prior GRADIO_SERVER_NAME/PORT
82
+ # ENV lines were dropped to remove that misleading dead configuration.
79
83
 
80
84
  # Expose Gradio UI port
81
85
  EXPOSE 7860
@@ -87,8 +91,12 @@ USER compass
87
91
  HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
88
92
  CMD python -c "from indexer import CompassIndex; idx = CompassIndex(); print('healthy' if idx.load_index() else 'no index')" || exit 1
89
93
 
90
- # Default command: Run Gradio UI
91
- CMD ["python", "ui.py"]
94
+ # Default command: Run Gradio UI.
95
+ # UI-DOCKER-BIND-001: bind 0.0.0.0 explicitly so the UI is reachable through the
96
+ # published port mapping (inside the container the container boundary is the
97
+ # isolation layer; external exposure is controlled by `docker run -p` /
98
+ # compose `ports:`). Without this the UI bound to 127.0.0.1 and was unreachable.
99
+ CMD ["python", "ui.py", "--host", "0.0.0.0", "--port", "7860"]
92
100
 
93
101
  # =============================================================================
94
102
  # Stage 3: MCP Gateway (alternative entrypoint)
@@ -96,7 +104,13 @@ CMD ["python", "ui.py"]
96
104
  FROM production AS mcp-gateway
97
105
 
98
106
  # Override for HTTP mode (Fly.io / Smithery)
99
- ENV PORT=8080
107
+ # ci-infra-01: gateway.py's _run_http defaults HOST to 127.0.0.1 (loopback), so
108
+ # without this the published image binds loopback and any orchestrator's proxy /
109
+ # health check (the HEALTHCHECK below hits localhost, but Fly/Smithery reach the
110
+ # container over the network) can't reach the gateway. 0.0.0.0 is the documented
111
+ # reverse-proxy path — this image is meant to sit behind an authenticated edge.
112
+ ENV PORT=8080 \
113
+ HOST=0.0.0.0
100
114
 
101
115
  # Expose MCP HTTP port
102
116
  EXPOSE 8080
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tool-compass
3
- Version: 2.4.0
3
+ Version: 2.5.0
4
4
  Summary: Semantic MCP tool discovery gateway - find tools by intent, not memory
5
5
  Project-URL: Homepage, https://github.com/mcp-tool-shop-org/tool-compass
6
6
  Project-URL: Documentation, https://github.com/mcp-tool-shop-org/tool-compass#readme
@@ -94,9 +94,10 @@ Tool Compass uses **semantic search** to find relevant tools from a natural lang
94
94
 
95
95
  ```bash
96
96
  npx @mcptoolshop/tool-compass --help
97
- npx @mcptoolshop/tool-compass serve # MCP gateway
98
- npx @mcptoolshop/tool-compass ui # Gradio UI
99
- npx @mcptoolshop/tool-compass doctor # Diagnose setup
97
+ npx @mcptoolshop/tool-compass serve # MCP gateway
98
+ npx @mcptoolshop/tool-compass ui # Gradio UI
99
+ npx @mcptoolshop/tool-compass doctor # Diagnose setup
100
+ npx @mcptoolshop/tool-compass execute fs:read_file '{"path":"README.md"}' # Smoke-test a proxied call
100
101
  ```
101
102
 
102
103
  Downloads a verified platform binary on first run (SHA256-checked against the GitHub Release). Cached locally — subsequent invocations launch instantly. See [@mcptoolshop/tool-compass](https://www.npmjs.com/package/@mcptoolshop/tool-compass) on npm.
@@ -157,13 +158,13 @@ docker-compose --profile with-ollama up
157
158
 
158
159
  ## Features
159
160
 
160
- - **Semantic Search** - Find tools by describing what you want to do
161
- - **Progressive Disclosure** - `compass()` → `describe()` → `execute()`
162
- - **Hot Cache** - Frequently used tools are pre-loaded
163
- - **Chain Detection** - Automatically discovers common tool workflows
164
- - **Analytics** - Track usage patterns and tool performance
165
- - **Cross-Platform** - Windows, macOS, Linux
166
- - **Docker Ready** - One-command deployment
161
+ - **Hybrid Search** - Semantic (HNSW) + lexical fusion with exact-name boost — describe what you want, or paste a tool name and it ranks #1
162
+ - **Full-Schema Progressive Disclosure** - `compass()` → `describe()` → `execute()`; `describe()` returns the complete `inputSchema` (required fields, descriptions, enums, defaults)
163
+ - **stdio + HTTP backends** - Front local subprocess MCP servers *and* remote / SaaS servers over streamable-http, with optional bearer-token auth
164
+ - **Per-tool timeouts & allow/deny** - Override the default timeout per backend/tool; expose a safe subset of a broad backend
165
+ - **Hot Cache & Chain Detection** - Frequently used tools pre-loaded; common tool workflows discovered automatically
166
+ - **Analytics** - Track usage patterns and tool performance (with retention/prune)
167
+ - **Cross-Platform & Docker Ready** - Windows, macOS, Linux; one-command deployment
167
168
 
168
169
  ## Architecture
169
170
 
@@ -220,16 +221,18 @@ Returns:
220
221
 
221
222
  | Tool | Description |
222
223
  |------|-------------|
223
- | `compass(intent)` | Semantic search for tools |
224
- | `describe(tool_name)` | Get full schema for a tool |
224
+ | `compass(intent)` | Hybrid semantic + lexical search with exact-name boost |
225
+ | `describe(tool_name)` | Get the full `inputSchema` for a tool (required/enums/defaults) |
225
226
  | `execute(tool_name, args)` | Run a tool on its backend |
226
227
  | `compass_categories()` | List categories and servers |
227
- | `compass_status()` | System health and config |
228
+ | `compass_status(active)` | System health and config; `active=True` runs a live backend liveness probe |
228
229
  | `compass_analytics(timeframe)` | Usage statistics |
229
230
  | `compass_chains(action)` | Manage tool workflows |
230
231
  | `compass_sync(force)` | Rebuild index from backends |
231
232
  | `compass_audit()` | Full system report |
232
233
 
234
+ The same actions are available from the CLI — including `tool-compass execute <tool> '<json>'` to smoke-test a proxied call from the terminal.
235
+
233
236
  ### Progressive Disclosure Pattern
234
237
 
235
238
  Tool Compass uses a three-step progressive disclosure pattern to minimize token usage:
@@ -273,13 +276,18 @@ The `hint` field in compass results guides this flow, suggesting when to use `de
273
276
  | `OLLAMA_URL` | Ollama server URL | `http://localhost:11434` |
274
277
  | `COMFYUI_URL` | ComfyUI server | `http://localhost:8188` |
275
278
  | `PORT` | Set to enable HTTP transport (e.g., for Fly.io) | unset (stdio) |
279
+ | `TOOL_COMPASS_GATEWAY_AUTH_TOKEN` | Bearer token required on the HTTP transport (opt-in; overrides the `gateway_auth_token` config field) | unset (no auth) |
276
280
 
277
281
  **Default data directories:**
278
282
  - **Windows:** `%LOCALAPPDATA%\tool-compass\`
279
283
  - **macOS:** `~/Library/Application Support/tool-compass/`
280
284
  - **Linux:** `~/.config/tool-compass/` (or `$XDG_CONFIG_HOME/tool-compass/`)
281
285
 
282
- See [`.env.example`](.env.example) for all options.
286
+ Config-file settings (in `compass_config.json`) added in v2.5.0 — `hybrid_search`,
287
+ `exact_name_boost`, per-backend `default_timeout` / `tool_timeouts`,
288
+ `allow_tools` / `deny_tools`, `analytics_retention_days`, and HTTP (`type: "http"`)
289
+ backends — are documented in the [Handbook → Configuration](https://mcp-tool-shop-org.github.io/tool-compass/handbook/configuration/).
290
+ See [`.env.example`](.env.example) for env-var options.
283
291
 
284
292
  ## Performance
285
293
 
@@ -368,14 +376,16 @@ intentionally not hand-authored. Hand-curated sections (Known Gaps,
368
376
  Remediation History) live outside the `<!-- SHIPCHECK-AUTO-START/END -->`
369
377
  markers in SCORECARD.md and survive regenerations.
370
378
 
379
+ Latest `shipcheck audit`: **32 checked · 0 unchecked · 5 skipped · 100% pass — all hard gates pass.**
380
+
371
381
  | Category | Score | Notes |
372
382
  |----------|-------|-------|
373
- | A. Security | TBD | SHA-pinned actions; digest-pinned base image; SLSA provenance + SBOM on PyPI + GHCR; pre-commit secrets scan |
374
- | B. Error Handling | TBD | Structured results, graceful degradation, exit codes |
375
- | C. Operator Docs | TBD | README, CHANGELOG, LICENSE, Makefile `verify` + `verify-metrics` + `scorecard` |
376
- | D. Shipping Hygiene | TBD | CI consolidated; timeout-minutes + retention-days on every job; pytest config in pyproject.toml |
377
- | E. Identity (soft) | TBD | Logo, landing page, GitHub metadata; explicit maintainers in pyproject.toml |
378
- | **Total** | **TBD** | Regenerate via `make scorecard` |
383
+ | A. Security | ✅ Pass | SHA-pinned actions; digest-pinned base image; SLSA provenance + SBOM on PyPI + GHCR; pre-commit secrets scan; opt-in gateway bearer auth |
384
+ | B. Error Handling | ✅ Pass | Structured results, graceful degradation, exit codes |
385
+ | C. Operator Docs | ✅ Pass | README, CHANGELOG, LICENSE, Makefile `verify` + `verify-metrics` + `scorecard` |
386
+ | D. Shipping Hygiene | ✅ Pass | CI consolidated; timeout-minutes + retention-days on every job; pytest config in pyproject.toml |
387
+ | E. Identity (soft) | ✅ Pass | Logo, landing page, GitHub metadata; explicit maintainers in pyproject.toml |
388
+ | **Total** | **100%** | All hard gates pass — regenerate via `make scorecard` |
379
389
 
380
390
  ## License
381
391
 
@@ -47,9 +47,10 @@ Tool Compass uses **semantic search** to find relevant tools from a natural lang
47
47
 
48
48
  ```bash
49
49
  npx @mcptoolshop/tool-compass --help
50
- npx @mcptoolshop/tool-compass serve # MCP gateway
51
- npx @mcptoolshop/tool-compass ui # Gradio UI
52
- npx @mcptoolshop/tool-compass doctor # Diagnose setup
50
+ npx @mcptoolshop/tool-compass serve # MCP gateway
51
+ npx @mcptoolshop/tool-compass ui # Gradio UI
52
+ npx @mcptoolshop/tool-compass doctor # Diagnose setup
53
+ npx @mcptoolshop/tool-compass execute fs:read_file '{"path":"README.md"}' # Smoke-test a proxied call
53
54
  ```
54
55
 
55
56
  Downloads a verified platform binary on first run (SHA256-checked against the GitHub Release). Cached locally — subsequent invocations launch instantly. See [@mcptoolshop/tool-compass](https://www.npmjs.com/package/@mcptoolshop/tool-compass) on npm.
@@ -110,13 +111,13 @@ docker-compose --profile with-ollama up
110
111
 
111
112
  ## Features
112
113
 
113
- - **Semantic Search** - Find tools by describing what you want to do
114
- - **Progressive Disclosure** - `compass()` → `describe()` → `execute()`
115
- - **Hot Cache** - Frequently used tools are pre-loaded
116
- - **Chain Detection** - Automatically discovers common tool workflows
117
- - **Analytics** - Track usage patterns and tool performance
118
- - **Cross-Platform** - Windows, macOS, Linux
119
- - **Docker Ready** - One-command deployment
114
+ - **Hybrid Search** - Semantic (HNSW) + lexical fusion with exact-name boost — describe what you want, or paste a tool name and it ranks #1
115
+ - **Full-Schema Progressive Disclosure** - `compass()` → `describe()` → `execute()`; `describe()` returns the complete `inputSchema` (required fields, descriptions, enums, defaults)
116
+ - **stdio + HTTP backends** - Front local subprocess MCP servers *and* remote / SaaS servers over streamable-http, with optional bearer-token auth
117
+ - **Per-tool timeouts & allow/deny** - Override the default timeout per backend/tool; expose a safe subset of a broad backend
118
+ - **Hot Cache & Chain Detection** - Frequently used tools pre-loaded; common tool workflows discovered automatically
119
+ - **Analytics** - Track usage patterns and tool performance (with retention/prune)
120
+ - **Cross-Platform & Docker Ready** - Windows, macOS, Linux; one-command deployment
120
121
 
121
122
  ## Architecture
122
123
 
@@ -173,16 +174,18 @@ Returns:
173
174
 
174
175
  | Tool | Description |
175
176
  |------|-------------|
176
- | `compass(intent)` | Semantic search for tools |
177
- | `describe(tool_name)` | Get full schema for a tool |
177
+ | `compass(intent)` | Hybrid semantic + lexical search with exact-name boost |
178
+ | `describe(tool_name)` | Get the full `inputSchema` for a tool (required/enums/defaults) |
178
179
  | `execute(tool_name, args)` | Run a tool on its backend |
179
180
  | `compass_categories()` | List categories and servers |
180
- | `compass_status()` | System health and config |
181
+ | `compass_status(active)` | System health and config; `active=True` runs a live backend liveness probe |
181
182
  | `compass_analytics(timeframe)` | Usage statistics |
182
183
  | `compass_chains(action)` | Manage tool workflows |
183
184
  | `compass_sync(force)` | Rebuild index from backends |
184
185
  | `compass_audit()` | Full system report |
185
186
 
187
+ The same actions are available from the CLI — including `tool-compass execute <tool> '<json>'` to smoke-test a proxied call from the terminal.
188
+
186
189
  ### Progressive Disclosure Pattern
187
190
 
188
191
  Tool Compass uses a three-step progressive disclosure pattern to minimize token usage:
@@ -226,13 +229,18 @@ The `hint` field in compass results guides this flow, suggesting when to use `de
226
229
  | `OLLAMA_URL` | Ollama server URL | `http://localhost:11434` |
227
230
  | `COMFYUI_URL` | ComfyUI server | `http://localhost:8188` |
228
231
  | `PORT` | Set to enable HTTP transport (e.g., for Fly.io) | unset (stdio) |
232
+ | `TOOL_COMPASS_GATEWAY_AUTH_TOKEN` | Bearer token required on the HTTP transport (opt-in; overrides the `gateway_auth_token` config field) | unset (no auth) |
229
233
 
230
234
  **Default data directories:**
231
235
  - **Windows:** `%LOCALAPPDATA%\tool-compass\`
232
236
  - **macOS:** `~/Library/Application Support/tool-compass/`
233
237
  - **Linux:** `~/.config/tool-compass/` (or `$XDG_CONFIG_HOME/tool-compass/`)
234
238
 
235
- See [`.env.example`](.env.example) for all options.
239
+ Config-file settings (in `compass_config.json`) added in v2.5.0 — `hybrid_search`,
240
+ `exact_name_boost`, per-backend `default_timeout` / `tool_timeouts`,
241
+ `allow_tools` / `deny_tools`, `analytics_retention_days`, and HTTP (`type: "http"`)
242
+ backends — are documented in the [Handbook → Configuration](https://mcp-tool-shop-org.github.io/tool-compass/handbook/configuration/).
243
+ See [`.env.example`](.env.example) for env-var options.
236
244
 
237
245
  ## Performance
238
246
 
@@ -321,14 +329,16 @@ intentionally not hand-authored. Hand-curated sections (Known Gaps,
321
329
  Remediation History) live outside the `<!-- SHIPCHECK-AUTO-START/END -->`
322
330
  markers in SCORECARD.md and survive regenerations.
323
331
 
332
+ Latest `shipcheck audit`: **32 checked · 0 unchecked · 5 skipped · 100% pass — all hard gates pass.**
333
+
324
334
  | Category | Score | Notes |
325
335
  |----------|-------|-------|
326
- | A. Security | TBD | SHA-pinned actions; digest-pinned base image; SLSA provenance + SBOM on PyPI + GHCR; pre-commit secrets scan |
327
- | B. Error Handling | TBD | Structured results, graceful degradation, exit codes |
328
- | C. Operator Docs | TBD | README, CHANGELOG, LICENSE, Makefile `verify` + `verify-metrics` + `scorecard` |
329
- | D. Shipping Hygiene | TBD | CI consolidated; timeout-minutes + retention-days on every job; pytest config in pyproject.toml |
330
- | E. Identity (soft) | TBD | Logo, landing page, GitHub metadata; explicit maintainers in pyproject.toml |
331
- | **Total** | **TBD** | Regenerate via `make scorecard` |
336
+ | A. Security | ✅ Pass | SHA-pinned actions; digest-pinned base image; SLSA provenance + SBOM on PyPI + GHCR; pre-commit secrets scan; opt-in gateway bearer auth |
337
+ | B. Error Handling | ✅ Pass | Structured results, graceful degradation, exit codes |
338
+ | C. Operator Docs | ✅ Pass | README, CHANGELOG, LICENSE, Makefile `verify` + `verify-metrics` + `scorecard` |
339
+ | D. Shipping Hygiene | ✅ Pass | CI consolidated; timeout-minutes + retention-days on every job; pytest config in pyproject.toml |
340
+ | E. Identity (soft) | ✅ Pass | Logo, landing page, GitHub metadata; explicit maintainers in pyproject.toml |
341
+ | **Total** | **100%** | All hard gates pass — regenerate via `make scorecard` |
332
342
 
333
343
  ## License
334
344
 
@@ -21,7 +21,7 @@ import hashlib
21
21
  import threading
22
22
  from collections import deque
23
23
  import numpy as np
24
- from datetime import datetime, timedelta
24
+ from datetime import datetime, timedelta, timezone
25
25
  from pathlib import Path
26
26
  from typing import Optional, List, Dict, Any, Deque
27
27
  from dataclasses import dataclass
@@ -574,6 +574,85 @@ class CompassAnalytics:
574
574
  # MCC-B-005: chain pattern save failure degrades analytics only.
575
575
  self._note_degraded("_save_chain_pattern", e)
576
576
 
577
+ async def prune_old_records(self, retention_days: int) -> int:
578
+ """Delete analytics rows older than the retention window (FEAT-04).
579
+
580
+ DELETEs from BOTH append-only tables — tool_calls and search_queries —
581
+ whose created_at is older than `retention_days` days, and returns the
582
+ total number of rows removed. A sibling gateway agent calls this as
583
+ ``await analytics.prune_old_records(config.analytics_retention_days)``.
584
+
585
+ retention_days <= 0 -> NO-OP (keep forever), returns 0.
586
+
587
+ CA-001 discipline: created_at is stamped by SQLite CURRENT_TIMESTAMP,
588
+ which is UTC. We compute the cutoff INSIDE SQLite via
589
+ ``datetime('now', '-N days')`` so BOTH sides of the comparison are UTC
590
+ — never datetime.now() naive local wall-clock, which would skew the
591
+ cutoff by the host's UTC offset (the exact bug fixed in
592
+ get_analytics_summary).
593
+
594
+ MCC-B-005 discipline: wrapped in the same _degraded / try-except the
595
+ other write methods use — a prune failure must degrade analytics, not
596
+ raise up through the caller. Respects the thread-safe lazy connection
597
+ and the write lock (self._lock) the rest of the module uses.
598
+ """
599
+ if self._degraded:
600
+ return 0
601
+ # retention_days <= 0 means "keep forever" — nothing to prune.
602
+ if retention_days <= 0:
603
+ return 0
604
+
605
+ total_deleted = 0
606
+ try:
607
+ db = self._get_db()
608
+
609
+ # Push the cutoff into SQL so it is evaluated in UTC by SQLite,
610
+ # matching how created_at was written. datetime('now', ?) with a
611
+ # '-N days' modifier yields a UTC 'YYYY-MM-DD HH:MM:SS' bound.
612
+ modifier = f"-{int(retention_days)} days"
613
+
614
+ with self._lock:
615
+ cur = db.execute(
616
+ "DELETE FROM tool_calls "
617
+ "WHERE created_at < datetime('now', ?)",
618
+ (modifier,),
619
+ )
620
+ total_deleted += cur.rowcount or 0
621
+
622
+ cur = db.execute(
623
+ "DELETE FROM search_queries "
624
+ "WHERE created_at < datetime('now', ?)",
625
+ (modifier,),
626
+ )
627
+ total_deleted += cur.rowcount or 0
628
+
629
+ db.commit()
630
+ except sqlite3.Error as e:
631
+ # MCC-B-005: a prune failure degrades analytics; it must never
632
+ # break the caller (the gateway prunes as housekeeping).
633
+ self._note_degraded("prune_old_records", e)
634
+ return 0
635
+
636
+ # VACUUM reclaims the pages freed by a large delete. It CANNOT run
637
+ # inside a transaction and takes its own file lock, so run it
638
+ # best-effort AFTER the committed delete and swallow any failure —
639
+ # reclaiming space is a nice-to-have, never worth raising for. Guard on
640
+ # a non-trivial delete so we don't VACUUM the whole DB on every no-op
641
+ # prune. Held under the write lock so it doesn't race another writer.
642
+ if total_deleted > 0:
643
+ try:
644
+ with self._lock:
645
+ db.execute("VACUUM")
646
+ except sqlite3.Error as e:
647
+ logger.debug(f"VACUUM after prune skipped (non-fatal): {e}")
648
+
649
+ if total_deleted:
650
+ logger.info(
651
+ f"Pruned {total_deleted} analytics row(s) older than "
652
+ f"{retention_days} day(s)."
653
+ )
654
+ return total_deleted
655
+
577
656
  async def refresh_hot_cache(self, embedder=None, index=None):
578
657
  """
579
658
  Update the hot cache with top N most used tools.
@@ -798,7 +877,12 @@ class CompassAnalytics:
798
877
 
799
878
  # Parse timeframe
800
879
  hours = {"1h": 1, "24h": 24, "7d": 168, "30d": 720}.get(timeframe, 24)
801
- since = datetime.now() - timedelta(hours=hours)
880
+ # CA-001: created_at is written by SQLite CURRENT_TIMESTAMP, which is UTC.
881
+ # Compute the window bound in UTC too — datetime.now() is naive LOCAL
882
+ # wall-clock, so a naive bound skews every window by the host's UTC
883
+ # offset. strftime drops the tz suffix, yielding a UTC
884
+ # 'YYYY-MM-DD HH:MM:SS' string that matches CURRENT_TIMESTAMP.
885
+ since = datetime.now(timezone.utc) - timedelta(hours=hours)
802
886
  # Use SQLite-compatible format (YYYY-MM-DD HH:MM:SS) for timestamp comparison
803
887
  since_str = since.strftime("%Y-%m-%d %H:%M:%S")
804
888
 
@@ -926,26 +1010,60 @@ class CompassAnalytics:
926
1010
  ORDER BY rank
927
1011
  """)
928
1012
 
1013
+ # PC-B-001: this runs on the mandatory init path — get_analytics_instance()
1014
+ # awaits it unconditionally and compass()/execute() call that UNWRAPPED,
1015
+ # so a raw exception here surfaces as a bare traceback through the MCP
1016
+ # JSON-RPC envelope (violates the BE-B-004 "handlers never raise"
1017
+ # invariant). Persisted blobs can be corrupt: a bad schema_json raises
1018
+ # json.JSONDecodeError and a truncated embedding blob raises ValueError
1019
+ # from np.frombuffer — neither is a sqlite3.Error, so nothing above
1020
+ # catches them. Guard per-row and SKIP a bad row rather than aborting the
1021
+ # whole load, matching record_search / record_tool_call which degrade
1022
+ # instead of raising. A per-row skip keeps every good row loadable.
1023
+ skipped = 0
929
1024
  for row in cursor.fetchall():
930
- embedding = None
931
- if row["embedding"]:
932
- embedding = np.frombuffer(row["embedding"], dtype=np.float32)
933
-
934
- schema = None
935
- if row["schema_json"]:
936
- schema = json.loads(row["schema_json"])
937
-
938
- self._hot_cache[row["tool_name"]] = HotToolEntry(
939
- tool_name=row["tool_name"],
940
- rank=row["rank"],
941
- call_count=row["call_count"],
942
- embedding=embedding,
943
- schema=schema,
944
- description=row["description"] or "",
945
- last_called_at=None,
946
- )
1025
+ try:
1026
+ embedding = None
1027
+ if row["embedding"]:
1028
+ blob = row["embedding"]
1029
+ # SC-002 (indexer._cache_get): validate the blob byte length
1030
+ # before np.frombuffer. float32 == 4 bytes/element; a length
1031
+ # that is not a whole multiple of 4 is a truncated/corrupt
1032
+ # write and would raise ValueError.
1033
+ if len(blob) % 4 != 0:
1034
+ raise ValueError(
1035
+ f"corrupt embedding blob for {row['tool_name']}: "
1036
+ f"{len(blob)} bytes not a multiple of 4"
1037
+ )
1038
+ embedding = np.frombuffer(blob, dtype=np.float32)
947
1039
 
948
- logger.info(f"Loaded {len(self._hot_cache)} tools into hot cache from DB")
1040
+ schema = None
1041
+ if row["schema_json"]:
1042
+ schema = json.loads(row["schema_json"])
1043
+
1044
+ self._hot_cache[row["tool_name"]] = HotToolEntry(
1045
+ tool_name=row["tool_name"],
1046
+ rank=row["rank"],
1047
+ call_count=row["call_count"],
1048
+ embedding=embedding,
1049
+ schema=schema,
1050
+ description=row["description"] or "",
1051
+ last_called_at=None,
1052
+ )
1053
+ except (json.JSONDecodeError, ValueError, TypeError, sqlite3.Error) as e:
1054
+ # Skip the bad row; a single corrupt persisted blob must not
1055
+ # take down the whole hot-cache load (and thus the first
1056
+ # compass()/execute() of the process).
1057
+ skipped += 1
1058
+ logger.warning(
1059
+ f"Skipping corrupt hot_tools row "
1060
+ f"'{row['tool_name'] if 'tool_name' in row.keys() else '?'}': {e}"
1061
+ )
1062
+
1063
+ logger.info(
1064
+ f"Loaded {len(self._hot_cache)} tools into hot cache from DB"
1065
+ + (f" ({skipped} corrupt row(s) skipped)" if skipped else "")
1066
+ )
949
1067
 
950
1068
  def close(self):
951
1069
  """Close database connection.