icycache 0.3.2__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.
- icycache-0.3.2/.gitignore +19 -0
- icycache-0.3.2/LICENSE +21 -0
- icycache-0.3.2/PKG-INFO +422 -0
- icycache-0.3.2/PROGRESS.md +320 -0
- icycache-0.3.2/README.md +378 -0
- icycache-0.3.2/UPDATE-NOTES.md +125 -0
- icycache-0.3.2/config/icycache.gateway.json +111 -0
- icycache-0.3.2/config/icycache.json +149 -0
- icycache-0.3.2/config/normalization_rules.json +5832 -0
- icycache-0.3.2/config/semantic_cache.json +8 -0
- icycache-0.3.2/config/tools_registry.json +10 -0
- icycache-0.3.2/docs/BUG-001-502-stuck-after-400.md +84 -0
- icycache-0.3.2/docs/BUG-002-no-semantic-cache.md +128 -0
- icycache-0.3.2/docs/CONFIGURATION.md +408 -0
- icycache-0.3.2/docs/DELTA-001-tool-proxy-top100.md +391 -0
- icycache-0.3.2/docs/DELTA-002-unified-fix.md +534 -0
- icycache-0.3.2/docs/INSTALL.md +187 -0
- icycache-0.3.2/docs/ZCode-Panel-main.zip +0 -0
- icycache-0.3.2/docs/images/dashboard.png +0 -0
- icycache-0.3.2/entrypoint.sh +20 -0
- icycache-0.3.2/icycache/__init__.py +32 -0
- icycache-0.3.2/icycache/__main__.py +9 -0
- icycache-0.3.2/icycache/cli.py +171 -0
- icycache-0.3.2/mcp_stdio.py +12 -0
- icycache-0.3.2/pyproject.toml +132 -0
- icycache-0.3.2/requirements.txt +34 -0
- icycache-0.3.2/src/icy_cache_store.py +217 -0
- icycache-0.3.2/src/icy_core.py +380 -0
- icycache-0.3.2/src/icy_normalize.py +197 -0
- icycache-0.3.2/src/icy_paths.py +76 -0
- icycache-0.3.2/src/icy_semantic.py +197 -0
- icycache-0.3.2/src/mcp_gateway.py +360 -0
- icycache-0.3.2/src/mcp_server.py +1319 -0
- icycache-0.3.2/src/mcp_stdio_bridge.py +83 -0
- icycache-0.3.2/src/mcp_stdio_entry.py +23 -0
- icycache-0.3.2/src/prefix_proxy.py +336 -0
- icycache-0.3.2/src/server.py +1142 -0
- icycache-0.3.2/src/tool_proxy.py +655 -0
- icycache-0.3.2/src/upstream_client.py +98 -0
- icycache-0.3.2/src/upstream_utils.py +135 -0
- icycache-0.3.2/static/dashboard.html +2385 -0
- icycache-0.3.2/static/icywebgl.js +2190 -0
- icycache-0.3.2/static/logo.svg +58 -0
- icycache-0.3.2/static/three.min.js +6 -0
- icycache-0.3.2/tests/granite_live_test.py +53 -0
- icycache-0.3.2/tests/run_isolated.py +148 -0
- icycache-0.3.2/tests/test_anchors.py +40 -0
- icycache-0.3.2/tests/test_cacheforge.py +123 -0
- icycache-0.3.2/tests/test_cacheforge_ext.py +202 -0
- icycache-0.3.2/tests/test_gateway.py +396 -0
- icycache-0.3.2/tests/test_prefix_proxy.py +136 -0
- icycache-0.3.2/tests/test_semantic_cache.py +60 -0
- icycache-0.3.2/tests/test_upstream_recovery.py +60 -0
- icycache-0.3.2/tools/enforce_gateway.ps1 +144 -0
- icycache-0.3.2/tools/fix_scheduled_task.ps1 +87 -0
- icycache-0.3.2/tools/smoke_gateway.py +244 -0
- icycache-0.3.2/tools/verify_enforcement.py +134 -0
- icycache-0.3.2/tray.py +152 -0
- icycache-0.3.2/warmup_cache.py +73 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
venv/
|
|
2
|
+
.venv/
|
|
3
|
+
db/
|
|
4
|
+
logs/
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.pyc
|
|
7
|
+
*.bak*
|
|
8
|
+
fix_*.py
|
|
9
|
+
*.pre_purge*
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
*.egg-info/
|
|
12
|
+
build/
|
|
13
|
+
dist/
|
|
14
|
+
node_modules/
|
|
15
|
+
*.tgz
|
|
16
|
+
.commit_msg.txt
|
|
17
|
+
|
|
18
|
+
# Scratch de travail (copies isolées, db, logs, captures) - hors livrable
|
|
19
|
+
scratch/
|
icycache-0.3.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Malik (MlkBlk-IT)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
icycache-0.3.2/PKG-INFO
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: icycache
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: IcyCache - universal LLM gateway with a local cache. I see cache, I burn cache.
|
|
5
|
+
Project-URL: Homepage, https://github.com/MlkBlk-IT/icycache
|
|
6
|
+
Project-URL: Repository, https://github.com/MlkBlk-IT/icycache
|
|
7
|
+
Project-URL: Issues, https://github.com/MlkBlk-IT/icycache/issues
|
|
8
|
+
Author: MlkBlk-IT
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cache,fastapi,llm,llm-proxy,local-llm,mcp,mcp-server,semantic-cache,token-economy
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Framework :: FastAPI
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Internet :: Proxy Servers
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: fastapi<1,>=0.115
|
|
24
|
+
Requires-Dist: httpx<1,>=0.27
|
|
25
|
+
Requires-Dist: mcp<2,>=1.2.0
|
|
26
|
+
Requires-Dist: nltk>=3.8
|
|
27
|
+
Requires-Dist: psutil>=6.0
|
|
28
|
+
Requires-Dist: pyahocorasick>=2.1
|
|
29
|
+
Requires-Dist: pydantic>=2.9
|
|
30
|
+
Requires-Dist: python-dotenv>=1.0
|
|
31
|
+
Requires-Dist: pyyaml>=6.0
|
|
32
|
+
Requires-Dist: sse-starlette>=2.1
|
|
33
|
+
Requires-Dist: tqdm>=4.66
|
|
34
|
+
Requires-Dist: uvicorn>=0.30
|
|
35
|
+
Requires-Dist: websockets>=12.0
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: ruff>=0.5.0; extra == 'dev'
|
|
39
|
+
Provides-Extra: tray
|
|
40
|
+
Requires-Dist: pillow>=10.0; extra == 'tray'
|
|
41
|
+
Requires-Dist: pystray>=0.19; extra == 'tray'
|
|
42
|
+
Requires-Dist: pywin32>=306; (sys_platform == 'win32') and extra == 'tray'
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
|
|
45
|
+
> ## ⚠️ NOTICE — The rest of this repository (code, comments, scripts, docs, commits, issues) is written in **French**. Only this README is in English.
|
|
46
|
+
> ✅ **SWAP DONE — 2026-10-24.** IcyCache was renamed from CacheForge. The **universal multi-upstream gateway**
|
|
47
|
+
> is now the **single active service on `:8080`**, and the historical server holds the MCP layer.
|
|
48
|
+
> - `src/server.py` → **the gateway** (18 upstreams in `config/icycache.gateway.json`: OmniRoute, LM Studio, Ollama, OpenAI, Anthropic, NVIDIA, Groq, Cerebras, OpenRouter, Kilo, Nous, Copilot, AI Gateway, HuggingFace, Google AI Studio, LLM7.io, Mistral, Cloudflare).
|
|
49
|
+
> - `src/mcp_server.py` → **the MCP layer** (SSE `:8081`, Streamable HTTP `:8084`, stdio).
|
|
50
|
+
> - Both start together via `tray.py`; `:8080` is the single entry point for all LLM traffic.
|
|
51
|
+
> - Legacy entry point kept: `python src/server.py --port 8090` for isolated testing.
|
|
52
|
+
>
|
|
53
|
+
> ### Update (2026-10-31) — proxy robustness
|
|
54
|
+
> - Legacy cache keys (built before `_tools_sig`) still hit, so v1 cache entries are not orphaned.
|
|
55
|
+
> - A client disconnect mid-stream is detected and logged; an aborted stream is not cached.
|
|
56
|
+
> - Upstream errors now return the upstream's own status and body instead of a generic 502.
|
|
57
|
+
>
|
|
58
|
+
> ### DELTA-002 — upstream resilience + opt-in semantic cache (2026-10-26)
|
|
59
|
+
> - **BUG-001 — 502 stuck after a 400:** the httpx client is now drained, recycled, and the request retried once
|
|
60
|
+
> (`src/upstream_client.py`). The gateway recovers instead of staying stuck.
|
|
61
|
+
> - **Number normalization:** `"two plus two"` → `2+2`. 11 new rules (1111 → **1122**).
|
|
62
|
+
> - **Semantic cache:** implemented (`src/icy_semantic.py`) but **off by default** (`semantic_enabled: false`).
|
|
63
|
+
> - **Tool registry:** the 100 verified MCP tool anchors (DELTA-001) are registered and asserted at startup.
|
|
64
|
+
>
|
|
65
|
+
> ### Available MCP transports (all three active at once)
|
|
66
|
+
>
|
|
67
|
+
> | Transport | Endpoint | Target client |
|
|
68
|
+
> |-----------|----------|---------------|
|
|
69
|
+
> | **stdio** | `python mcp_stdio.py` | Claude Desktop, IDEs, any stdio client |
|
|
70
|
+
> | **SSE** (legacy) | `http://127.0.0.1:8081/sse` | Hermes, older clients |
|
|
71
|
+
> | **Streamable HTTP** (standard) | `http://127.0.0.1:8084/mcp` | Recent MCP clients, MCP Inspector |
|
|
72
|
+
>
|
|
73
|
+
> In stdio mode, go through `mcp_stdio.py`: it reserves stdout for JSON-RPC.
|
|
74
|
+
> (Nothing is written to stdout anymore, but the wrapper remains the safety net.)
|
|
75
|
+
|
|
76
|
+
# 🔥 IcyCache — Universal LLM Gateway with local cache
|
|
77
|
+
|
|
78
|
+
[](https://github.com/MlkBlk-IT/icycache/releases) [](https://m8ven.ai/mcp/mlkblk-it-mcp-cacheforge-16d3le?s=readme)
|
|
79
|
+
|
|
80
|
+
> **"Forge the cache, not the answer."**
|
|
81
|
+
> HTTP gateway that sits between **any client** and **any upstream** (local or cloud),
|
|
82
|
+
> caches responses and serves them back **instantly** — no CPU regeneration.
|
|
83
|
+
>
|
|
84
|
+
> 📖 **Full configuration guide** : [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md)
|
|
85
|
+
> — multi-upstream routing, per-client setup (Hermes, Claude Desktop, Cursor, VS Code,
|
|
86
|
+
> Ollama, direct SDK), MISS→HIT proof, troubleshooting, firewall enforcement.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Dashboard
|
|
91
|
+
|
|
92
|
+

|
|
93
|
+
|
|
94
|
+
*Read-only web dashboard (`http://127.0.0.1:8080/dashboard`) on a local instance, captured 2026-10-31.*
|
|
95
|
+
|
|
96
|
+
## The problem
|
|
97
|
+
|
|
98
|
+
| Problem | Consequence |
|
|
99
|
+
|---------|-------------|
|
|
100
|
+
| **Latency** | An identical request burns CPU/RAM again for an already-computed answer |
|
|
101
|
+
| **No semantic cache** | Standard systems hash the raw prompt: `Explain Hyper-V` and `Please explain Hyper-V thanks` generate **two** distinct requests |
|
|
102
|
+
| **No observability** | Impossible to measure the tokens and time actually saved |
|
|
103
|
+
| **Traffic bypassing the proxy** | Every client talking directly to its provider breaks the cache promise |
|
|
104
|
+
|
|
105
|
+
IcyCache attacks all four: normalization before hashing, SQLite cache with TTL, exportable metrics,
|
|
106
|
+
and **a single entry point** for all LLM traffic.
|
|
107
|
+
|
|
108
|
+
> **Semantic cache status:** implemented but **opt-in** (`semantic_enabled: false` by default). It needs an
|
|
109
|
+
> embedding endpoint (LM Studio on `:1234`) and a Qdrant instance; when either is missing the cache
|
|
110
|
+
> degrades silently to a miss instead of failing the request. See [`config/semantic_cache.json`](config/semantic_cache.json).
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Target architecture
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
Any client (Hermes, Claude Code, scripts, IDE, SDK, CLI)
|
|
118
|
+
base_url = http://127.0.0.1:8080
|
|
119
|
+
│
|
|
120
|
+
▼
|
|
121
|
+
┌───────────────────────────────────────────────────────────────────────┐
|
|
122
|
+
│ ICYCACHE GATEWAY 127.0.0.1:8080 (src/server.py) │
|
|
123
|
+
│ │
|
|
124
|
+
│ ROUTING URL prefix /<upstream>/v1/... │
|
|
125
|
+
│ header X-IcyCache-Upstream: lmstudio │
|
|
126
|
+
│ body "upstream": "ollama" │
|
|
127
|
+
│ default gateway.default_upstream (config) │
|
|
128
|
+
│ │
|
|
129
|
+
│ NORMALIZATION NFKD → lowercase → Aho-Corasick → accents → punctuation│
|
|
130
|
+
│ CACHE KEY SHA256 of the NORMALIZED payload (+ upstream + endpoint)│
|
|
131
|
+
│ DECISION HIT → instant response | MISS → forward + save │
|
|
132
|
+
│ TOKENS REAL usage from the upstream. Never an estimate. │
|
|
133
|
+
│ OBSERVABLE WebSocket /ws/events : live deltas + final │
|
|
134
|
+
│ MCP (facade) stats / clear / search / realtime / tool-cache │
|
|
135
|
+
└───────────────────────────────────────────────────────────────────────┘
|
|
136
|
+
│ ▲ │
|
|
137
|
+
▼ │ same path back │
|
|
138
|
+
{ OmniRoute | LM Studio | Ollama | OpenAI | Anthropic | … }
|
|
139
|
+
│
|
|
140
|
+
▼
|
|
141
|
+
SQLite db/cache.db (events: upstream, real tokens, latencies, ttft)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Rule: everything that leaves a client and comes back to it goes through IcyCache.**
|
|
145
|
+
OmniRoute is no longer the entry point: it is **one upstream among others**.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Components
|
|
150
|
+
|
|
151
|
+
| File | Role |
|
|
152
|
+
|------|------|
|
|
153
|
+
| `src/server.py` | **Universal multi-upstream gateway** (FastAPI): routing, cache, real tokens, WebSocket |
|
|
154
|
+
| `src/mcp_gateway.py` | **MCP facade** of the gateway: 7 tools, tool-cache, loop guard, mirror REST API |
|
|
155
|
+
| `src/icy_core.py` | Shared core: DB (`events`, `agent_events`, `tool_cache`), WebSocket hub, loop guard |
|
|
156
|
+
| `src/icy_normalize.py` | Standalone normalization engine (faithful port, no import side effects) |
|
|
157
|
+
| `src/icy_cache_store.py` | LLM cache store (compatible `cache` table, additive `raw_hash` column) |
|
|
158
|
+
| `src/upstream_utils.py` | Usage normalization, headers, cache_control/prompt_cache_key mutation, SSE meter |
|
|
159
|
+
| `src/mcp_stdio_bridge.py` | stdio bridge: reserves stdout for MCP JSON-RPC, logs and prints to stderr |
|
|
160
|
+
| `src/mcp_server.py` | **MCP server** (FastMCP): tools, tool-cache, loop guard — listens on :8081 (SSE) / :8084 (HTTP) |
|
|
161
|
+
| `config/icycache.gateway.json` | Gateway config: upstreams, cache, loop guard, tool cache |
|
|
162
|
+
| `config/normalization_rules.json` | 1122 normalization rules (incl. number words → digits) |
|
|
163
|
+
| `src/upstream_client.py` | **Resilient upstream client** — one pool per base URL, drain + recycle + retry |
|
|
164
|
+
| `src/icy_semantic.py` | **Opt-in semantic cache** — embeddings + Qdrant store, silent miss when unavailable |
|
|
165
|
+
| `config/semantic_cache.json` | Semantic cache settings (`semantic_enabled`, thresholds, embedder URL) |
|
|
166
|
+
| `config/tools_registry.json` | Registry of the 100 verified MCP tool anchors (DELTA-001) |
|
|
167
|
+
| `tools/smoke_gateway.py` | **Real** multi-upstream smoke test (isolated ports, temp DB) |
|
|
168
|
+
| `tools/enforce_gateway.ps1` | Firewall rules: makes bypass impossible |
|
|
169
|
+
| `tools/verify_enforcement.py` | **Proof**: can the client still reach an upstream directly? |
|
|
170
|
+
| `tests/test_gateway.py` | 10 isolated tests (mock upstream, temp DB) |
|
|
171
|
+
| `tests/test_semantic_cache.py` | Semantic cache unit tests |
|
|
172
|
+
| `tests/test_upstream_recovery.py` | Upstream recovery / retry tests (BUG-001) |
|
|
173
|
+
| `tests/test_anchors.py` | Normalization anchor tests (DELTA-002) |
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Routing: pick your upstream
|
|
178
|
+
|
|
179
|
+
| Method | Example | Priority |
|
|
180
|
+
|--------|---------|----------|
|
|
181
|
+
| URL prefix | `POST http://127.0.0.1:8080/lmstudio/v1/chat/completions` | 1 |
|
|
182
|
+
| Header | `X-IcyCache-Upstream: lmstudio` | 2 |
|
|
183
|
+
| Body | `{"upstream": "lmstudio", ...}` | 3 |
|
|
184
|
+
| Config default | `gateway.default_upstream` | 4 |
|
|
185
|
+
|
|
186
|
+
No **implicit** default in the code: without an explicit source, the request is rejected with
|
|
187
|
+
`400` listing the available upstreams. An unknown upstream is rejected with `404`.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## HTTP routes (gateway)
|
|
192
|
+
|
|
193
|
+
| Route | Method | Role |
|
|
194
|
+
|-------|--------|------|
|
|
195
|
+
| `/v1/chat/completions` | POST | OpenAI-compatible (streaming and non-streaming) |
|
|
196
|
+
| `/v1/messages` | POST | Native Anthropic (`kind: anthropic`) |
|
|
197
|
+
| `/v1/embeddings` | POST | Embeddings (cached) |
|
|
198
|
+
| `/v1/models` | GET | Upstream models |
|
|
199
|
+
| `/{upstream}/v1/{rest}` | * | Explicit routing by prefix |
|
|
200
|
+
| `/ws/events` | WS | Realtime events: live `delta` + `final` (real tokens) |
|
|
201
|
+
| `/health`, `/api/status` | GET | Health, reachable upstreams |
|
|
202
|
+
| `/api/metrics`, `/api/metrics/series`, `/api/metrics/export` | GET | Metrics (dashboard shapes) |
|
|
203
|
+
| `/api/agent-events` | GET | Agent / tool-cache events |
|
|
204
|
+
| `/api/cache-clear` | POST | Empty the cache — header `X-IcyCache-Confirm: FORCE_DELETE` |
|
|
205
|
+
| `/dashboard` | GET | Web dashboard (read-only) |
|
|
206
|
+
|
|
207
|
+
**Confirmation headers:** `X-IcyCache-Confirm` (unified naming). The old `X-CacheForge-Confirm`
|
|
208
|
+
is still accepted by the gateway for compatibility.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Tokens: the rule of the real
|
|
213
|
+
|
|
214
|
+
| Source | Value |
|
|
215
|
+
|--------|-------|
|
|
216
|
+
| `usage` field of the upstream response | **always authoritative** |
|
|
217
|
+
| OpenAI streaming | `stream_options.include_usage=true` injected → usage from last chunk |
|
|
218
|
+
| Anthropic streaming | `message_start` + `message_delta` |
|
|
219
|
+
| Usage absent | `NULL` + `tokens_source: "n/a"` — **never an estimate** |
|
|
220
|
+
| HIT | `saved_prompt_tokens` / `saved_completion_tokens` = real usage of the 1st call |
|
|
221
|
+
|
|
222
|
+
`events` columns: `upstream`, `client`, `prompt_tokens`, `completion_tokens`, `cached_tokens`,
|
|
223
|
+
`cache_write_tokens`, `latency_ms`, `ttft_ms`.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## MCP tools (facade)
|
|
228
|
+
|
|
229
|
+
### MCP server (`src/mcp_server.py`) — 11 tools
|
|
230
|
+
|
|
231
|
+
| Tool | Role |
|
|
232
|
+
|------|------|
|
|
233
|
+
| `get_cache_stats` | Entries, hits, breakdown by upstream/model |
|
|
234
|
+
| `clear_cache` | Empty the cache (`confirm='FORCE_DELETE'`) |
|
|
235
|
+
| `normalize_preview` | Preview of normalization + cache key |
|
|
236
|
+
| `search_cached_prompts` | Search in normalized prompts |
|
|
237
|
+
| **`proxy_tool`** | **Call a tool on a target MCP server** (tool calling) |
|
|
238
|
+
| **`proxy_tools`** | **Batch call of target tools** |
|
|
239
|
+
| **`list_proxy_servers`** | **Routable MCP servers** |
|
|
240
|
+
| **`list_proxy_tools`** | **Tools exposed by a target server** |
|
|
241
|
+
| `get_tool_cache_stats` | Tool-cache state + agent events |
|
|
242
|
+
| `clear_tool_cache` | Empty the tool cache (`confirm='FORCE_DELETE'`) |
|
|
243
|
+
| `check_tool_loop` | Detect an agent loop in an LLM history |
|
|
244
|
+
|
|
245
|
+
### Gateway server (`src/mcp_gateway.py`) — 11 tools
|
|
246
|
+
|
|
247
|
+
The 7 observability tools (`get_cache_stats`, `clear_cache`, `normalize_preview`,
|
|
248
|
+
`search_cached_prompts`, `get_realtime_tokens`, `list_upstreams`, `tool_cache_stats`)
|
|
249
|
+
**plus** the 4 relay tools (`proxy_tool`, `proxy_tools`, `list_proxy_servers`,
|
|
250
|
+
`list_proxy_tools`).
|
|
251
|
+
|
|
252
|
+
MCP is a **facade**: it observes and drives. It is not in the request path
|
|
253
|
+
(an MCP client calls its tools deliberately — you cannot intercept an
|
|
254
|
+
OpenAI/Anthropic stream with it). The request path is the HTTP gateway.
|
|
255
|
+
|
|
256
|
+
Agent loops: `icy_core.llm_loop_check()` detects the same tool called again with the same
|
|
257
|
+
arguments **and** the same results (Anthropic and OpenAI formats). Threshold: 3 repetitions = warning.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Tool calling: safety rules
|
|
262
|
+
|
|
263
|
+
The tool proxy (`src/tool_proxy.py`) relays calls to target MCP servers.
|
|
264
|
+
Three non-negotiable rules:
|
|
265
|
+
|
|
266
|
+
1. **An MCP error is never cached** and never returns `ok: true`.
|
|
267
|
+
An `isError: true` becomes `ok: false` with the real message (no opaque `ExceptionGroup`).
|
|
268
|
+
2. **Write tools are never cached.** Replaying a cached response
|
|
269
|
+
must have no side effect; otherwise a write is silently skipped.
|
|
270
|
+
- `cache_tools` present → **strict allowlist** (only tools listed as `true` are cached).
|
|
271
|
+
- otherwise `cache: true` → **anti-write heuristic** (`create_`, `write_`, `delete_`, `push_`, … excluded).
|
|
272
|
+
- otherwise → no cache.
|
|
273
|
+
3. **MCP sessions are persistent** (one process per server, not per call).
|
|
274
|
+
Without this, every tool call respawned a process: 21s measured on `npx`, 12s on `uvx`.
|
|
275
|
+
|
|
276
|
+
### Example
|
|
277
|
+
|
|
278
|
+
```json
|
|
279
|
+
// via any MCP client
|
|
280
|
+
{"server": "github", "tool": "search_repositories", "args": {"query": "icycache"}}
|
|
281
|
+
// -> {"ok": true, "cached": false, "latency_ms": 4867, "result": {...}}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Target server configuration
|
|
285
|
+
|
|
286
|
+
`config/icycache.json` → `tool_proxy.servers`. Two portability tokens:
|
|
287
|
+
|
|
288
|
+
| Token | Value |
|
|
289
|
+
|-------|-------|
|
|
290
|
+
| `{PROJECT_ROOT}` | project root (moving IcyCache needs no edit) |
|
|
291
|
+
| `{PYTHON}` | project venv interpreter (the only one with `fastapi`/`mcp`) |
|
|
292
|
+
|
|
293
|
+
On Windows, `.cmd` commands (like `npx`) are resolved automatically:
|
|
294
|
+
`CreateProcess` cannot launch an `npx.cmd` script without a shell.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Enforcement: making bypass impossible
|
|
299
|
+
|
|
300
|
+
```powershell
|
|
301
|
+
# 1. Install the rules (ELEVATED PowerShell)
|
|
302
|
+
powershell -ExecutionPolicy Bypass -File tools\enforce_gateway.ps1 -Action apply `
|
|
303
|
+
-TargetProgram "C:\Users\Tech\AppData\Local\hermes\hermes-agent\apps\desktop\release\win-unpacked\Hermes.exe" `
|
|
304
|
+
-AllowPort 8080 -BlockPorts 20128,1234,11434
|
|
305
|
+
|
|
306
|
+
# 2. The proof: can the client still reach an upstream directly?
|
|
307
|
+
python tools\verify_enforcement.py --program "D:\IcyCache\venv\Scripts\python.exe" --allow-port 8080
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Strategy (Windows Firewall, per program): block upstream local ports **including on
|
|
311
|
+
127.0.0.1**, block all remote traffic, explicitly allow `127.0.0.1:8080`.
|
|
312
|
+
The `verify_enforcement.py` program runs **with the client binary** (rules are per program)
|
|
313
|
+
and checks that only IcyCache remains reachable.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Environment variables (unified naming)
|
|
318
|
+
|
|
319
|
+
| Variable | Role | Old name (still read by `src/mcp_server.py` for `ICYCACHE_*`) |
|
|
320
|
+
|----------|------|----------------------------------------------------------|
|
|
321
|
+
| `ICYCACHE_DB` | Cache DB path | `CACHEFORGE_DB` |
|
|
322
|
+
| `ICYCACHE_LOG` | Log path | `CACHEFORGE_LOG` |
|
|
323
|
+
| `ICYCACHE_CONFIG` | Config file (gateway) | — |
|
|
324
|
+
| `ICYCACHE_TARGET` | Upstream override (isolated tests) | `CACHEFORGE_TARGET` |
|
|
325
|
+
| `ICYCACHE_TOOLPROXY_CONFIG` | Tool-proxy config (default: `config/icycache.json`) | — |
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Installation
|
|
330
|
+
|
|
331
|
+
> **The registry publication is pending.** The repository, the PyPI distribution and the npm package
|
|
332
|
+
> all carry the same name — **`icycache`** — and the packages are built, tagged and ready to publish
|
|
333
|
+
> (`v0.3.2`). The names were free on both registries, and `icycache-mcp` is the second binary the
|
|
334
|
+
> npm package exposes.
|
|
335
|
+
>
|
|
336
|
+
> The registry commands below are the final ones, but they return a 404 until the first publish.
|
|
337
|
+
> Install from source in the meantime:
|
|
338
|
+
> `pip install "icycache @ git+https://github.com/MlkBlk-IT/icycache.git"`.
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
uv tool install icycache # or: pip install icycache
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
| Registry | Package | Page |
|
|
345
|
+
|---|---|---|
|
|
346
|
+
| PyPI | `icycache` | [pypi.org/project/icycache](https://pypi.org/project/icycache/) |
|
|
347
|
+
| npm | `icycache` | [npmjs.com/package/icycache](https://www.npmjs.com/package/icycache) |
|
|
348
|
+
|
|
349
|
+
This gives you the `icycache` console script:
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
icycache serve # the gateway, http://127.0.0.1:8080
|
|
353
|
+
icycache mcp # the MCP layer (SSE :8081 / HTTP :8084 / stdio)
|
|
354
|
+
icycache doctor # diagnostic: paths, config, upstreams, database
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Installed packages never write into `site-packages`: `db/` and `logs/` go to
|
|
358
|
+
`%LOCALAPPDATA%\IcyCache` (Windows) or `~/.local/share/IcyCache`.
|
|
359
|
+
Full guide: [`docs/INSTALL.md`](docs/INSTALL.md).
|
|
360
|
+
|
|
361
|
+
### npm (Node-only clients)
|
|
362
|
+
|
|
363
|
+
The npm package is `icycache` — the same name as the repository. It exposes two binaries:
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
npx -y icycache --help # the CLI (serve / mcp / doctor)
|
|
367
|
+
npx -y -p icycache icycache-mcp # MCP server on stdio
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### From source (Windows)
|
|
371
|
+
|
|
372
|
+
```powershell
|
|
373
|
+
# 1. Environment (Python 3.12.10)
|
|
374
|
+
python -m venv venv
|
|
375
|
+
.\venv\Scripts\Activate.ps1
|
|
376
|
+
pip install -e ".[dev]"
|
|
377
|
+
|
|
378
|
+
# 2. Manual start
|
|
379
|
+
.\start_icycache.ps1
|
|
380
|
+
|
|
381
|
+
# 3. Start at login (Task Scheduler, or Startup fallback without admin)
|
|
382
|
+
.\install_autostart.ps1
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Build the packages yourself
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
python -m build # -> dist/icycache-<version>-py3-none-any.whl + .tar.gz
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
> Building and publishing this package: see [`PUBLISHING.md`](PUBLISHING.md).
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Tests
|
|
396
|
+
|
|
397
|
+
```powershell
|
|
398
|
+
# Isolated suite (mock upstream, temp DB, no port occupied)
|
|
399
|
+
.\venv\Scripts\python.exe -m pytest tests\test_gateway.py -v
|
|
400
|
+
|
|
401
|
+
# Isolated integration suite (temp DB + fake upstream :8190/:8191)
|
|
402
|
+
.\venv\Scripts\python.exe tests\run_isolated.py
|
|
403
|
+
|
|
404
|
+
# Offline PrefixProxy test (fake provider, no external call)
|
|
405
|
+
.\venv\Scripts\python.exe tests\test_prefix_proxy.py
|
|
406
|
+
|
|
407
|
+
# Real smoke test (LM Studio / OmniRoute, port 8090, temp DB)
|
|
408
|
+
.\venv\Scripts\python.exe tools\smoke_gateway.py --port 8090
|
|
409
|
+
|
|
410
|
+
# DELTA-002 suite (semantic cache, upstream recovery, normalization anchors)
|
|
411
|
+
.\venv\Scripts\python.exe -m pytest tests\test_semantic_cache.py tests\test_upstream_recovery.py tests\test_anchors.py -v
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
The smoke test touches **neither** the production service (`:8080`) **nor** the dashboard.
|
|
415
|
+
`tests\run_isolated.py` uses a **temporary** database (`ICYCACHE_DB`) and never touches
|
|
416
|
+
the production database.
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## License
|
|
421
|
+
|
|
422
|
+
MIT
|