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.
@@ -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,7 @@
1
+ """Polypack's Model Context Protocol integration."""
2
+
3
+ from .backend import InMemoryBackend, MemoryBackend, PolypackBackend
4
+ from .service import MemoryService
5
+
6
+ __all__ = ["InMemoryBackend", "MemoryBackend", "PolypackBackend", "MemoryService"]
7
+
@@ -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,2 @@
1
+ [console_scripts]
2
+ polypack-mcp = polypack_mcp.server:main
@@ -0,0 +1,8 @@
1
+ mcp<1.11,>=1.10
2
+ anyio<4.10,>=4.5
3
+
4
+ [dev]
5
+ pytest>=8
6
+
7
+ [polypack]
8
+ polypack-db>=3.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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+