sharp-context 0.1.0__py3-none-any.whl
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.
- sharp_context/__init__.py +27 -0
- sharp_context/checkpoint.py +295 -0
- sharp_context/config.py +72 -0
- sharp_context/dedup.py +239 -0
- sharp_context/entropy.py +277 -0
- sharp_context/knapsack.py +348 -0
- sharp_context/prefetch.py +297 -0
- sharp_context/server.py +624 -0
- sharp_context-0.1.0.dist-info/METADATA +201 -0
- sharp_context-0.1.0.dist-info/RECORD +12 -0
- sharp_context-0.1.0.dist-info/WHEEL +4 -0
- sharp_context-0.1.0.dist-info/entry_points.txt +2 -0
sharp_context/server.py
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SharpContext MCP Server
|
|
3
|
+
========================
|
|
4
|
+
|
|
5
|
+
The main MCP server that exposes all five mathematical engines
|
|
6
|
+
as callable tools for any MCP-compatible AI coding tool.
|
|
7
|
+
|
|
8
|
+
Architecture mirrors the Ebbiforge Rust core's ContextScorer pipeline:
|
|
9
|
+
1. Fragments ingested → SimHash fingerprinted → dedup checked
|
|
10
|
+
2. Shannon entropy scored → information density computed
|
|
11
|
+
3. Ebbinghaus decay applied per turn → staleness measured
|
|
12
|
+
4. Knapsack DP selects optimal subset within token budget
|
|
13
|
+
5. Predictive pre-fetcher pre-loads likely-needed context
|
|
14
|
+
6. Auto-checkpoint every N tool calls for crash recovery
|
|
15
|
+
|
|
16
|
+
Supported clients:
|
|
17
|
+
- Cursor (add to .cursor/mcp.json)
|
|
18
|
+
- Claude Code (claude mcp add)
|
|
19
|
+
- Cline (add to mcp settings)
|
|
20
|
+
- Any MCP-compatible client
|
|
21
|
+
|
|
22
|
+
Run:
|
|
23
|
+
sharp-context # Start as STDIO server
|
|
24
|
+
python -m sharp_context.server # Alternative
|
|
25
|
+
|
|
26
|
+
References:
|
|
27
|
+
- MCP Protocol: https://modelcontextprotocol.io
|
|
28
|
+
- Ebbiforge ContextScorer (lsh.rs) — multi-dimensional weighted scoring
|
|
29
|
+
- Ebbiforge HippocampusEngine — Ebbinghaus decay + spaced repetition
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import hashlib
|
|
35
|
+
import sys
|
|
36
|
+
import json
|
|
37
|
+
import logging
|
|
38
|
+
from collections import Counter
|
|
39
|
+
from typing import Any, Dict, List, Optional
|
|
40
|
+
|
|
41
|
+
from .config import SharpContextConfig
|
|
42
|
+
from .knapsack import (
|
|
43
|
+
ContextFragment,
|
|
44
|
+
apply_ebbinghaus_decay,
|
|
45
|
+
compute_relevance,
|
|
46
|
+
knapsack_optimize,
|
|
47
|
+
)
|
|
48
|
+
from .entropy import compute_information_score
|
|
49
|
+
from .dedup import DedupIndex, simhash
|
|
50
|
+
from .prefetch import PrefetchEngine
|
|
51
|
+
from .checkpoint import CheckpointManager
|
|
52
|
+
|
|
53
|
+
# Configure logging to stderr (MCP requires stdout for JSON-RPC)
|
|
54
|
+
logging.basicConfig(
|
|
55
|
+
level=logging.INFO,
|
|
56
|
+
format="%(asctime)s [sharp-context] %(message)s",
|
|
57
|
+
stream=sys.stderr,
|
|
58
|
+
)
|
|
59
|
+
logger = logging.getLogger("sharp-context")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SharpContextEngine:
|
|
63
|
+
"""
|
|
64
|
+
The core engine that orchestrates all five subsystems.
|
|
65
|
+
|
|
66
|
+
This is the brains of SharpContext — it manages the fragment store,
|
|
67
|
+
applies Ebbinghaus decay each turn, scores fragments using Shannon
|
|
68
|
+
entropy, deduplicates via SimHash, and selects the optimal context
|
|
69
|
+
subset using knapsack DP.
|
|
70
|
+
|
|
71
|
+
Modeled after Ebbiforge's HippocampusEngine which uses the same
|
|
72
|
+
pipeline: SimHash → LSH index → ContextScorer → ranked results.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, config: Optional[SharpContextConfig] = None):
|
|
76
|
+
self.config = config or SharpContextConfig()
|
|
77
|
+
|
|
78
|
+
# Fragment store
|
|
79
|
+
self._fragments: Dict[str, ContextFragment] = {}
|
|
80
|
+
self._current_turn: int = 0
|
|
81
|
+
|
|
82
|
+
# Global token distribution (for surprisal computation)
|
|
83
|
+
self._global_token_counts: Counter = Counter()
|
|
84
|
+
self._total_token_count: int = 0
|
|
85
|
+
|
|
86
|
+
# Subsystems
|
|
87
|
+
self._dedup = DedupIndex(hamming_threshold=3)
|
|
88
|
+
self._prefetch = PrefetchEngine(co_access_window=5)
|
|
89
|
+
self._checkpoint_mgr = CheckpointManager(
|
|
90
|
+
checkpoint_dir=self.config.checkpoint_dir,
|
|
91
|
+
auto_interval=self.config.auto_checkpoint_interval,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Stats
|
|
95
|
+
self._total_tokens_saved: int = 0
|
|
96
|
+
self._total_optimizations: int = 0
|
|
97
|
+
self._total_fragments_ingested: int = 0
|
|
98
|
+
self._total_duplicates_caught: int = 0
|
|
99
|
+
|
|
100
|
+
def advance_turn(self) -> None:
|
|
101
|
+
"""Advance the turn counter and apply Ebbinghaus decay."""
|
|
102
|
+
self._current_turn += 1
|
|
103
|
+
fragments = list(self._fragments.values())
|
|
104
|
+
apply_ebbinghaus_decay(
|
|
105
|
+
fragments,
|
|
106
|
+
self._current_turn,
|
|
107
|
+
self.config.decay_half_life_turns,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Evict fragments below minimum relevance
|
|
111
|
+
to_evict = [
|
|
112
|
+
fid for fid, f in self._fragments.items()
|
|
113
|
+
if f.recency_score < self.config.min_relevance_threshold
|
|
114
|
+
and not f.is_pinned
|
|
115
|
+
]
|
|
116
|
+
for fid in to_evict:
|
|
117
|
+
self._dedup.remove(fid)
|
|
118
|
+
del self._fragments[fid]
|
|
119
|
+
|
|
120
|
+
def ingest_fragment(
|
|
121
|
+
self,
|
|
122
|
+
content: str,
|
|
123
|
+
source: str = "",
|
|
124
|
+
token_count: int = 0,
|
|
125
|
+
is_pinned: bool = False,
|
|
126
|
+
) -> Dict[str, Any]:
|
|
127
|
+
"""
|
|
128
|
+
Ingest a new context fragment.
|
|
129
|
+
|
|
130
|
+
Pipeline:
|
|
131
|
+
1. Estimate token count if not provided
|
|
132
|
+
2. Compute SimHash fingerprint
|
|
133
|
+
3. Check for near-duplicates
|
|
134
|
+
4. Compute Shannon entropy score
|
|
135
|
+
5. Update global token distribution
|
|
136
|
+
6. Store fragment
|
|
137
|
+
|
|
138
|
+
Returns a dict with status and fragment metadata.
|
|
139
|
+
"""
|
|
140
|
+
self._total_fragments_ingested += 1
|
|
141
|
+
|
|
142
|
+
# Estimate tokens (~4 chars per token for English/code)
|
|
143
|
+
if token_count <= 0:
|
|
144
|
+
token_count = max(1, len(content) // 4)
|
|
145
|
+
|
|
146
|
+
# Generate fragment ID
|
|
147
|
+
frag_id = hashlib.sha256(
|
|
148
|
+
f"{source}:{content[:200]}:{self._total_fragments_ingested}".encode()
|
|
149
|
+
).hexdigest()[:16]
|
|
150
|
+
|
|
151
|
+
# Check for duplicates
|
|
152
|
+
dup_id = self._dedup.insert(frag_id, content)
|
|
153
|
+
if dup_id is not None:
|
|
154
|
+
# Duplicate found — boost existing fragment
|
|
155
|
+
self._total_duplicates_caught += 1
|
|
156
|
+
existing = self._fragments.get(dup_id)
|
|
157
|
+
if existing:
|
|
158
|
+
existing.access_count += 1
|
|
159
|
+
existing.turn_last_accessed = self._current_turn
|
|
160
|
+
# Boost frequency score (spaced repetition from hippocampus engine)
|
|
161
|
+
max_freq = max(
|
|
162
|
+
f.access_count for f in self._fragments.values()
|
|
163
|
+
) or 1
|
|
164
|
+
existing.frequency_score = min(
|
|
165
|
+
existing.access_count / max_freq, 1.0
|
|
166
|
+
)
|
|
167
|
+
return {
|
|
168
|
+
"status": "duplicate",
|
|
169
|
+
"duplicate_of": dup_id,
|
|
170
|
+
"fragment_id": frag_id,
|
|
171
|
+
"tokens_saved": token_count,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# Compute entropy score
|
|
175
|
+
other_contents = [
|
|
176
|
+
f.content for f in self._fragments.values()
|
|
177
|
+
]
|
|
178
|
+
entropy_score = compute_information_score(
|
|
179
|
+
content,
|
|
180
|
+
global_token_counts=dict(self._global_token_counts),
|
|
181
|
+
total_tokens=self._total_token_count,
|
|
182
|
+
other_fragments=other_contents[:50], # Limit for performance
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Update global token distribution
|
|
186
|
+
tokens = content.lower().split()
|
|
187
|
+
self._global_token_counts.update(tokens)
|
|
188
|
+
self._total_token_count += len(tokens)
|
|
189
|
+
|
|
190
|
+
# Create fragment
|
|
191
|
+
frag = ContextFragment(
|
|
192
|
+
fragment_id=frag_id,
|
|
193
|
+
content=content,
|
|
194
|
+
token_count=token_count,
|
|
195
|
+
source=source,
|
|
196
|
+
recency_score=1.0,
|
|
197
|
+
frequency_score=0.0,
|
|
198
|
+
semantic_score=0.0,
|
|
199
|
+
entropy_score=entropy_score,
|
|
200
|
+
turn_created=self._current_turn,
|
|
201
|
+
turn_last_accessed=self._current_turn,
|
|
202
|
+
access_count=1,
|
|
203
|
+
is_pinned=is_pinned,
|
|
204
|
+
simhash=simhash(content),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
self._fragments[frag_id] = frag
|
|
208
|
+
|
|
209
|
+
# Record access for pre-fetcher
|
|
210
|
+
if source:
|
|
211
|
+
self._prefetch.record_access(source, self._current_turn)
|
|
212
|
+
|
|
213
|
+
# Auto-checkpoint
|
|
214
|
+
if self._checkpoint_mgr.should_auto_checkpoint():
|
|
215
|
+
self._auto_checkpoint()
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
"status": "ingested",
|
|
219
|
+
"fragment_id": frag_id,
|
|
220
|
+
"token_count": token_count,
|
|
221
|
+
"entropy_score": round(entropy_score, 4),
|
|
222
|
+
"total_fragments": len(self._fragments),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
def optimize_context(
|
|
226
|
+
self,
|
|
227
|
+
token_budget: int = 0,
|
|
228
|
+
query: str = "",
|
|
229
|
+
) -> Dict[str, Any]:
|
|
230
|
+
"""
|
|
231
|
+
Select the mathematically optimal subset of context fragments
|
|
232
|
+
that fits within the token budget.
|
|
233
|
+
|
|
234
|
+
Uses the knapsack DP algorithm with four-dimensional scoring:
|
|
235
|
+
- Recency (Ebbinghaus decay)
|
|
236
|
+
- Frequency (spaced repetition)
|
|
237
|
+
- Semantic similarity (SimHash distance to query)
|
|
238
|
+
- Information density (Shannon entropy)
|
|
239
|
+
|
|
240
|
+
This is the core innovation — the context equivalent of
|
|
241
|
+
Ebbiforge's ContextScorer.score() method, but applied to
|
|
242
|
+
the knapsack selection problem.
|
|
243
|
+
"""
|
|
244
|
+
if token_budget <= 0:
|
|
245
|
+
token_budget = self.config.default_token_budget
|
|
246
|
+
|
|
247
|
+
self._total_optimizations += 1
|
|
248
|
+
|
|
249
|
+
# Update semantic scores if a query is provided
|
|
250
|
+
if query:
|
|
251
|
+
query_hash = simhash(query)
|
|
252
|
+
for frag in self._fragments.values():
|
|
253
|
+
# Use SimHash Hamming distance for semantic similarity
|
|
254
|
+
# Same approach as Ebbiforge's ContextScorer:
|
|
255
|
+
# similarity = 1.0 - (hamming_dist / max_bits)
|
|
256
|
+
from .dedup import hamming_distance
|
|
257
|
+
dist = hamming_distance(query_hash, frag.simhash)
|
|
258
|
+
frag.semantic_score = max(0.0, 1.0 - (dist / 32.0))
|
|
259
|
+
|
|
260
|
+
# Run knapsack optimizer
|
|
261
|
+
fragments = list(self._fragments.values())
|
|
262
|
+
selected, stats = knapsack_optimize(
|
|
263
|
+
fragments,
|
|
264
|
+
token_budget,
|
|
265
|
+
w_recency=self.config.weight_recency,
|
|
266
|
+
w_frequency=self.config.weight_frequency,
|
|
267
|
+
w_semantic=self.config.weight_semantic_sim,
|
|
268
|
+
w_entropy=self.config.weight_entropy,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Track savings
|
|
272
|
+
total_available_tokens = sum(f.token_count for f in fragments)
|
|
273
|
+
tokens_saved = total_available_tokens - stats["total_tokens"]
|
|
274
|
+
self._total_tokens_saved += max(0, tokens_saved)
|
|
275
|
+
|
|
276
|
+
# Mark selected fragments as accessed
|
|
277
|
+
for frag in selected:
|
|
278
|
+
frag.turn_last_accessed = self._current_turn
|
|
279
|
+
frag.access_count += 1
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
"selected_fragments": [
|
|
283
|
+
{
|
|
284
|
+
"id": f.fragment_id,
|
|
285
|
+
"source": f.source,
|
|
286
|
+
"token_count": f.token_count,
|
|
287
|
+
"relevance": round(
|
|
288
|
+
compute_relevance(
|
|
289
|
+
f,
|
|
290
|
+
self.config.weight_recency,
|
|
291
|
+
self.config.weight_frequency,
|
|
292
|
+
self.config.weight_semantic_sim,
|
|
293
|
+
self.config.weight_entropy,
|
|
294
|
+
), 4
|
|
295
|
+
),
|
|
296
|
+
"content_preview": f.content[:100] + "..." if len(f.content) > 100 else f.content,
|
|
297
|
+
}
|
|
298
|
+
for f in selected
|
|
299
|
+
],
|
|
300
|
+
"optimization_stats": stats,
|
|
301
|
+
"tokens_saved_this_call": max(0, tokens_saved),
|
|
302
|
+
"total_tokens_saved_session": self._total_tokens_saved,
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
def recall_relevant(
|
|
306
|
+
self,
|
|
307
|
+
query: str,
|
|
308
|
+
top_k: int = 5,
|
|
309
|
+
) -> List[Dict[str, Any]]:
|
|
310
|
+
"""
|
|
311
|
+
Semantic recall of the most relevant fragments for a query.
|
|
312
|
+
|
|
313
|
+
Uses the same SimHash + weighted scoring pipeline as the
|
|
314
|
+
Ebbiforge HippocampusEngine.recall() method.
|
|
315
|
+
"""
|
|
316
|
+
if not self._fragments:
|
|
317
|
+
return []
|
|
318
|
+
|
|
319
|
+
query_hash = simhash(query)
|
|
320
|
+
|
|
321
|
+
scored = []
|
|
322
|
+
for frag in self._fragments.values():
|
|
323
|
+
from .dedup import hamming_distance
|
|
324
|
+
dist = hamming_distance(query_hash, frag.simhash)
|
|
325
|
+
frag.semantic_score = max(0.0, 1.0 - (dist / 32.0))
|
|
326
|
+
|
|
327
|
+
relevance = compute_relevance(
|
|
328
|
+
frag,
|
|
329
|
+
self.config.weight_recency,
|
|
330
|
+
self.config.weight_frequency,
|
|
331
|
+
self.config.weight_semantic_sim,
|
|
332
|
+
self.config.weight_entropy,
|
|
333
|
+
)
|
|
334
|
+
scored.append((frag, relevance))
|
|
335
|
+
|
|
336
|
+
scored.sort(key=lambda x: x[1], reverse=True)
|
|
337
|
+
|
|
338
|
+
return [
|
|
339
|
+
{
|
|
340
|
+
"fragment_id": f.fragment_id,
|
|
341
|
+
"source": f.source,
|
|
342
|
+
"relevance": round(rel, 4),
|
|
343
|
+
"entropy": round(f.entropy_score, 4),
|
|
344
|
+
"content": f.content,
|
|
345
|
+
}
|
|
346
|
+
for f, rel in scored[:top_k]
|
|
347
|
+
]
|
|
348
|
+
|
|
349
|
+
def prefetch_related(
|
|
350
|
+
self,
|
|
351
|
+
file_path: str,
|
|
352
|
+
source_content: str = "",
|
|
353
|
+
language: str = "python",
|
|
354
|
+
) -> List[Dict[str, Any]]:
|
|
355
|
+
"""
|
|
356
|
+
Predict what context will be needed next and pre-fetch it.
|
|
357
|
+
"""
|
|
358
|
+
predictions = self._prefetch.predict(
|
|
359
|
+
file_path, source_content, language
|
|
360
|
+
)
|
|
361
|
+
return [
|
|
362
|
+
{
|
|
363
|
+
"path": p.path,
|
|
364
|
+
"reason": p.reason,
|
|
365
|
+
"confidence": p.confidence,
|
|
366
|
+
}
|
|
367
|
+
for p in predictions
|
|
368
|
+
]
|
|
369
|
+
|
|
370
|
+
def checkpoint(self, metadata: Optional[Dict[str, Any]] = None) -> str:
|
|
371
|
+
"""Manually create a checkpoint."""
|
|
372
|
+
return self._auto_checkpoint(metadata)
|
|
373
|
+
|
|
374
|
+
def resume(self) -> Dict[str, Any]:
|
|
375
|
+
"""Resume from the latest checkpoint."""
|
|
376
|
+
ckpt = self._checkpoint_mgr.load_latest()
|
|
377
|
+
if ckpt is None:
|
|
378
|
+
return {"status": "no_checkpoint_found"}
|
|
379
|
+
|
|
380
|
+
# Restore fragments
|
|
381
|
+
self._fragments.clear()
|
|
382
|
+
for frag in self._checkpoint_mgr.restore_fragments(ckpt):
|
|
383
|
+
self._fragments[frag.fragment_id] = frag
|
|
384
|
+
|
|
385
|
+
# Restore dedup index
|
|
386
|
+
self._dedup = DedupIndex(hamming_threshold=3)
|
|
387
|
+
for fid, fp in ckpt.dedup_fingerprints.items():
|
|
388
|
+
self._dedup._fingerprints[fid] = fp
|
|
389
|
+
|
|
390
|
+
# Restore co-access
|
|
391
|
+
for src, targets in ckpt.co_access_data.items():
|
|
392
|
+
self._prefetch._co_access[src] = Counter(targets)
|
|
393
|
+
|
|
394
|
+
self._current_turn = ckpt.current_turn
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
"status": "resumed",
|
|
398
|
+
"checkpoint_id": ckpt.checkpoint_id,
|
|
399
|
+
"restored_fragments": len(self._fragments),
|
|
400
|
+
"restored_turn": self._current_turn,
|
|
401
|
+
"metadata": ckpt.metadata,
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
405
|
+
"""Get comprehensive session statistics."""
|
|
406
|
+
fragments = list(self._fragments.values())
|
|
407
|
+
total_tokens = sum(f.token_count for f in fragments)
|
|
408
|
+
avg_entropy = (
|
|
409
|
+
sum(f.entropy_score for f in fragments) / len(fragments)
|
|
410
|
+
if fragments else 0.0
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
"session": {
|
|
415
|
+
"current_turn": self._current_turn,
|
|
416
|
+
"total_fragments": len(fragments),
|
|
417
|
+
"total_tokens_tracked": total_tokens,
|
|
418
|
+
"avg_entropy_score": round(avg_entropy, 4),
|
|
419
|
+
"pinned_fragments": sum(1 for f in fragments if f.is_pinned),
|
|
420
|
+
},
|
|
421
|
+
"savings": {
|
|
422
|
+
"total_tokens_saved": self._total_tokens_saved,
|
|
423
|
+
"total_duplicates_caught": self._total_duplicates_caught,
|
|
424
|
+
"total_optimizations": self._total_optimizations,
|
|
425
|
+
"total_fragments_ingested": self._total_fragments_ingested,
|
|
426
|
+
"estimated_cost_saved_usd": round(
|
|
427
|
+
self._total_tokens_saved * 0.000003, 4 # ~$3/1M tokens
|
|
428
|
+
),
|
|
429
|
+
},
|
|
430
|
+
"dedup": self._dedup.stats(),
|
|
431
|
+
"prefetch": self._prefetch.stats(),
|
|
432
|
+
"checkpoint": self._checkpoint_mgr.stats(),
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
def _auto_checkpoint(
|
|
436
|
+
self,
|
|
437
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
438
|
+
) -> str:
|
|
439
|
+
"""Create an auto-checkpoint."""
|
|
440
|
+
co_access = {
|
|
441
|
+
k: dict(v)
|
|
442
|
+
for k, v in self._prefetch._co_access.items()
|
|
443
|
+
}
|
|
444
|
+
return self._checkpoint_mgr.save(
|
|
445
|
+
fragments=list(self._fragments.values()),
|
|
446
|
+
dedup_fingerprints=dict(self._dedup._fingerprints),
|
|
447
|
+
co_access_data=co_access,
|
|
448
|
+
current_turn=self._current_turn,
|
|
449
|
+
metadata=metadata,
|
|
450
|
+
stats=self.get_stats(),
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
# ══════════════════════════════════════════════════════════════════════
|
|
455
|
+
# MCP Server Definition
|
|
456
|
+
# ══════════════════════════════════════════════════════════════════════
|
|
457
|
+
|
|
458
|
+
def create_mcp_server():
|
|
459
|
+
"""
|
|
460
|
+
Create the MCP server with all tools registered.
|
|
461
|
+
|
|
462
|
+
Uses the FastMCP SDK for automatic tool schema generation
|
|
463
|
+
from Python type hints and docstrings.
|
|
464
|
+
"""
|
|
465
|
+
try:
|
|
466
|
+
from mcp.server.fastmcp import FastMCP
|
|
467
|
+
except ImportError:
|
|
468
|
+
logger.error(
|
|
469
|
+
"MCP SDK not installed. Install with: pip install mcp"
|
|
470
|
+
)
|
|
471
|
+
raise
|
|
472
|
+
|
|
473
|
+
mcp = FastMCP(
|
|
474
|
+
"sharp-context",
|
|
475
|
+
version="0.1.0",
|
|
476
|
+
description=(
|
|
477
|
+
"Information-theoretic context optimization for AI coding agents. "
|
|
478
|
+
"Knapsack-optimal token budgeting, Shannon entropy scoring, "
|
|
479
|
+
"SimHash deduplication, predictive pre-fetch, and checkpoint/resume."
|
|
480
|
+
),
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
# Shared engine instance
|
|
484
|
+
engine = SharpContextEngine()
|
|
485
|
+
|
|
486
|
+
@mcp.tool()
|
|
487
|
+
def remember_fragment(
|
|
488
|
+
content: str,
|
|
489
|
+
source: str = "",
|
|
490
|
+
token_count: int = 0,
|
|
491
|
+
is_pinned: bool = False,
|
|
492
|
+
) -> str:
|
|
493
|
+
"""Store a context fragment with automatic dedup and entropy scoring.
|
|
494
|
+
|
|
495
|
+
Fragments are fingerprinted via SimHash for O(1) duplicate detection.
|
|
496
|
+
Each fragment's information density is scored using Shannon entropy.
|
|
497
|
+
Duplicates are automatically merged with salience boosting.
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
content: The text content to store (code, tool output, etc.)
|
|
501
|
+
source: Origin label (e.g., 'file:utils.py', 'tool:grep')
|
|
502
|
+
token_count: Token count (auto-estimated if 0)
|
|
503
|
+
is_pinned: If True, always include in optimized context
|
|
504
|
+
"""
|
|
505
|
+
engine.advance_turn()
|
|
506
|
+
result = engine.ingest_fragment(content, source, token_count, is_pinned)
|
|
507
|
+
return json.dumps(result, indent=2)
|
|
508
|
+
|
|
509
|
+
@mcp.tool()
|
|
510
|
+
def optimize_context(
|
|
511
|
+
token_budget: int = 128000,
|
|
512
|
+
query: str = "",
|
|
513
|
+
) -> str:
|
|
514
|
+
"""Select the mathematically optimal context subset for a token budget.
|
|
515
|
+
|
|
516
|
+
Uses 0/1 Knapsack dynamic programming to maximize relevance within
|
|
517
|
+
the budget. Scores fragments on four dimensions: recency (Ebbinghaus
|
|
518
|
+
decay), access frequency (spaced repetition), semantic similarity
|
|
519
|
+
(SimHash), and information density (Shannon entropy).
|
|
520
|
+
|
|
521
|
+
This is the core tool — call it before sending context to the LLM.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
token_budget: Maximum tokens allowed (default: 128K)
|
|
525
|
+
query: Current query/task for semantic relevance scoring
|
|
526
|
+
"""
|
|
527
|
+
result = engine.optimize_context(token_budget, query)
|
|
528
|
+
return json.dumps(result, indent=2)
|
|
529
|
+
|
|
530
|
+
@mcp.tool()
|
|
531
|
+
def recall_relevant(
|
|
532
|
+
query: str,
|
|
533
|
+
top_k: int = 5,
|
|
534
|
+
) -> str:
|
|
535
|
+
"""Semantic recall of the most relevant stored fragments.
|
|
536
|
+
|
|
537
|
+
Uses SimHash fingerprint distance + multi-dimensional scoring
|
|
538
|
+
(same pipeline as Ebbiforge's HippocampusEngine.recall()).
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
query: The search query
|
|
542
|
+
top_k: Number of results to return
|
|
543
|
+
"""
|
|
544
|
+
results = engine.recall_relevant(query, top_k)
|
|
545
|
+
return json.dumps(results, indent=2)
|
|
546
|
+
|
|
547
|
+
@mcp.tool()
|
|
548
|
+
def checkpoint_state(
|
|
549
|
+
task_description: str = "",
|
|
550
|
+
current_step: str = "",
|
|
551
|
+
) -> str:
|
|
552
|
+
"""Save current state to disk for crash recovery and session resume.
|
|
553
|
+
|
|
554
|
+
Checkpoints include all fragments, dedup index, co-access patterns,
|
|
555
|
+
and custom metadata. Stored as gzipped JSON (~50-200 KB).
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
task_description: What the agent is working on
|
|
559
|
+
current_step: Where in the task it currently is
|
|
560
|
+
"""
|
|
561
|
+
metadata = {}
|
|
562
|
+
if task_description:
|
|
563
|
+
metadata["task"] = task_description
|
|
564
|
+
if current_step:
|
|
565
|
+
metadata["step"] = current_step
|
|
566
|
+
|
|
567
|
+
path = engine.checkpoint(metadata)
|
|
568
|
+
return json.dumps({
|
|
569
|
+
"status": "checkpoint_saved",
|
|
570
|
+
"path": path,
|
|
571
|
+
"fragments_saved": len(engine._fragments),
|
|
572
|
+
}, indent=2)
|
|
573
|
+
|
|
574
|
+
@mcp.tool()
|
|
575
|
+
def resume_state() -> str:
|
|
576
|
+
"""Resume from the latest checkpoint.
|
|
577
|
+
|
|
578
|
+
Restores all context fragments, dedup index, co-access patterns,
|
|
579
|
+
and custom metadata from the most recent checkpoint.
|
|
580
|
+
"""
|
|
581
|
+
result = engine.resume()
|
|
582
|
+
return json.dumps(result, indent=2)
|
|
583
|
+
|
|
584
|
+
@mcp.tool()
|
|
585
|
+
def prefetch_related(
|
|
586
|
+
file_path: str,
|
|
587
|
+
source_content: str = "",
|
|
588
|
+
language: str = "python",
|
|
589
|
+
) -> str:
|
|
590
|
+
"""Predict and pre-load context that will likely be needed next.
|
|
591
|
+
|
|
592
|
+
Combines static analysis (imports, callees, test files) with
|
|
593
|
+
learned co-access patterns to predict what the agent will need.
|
|
594
|
+
|
|
595
|
+
Args:
|
|
596
|
+
file_path: The file currently being accessed
|
|
597
|
+
source_content: The source code content (for static analysis)
|
|
598
|
+
language: Programming language (python, typescript, rust)
|
|
599
|
+
"""
|
|
600
|
+
predictions = engine.prefetch_related(file_path, source_content, language)
|
|
601
|
+
return json.dumps(predictions, indent=2)
|
|
602
|
+
|
|
603
|
+
@mcp.tool()
|
|
604
|
+
def get_stats() -> str:
|
|
605
|
+
"""Get comprehensive session statistics.
|
|
606
|
+
|
|
607
|
+
Shows token savings, duplicate detection counts, entropy
|
|
608
|
+
distribution, checkpoint status, and cost estimates.
|
|
609
|
+
"""
|
|
610
|
+
stats = engine.get_stats()
|
|
611
|
+
return json.dumps(stats, indent=2)
|
|
612
|
+
|
|
613
|
+
return mcp
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def main():
|
|
617
|
+
"""Entry point for the sharp-context MCP server."""
|
|
618
|
+
logger.info("Starting SharpContext MCP server v0.1.0")
|
|
619
|
+
mcp = create_mcp_server()
|
|
620
|
+
mcp.run()
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
if __name__ == "__main__":
|
|
624
|
+
main()
|