ace-mcp-server 0.3.0__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.
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: ace-mcp-server
3
+ Version: 0.3.0
4
+ Summary: Ace MCP Server — Free AI memory that learns. Zero cloud, zero API keys, zero cost.
5
+ Author-email: Wenhui Tian <rocky007cn@outlook.com>
6
+ License: BSL-1.1
7
+ Project-URL: Homepage, https://github.com/rocky007cn/ace-memory
8
+ Project-URL: Repository, https://github.com/rocky007cn/ace-memory
9
+ Keywords: mcp,mcp-server,ai-memory,memory,llm,agent-memory,rag,long-term-memory,local-first,free,zero-cost
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: Other/Proprietary License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: pvm-memory>=1.1.0
21
+ Requires-Dist: mcp>=1.0.0
22
+ Provides-Extra: semantic
23
+ Requires-Dist: sentence-transformers>=2.2.0; extra == "semantic"
24
+
25
+ # Ace MCP Server
26
+
27
+ ♠ **Ace your AI memory. Free. Simple. Local.**
28
+
29
+ Zero-cost MCP server that gives any AI tool persistent, self-improving memory.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install ace-mcp
35
+ ```
36
+
37
+ Requires `pvm-memory` (installed automatically as dependency).
38
+
39
+ ## Quick Start
40
+
41
+ ### VS Code / Cursor
42
+
43
+ Add to `.vscode/mcp.json`:
44
+
45
+ ```json
46
+ {
47
+ "servers": {
48
+ "ace": {
49
+ "command": "python",
50
+ "args": ["-m", "ace_mcp.server"]
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ ### Claude Desktop
57
+
58
+ Add to `claude_desktop_config.json`:
59
+
60
+ ```json
61
+ {
62
+ "mcpServers": {
63
+ "ace": {
64
+ "command": "python",
65
+ "args": ["-m", "ace_mcp.server"]
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ ### Any MCP-compatible tool
72
+
73
+ ```bash
74
+ ace-mcp # default: ~/.ace/memory.jsonl
75
+ ace-mcp --path ./my-mem.jsonl
76
+ ace-mcp --no-embedding # keyword-only, faster
77
+ ```
78
+
79
+ ## Tools
80
+
81
+ | Tool | What |
82
+ |------|------|
83
+ | `ace_save` | Store a memory |
84
+ | `ace_search` | Keyword + semantic search |
85
+ | `ace_get_context` | Recent memory context |
86
+ | `ace_calibrate` | 👍/👎 feedback to train weights |
87
+ | `ace_stats` | Engine statistics |
88
+
89
+ ## vs Mem0
90
+
91
+ | | Mem0 | Ace |
92
+ |---|---|---|
93
+ | Price | 💰 Paid | 🆓 Free |
94
+ | API Key | Required | None |
95
+ | Cloud | Required | 100% Local |
96
+ | GPU | Recommended | Not needed |
97
+ | Dependencies | 50+ | 2 |
98
+ | Information | Chunked/lossy | Full text |
99
+
100
+ ## License
101
+
102
+ BSL 1.1 → MIT (2030)
@@ -0,0 +1,78 @@
1
+ # Ace MCP Server
2
+
3
+ ♠ **Ace your AI memory. Free. Simple. Local.**
4
+
5
+ Zero-cost MCP server that gives any AI tool persistent, self-improving memory.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install ace-mcp
11
+ ```
12
+
13
+ Requires `pvm-memory` (installed automatically as dependency).
14
+
15
+ ## Quick Start
16
+
17
+ ### VS Code / Cursor
18
+
19
+ Add to `.vscode/mcp.json`:
20
+
21
+ ```json
22
+ {
23
+ "servers": {
24
+ "ace": {
25
+ "command": "python",
26
+ "args": ["-m", "ace_mcp.server"]
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ ### Claude Desktop
33
+
34
+ Add to `claude_desktop_config.json`:
35
+
36
+ ```json
37
+ {
38
+ "mcpServers": {
39
+ "ace": {
40
+ "command": "python",
41
+ "args": ["-m", "ace_mcp.server"]
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ ### Any MCP-compatible tool
48
+
49
+ ```bash
50
+ ace-mcp # default: ~/.ace/memory.jsonl
51
+ ace-mcp --path ./my-mem.jsonl
52
+ ace-mcp --no-embedding # keyword-only, faster
53
+ ```
54
+
55
+ ## Tools
56
+
57
+ | Tool | What |
58
+ |------|------|
59
+ | `ace_save` | Store a memory |
60
+ | `ace_search` | Keyword + semantic search |
61
+ | `ace_get_context` | Recent memory context |
62
+ | `ace_calibrate` | 👍/👎 feedback to train weights |
63
+ | `ace_stats` | Engine statistics |
64
+
65
+ ## vs Mem0
66
+
67
+ | | Mem0 | Ace |
68
+ |---|---|---|
69
+ | Price | 💰 Paid | 🆓 Free |
70
+ | API Key | Required | None |
71
+ | Cloud | Required | 100% Local |
72
+ | GPU | Recommended | Not needed |
73
+ | Dependencies | 50+ | 2 |
74
+ | Information | Chunked/lossy | Full text |
75
+
76
+ ## License
77
+
78
+ BSL 1.1 → MIT (2030)
@@ -0,0 +1,18 @@
1
+ """
2
+ Ace MCP Server
3
+
4
+ Ace your AI memory. Free. Simple. Local.
5
+
6
+ Exposes PVM persistent memory as MCP tools for any AI tool
7
+ (Cursor, Claude Desktop, Windsurf, Codex, Copilot, etc.)
8
+
9
+ Author: Wenhui Tian (田文辉)
10
+ Email: rocky007cn@outlook.com
11
+ GitHub: https://github.com/rocky007cn/ace-memory
12
+ License: BSL 1.1 → MIT 2030
13
+ """
14
+
15
+ __version__ = "0.3.0"
16
+ __author__ = "Wenhui Tian"
17
+ __email__ = "rocky007cn@outlook.com"
18
+ __url__ = "https://github.com/rocky007cn/ace-memory"
@@ -0,0 +1,315 @@
1
+ """
2
+ Fuzzy Attribution — PVM Paper Chapter 6 Implementation
3
+
4
+ Author: Wenhui Tian (田文辉)
5
+ Email: rocky007cn@outlook.com
6
+ GitHub: https://github.com/rocky007cn/ace-memory
7
+
8
+ When multiple candidates are comparably scored, PVM does not guess.
9
+ Instead, it honestly reports the common features of all candidates,
10
+ maintaining user trust while accumulating calibration signals.
11
+
12
+ Trigger conditions (all three must be met):
13
+ 1. Candidate count >= 2
14
+ 2. Temporal span between candidates > threshold (default: 180 days)
15
+ 3. Similarity score variance < threshold (default: 0.2)
16
+
17
+ Reference: "Persistent Vector Memory" Section 6.2
18
+ """
19
+
20
+ import re
21
+ import time
22
+ from collections import Counter
23
+ from typing import Optional
24
+
25
+ from pvm import PVMRecord
26
+ from pvm.ranking import composite_score
27
+
28
+
29
+ class FuzzyAttributor:
30
+ """Implements the full fuzzy attribution mechanism from PVM Chapter 6."""
31
+
32
+ def __init__(
33
+ self,
34
+ temporal_span_threshold: float = 180.0,
35
+ score_variance_threshold: float = 0.2,
36
+ min_candidates: int = 2,
37
+ ):
38
+ """
39
+ Args:
40
+ temporal_span_threshold: Min days between oldest and newest
41
+ candidate to trigger fuzzy mode. Paper default: 180 days.
42
+ score_variance_threshold: Max score gap between top and bottom
43
+ candidate. Paper default: 0.2.
44
+ min_candidates: Minimum candidates to trigger. Paper default: 2.
45
+ """
46
+ self.temporal_span_threshold = temporal_span_threshold
47
+ self.score_variance_threshold = score_variance_threshold
48
+ self.min_candidates = min_candidates
49
+
50
+ def analyze(
51
+ self,
52
+ candidates: list[PVMRecord],
53
+ lambda_decay: float = 0.01,
54
+ ) -> Optional[dict]:
55
+ """Analyze candidates and return fuzzy attribution if triggered.
56
+
57
+ This implements the full three-condition check from Section 6.2:
58
+ 1. Enough candidates
59
+ 2. Temporal span exceeds threshold
60
+ 3. Score variance below threshold (no clear winner)
61
+
62
+ Args:
63
+ candidates: Ranked list of PVMRecord (already sorted by score).
64
+ lambda_decay: Temporal decay rate for score computation.
65
+
66
+ Returns:
67
+ None if attribution is clear-cut (single winner dominates).
68
+ Dict with fuzzy attribution info if triggered.
69
+ """
70
+ if len(candidates) < self.min_candidates:
71
+ return None
72
+
73
+ # --- Condition 1: Score variance ---
74
+ scores = [composite_score(c, lambda_decay) for c in candidates]
75
+ top_score = max(scores)
76
+ bottom_score = min(scores)
77
+ score_gap = top_score - bottom_score
78
+
79
+ if score_gap > self.score_variance_threshold:
80
+ return None # Clear winner, no fuzzy needed
81
+
82
+ # --- Condition 2: Temporal span ---
83
+ timestamps = [c.last_activated for c in candidates if c.last_activated]
84
+ if not timestamps:
85
+ return None
86
+
87
+ earliest = min(timestamps)
88
+ latest = max(timestamps)
89
+ temporal_span_days = (latest - earliest) / 86400.0
90
+
91
+ if temporal_span_days < self.temporal_span_threshold:
92
+ # Temporal span too small — candidates are from the same period.
93
+ # Still return fuzzy info but mark it as "low_confidence".
94
+ # The paper's default is 180 days, but we don't want to hide
95
+ # ambiguous results just because they're recent.
96
+ pass
97
+
98
+ # --- Extract common features ---
99
+ common_features = self._extract_common_features(candidates)
100
+ category = self._classify_candidates(candidates)
101
+
102
+ # --- Build honest summary ---
103
+ summary = self._build_summary(
104
+ candidates, common_features, category,
105
+ temporal_span_days, score_gap,
106
+ )
107
+
108
+ triggered = (
109
+ temporal_span_days >= self.temporal_span_threshold
110
+ and score_gap <= self.score_variance_threshold
111
+ )
112
+
113
+ return {
114
+ "mode": "fuzzy_attribution",
115
+ "triggered": triggered,
116
+ "trigger_conditions": {
117
+ "candidate_count": len(candidates),
118
+ "candidate_count_met": len(candidates) >= self.min_candidates,
119
+ "temporal_span_days": round(temporal_span_days, 1),
120
+ "temporal_span_met": temporal_span_days >= self.temporal_span_threshold,
121
+ "score_gap": round(score_gap, 4),
122
+ "score_variance_met": score_gap <= self.score_variance_threshold,
123
+ },
124
+ "category": category,
125
+ "common_features": common_features,
126
+ "honest_summary": summary,
127
+ "candidates": [
128
+ {
129
+ "fingerprint": c.fingerprint.hex(),
130
+ "content_preview": c.content[:150],
131
+ "weight": round(c.weight, 4),
132
+ "score": round(composite_score(c, lambda_decay), 4),
133
+ "created_at": c.created_at,
134
+ "last_activated": c.last_activated,
135
+ }
136
+ for c in candidates
137
+ ],
138
+ "advice": (
139
+ "Multiple memories match this query with similar scores. "
140
+ "Do NOT pick one and present it as certain. Instead, "
141
+ "summarize the common pattern across these memories and "
142
+ "let the user confirm which one they mean. "
143
+ "If the user confirms, call ace_calibrate with signal=1.0 "
144
+ "for the correct memory to sharpen future retrieval."
145
+ ),
146
+ }
147
+
148
+ def _extract_common_features(
149
+ self, candidates: list[PVMRecord]
150
+ ) -> dict:
151
+ """Extract common features across candidates.
152
+
153
+ Goes beyond simple token intersection:
154
+ - Shared keywords (content tokens)
155
+ - Shared memory types
156
+ - Shared sources
157
+ - Shared tags
158
+ """
159
+ # Tokenize each candidate's content
160
+ token_lists = [self._tokenize(c.content) for c in candidates]
161
+ token_sets = [set(tl) for tl in token_lists]
162
+
163
+ # Intersection of all token sets
164
+ common_tokens = token_sets[0]
165
+ for ts in token_sets[1:]:
166
+ common_tokens = common_tokens & ts
167
+
168
+ # Remove stop words and noise
169
+ common_tokens = self._filter_stop_words(common_tokens)
170
+
171
+ # Frequency across candidates (how many candidates have this token)
172
+ token_frequency = Counter()
173
+ for tl in token_lists:
174
+ for t in set(tl): # unique per candidate
175
+ token_frequency[t] += 1
176
+
177
+ # Sort by frequency (most common first)
178
+ common_sorted = sorted(
179
+ common_tokens,
180
+ key=lambda t: (-token_frequency[t], t),
181
+ )
182
+
183
+ # Extract memory types
184
+ types = set()
185
+ sources = set()
186
+ for c in candidates:
187
+ content = c.content
188
+ # Extract type: [source:X] [type] content
189
+ if "[source:" in content:
190
+ src_start = content.find("[source:") + 8
191
+ src_end = content.find("]", src_start)
192
+ if src_end > 0:
193
+ sources.add(content[src_start:src_end])
194
+ # Extract type tag
195
+ type_match = re.search(r"\[(fact|preference|decision|correction|context)\]", content)
196
+ if type_match:
197
+ types.add(type_match.group(1))
198
+
199
+ return {
200
+ "shared_keywords": common_sorted[:20],
201
+ "shared_types": sorted(types) if types else [],
202
+ "shared_sources": sorted(sources) if sources else [],
203
+ "total_unique_keywords": len(common_tokens),
204
+ }
205
+
206
+ def _classify_candidates(
207
+ self, candidates: list[PVMRecord]
208
+ ) -> str:
209
+ """Classify what kind of ambiguity this is.
210
+
211
+ Returns one of:
212
+ - "temporal_ambiguity": same topic, different time periods
213
+ - "topic_overlap": different events with shared elements
214
+ - "correction_chain": original + corrected versions
215
+ - "general": generic ambiguity
216
+ """
217
+ # Check if any candidate is a correction of another
218
+ for c in candidates:
219
+ for e in c.edges:
220
+ if e.relation_type == "corrects":
221
+ for other in candidates:
222
+ if other.fingerprint == e.target_id:
223
+ return "correction_chain"
224
+
225
+ # Check temporal spread
226
+ timestamps = [c.last_activated for c in candidates if c.last_activated]
227
+ if timestamps:
228
+ span = (max(timestamps) - min(timestamps)) / 86400.0
229
+ if span > 90:
230
+ return "temporal_ambiguity"
231
+
232
+ return "topic_overlap"
233
+
234
+ def _build_summary(
235
+ self,
236
+ candidates: list[PVMRecord],
237
+ features: dict,
238
+ category: str,
239
+ temporal_span: float,
240
+ score_gap: float,
241
+ ) -> str:
242
+ """Build an honest summary for the AI to use.
243
+
244
+ This is NOT the final user-facing text — the AI (LLM) should
245
+ use this as input to generate a natural response.
246
+ The summary provides the structured facts; the LLM provides
247
+ the language.
248
+ """
249
+ keywords = features.get("shared_keywords", [])
250
+ keyword_str = ", ".join(keywords[:10]) if keywords else "(none)"
251
+
252
+ parts = [
253
+ f"Fuzzy attribution triggered: {len(candidates)} candidates "
254
+ f"with score gap {score_gap:.4f} (threshold: {self.score_variance_threshold}).",
255
+ f"Temporal span: {temporal_span:.1f} days "
256
+ f"(threshold: {self.temporal_span_threshold} days).",
257
+ f"Category: {category}.",
258
+ f"Common keywords: {keyword_str}.",
259
+ ]
260
+
261
+ if category == "correction_chain":
262
+ parts.append(
263
+ "This includes an original memory and its correction. "
264
+ "Present both versions to the user."
265
+ )
266
+ elif category == "temporal_ambiguity":
267
+ parts.append(
268
+ "These memories span a long time period. The user may be "
269
+ "referring to any of them. Summarize the pattern rather "
270
+ "than picking one."
271
+ )
272
+ else:
273
+ parts.append(
274
+ "These memories share common features but may refer to "
275
+ "different events. Present the common theme and ask "
276
+ "the user to clarify."
277
+ )
278
+
279
+ return " ".join(parts)
280
+
281
+ @staticmethod
282
+ def _tokenize(text: str) -> list[str]:
283
+ """Simple tokenizer for feature extraction."""
284
+ tokens = re.findall(r"\w+", text.lower())
285
+ # Also extract CJK characters as individual tokens
286
+ cjk = re.findall(r"[\u4e00-\u9fff]+", text)
287
+ for segment in cjk:
288
+ # Simple bigram extraction for CJK
289
+ for i in range(len(segment) - 1):
290
+ tokens.append(segment[i:i+2])
291
+ tokens.append(segment)
292
+ return tokens
293
+
294
+ @staticmethod
295
+ def _filter_stop_words(tokens: set[str]) -> set[str]:
296
+ """Filter common stop words from token set."""
297
+ stop_words = {
298
+ "the", "a", "an", "is", "are", "was", "were", "be", "been",
299
+ "have", "has", "had", "do", "does", "did", "will", "would",
300
+ "could", "should", "may", "might", "must", "shall", "can",
301
+ "to", "of", "in", "for", "on", "with", "at", "by", "from",
302
+ "as", "into", "about", "like", "through", "after", "over",
303
+ "between", "out", "against", "during", "without", "before",
304
+ "under", "around", "among", "and", "but", "or", "not",
305
+ "no", "so", "if", "then", "that", "this", "these", "those",
306
+ "it", "i", "you", "he", "she", "we", "they", "me", "him",
307
+ "her", "us", "them", "my", "your", "his", "its", "our",
308
+ "their", "what", "which", "who", "when", "where", "why",
309
+ "how", "all", "each", "every", "both", "few", "more",
310
+ "most", "other", "some", "such", "only", "own", "same",
311
+ "than", "too", "very", "just", "also", "now", "here",
312
+ "there", "source", "fact", "preference", "decision",
313
+ "correction", "context", "reason",
314
+ }
315
+ return {t for t in tokens if t not in stop_words and len(t) > 1}