polypack-mcp 0.1.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.
- polypack_mcp-0.1.0/PKG-INFO +42 -0
- polypack_mcp-0.1.0/README.md +29 -0
- polypack_mcp-0.1.0/polypack_mcp/__init__.py +7 -0
- polypack_mcp-0.1.0/polypack_mcp/backend.py +217 -0
- polypack_mcp-0.1.0/polypack_mcp/server.py +88 -0
- polypack_mcp-0.1.0/polypack_mcp/service.py +18 -0
- polypack_mcp-0.1.0/polypack_mcp.egg-info/PKG-INFO +42 -0
- polypack_mcp-0.1.0/polypack_mcp.egg-info/SOURCES.txt +14 -0
- polypack_mcp-0.1.0/polypack_mcp.egg-info/dependency_links.txt +1 -0
- polypack_mcp-0.1.0/polypack_mcp.egg-info/entry_points.txt +2 -0
- polypack_mcp-0.1.0/polypack_mcp.egg-info/requires.txt +8 -0
- polypack_mcp-0.1.0/polypack_mcp.egg-info/top_level.txt +1 -0
- polypack_mcp-0.1.0/pyproject.toml +24 -0
- polypack_mcp-0.1.0/setup.cfg +4 -0
- polypack_mcp-0.1.0/tests/test_mcp_protocol.py +29 -0
- polypack_mcp-0.1.0/tests/test_service.py +19 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: polypack-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server exposing Polypack as persistent adaptive memory
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: mcp<1.11,>=1.10
|
|
8
|
+
Requires-Dist: anyio<4.10,>=4.5
|
|
9
|
+
Provides-Extra: polypack
|
|
10
|
+
Requires-Dist: polypack-db>=3.1; extra == "polypack"
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# polypack-mcp
|
|
15
|
+
|
|
16
|
+
An MCP server that exposes Polypack as persistent adaptive memory. MCP-specific
|
|
17
|
+
tools live here; the database remains an independent dependency.
|
|
18
|
+
|
|
19
|
+
## Run
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pip install -e '.[polypack]'
|
|
23
|
+
polypack-mcp --store ./polypack-data
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The server exposes eight focused tools: `memory_store`, `memory_recall`,
|
|
27
|
+
`memory_context`, `memory_feedback`, `memory_suppress`, `memory_supersede`,
|
|
28
|
+
`memory_consolidate`, and `graph_query`. It also publishes context, active-memory,
|
|
29
|
+
schema, and stats resources under `polypack://`.
|
|
30
|
+
|
|
31
|
+
Pass `--store` to open a durable Polypack directory. Without it, the server uses
|
|
32
|
+
the in-memory reference backend, which is convenient for smoke tests.
|
|
33
|
+
|
|
34
|
+
## Development
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pip install -e '.[dev]'
|
|
38
|
+
pytest
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The test suite includes an MCP client/server protocol smoke test covering tool
|
|
42
|
+
discovery, memory storage, recall, and resource reads.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# polypack-mcp
|
|
2
|
+
|
|
3
|
+
An MCP server that exposes Polypack as persistent adaptive memory. MCP-specific
|
|
4
|
+
tools live here; the database remains an independent dependency.
|
|
5
|
+
|
|
6
|
+
## Run
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
pip install -e '.[polypack]'
|
|
10
|
+
polypack-mcp --store ./polypack-data
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The server exposes eight focused tools: `memory_store`, `memory_recall`,
|
|
14
|
+
`memory_context`, `memory_feedback`, `memory_suppress`, `memory_supersede`,
|
|
15
|
+
`memory_consolidate`, and `graph_query`. It also publishes context, active-memory,
|
|
16
|
+
schema, and stats resources under `polypack://`.
|
|
17
|
+
|
|
18
|
+
Pass `--store` to open a durable Polypack directory. Without it, the server uses
|
|
19
|
+
the in-memory reference backend, which is convenient for smoke tests.
|
|
20
|
+
|
|
21
|
+
## Development
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
pip install -e '.[dev]'
|
|
25
|
+
pytest
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The test suite includes an MCP client/server protocol smoke test covering tool
|
|
29
|
+
discovery, memory storage, recall, and resource reads.
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Backend boundary between MCP semantics and Polypack implementations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, Protocol
|
|
10
|
+
|
|
11
|
+
CLASSES = {"episodic", "semantic", "procedural", "entity"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Memory:
|
|
16
|
+
id: str
|
|
17
|
+
content: str
|
|
18
|
+
memory_class: str = "semantic"
|
|
19
|
+
context: str | None = None
|
|
20
|
+
confidence: float = 1.0
|
|
21
|
+
provenance: dict[str, Any] = field(default_factory=dict)
|
|
22
|
+
activation: float = 0.0
|
|
23
|
+
created_at: float = field(default_factory=time.time)
|
|
24
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
25
|
+
superseded_by: str | None = None
|
|
26
|
+
|
|
27
|
+
def as_dict(self) -> dict[str, Any]:
|
|
28
|
+
return {"id": self.id, "content": self.content, "class": self.memory_class,
|
|
29
|
+
"context": self.context, "confidence": self.confidence,
|
|
30
|
+
"provenance": self.provenance, "activation": round(self.activation, 6),
|
|
31
|
+
"createdAt": self.created_at, "metadata": self.metadata,
|
|
32
|
+
"supersededBy": self.superseded_by}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MemoryBackend(Protocol):
|
|
36
|
+
def store(self, content: str, **kwargs: Any) -> dict[str, Any]: ...
|
|
37
|
+
def recall(self, query: str, **kwargs: Any) -> list[dict[str, Any]]: ...
|
|
38
|
+
def context(self, context: str, **kwargs: Any) -> list[dict[str, Any]]: ...
|
|
39
|
+
def feedback(self, memory_id: str, useful: bool, **kwargs: Any) -> dict[str, Any]: ...
|
|
40
|
+
def suppress(self, memory_id: str, **kwargs: Any) -> dict[str, Any]: ...
|
|
41
|
+
def supersede(self, new_id: str, old_id: str) -> dict[str, Any]: ...
|
|
42
|
+
def consolidate(self, source_ids: list[str], content: str, **kwargs: Any) -> dict[str, Any]: ...
|
|
43
|
+
def graph_query(self, operation: str, **kwargs: Any) -> dict[str, Any]: ...
|
|
44
|
+
def stats(self) -> dict[str, Any]: ...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class InMemoryBackend:
|
|
48
|
+
"""Small reference backend; useful for tests and MCP smoke tests."""
|
|
49
|
+
|
|
50
|
+
def __init__(self) -> None:
|
|
51
|
+
self.memories: dict[str, Memory] = {}
|
|
52
|
+
self.edges: list[dict[str, Any]] = []
|
|
53
|
+
|
|
54
|
+
def _get(self, memory_id: str) -> Memory:
|
|
55
|
+
if memory_id not in self.memories:
|
|
56
|
+
raise ValueError(f"Unknown memory: {memory_id}")
|
|
57
|
+
return self.memories[memory_id]
|
|
58
|
+
|
|
59
|
+
def store(self, content: str, **kwargs: Any) -> dict[str, Any]:
|
|
60
|
+
memory = Memory(id=kwargs.get("id", str(uuid.uuid4())), content=content,
|
|
61
|
+
memory_class=kwargs.get("memory_class", "semantic"),
|
|
62
|
+
context=kwargs.get("context"), confidence=kwargs.get("confidence", 1.0),
|
|
63
|
+
provenance=kwargs.get("provenance", {}), metadata=kwargs.get("metadata", {}),
|
|
64
|
+
activation=kwargs.get("activation", 0.1))
|
|
65
|
+
self.memories[memory.id] = memory
|
|
66
|
+
return memory.as_dict()
|
|
67
|
+
|
|
68
|
+
def recall(self, query: str, **kwargs: Any) -> list[dict[str, Any]]:
|
|
69
|
+
terms = set(query.lower().split())
|
|
70
|
+
context, limit = kwargs.get("context"), kwargs.get("limit", 10)
|
|
71
|
+
ranked = []
|
|
72
|
+
for memory in self.memories.values():
|
|
73
|
+
if memory.superseded_by or (context and memory.context not in (None, context)):
|
|
74
|
+
continue
|
|
75
|
+
words = set(memory.content.lower().split())
|
|
76
|
+
lexical = len(terms & words) / max(len(terms), 1)
|
|
77
|
+
contextual = 0.15 if context and memory.context == context else 0
|
|
78
|
+
score = lexical * 0.65 + memory.activation * 0.25 + memory.confidence * 0.1 + contextual
|
|
79
|
+
if score > 0:
|
|
80
|
+
ranked.append((score, memory))
|
|
81
|
+
ranked.sort(key=lambda item: item[0], reverse=True)
|
|
82
|
+
return [{**memory.as_dict(), "score": round(score, 6)} for score, memory in ranked[:limit]]
|
|
83
|
+
|
|
84
|
+
def context(self, context: str, **kwargs: Any) -> list[dict[str, Any]]:
|
|
85
|
+
items = [m for m in self.memories.values() if (not context or m.context == context) and not m.superseded_by]
|
|
86
|
+
items.sort(key=lambda m: m.activation, reverse=True)
|
|
87
|
+
budget, used = kwargs.get("token_budget"), 0
|
|
88
|
+
result = []
|
|
89
|
+
for memory in items:
|
|
90
|
+
cost = len(memory.content.split())
|
|
91
|
+
if budget is not None and used + cost > budget:
|
|
92
|
+
continue
|
|
93
|
+
used += cost
|
|
94
|
+
result.append(memory.as_dict())
|
|
95
|
+
if len(result) >= kwargs.get("limit", 10):
|
|
96
|
+
break
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
def feedback(self, memory_id: str, useful: bool, **kwargs: Any) -> dict[str, Any]:
|
|
100
|
+
memory = self._get(memory_id)
|
|
101
|
+
memory.activation = min(1.0, max(0.0, memory.activation + (0.1 if useful else -0.1)))
|
|
102
|
+
return {"id": memory_id, "useful": useful, "activation": memory.activation}
|
|
103
|
+
|
|
104
|
+
def suppress(self, memory_id: str, **kwargs: Any) -> dict[str, Any]:
|
|
105
|
+
memory = self._get(memory_id)
|
|
106
|
+
memory.activation = max(0.0, memory.activation - kwargs.get("amount", 0.5))
|
|
107
|
+
return {"id": memory_id, "activation": memory.activation}
|
|
108
|
+
|
|
109
|
+
def supersede(self, new_id: str, old_id: str) -> dict[str, Any]:
|
|
110
|
+
old = self._get(old_id); self._get(new_id); old.superseded_by = new_id; old.activation = 0.0
|
|
111
|
+
return {"superseded": old_id, "by": new_id}
|
|
112
|
+
|
|
113
|
+
def consolidate(self, source_ids: list[str], content: str, **kwargs: Any) -> dict[str, Any]:
|
|
114
|
+
for source_id in source_ids: self._get(source_id)
|
|
115
|
+
return self.store(content, memory_class=kwargs.get("memory_class", "semantic"),
|
|
116
|
+
context=kwargs.get("context"), confidence=kwargs.get("confidence", 1.0),
|
|
117
|
+
provenance={"derivedFrom": source_ids}, activation=0.7)
|
|
118
|
+
|
|
119
|
+
def graph_query(self, operation: str, **kwargs: Any) -> dict[str, Any]:
|
|
120
|
+
if operation == "add_edge":
|
|
121
|
+
self._get(kwargs["source"]); self._get(kwargs["target"])
|
|
122
|
+
edge = {"source": kwargs["source"], "type": kwargs["type"], "target": kwargs["target"]}
|
|
123
|
+
self.edges.append(edge); return edge
|
|
124
|
+
if operation == "neighbors":
|
|
125
|
+
node_id = kwargs["id"]
|
|
126
|
+
return {"id": node_id, "neighbors": [e for e in self.edges if e["source"] == node_id or e["target"] == node_id]}
|
|
127
|
+
if operation == "schema": return {"nodes": ["memory"], "edges": sorted({e["type"] for e in self.edges})}
|
|
128
|
+
raise ValueError(f"Unsupported graph operation: {operation}")
|
|
129
|
+
|
|
130
|
+
def stats(self) -> dict[str, Any]:
|
|
131
|
+
return {"memories": len(self.memories), "edges": len(self.edges)}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class PolypackBackend(InMemoryBackend):
|
|
135
|
+
"""Adapter using the real Python Polypack graph and activation engine."""
|
|
136
|
+
|
|
137
|
+
def __init__(self, graph: Any | None = None) -> None:
|
|
138
|
+
try:
|
|
139
|
+
from polypack import ActivationEngine, PolyGraph
|
|
140
|
+
except ImportError as exc:
|
|
141
|
+
raise RuntimeError("Install polypack-db or use InMemoryBackend") from exc
|
|
142
|
+
self.graph = graph or PolyGraph()
|
|
143
|
+
self.engine = ActivationEngine(self.graph)
|
|
144
|
+
|
|
145
|
+
def store(self, content: str, **kwargs: Any) -> dict[str, Any]:
|
|
146
|
+
now = int(time.time() * 1000); memory_id = kwargs.get("id", str(uuid.uuid4()))
|
|
147
|
+
node = {"id": memory_id, "type": "memory", "memoryClass": kwargs.get("memory_class", "semantic"),
|
|
148
|
+
"data": {"content": content, "context": kwargs.get("context"), "provenance": kwargs.get("provenance", {}),
|
|
149
|
+
"confidence": kwargs.get("confidence", 1.0), "metadata": kwargs.get("metadata", {})},
|
|
150
|
+
"insertedAt": now, "updatedAt": now}
|
|
151
|
+
self.graph.add_node(node); self.graph.reinforce_node(memory_id, kwargs.get("activation", 0.1), "memory_store",
|
|
152
|
+
context=kwargs.get("context"))
|
|
153
|
+
return self._node(memory_id)
|
|
154
|
+
|
|
155
|
+
def _node(self, memory_id: str) -> dict[str, Any]:
|
|
156
|
+
node = self.graph._nodes[memory_id]
|
|
157
|
+
return {"id": memory_id, "content": node.get("data", {}).get("content", ""), "class": node.get("memoryClass", "semantic"),
|
|
158
|
+
"context": node.get("data", {}).get("context"), "confidence": node.get("data", {}).get("confidence", 1.0),
|
|
159
|
+
"provenance": node.get("data", {}).get("provenance", {}), "activation": self.graph.get_activation(memory_id) or 0}
|
|
160
|
+
|
|
161
|
+
def feedback(self, memory_id: str, useful: bool, **kwargs: Any) -> dict[str, Any]:
|
|
162
|
+
self.engine.record_feedback(memory_id, useful)
|
|
163
|
+
self.graph.reinforce_node(memory_id, 0.1 if useful else -0.1, "mcp_feedback")
|
|
164
|
+
return {"id": memory_id, "useful": useful, "activation": self.graph.get_activation(memory_id)}
|
|
165
|
+
|
|
166
|
+
def suppress(self, memory_id: str, **kwargs: Any) -> dict[str, Any]:
|
|
167
|
+
self.graph.suppress_node(memory_id, kwargs.get("amount", 0.5), "mcp_suppress")
|
|
168
|
+
return {"id": memory_id, "activation": self.graph.get_activation(memory_id)}
|
|
169
|
+
|
|
170
|
+
def supersede(self, new_id: str, old_id: str) -> dict[str, Any]:
|
|
171
|
+
self.graph.supersede(new_id, old_id)
|
|
172
|
+
return {"superseded": old_id, "by": new_id}
|
|
173
|
+
|
|
174
|
+
def consolidate(self, source_ids: list[str], content: str, **kwargs: Any) -> dict[str, Any]:
|
|
175
|
+
memory_id = kwargs.get("id", str(uuid.uuid4()))
|
|
176
|
+
now = int(time.time() * 1000)
|
|
177
|
+
node = {"id": memory_id, "type": "memory", "memoryClass": kwargs.get("memory_class", "semantic"),
|
|
178
|
+
"data": {"content": content, "context": kwargs.get("context"),
|
|
179
|
+
"confidence": kwargs.get("confidence", 1.0), "derivedFrom": source_ids},
|
|
180
|
+
"insertedAt": now, "updatedAt": now}
|
|
181
|
+
self.graph.consolidate(node, source_ids)
|
|
182
|
+
return self._node(memory_id)
|
|
183
|
+
|
|
184
|
+
def recall(self, query: str, **kwargs: Any) -> list[dict[str, Any]]:
|
|
185
|
+
# Embeddings are intentionally supplied by a future caller; lexical graph
|
|
186
|
+
# traversal remains a useful fallback for text-only MCP clients.
|
|
187
|
+
terms = set(query.lower().split())
|
|
188
|
+
context, limit = kwargs.get("context"), kwargs.get("limit", 10)
|
|
189
|
+
ranked = []
|
|
190
|
+
for node_id, node in self.graph._nodes.items():
|
|
191
|
+
item = self._node(node_id)
|
|
192
|
+
if context and item["context"] not in (None, context): continue
|
|
193
|
+
score = len(terms & set(item["content"].lower().split())) / max(len(terms), 1)
|
|
194
|
+
score += item["activation"] * 0.25
|
|
195
|
+
if score: ranked.append((score, item))
|
|
196
|
+
ranked.sort(key=lambda pair: pair[0], reverse=True)
|
|
197
|
+
return [{**item, "score": round(score, 6)} for score, item in ranked[:limit]]
|
|
198
|
+
|
|
199
|
+
def context(self, context: str, **kwargs: Any) -> list[dict[str, Any]]:
|
|
200
|
+
result = self.graph.top_activated(kwargs.get("limit", 10))
|
|
201
|
+
items = [self._node(node["id"]) for node in result if not context or self._node(node["id"])["context"] == context]
|
|
202
|
+
return items
|
|
203
|
+
|
|
204
|
+
def graph_query(self, operation: str, **kwargs: Any) -> dict[str, Any]:
|
|
205
|
+
if operation == "schema": return {"nodes": ["memory"], "edges": sorted({e.get("type") for e in self.graph._edges.values() for e in e.values()})}
|
|
206
|
+
if operation == "add_edge":
|
|
207
|
+
self.graph.add_edge(kwargs["source"], kwargs["type"], kwargs["target"])
|
|
208
|
+
return {"source": kwargs["source"], "type": kwargs["type"], "target": kwargs["target"]}
|
|
209
|
+
if operation == "neighbors":
|
|
210
|
+
node_id = kwargs["id"]
|
|
211
|
+
edges = [edge for grouped in self.graph._edges.values() for edge in grouped.values()
|
|
212
|
+
if edge.get("source") == node_id or edge.get("target") == node_id]
|
|
213
|
+
return {"id": node_id, "neighbors": edges}
|
|
214
|
+
raise ValueError(f"Unsupported graph operation: {operation}")
|
|
215
|
+
|
|
216
|
+
def stats(self) -> dict[str, Any]:
|
|
217
|
+
return {"memories": self.graph.size, "edges": sum(len(edges) for edges in self.graph._edges.values())}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""FastMCP server for Polypack adaptive memory."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from .backend import InMemoryBackend, MemoryBackend
|
|
6
|
+
from .service import MemoryService
|
|
7
|
+
|
|
8
|
+
def create_server(backend: MemoryBackend | None = None):
|
|
9
|
+
try:
|
|
10
|
+
from mcp.server.fastmcp import FastMCP
|
|
11
|
+
except ImportError as exc:
|
|
12
|
+
raise RuntimeError("Install dependencies with: pip install -e '.[polypack]'") from exc
|
|
13
|
+
backend = backend or InMemoryBackend()
|
|
14
|
+
service = MemoryService(backend)
|
|
15
|
+
mcp = FastMCP("Polypack")
|
|
16
|
+
|
|
17
|
+
@mcp.tool()
|
|
18
|
+
def memory_store(content: str, memory_class: str = "semantic", context: str | None = None,
|
|
19
|
+
confidence: float = 1.0, provenance: dict | None = None, metadata: dict | None = None) -> dict:
|
|
20
|
+
"""Store durable adaptive memory with provenance and confidence."""
|
|
21
|
+
return service.store(content, memory_class, context, confidence, provenance, metadata)
|
|
22
|
+
|
|
23
|
+
@mcp.tool()
|
|
24
|
+
def memory_recall(query: str, context: str | None = None, limit: int = 10) -> list[dict]:
|
|
25
|
+
"""Hybrid semantic, graph, and activation-weighted retrieval."""
|
|
26
|
+
return backend.recall(query, context=context, limit=max(1, min(limit, 100)))
|
|
27
|
+
|
|
28
|
+
@mcp.tool()
|
|
29
|
+
def memory_context(context: str, limit: int = 10, token_budget: int | None = None) -> list[dict]:
|
|
30
|
+
"""Return a budgeted working-memory set for a context."""
|
|
31
|
+
return backend.context(context, limit=max(1, min(limit, 100)), token_budget=token_budget)
|
|
32
|
+
|
|
33
|
+
@mcp.tool()
|
|
34
|
+
def memory_feedback(memory_id: str, useful: bool, agent_id: str = "default") -> dict:
|
|
35
|
+
"""Record whether a retrieved memory helped this agent session."""
|
|
36
|
+
return backend.feedback(memory_id, useful, agent_id=agent_id)
|
|
37
|
+
|
|
38
|
+
@mcp.tool()
|
|
39
|
+
def memory_suppress(memory_id: str, amount: float = 0.5) -> dict:
|
|
40
|
+
"""Inhibit a stale or unhelpful memory without deleting it."""
|
|
41
|
+
return backend.suppress(memory_id, amount=amount)
|
|
42
|
+
|
|
43
|
+
@mcp.tool()
|
|
44
|
+
def memory_supersede(new_memory_id: str, old_memory_id: str) -> dict:
|
|
45
|
+
"""Replace an outdated fact while retaining its history."""
|
|
46
|
+
return backend.supersede(new_memory_id, old_memory_id)
|
|
47
|
+
|
|
48
|
+
@mcp.tool()
|
|
49
|
+
def memory_consolidate(source_ids: list[str], content: str, context: str | None = None,
|
|
50
|
+
memory_class: str = "semantic", confidence: float = 1.0) -> dict:
|
|
51
|
+
"""Consolidate episodic memories into a durable higher-level memory."""
|
|
52
|
+
return backend.consolidate(source_ids, content, context=context, memory_class=memory_class, confidence=confidence)
|
|
53
|
+
|
|
54
|
+
@mcp.tool()
|
|
55
|
+
def graph_query(operation: str, id: str | None = None, source: str | None = None,
|
|
56
|
+
target: str | None = None, type: str | None = None) -> dict:
|
|
57
|
+
"""Escape hatch for narrowly scoped graph operations (neighbors, add_edge, schema)."""
|
|
58
|
+
args = {k: v for k, v in {"id": id, "source": source, "target": target, "type": type}.items() if v is not None}
|
|
59
|
+
return backend.graph_query(operation, **args)
|
|
60
|
+
|
|
61
|
+
@mcp.resource("polypack://memory/context/{context}")
|
|
62
|
+
def context_resource(context: str) -> str: return json.dumps(backend.context(context), indent=2)
|
|
63
|
+
|
|
64
|
+
@mcp.resource("polypack://memory/active")
|
|
65
|
+
def active_resource() -> str: return json.dumps(backend.context("", limit=20), indent=2)
|
|
66
|
+
|
|
67
|
+
@mcp.resource("polypack://graph/schema")
|
|
68
|
+
def schema_resource() -> str: return json.dumps(backend.graph_query("schema"), indent=2)
|
|
69
|
+
|
|
70
|
+
@mcp.resource("polypack://stats")
|
|
71
|
+
def stats_resource() -> str: return json.dumps(backend.stats(), indent=2)
|
|
72
|
+
return mcp
|
|
73
|
+
|
|
74
|
+
def main() -> None:
|
|
75
|
+
parser = argparse.ArgumentParser(description="Polypack adaptive-memory MCP server")
|
|
76
|
+
parser.add_argument("--transport", choices=("stdio", "sse"), default="stdio")
|
|
77
|
+
parser.add_argument("--store", help="Polypack directory for durable storage")
|
|
78
|
+
args = parser.parse_args()
|
|
79
|
+
backend = None
|
|
80
|
+
if args.store:
|
|
81
|
+
from .backend import PolypackBackend
|
|
82
|
+
from polypack import PolyGraph
|
|
83
|
+
backend = PolypackBackend(PolyGraph.open(args.store))
|
|
84
|
+
create_server(backend).run(transport=args.transport)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
main()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Validated, agent-facing operations shared by MCP transports and tests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
from .backend import CLASSES, MemoryBackend
|
|
7
|
+
|
|
8
|
+
class MemoryService:
|
|
9
|
+
def __init__(self, backend: MemoryBackend): self.backend = backend
|
|
10
|
+
|
|
11
|
+
def store(self, content: str, memory_class: str = "semantic", context: str | None = None,
|
|
12
|
+
confidence: float = 1.0, provenance: dict[str, Any] | None = None, metadata: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
13
|
+
if not content.strip(): raise ValueError("content must not be empty")
|
|
14
|
+
if memory_class not in CLASSES: raise ValueError(f"class must be one of {sorted(CLASSES)}")
|
|
15
|
+
if not 0 <= confidence <= 1: raise ValueError("confidence must be between 0 and 1")
|
|
16
|
+
return self.backend.store(content, memory_class=memory_class, context=context, confidence=confidence,
|
|
17
|
+
provenance=provenance or {}, metadata=metadata or {})
|
|
18
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: polypack-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server exposing Polypack as persistent adaptive memory
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: mcp<1.11,>=1.10
|
|
8
|
+
Requires-Dist: anyio<4.10,>=4.5
|
|
9
|
+
Provides-Extra: polypack
|
|
10
|
+
Requires-Dist: polypack-db>=3.1; extra == "polypack"
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# polypack-mcp
|
|
15
|
+
|
|
16
|
+
An MCP server that exposes Polypack as persistent adaptive memory. MCP-specific
|
|
17
|
+
tools live here; the database remains an independent dependency.
|
|
18
|
+
|
|
19
|
+
## Run
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pip install -e '.[polypack]'
|
|
23
|
+
polypack-mcp --store ./polypack-data
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The server exposes eight focused tools: `memory_store`, `memory_recall`,
|
|
27
|
+
`memory_context`, `memory_feedback`, `memory_suppress`, `memory_supersede`,
|
|
28
|
+
`memory_consolidate`, and `graph_query`. It also publishes context, active-memory,
|
|
29
|
+
schema, and stats resources under `polypack://`.
|
|
30
|
+
|
|
31
|
+
Pass `--store` to open a durable Polypack directory. Without it, the server uses
|
|
32
|
+
the in-memory reference backend, which is convenient for smoke tests.
|
|
33
|
+
|
|
34
|
+
## Development
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pip install -e '.[dev]'
|
|
38
|
+
pytest
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The test suite includes an MCP client/server protocol smoke test covering tool
|
|
42
|
+
discovery, memory storage, recall, and resource reads.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
polypack_mcp/__init__.py
|
|
4
|
+
polypack_mcp/backend.py
|
|
5
|
+
polypack_mcp/server.py
|
|
6
|
+
polypack_mcp/service.py
|
|
7
|
+
polypack_mcp.egg-info/PKG-INFO
|
|
8
|
+
polypack_mcp.egg-info/SOURCES.txt
|
|
9
|
+
polypack_mcp.egg-info/dependency_links.txt
|
|
10
|
+
polypack_mcp.egg-info/entry_points.txt
|
|
11
|
+
polypack_mcp.egg-info/requires.txt
|
|
12
|
+
polypack_mcp.egg-info/top_level.txt
|
|
13
|
+
tests/test_mcp_protocol.py
|
|
14
|
+
tests/test_service.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
polypack_mcp
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "polypack-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP server exposing Polypack as persistent adaptive memory"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = ["mcp>=1.10,<1.11", "anyio>=4.5,<4.10"]
|
|
12
|
+
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
polypack = ["polypack-db>=3.1"]
|
|
15
|
+
dev = ["pytest>=8"]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
polypack-mcp = "polypack_mcp.server:main"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.packages.find]
|
|
21
|
+
include = ["polypack_mcp*"]
|
|
22
|
+
|
|
23
|
+
[tool.pytest.ini_options]
|
|
24
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from mcp.shared.memory import create_connected_server_and_client_session
|
|
5
|
+
from polypack_mcp.server import create_server
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_stdio_protocol_lists_surface_and_round_trips_memory():
|
|
9
|
+
async def exercise():
|
|
10
|
+
server = create_server()
|
|
11
|
+
async with create_connected_server_and_client_session(server._mcp_server) as session:
|
|
12
|
+
tools = await session.list_tools()
|
|
13
|
+
assert {tool.name for tool in tools.tools} == {
|
|
14
|
+
"memory_store", "memory_recall", "memory_context", "memory_feedback",
|
|
15
|
+
"memory_suppress", "memory_supersede", "memory_consolidate", "graph_query",
|
|
16
|
+
}
|
|
17
|
+
stored = await session.call_tool("memory_store", {
|
|
18
|
+
"content": "Polypack MCP protocol works", "context": "test"
|
|
19
|
+
})
|
|
20
|
+
memory = json.loads(stored.content[0].text)
|
|
21
|
+
recalled = await session.call_tool("memory_recall", {
|
|
22
|
+
"query": "MCP protocol", "context": "test"
|
|
23
|
+
})
|
|
24
|
+
recalled_payload = [json.loads(block.text) for block in recalled.content]
|
|
25
|
+
assert recalled_payload[0]["id"] == memory["id"]
|
|
26
|
+
stats = await session.read_resource("polypack://stats")
|
|
27
|
+
assert '"memories": 1' in stats.contents[0].text
|
|
28
|
+
|
|
29
|
+
asyncio.run(exercise())
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from polypack_mcp import InMemoryBackend, MemoryService
|
|
2
|
+
|
|
3
|
+
def test_store_recall_feedback_and_supersede():
|
|
4
|
+
backend = InMemoryBackend(); service = MemoryService(backend)
|
|
5
|
+
old = service.store("Python graph queries need optimisation", context="polypack")
|
|
6
|
+
new = service.store("Python graph queries now use the native index", context="polypack")
|
|
7
|
+
found = backend.recall("What performance work remains?", context="polypack")
|
|
8
|
+
assert found and found[0]["id"] == old["id"]
|
|
9
|
+
backend.feedback(old["id"], True)
|
|
10
|
+
backend.supersede(new["id"], old["id"])
|
|
11
|
+
assert all(item["id"] != old["id"] for item in backend.recall("graph queries", context="polypack"))
|
|
12
|
+
|
|
13
|
+
def test_context_budget_and_graph():
|
|
14
|
+
backend = InMemoryBackend(); service = MemoryService(backend)
|
|
15
|
+
a = service.store("one two", context="coding"); b = service.store("three four five", context="coding")
|
|
16
|
+
assert len(backend.context("coding", token_budget=2)) == 1
|
|
17
|
+
backend.graph_query("add_edge", source=a["id"], target=b["id"], type="related")
|
|
18
|
+
assert len(backend.graph_query("neighbors", id=a["id"])["neighbors"]) == 1
|
|
19
|
+
|