contextforge-mcp 0.2.0__tar.gz → 0.2.1__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.
- contextforge_mcp-0.2.1/PKG-INFO +130 -0
- contextforge_mcp-0.2.1/contextforge-mcp-v2/contextforge-mcp/README.md +108 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/pyproject.toml +1 -1
- contextforge_mcp-0.2.1/src/contextforge_mcp/__init__.py +3 -0
- contextforge_mcp-0.2.1/src/contextforge_mcp/cli.py +190 -0
- contextforge_mcp-0.2.1/src/contextforge_mcp/compressor.py +200 -0
- contextforge_mcp-0.2.1/src/contextforge_mcp/server.py +250 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/src/contextforge_mcp/speckit.py +23 -14
- contextforge_mcp-0.2.1/tests/test_compressor.py +117 -0
- contextforge_mcp-0.2.1/tests/test_server.py +113 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/tests/test_speckit.py +21 -20
- contextforge_mcp-0.2.0/PKG-INFO +0 -119
- contextforge_mcp-0.2.0/README.md +0 -97
- contextforge_mcp-0.2.0/src/contextforge_mcp/__init__.py +0 -3
- contextforge_mcp-0.2.0/src/contextforge_mcp/cli.py +0 -127
- contextforge_mcp-0.2.0/src/contextforge_mcp/compressor.py +0 -138
- contextforge_mcp-0.2.0/src/contextforge_mcp/server.py +0 -203
- contextforge_mcp-0.2.0/tests/test_compressor.py +0 -100
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/.github/workflows/ci.yml +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/.github/workflows/publish.yml +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/.gitignore +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/LICENSE +0 -0
- {contextforge_mcp-0.2.0/contextforge-mcp-v2/contextforge-mcp → contextforge_mcp-0.2.1}/README.md +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/.github/workflows/ci.yml +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/.github/workflows/publish.yml +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/.gitignore +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/LICENSE +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/extensions/speckit/commands/cf-analyze.md +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/extensions/speckit/commands/cf-implement.md +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/extensions/speckit/commands/cf-index.md +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/extensions/speckit/commands/cf-stats.md +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/extensions/speckit/extension.json +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/extensions/speckit/extension.yml +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/extensions/speckit/templates/CONTEXTFORGE.md +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/pyproject.toml +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/src/contextforge_mcp/__init__.py +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/src/contextforge_mcp/cli.py +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/src/contextforge_mcp/compressor.py +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/src/contextforge_mcp/server.py +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/src/contextforge_mcp/speckit.py +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/tests/__init__.py +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/tests/test_compressor.py +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/tests/test_server.py +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/tests/test_speckit.py +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/extensions/speckit/commands/cf-analyze.md +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/extensions/speckit/commands/cf-implement.md +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/extensions/speckit/commands/cf-index.md +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/extensions/speckit/commands/cf-stats.md +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/extensions/speckit/extension.json +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/extensions/speckit/extension.yml +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/extensions/speckit/templates/CONTEXTFORGE.md +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/src/contextforge_mcp/cbm_client.py +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/tests/__init__.py +0 -0
- {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/tests/test_cbm_client.py +0 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: contextforge-mcp
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: MCP orchestrator: codebase-memory-mcp + headroom + Spec Kit
|
|
5
|
+
Project-URL: Repository, https://github.com/capatinore/contextforge-mcp
|
|
6
|
+
Author-email: Camilo Patiño <capatinore@github.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: ai-agents,code-intelligence,compression,context,llm,mcp,spec-kit
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Requires-Dist: anyio>=4.0.0
|
|
12
|
+
Requires-Dist: headroom-ai>=0.9.0
|
|
13
|
+
Requires-Dist: httpx>=0.27.0
|
|
14
|
+
Requires-Dist: mcp>=1.0.0
|
|
15
|
+
Requires-Dist: pydantic>=2.0.0
|
|
16
|
+
Requires-Dist: rich>=13.0.0
|
|
17
|
+
Requires-Dist: typer>=0.12.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# contextforge-mcp
|
|
24
|
+
|
|
25
|
+
**MCP compression middleware** that connects [codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp) + [headroom](https://github.com/headroomlabs-ai/headroom) + [Spec Kit](https://github.com/github/spec-kit) into a unified, token-efficient workflow.
|
|
26
|
+
|
|
27
|
+
## Architecture
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
Claude Code
|
|
31
|
+
├── codebase-memory-mcp ← graph queries (cbm_* tools)
|
|
32
|
+
│ │
|
|
33
|
+
│ └── large result
|
|
34
|
+
│ │
|
|
35
|
+
└── contextforge-mcp ← YOU ARE HERE
|
|
36
|
+
│
|
|
37
|
+
cf_compress_cbm(result, tool_name)
|
|
38
|
+
│
|
|
39
|
+
└── compressed result (60-95% fewer tokens)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**ContextForge does NOT proxy codebase-memory-mcp** — both servers run independently. The agent calls CBM for graph queries, then passes results through ContextForge for compression. This design is reliable, cross-platform, and works with any MCP client.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install -g codebase-memory-mcp
|
|
48
|
+
pip install "headroom-ai[all]"
|
|
49
|
+
pip install contextforge-mcp
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Setup
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Health check
|
|
56
|
+
contextforge-mcp doctor
|
|
57
|
+
|
|
58
|
+
# Configure Claude Code (writes .mcp.json with both servers)
|
|
59
|
+
contextforge-mcp install --target claude
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Workflow
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
# 1. Query the graph (via codebase-memory-mcp)
|
|
66
|
+
result = cbm_search_graph(name_pattern=".*Payment.*", label="Function")
|
|
67
|
+
|
|
68
|
+
# 2. Compress the result (via contextforge-mcp)
|
|
69
|
+
compressed = cf_compress_cbm(result=result, tool_name="search_graph")
|
|
70
|
+
# → [ContextForge ✓ search_graph: 8420→612 tokens (93% saved in 45ms)]
|
|
71
|
+
|
|
72
|
+
# 3. Use compressed result in your context
|
|
73
|
+
# 4. Check savings
|
|
74
|
+
cf_stats()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Tools (9 total)
|
|
78
|
+
|
|
79
|
+
### Compression
|
|
80
|
+
| Tool | Description |
|
|
81
|
+
|------|-------------|
|
|
82
|
+
| `cf_compress_cbm(result, tool_name)` | Compress CBM tool output |
|
|
83
|
+
| `cf_compress(text, hint)` | Compress arbitrary text |
|
|
84
|
+
|
|
85
|
+
### Stats
|
|
86
|
+
| Tool | Description |
|
|
87
|
+
|------|-------------|
|
|
88
|
+
| `cf_stats()` | Session token savings + cost estimate |
|
|
89
|
+
| `cf_reset_stats()` | Reset session counters |
|
|
90
|
+
|
|
91
|
+
### Spec Kit
|
|
92
|
+
| Tool | Description |
|
|
93
|
+
|------|-------------|
|
|
94
|
+
| `cf_read_spec(feature_id)` | Compressed spec.md |
|
|
95
|
+
| `cf_read_plan(feature_id)` | Compressed plan.md |
|
|
96
|
+
| `cf_read_tasks(feature_id)` | Compressed tasks.md |
|
|
97
|
+
| `cf_read_artifact(artifact, feature_id)` | Any artifact |
|
|
98
|
+
| `cf_implement_context(feature_id)` | Full bundle (spec+plan+tasks) |
|
|
99
|
+
| `cf_speckit_status()` | List all features + phase |
|
|
100
|
+
|
|
101
|
+
## Supported CBM tool names for cf_compress_cbm
|
|
102
|
+
|
|
103
|
+
`search_graph` · `search_code` · `get_architecture` · `find_dead_code` · `find_similar_code` · `get_impact` · `trace_path` · `trace_call_path` · `cypher_query` · `get_cross_service_links` · `get_node_details`
|
|
104
|
+
|
|
105
|
+
## Add to CLAUDE.md
|
|
106
|
+
|
|
107
|
+
```markdown
|
|
108
|
+
## ContextForge MCP — Compression Workflow
|
|
109
|
+
|
|
110
|
+
After EVERY codebase-memory-mcp tool call that returns a large result,
|
|
111
|
+
immediately call cf_compress_cbm(result, tool_name) to compress it.
|
|
112
|
+
|
|
113
|
+
| CBM Query | Then compress with |
|
|
114
|
+
|-----------|-------------------|
|
|
115
|
+
| cbm_search_graph(…) | cf_compress_cbm(result, "search_graph") |
|
|
116
|
+
| cbm_get_architecture() | cf_compress_cbm(result, "get_architecture") |
|
|
117
|
+
| cbm_search_code(…) | cf_compress_cbm(result, "search_code") |
|
|
118
|
+
| cbm_trace_path(…) | cf_compress_cbm(result, "trace_path") |
|
|
119
|
+
| cbm_get_impact(…) | cf_compress_cbm(result, "get_impact") |
|
|
120
|
+
|
|
121
|
+
Call cf_stats() at end of session to measure total savings.
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Credits
|
|
125
|
+
- [codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp) — MIT
|
|
126
|
+
- [headroom](https://github.com/headroomlabs-ai/headroom) — Apache 2.0
|
|
127
|
+
- [spec-kit](https://github.com/github/spec-kit) — MIT
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
MIT
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# contextforge-mcp
|
|
2
|
+
|
|
3
|
+
**MCP compression middleware** that connects [codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp) + [headroom](https://github.com/headroomlabs-ai/headroom) + [Spec Kit](https://github.com/github/spec-kit) into a unified, token-efficient workflow.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Claude Code
|
|
9
|
+
├── codebase-memory-mcp ← graph queries (cbm_* tools)
|
|
10
|
+
│ │
|
|
11
|
+
│ └── large result
|
|
12
|
+
│ │
|
|
13
|
+
└── contextforge-mcp ← YOU ARE HERE
|
|
14
|
+
│
|
|
15
|
+
cf_compress_cbm(result, tool_name)
|
|
16
|
+
│
|
|
17
|
+
└── compressed result (60-95% fewer tokens)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**ContextForge does NOT proxy codebase-memory-mcp** — both servers run independently. The agent calls CBM for graph queries, then passes results through ContextForge for compression. This design is reliable, cross-platform, and works with any MCP client.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -g codebase-memory-mcp
|
|
26
|
+
pip install "headroom-ai[all]"
|
|
27
|
+
pip install contextforge-mcp
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Setup
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Health check
|
|
34
|
+
contextforge-mcp doctor
|
|
35
|
+
|
|
36
|
+
# Configure Claude Code (writes .mcp.json with both servers)
|
|
37
|
+
contextforge-mcp install --target claude
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Workflow
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
# 1. Query the graph (via codebase-memory-mcp)
|
|
44
|
+
result = cbm_search_graph(name_pattern=".*Payment.*", label="Function")
|
|
45
|
+
|
|
46
|
+
# 2. Compress the result (via contextforge-mcp)
|
|
47
|
+
compressed = cf_compress_cbm(result=result, tool_name="search_graph")
|
|
48
|
+
# → [ContextForge ✓ search_graph: 8420→612 tokens (93% saved in 45ms)]
|
|
49
|
+
|
|
50
|
+
# 3. Use compressed result in your context
|
|
51
|
+
# 4. Check savings
|
|
52
|
+
cf_stats()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Tools (9 total)
|
|
56
|
+
|
|
57
|
+
### Compression
|
|
58
|
+
| Tool | Description |
|
|
59
|
+
|------|-------------|
|
|
60
|
+
| `cf_compress_cbm(result, tool_name)` | Compress CBM tool output |
|
|
61
|
+
| `cf_compress(text, hint)` | Compress arbitrary text |
|
|
62
|
+
|
|
63
|
+
### Stats
|
|
64
|
+
| Tool | Description |
|
|
65
|
+
|------|-------------|
|
|
66
|
+
| `cf_stats()` | Session token savings + cost estimate |
|
|
67
|
+
| `cf_reset_stats()` | Reset session counters |
|
|
68
|
+
|
|
69
|
+
### Spec Kit
|
|
70
|
+
| Tool | Description |
|
|
71
|
+
|------|-------------|
|
|
72
|
+
| `cf_read_spec(feature_id)` | Compressed spec.md |
|
|
73
|
+
| `cf_read_plan(feature_id)` | Compressed plan.md |
|
|
74
|
+
| `cf_read_tasks(feature_id)` | Compressed tasks.md |
|
|
75
|
+
| `cf_read_artifact(artifact, feature_id)` | Any artifact |
|
|
76
|
+
| `cf_implement_context(feature_id)` | Full bundle (spec+plan+tasks) |
|
|
77
|
+
| `cf_speckit_status()` | List all features + phase |
|
|
78
|
+
|
|
79
|
+
## Supported CBM tool names for cf_compress_cbm
|
|
80
|
+
|
|
81
|
+
`search_graph` · `search_code` · `get_architecture` · `find_dead_code` · `find_similar_code` · `get_impact` · `trace_path` · `trace_call_path` · `cypher_query` · `get_cross_service_links` · `get_node_details`
|
|
82
|
+
|
|
83
|
+
## Add to CLAUDE.md
|
|
84
|
+
|
|
85
|
+
```markdown
|
|
86
|
+
## ContextForge MCP — Compression Workflow
|
|
87
|
+
|
|
88
|
+
After EVERY codebase-memory-mcp tool call that returns a large result,
|
|
89
|
+
immediately call cf_compress_cbm(result, tool_name) to compress it.
|
|
90
|
+
|
|
91
|
+
| CBM Query | Then compress with |
|
|
92
|
+
|-----------|-------------------|
|
|
93
|
+
| cbm_search_graph(…) | cf_compress_cbm(result, "search_graph") |
|
|
94
|
+
| cbm_get_architecture() | cf_compress_cbm(result, "get_architecture") |
|
|
95
|
+
| cbm_search_code(…) | cf_compress_cbm(result, "search_code") |
|
|
96
|
+
| cbm_trace_path(…) | cf_compress_cbm(result, "trace_path") |
|
|
97
|
+
| cbm_get_impact(…) | cf_compress_cbm(result, "get_impact") |
|
|
98
|
+
|
|
99
|
+
Call cf_stats() at end of session to measure total savings.
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Credits
|
|
103
|
+
- [codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp) — MIT
|
|
104
|
+
- [headroom](https://github.com/headroomlabs-ai/headroom) — Apache 2.0
|
|
105
|
+
- [spec-kit](https://github.com/github/spec-kit) — MIT
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
MIT
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""ContextForge MCP CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(
|
|
15
|
+
name="contextforge-mcp",
|
|
16
|
+
help="MCP compression middleware: codebase-memory-mcp + headroom + Spec Kit",
|
|
17
|
+
no_args_is_help=True,
|
|
18
|
+
)
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@app.command()
|
|
23
|
+
def run() -> None:
|
|
24
|
+
"""Start ContextForge as a stdio MCP server."""
|
|
25
|
+
from .server import main
|
|
26
|
+
main()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.command()
|
|
30
|
+
def doctor() -> None:
|
|
31
|
+
"""Check all dependencies."""
|
|
32
|
+
console.rule("[bold]ContextForge MCP — Doctor[/bold]")
|
|
33
|
+
ok = True
|
|
34
|
+
|
|
35
|
+
major, minor = sys.version_info[:2]
|
|
36
|
+
if major == 3 and minor >= 10:
|
|
37
|
+
console.print(f" ✅ Python {major}.{minor}")
|
|
38
|
+
else:
|
|
39
|
+
console.print(f" ❌ Python {major}.{minor} — need 3.10+")
|
|
40
|
+
ok = False
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
import mcp
|
|
44
|
+
console.print(" ✅ mcp (Model Context Protocol SDK)")
|
|
45
|
+
except ImportError:
|
|
46
|
+
console.print(" ❌ mcp — run: pip install mcp")
|
|
47
|
+
ok = False
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
import headroom
|
|
51
|
+
console.print(" ✅ headroom-ai (compression enabled)")
|
|
52
|
+
except ImportError:
|
|
53
|
+
console.print(" ⚠️ headroom-ai not installed — compression disabled")
|
|
54
|
+
console.print(" Fix: pip install 'headroom-ai[all]'")
|
|
55
|
+
|
|
56
|
+
cbm = shutil.which("codebase-memory-mcp") or shutil.which("codebase-memory-mcp.exe")
|
|
57
|
+
if cbm:
|
|
58
|
+
console.print(f" ✅ codebase-memory-mcp → {cbm}")
|
|
59
|
+
else:
|
|
60
|
+
console.print(" ❌ codebase-memory-mcp not found")
|
|
61
|
+
console.print(" Fix: npm install -g codebase-memory-mcp")
|
|
62
|
+
ok = False
|
|
63
|
+
|
|
64
|
+
specify = shutil.which("specify")
|
|
65
|
+
if specify:
|
|
66
|
+
console.print(f" ✅ specify (Spec Kit) → {specify}")
|
|
67
|
+
else:
|
|
68
|
+
console.print(" ⚠️ specify not found — optional")
|
|
69
|
+
console.print(" Fix: pip install specify")
|
|
70
|
+
|
|
71
|
+
console.print()
|
|
72
|
+
if ok:
|
|
73
|
+
console.print("[green]✅ Everything looks good![/green]")
|
|
74
|
+
console.print()
|
|
75
|
+
console.print("[dim]Workflow:[/dim]")
|
|
76
|
+
console.print(" 1. Add contextforge-mcp to .mcp.json")
|
|
77
|
+
console.print(" 2. In Claude Code: cbm_search_graph(...) → cf_compress_cbm(result, 'search_graph')")
|
|
78
|
+
console.print(" 3. cf_stats() to see token savings")
|
|
79
|
+
else:
|
|
80
|
+
console.print("[yellow]⚠️ Some dependencies missing. See above.[/yellow]")
|
|
81
|
+
raise typer.Exit(code=1)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@app.command()
|
|
85
|
+
def install(
|
|
86
|
+
target: str = typer.Option("claude", help="claude | speckit | all"),
|
|
87
|
+
project_dir: Path = typer.Option(Path("."), help="Project directory"),
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Configure ContextForge for Claude Code and/or Spec Kit."""
|
|
90
|
+
|
|
91
|
+
if target in ("claude", "all"):
|
|
92
|
+
_install_claude(project_dir)
|
|
93
|
+
|
|
94
|
+
if target in ("speckit", "all"):
|
|
95
|
+
_install_speckit(project_dir)
|
|
96
|
+
|
|
97
|
+
if target not in ("claude", "speckit", "all"):
|
|
98
|
+
console.print(f"[yellow]Unknown target: {target!r}. Use: claude | speckit | all[/yellow]")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _install_claude(project_dir: Path) -> None:
|
|
102
|
+
"""Write .mcp.json with both codebase-memory-mcp and contextforge-mcp."""
|
|
103
|
+
import shutil as sh
|
|
104
|
+
|
|
105
|
+
mcp_path = project_dir / ".mcp.json"
|
|
106
|
+
config: dict = {}
|
|
107
|
+
if mcp_path.exists():
|
|
108
|
+
try:
|
|
109
|
+
config = json.loads(mcp_path.read_text())
|
|
110
|
+
except json.JSONDecodeError:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
config.setdefault("mcpServers", {})
|
|
114
|
+
|
|
115
|
+
# Add codebase-memory-mcp if not already present
|
|
116
|
+
cbm_bin = sh.which("codebase-memory-mcp") or "codebase-memory-mcp"
|
|
117
|
+
if "codebase-memory-mcp" not in config["mcpServers"]:
|
|
118
|
+
config["mcpServers"]["codebase-memory-mcp"] = {
|
|
119
|
+
"command": cbm_bin,
|
|
120
|
+
"args": [],
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Add/update contextforge-mcp
|
|
124
|
+
config["mcpServers"]["contextforge-mcp"] = {
|
|
125
|
+
"command": "contextforge-mcp",
|
|
126
|
+
"args": ["run"],
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Remove old contextforge entry if present
|
|
130
|
+
config["mcpServers"].pop("contextforge", None)
|
|
131
|
+
|
|
132
|
+
mcp_path.write_text(json.dumps(config, indent=2))
|
|
133
|
+
console.print(f"✅ Updated {mcp_path}")
|
|
134
|
+
console.print()
|
|
135
|
+
console.print("[bold]Two MCP servers configured:[/bold]")
|
|
136
|
+
console.print(" codebase-memory-mcp → graph queries (cbm_* tools)")
|
|
137
|
+
console.print(" contextforge-mcp → compression middleware (cf_* tools)")
|
|
138
|
+
console.print()
|
|
139
|
+
console.print("[dim]Workflow in Claude Code:[/dim]")
|
|
140
|
+
console.print(" result = cbm_search_graph(name_pattern='.*Payment.*')")
|
|
141
|
+
console.print(" compressed = cf_compress_cbm(result=result, tool_name='search_graph')")
|
|
142
|
+
console.print(" cf_stats() # see token savings")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _install_speckit(project_dir: Path) -> None:
|
|
146
|
+
import shutil as sh
|
|
147
|
+
pkg_dir = Path(__file__).parent.parent.parent
|
|
148
|
+
ext_src = pkg_dir / "extensions" / "speckit"
|
|
149
|
+
|
|
150
|
+
if not ext_src.exists():
|
|
151
|
+
console.print("[yellow]Extension source not found — clone the repo and run from there.[/yellow]")
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
ext_dst = project_dir / ".specify" / "extensions" / "contextforge-mcp"
|
|
155
|
+
ext_dst.mkdir(parents=True, exist_ok=True)
|
|
156
|
+
sh.copytree(ext_src, ext_dst, dirs_exist_ok=True)
|
|
157
|
+
console.print(f"✅ Spec Kit extension → {ext_dst}")
|
|
158
|
+
|
|
159
|
+
claude_commands = project_dir / ".claude" / "commands"
|
|
160
|
+
if (project_dir / ".claude").exists():
|
|
161
|
+
claude_commands.mkdir(parents=True, exist_ok=True)
|
|
162
|
+
for f in (ext_src / "commands").glob("*.md"):
|
|
163
|
+
sh.copy2(f, claude_commands / f.name)
|
|
164
|
+
console.print(f"✅ Slash commands → {claude_commands}")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@app.command()
|
|
168
|
+
def info() -> None:
|
|
169
|
+
"""Show version and info."""
|
|
170
|
+
from . import __version__
|
|
171
|
+
|
|
172
|
+
table = Table(title="ContextForge MCP", show_header=False)
|
|
173
|
+
table.add_column("Key", style="bold cyan")
|
|
174
|
+
table.add_column("Value")
|
|
175
|
+
table.add_row("Version", __version__)
|
|
176
|
+
table.add_row("Architecture", "Middleware — runs alongside codebase-memory-mcp")
|
|
177
|
+
table.add_row("Tools", "cf_compress_cbm, cf_compress, cf_stats + Spec Kit tools")
|
|
178
|
+
table.add_row("", "")
|
|
179
|
+
table.add_row("Built on", "")
|
|
180
|
+
table.add_row(" codebase-memory-mcp", "github.com/DeusData/codebase-memory-mcp")
|
|
181
|
+
table.add_row(" headroom-ai", "github.com/headroomlabs-ai/headroom")
|
|
182
|
+
table.add_row(" spec-kit", "github.com/github/spec-kit")
|
|
183
|
+
table.add_row("", "")
|
|
184
|
+
table.add_row("Repo", "github.com/capatinore/contextforge-mcp")
|
|
185
|
+
table.add_row("PyPI", "pip install contextforge-mcp")
|
|
186
|
+
console.print(table)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
if __name__ == "__main__":
|
|
190
|
+
app()
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Headroom compression layer with per-tool profiles and session stats."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# Tools that return large payloads — compress aggressively
|
|
14
|
+
COMPRESS_PROFILES: dict[str, dict[str, Any]] = {
|
|
15
|
+
"search_graph": {"max_items": 50},
|
|
16
|
+
"search_code": {"max_items": 30},
|
|
17
|
+
"get_architecture": {"max_items": 100},
|
|
18
|
+
"find_dead_code": {"max_items": 80},
|
|
19
|
+
"find_similar_code": {"max_items": 40},
|
|
20
|
+
"cypher_query": {"max_items": 60},
|
|
21
|
+
"get_impact": {"max_items": 40},
|
|
22
|
+
"trace_path": {"max_items": 30},
|
|
23
|
+
"trace_call_path": {"max_items": 30},
|
|
24
|
+
"get_cross_service_links": {"max_items": 50},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Tools that return small status payloads — skip compression
|
|
28
|
+
SKIP_COMPRESS: set[str] = {
|
|
29
|
+
"index_repository",
|
|
30
|
+
"get_indexing_status",
|
|
31
|
+
"manage_adr",
|
|
32
|
+
"get_node_details",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class CompressionResult:
|
|
38
|
+
original_tokens: int
|
|
39
|
+
compressed_tokens: int
|
|
40
|
+
tokens_saved: int
|
|
41
|
+
ratio: float
|
|
42
|
+
content: str
|
|
43
|
+
elapsed_ms: float
|
|
44
|
+
skipped: bool = False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class SessionStats:
|
|
49
|
+
total_calls: int = 0
|
|
50
|
+
total_original_tokens: int = 0
|
|
51
|
+
total_compressed_tokens: int = 0
|
|
52
|
+
total_elapsed_ms: float = 0.0
|
|
53
|
+
errors: int = 0
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def tokens_saved(self) -> int:
|
|
57
|
+
return self.total_original_tokens - self.total_compressed_tokens
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def overall_ratio(self) -> float:
|
|
61
|
+
if self.total_original_tokens == 0:
|
|
62
|
+
return 0.0
|
|
63
|
+
return 1.0 - (self.total_compressed_tokens / self.total_original_tokens)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def estimated_cost_saved_usd(self) -> float:
|
|
67
|
+
"""Estimate at $3/M tokens (Claude Sonnet input pricing)."""
|
|
68
|
+
return (self.tokens_saved / 1_000_000) * 3.0
|
|
69
|
+
|
|
70
|
+
def to_dict(self) -> dict[str, Any]:
|
|
71
|
+
return {
|
|
72
|
+
"total_calls": self.total_calls,
|
|
73
|
+
"total_original_tokens": self.total_original_tokens,
|
|
74
|
+
"total_compressed_tokens": self.total_compressed_tokens,
|
|
75
|
+
"tokens_saved": self.tokens_saved,
|
|
76
|
+
"overall_compression_ratio": f"{self.overall_ratio:.1%}",
|
|
77
|
+
"estimated_cost_saved_usd": f"${self.estimated_cost_saved_usd:.4f}",
|
|
78
|
+
"total_elapsed_ms": round(self.total_elapsed_ms, 1),
|
|
79
|
+
"errors": self.errors,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class HeadroomCompressor:
|
|
84
|
+
"""
|
|
85
|
+
Wraps headroom-ai compress() with per-tool profiles and session stats.
|
|
86
|
+
Falls back gracefully when headroom-ai is not installed.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, model: str = "claude-sonnet-4-6"):
|
|
90
|
+
self.model = model
|
|
91
|
+
self.stats = SessionStats()
|
|
92
|
+
self._available = self._check_availability()
|
|
93
|
+
|
|
94
|
+
async def compress(
|
|
95
|
+
self,
|
|
96
|
+
content: Any,
|
|
97
|
+
tool_name: str = "text",
|
|
98
|
+
*,
|
|
99
|
+
force: bool = False,
|
|
100
|
+
) -> CompressionResult:
|
|
101
|
+
"""
|
|
102
|
+
Compress content from a CBM tool or arbitrary text.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
content: Raw result (str, dict, list).
|
|
106
|
+
tool_name: CBM tool name or hint (text/code/json/logs).
|
|
107
|
+
force: Compress even skip-marked tools.
|
|
108
|
+
"""
|
|
109
|
+
t0 = time.perf_counter()
|
|
110
|
+
self.stats.total_calls += 1
|
|
111
|
+
|
|
112
|
+
content_str = content if isinstance(content, str) else json.dumps(content, indent=2)
|
|
113
|
+
|
|
114
|
+
should_skip = (tool_name in SKIP_COMPRESS and not force) or not self._available
|
|
115
|
+
|
|
116
|
+
if should_skip:
|
|
117
|
+
return self._passthrough(content_str, t0)
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
return await self._run_headroom(content_str, tool_name, t0)
|
|
121
|
+
except Exception as exc:
|
|
122
|
+
logger.warning("Headroom compression failed [%s]: %s", tool_name, exc)
|
|
123
|
+
self.stats.errors += 1
|
|
124
|
+
return self._passthrough(content_str, t0)
|
|
125
|
+
|
|
126
|
+
def get_stats(self) -> dict[str, Any]:
|
|
127
|
+
return {
|
|
128
|
+
"headroom_available": self._available,
|
|
129
|
+
"model": self.model,
|
|
130
|
+
**self.stats.to_dict(),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
def reset_stats(self) -> None:
|
|
134
|
+
self.stats = SessionStats()
|
|
135
|
+
|
|
136
|
+
# ------------------------------------------------------------------
|
|
137
|
+
# Internal
|
|
138
|
+
# ------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
def _passthrough(self, content: str, t0: float) -> CompressionResult:
|
|
141
|
+
elapsed = (time.perf_counter() - t0) * 1000
|
|
142
|
+
tokens = len(content.split())
|
|
143
|
+
self.stats.total_original_tokens += tokens
|
|
144
|
+
self.stats.total_compressed_tokens += tokens
|
|
145
|
+
self.stats.total_elapsed_ms += elapsed
|
|
146
|
+
return CompressionResult(
|
|
147
|
+
original_tokens=tokens, compressed_tokens=tokens,
|
|
148
|
+
tokens_saved=0, ratio=0.0,
|
|
149
|
+
content=content, elapsed_ms=elapsed, skipped=True,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
async def _run_headroom(self, content: str, tool_name: str, t0: float) -> CompressionResult:
|
|
153
|
+
import asyncio
|
|
154
|
+
from headroom import compress # type: ignore[import]
|
|
155
|
+
|
|
156
|
+
profile = COMPRESS_PROFILES.get(tool_name, {})
|
|
157
|
+
messages = [{"role": "tool", "tool_call_id": f"cf_{tool_name}", "content": content}]
|
|
158
|
+
|
|
159
|
+
loop = asyncio.get_event_loop()
|
|
160
|
+
result = await loop.run_in_executor(
|
|
161
|
+
None,
|
|
162
|
+
lambda: compress(
|
|
163
|
+
messages,
|
|
164
|
+
model=self.model,
|
|
165
|
+
headroom_tool_profiles={tool_name: profile} if profile else None,
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
elapsed = (time.perf_counter() - t0) * 1000
|
|
170
|
+
compressed_content = content
|
|
171
|
+
if result.messages:
|
|
172
|
+
msg = result.messages[0]
|
|
173
|
+
if isinstance(msg, dict):
|
|
174
|
+
compressed_content = msg.get("content", content)
|
|
175
|
+
|
|
176
|
+
orig = getattr(result, "tokens_before", len(content.split()))
|
|
177
|
+
comp = getattr(result, "tokens_after", len(compressed_content.split()))
|
|
178
|
+
saved = orig - comp
|
|
179
|
+
ratio = saved / orig if orig > 0 else 0.0
|
|
180
|
+
|
|
181
|
+
self.stats.total_original_tokens += orig
|
|
182
|
+
self.stats.total_compressed_tokens += comp
|
|
183
|
+
self.stats.total_elapsed_ms += elapsed
|
|
184
|
+
|
|
185
|
+
logger.info("[%s] %d→%d tokens (%.0f%% saved) %.1fms", tool_name, orig, comp, ratio * 100, elapsed)
|
|
186
|
+
|
|
187
|
+
return CompressionResult(
|
|
188
|
+
original_tokens=orig, compressed_tokens=comp,
|
|
189
|
+
tokens_saved=saved, ratio=ratio,
|
|
190
|
+
content=compressed_content, elapsed_ms=elapsed,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def _check_availability() -> bool:
|
|
195
|
+
try:
|
|
196
|
+
import headroom # noqa: F401
|
|
197
|
+
return True
|
|
198
|
+
except ImportError:
|
|
199
|
+
logger.warning("headroom-ai not installed — compression disabled. pip install 'headroom-ai[all]'")
|
|
200
|
+
return False
|