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.
@@ -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()