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.
- {tool_compass-2.4.0 → tool_compass-2.5.0}/.env.example +9 -8
- {tool_compass-2.4.0 → tool_compass-2.5.0}/.gitignore +8 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/CHANGELOG.md +80 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/Dockerfile +21 -7
- {tool_compass-2.4.0 → tool_compass-2.5.0}/PKG-INFO +31 -21
- {tool_compass-2.4.0 → tool_compass-2.5.0}/README.md +30 -20
- {tool_compass-2.4.0 → tool_compass-2.5.0}/analytics.py +138 -20
- {tool_compass-2.4.0 → tool_compass-2.5.0}/backend_client_simple.py +499 -7
- {tool_compass-2.4.0 → tool_compass-2.5.0}/cli.py +302 -15
- {tool_compass-2.4.0 → tool_compass-2.5.0}/config.py +229 -0
- tool_compass-2.5.0/docs/index.md +28 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/fly.toml +6 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/gateway.py +692 -13
- {tool_compass-2.4.0 → tool_compass-2.5.0}/indexer.py +138 -18
- {tool_compass-2.4.0 → tool_compass-2.5.0}/llms.txt +5 -3
- {tool_compass-2.4.0 → tool_compass-2.5.0}/pyproject.toml +1 -1
- {tool_compass-2.4.0 → tool_compass-2.5.0}/requirements.txt +6 -0
- tool_compass-2.5.0/scripts/sync-version.mjs +136 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/sync_manager.py +175 -82
- {tool_compass-2.4.0 → tool_compass-2.5.0}/tool_manifest.py +7 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/ui.py +66 -37
- tool_compass-2.4.0/docs/index.md +0 -52
- {tool_compass-2.4.0 → tool_compass-2.5.0}/.dockerignore +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/.github/dependabot.yml +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/.github/workflows/ci.yml +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/.github/workflows/publish.yml +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/.github/workflows/release-binaries.yml +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/.github/workflows/release.yml +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/.pre-commit-config.yaml +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/CODE_OF_CONDUCT.md +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/CONTRIBUTING.md +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/LICENSE +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/Makefile +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/SCORECARD.md +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/SECURITY.md +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/SHIP_GATE.md +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/_version.py +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/backend_client_mcp.py +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/bootstrap.py +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/chain_indexer.py +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/compass_config.example.json +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/docker-compose.yml +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/embedder.py +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/requirements-dev.txt +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/scripts/check-org-urls.sh +0 -0
- {tool_compass-2.4.0 → tool_compass-2.5.0}/scripts/regenerate-scorecard.sh +0 -0
- {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
|
-
#
|
|
39
|
-
GRADIO_SERVER_NAME
|
|
40
|
-
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
#
|
|
45
|
-
#
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
98
|
-
npx @mcptoolshop/tool-compass ui
|
|
99
|
-
npx @mcptoolshop/tool-compass doctor
|
|
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
|
-
- **
|
|
161
|
-
- **Progressive Disclosure** - `compass()` → `describe()` → `execute()`
|
|
162
|
-
- **
|
|
163
|
-
- **
|
|
164
|
-
- **
|
|
165
|
-
- **
|
|
166
|
-
- **Docker Ready** -
|
|
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)` |
|
|
224
|
-
| `describe(tool_name)` | Get full
|
|
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
|
-
|
|
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 |
|
|
374
|
-
| B. Error Handling |
|
|
375
|
-
| C. Operator Docs |
|
|
376
|
-
| D. Shipping Hygiene |
|
|
377
|
-
| E. Identity (soft) |
|
|
378
|
-
| **Total** | **
|
|
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
|
|
51
|
-
npx @mcptoolshop/tool-compass ui
|
|
52
|
-
npx @mcptoolshop/tool-compass doctor
|
|
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
|
-
- **
|
|
114
|
-
- **Progressive Disclosure** - `compass()` → `describe()` → `execute()`
|
|
115
|
-
- **
|
|
116
|
-
- **
|
|
117
|
-
- **
|
|
118
|
-
- **
|
|
119
|
-
- **Docker Ready** -
|
|
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)` |
|
|
177
|
-
| `describe(tool_name)` | Get full
|
|
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
|
-
|
|
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 |
|
|
327
|
-
| B. Error Handling |
|
|
328
|
-
| C. Operator Docs |
|
|
329
|
-
| D. Shipping Hygiene |
|
|
330
|
-
| E. Identity (soft) |
|
|
331
|
-
| **Total** | **
|
|
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
|
-
|
|
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
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
-
|
|
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.
|