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.
Files changed (54) hide show
  1. contextforge_mcp-0.2.1/PKG-INFO +130 -0
  2. contextforge_mcp-0.2.1/contextforge-mcp-v2/contextforge-mcp/README.md +108 -0
  3. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/pyproject.toml +1 -1
  4. contextforge_mcp-0.2.1/src/contextforge_mcp/__init__.py +3 -0
  5. contextforge_mcp-0.2.1/src/contextforge_mcp/cli.py +190 -0
  6. contextforge_mcp-0.2.1/src/contextforge_mcp/compressor.py +200 -0
  7. contextforge_mcp-0.2.1/src/contextforge_mcp/server.py +250 -0
  8. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/src/contextforge_mcp/speckit.py +23 -14
  9. contextforge_mcp-0.2.1/tests/test_compressor.py +117 -0
  10. contextforge_mcp-0.2.1/tests/test_server.py +113 -0
  11. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/tests/test_speckit.py +21 -20
  12. contextforge_mcp-0.2.0/PKG-INFO +0 -119
  13. contextforge_mcp-0.2.0/README.md +0 -97
  14. contextforge_mcp-0.2.0/src/contextforge_mcp/__init__.py +0 -3
  15. contextforge_mcp-0.2.0/src/contextforge_mcp/cli.py +0 -127
  16. contextforge_mcp-0.2.0/src/contextforge_mcp/compressor.py +0 -138
  17. contextforge_mcp-0.2.0/src/contextforge_mcp/server.py +0 -203
  18. contextforge_mcp-0.2.0/tests/test_compressor.py +0 -100
  19. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/.github/workflows/ci.yml +0 -0
  20. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/.github/workflows/publish.yml +0 -0
  21. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/.gitignore +0 -0
  22. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/LICENSE +0 -0
  23. {contextforge_mcp-0.2.0/contextforge-mcp-v2/contextforge-mcp → contextforge_mcp-0.2.1}/README.md +0 -0
  24. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/.github/workflows/ci.yml +0 -0
  25. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/.github/workflows/publish.yml +0 -0
  26. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/.gitignore +0 -0
  27. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/LICENSE +0 -0
  28. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/extensions/speckit/commands/cf-analyze.md +0 -0
  29. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/extensions/speckit/commands/cf-implement.md +0 -0
  30. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/extensions/speckit/commands/cf-index.md +0 -0
  31. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/extensions/speckit/commands/cf-stats.md +0 -0
  32. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/extensions/speckit/extension.json +0 -0
  33. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/extensions/speckit/extension.yml +0 -0
  34. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/extensions/speckit/templates/CONTEXTFORGE.md +0 -0
  35. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/pyproject.toml +0 -0
  36. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/src/contextforge_mcp/__init__.py +0 -0
  37. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/src/contextforge_mcp/cli.py +0 -0
  38. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/src/contextforge_mcp/compressor.py +0 -0
  39. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/src/contextforge_mcp/server.py +0 -0
  40. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/src/contextforge_mcp/speckit.py +0 -0
  41. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/tests/__init__.py +0 -0
  42. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/tests/test_compressor.py +0 -0
  43. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/tests/test_server.py +0 -0
  44. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/contextforge-mcp-v2/contextforge-mcp/tests/test_speckit.py +0 -0
  45. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/extensions/speckit/commands/cf-analyze.md +0 -0
  46. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/extensions/speckit/commands/cf-implement.md +0 -0
  47. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/extensions/speckit/commands/cf-index.md +0 -0
  48. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/extensions/speckit/commands/cf-stats.md +0 -0
  49. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/extensions/speckit/extension.json +0 -0
  50. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/extensions/speckit/extension.yml +0 -0
  51. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/extensions/speckit/templates/CONTEXTFORGE.md +0 -0
  52. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/src/contextforge_mcp/cbm_client.py +0 -0
  53. {contextforge_mcp-0.2.0 → contextforge_mcp-0.2.1}/tests/__init__.py +0 -0
  54. {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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "contextforge-mcp"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "MCP orchestrator: codebase-memory-mcp + headroom + Spec Kit"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -0,0 +1,3 @@
1
+ """contextforge-mcp — MCP compression middleware for codebase-memory-mcp + Spec Kit."""
2
+
3
+ __version__ = "0.2.0"
@@ -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