amfs-mcp-server 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,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: amfs-mcp-server
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AMFS MCP Server — expose Agent Memory as MCP tools for Cursor and Claude Code
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: amfs
|
|
8
|
+
Requires-Dist: amfs-adapter-http
|
|
9
|
+
Requires-Dist: fastmcp>=2.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "amfs-mcp-server"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "AMFS MCP Server — expose Agent Memory as MCP tools for Cursor and Claude Code"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"amfs",
|
|
9
|
+
"amfs-adapter-http",
|
|
10
|
+
"fastmcp>=2.0",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
amfs-mcp-server = "amfs_mcp.server:main"
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["hatchling"]
|
|
18
|
+
build-backend = "hatchling.build"
|
|
19
|
+
|
|
20
|
+
[tool.hatch.build.targets.wheel]
|
|
21
|
+
packages = ["src/amfs_mcp"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Auto-detect agent identity from environment.
|
|
2
|
+
|
|
3
|
+
Determines the platform (cursor, claude-code, or generic) and username
|
|
4
|
+
so that every memory write carries automatic provenance without the
|
|
5
|
+
agent needing to configure anything.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import getpass
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def detect_agent_id() -> str:
|
|
15
|
+
"""Build an agent_id from environment signals.
|
|
16
|
+
|
|
17
|
+
Detection order:
|
|
18
|
+
1. Explicit ``AMFS_AGENT_ID`` env var (escape hatch).
|
|
19
|
+
2. Cursor — presence of ``CURSOR_SESSION_ID`` or ``VSCODE_PID``.
|
|
20
|
+
3. Claude Code — presence of ``CLAUDE_CODE_SESSION``.
|
|
21
|
+
4. Fallback to ``agent/<username>``.
|
|
22
|
+
"""
|
|
23
|
+
explicit = os.environ.get("AMFS_AGENT_ID")
|
|
24
|
+
if explicit:
|
|
25
|
+
return explicit
|
|
26
|
+
|
|
27
|
+
username = _get_username()
|
|
28
|
+
|
|
29
|
+
if os.environ.get("CURSOR_SESSION_ID") or os.environ.get("VSCODE_PID"):
|
|
30
|
+
return f"cursor/{username}"
|
|
31
|
+
|
|
32
|
+
if os.environ.get("CLAUDE_CODE_SESSION"):
|
|
33
|
+
return f"claude-code/{username}"
|
|
34
|
+
|
|
35
|
+
return f"agent/{username}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_username() -> str:
|
|
39
|
+
"""Best-effort username from env or system."""
|
|
40
|
+
return os.environ.get("USER") or os.environ.get("USERNAME") or getpass.getuser()
|
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
"""AMFS MCP Server — exposes Agent Memory as MCP tools.
|
|
2
|
+
|
|
3
|
+
Designed for Cursor, Claude Code, and any MCP-compatible AI agent.
|
|
4
|
+
One AgentMemory instance persists for the lifetime of the server process,
|
|
5
|
+
giving agents a continuous session with automatic causal tracking.
|
|
6
|
+
|
|
7
|
+
Supports two transports:
|
|
8
|
+
|
|
9
|
+
- **stdio** (default) — for Cursor and Claude Code local MCP integration.
|
|
10
|
+
- **streamable-http** — for remote/team access over HTTP. Ideal when the
|
|
11
|
+
AMFS server runs on a shared host and multiple agents connect remotely.
|
|
12
|
+
|
|
13
|
+
Transport selection via CLI or environment:
|
|
14
|
+
|
|
15
|
+
amfs-mcp-server # stdio (default)
|
|
16
|
+
amfs-mcp-server --transport http # streamable-http on 0.0.0.0:8000/mcp
|
|
17
|
+
amfs-mcp-server --transport http --port 9000 --host 127.0.0.1 --path /amfs
|
|
18
|
+
|
|
19
|
+
AMFS_TRANSPORT=http amfs-mcp-server # env-based selection
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import json
|
|
26
|
+
import logging
|
|
27
|
+
import os
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from fastmcp import FastMCP
|
|
31
|
+
|
|
32
|
+
from amfs import AgentMemory, MemoryType, OutcomeType
|
|
33
|
+
from amfs.config import load_config_or_default
|
|
34
|
+
from amfs_core.models import AMFSConfig, LayerConfig
|
|
35
|
+
|
|
36
|
+
from amfs_mcp.agent_id import detect_agent_id
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
mcp = FastMCP(name="amfs")
|
|
41
|
+
|
|
42
|
+
_memory: AgentMemory | None = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _get_memory() -> AgentMemory:
|
|
46
|
+
"""Lazily initialise the shared AgentMemory singleton."""
|
|
47
|
+
global _memory
|
|
48
|
+
if _memory is not None:
|
|
49
|
+
return _memory
|
|
50
|
+
|
|
51
|
+
agent_id = detect_agent_id()
|
|
52
|
+
|
|
53
|
+
http_url = os.environ.get("AMFS_HTTP_URL")
|
|
54
|
+
if http_url:
|
|
55
|
+
if os.environ.get("AMFS_POSTGRES_DSN"):
|
|
56
|
+
logger.warning(
|
|
57
|
+
"Both AMFS_HTTP_URL and AMFS_POSTGRES_DSN are set. "
|
|
58
|
+
"The HTTP adapter takes precedence — direct DB access "
|
|
59
|
+
"is bypassed in favour of the authenticated HTTP API."
|
|
60
|
+
)
|
|
61
|
+
try:
|
|
62
|
+
from amfs_adapter_http import HttpAdapter
|
|
63
|
+
|
|
64
|
+
api_key = os.environ.get("AMFS_API_KEY", "")
|
|
65
|
+
logger.info(
|
|
66
|
+
"AMFS HTTP adapter mode — routing through %s", http_url
|
|
67
|
+
)
|
|
68
|
+
adapter = HttpAdapter(base_url=http_url, api_key=api_key)
|
|
69
|
+
_memory = AgentMemory(agent_id=agent_id, adapter=adapter)
|
|
70
|
+
return _memory
|
|
71
|
+
except ImportError:
|
|
72
|
+
logger.warning(
|
|
73
|
+
"AMFS_HTTP_URL is set but amfs-adapter-http is not installed. "
|
|
74
|
+
"Falling back to local adapter. "
|
|
75
|
+
"Install with: pip install amfs-adapter-http"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
config = _resolve_config()
|
|
79
|
+
|
|
80
|
+
ttl_interval_str = os.environ.get("AMFS_TTL_SWEEP_INTERVAL")
|
|
81
|
+
ttl_sweep_interval = float(ttl_interval_str) if ttl_interval_str else 300.0
|
|
82
|
+
|
|
83
|
+
logger.info("AMFS MCP server starting — agent_id=%s", agent_id)
|
|
84
|
+
_memory = AgentMemory(
|
|
85
|
+
agent_id=agent_id,
|
|
86
|
+
config_path=None,
|
|
87
|
+
adapter=None,
|
|
88
|
+
ttl_sweep_interval=ttl_sweep_interval,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
_memory._config = config
|
|
92
|
+
from amfs.factory import create_adapter_from_config
|
|
93
|
+
|
|
94
|
+
adapter = create_adapter_from_config(config)
|
|
95
|
+
_memory._adapter = adapter
|
|
96
|
+
_memory._engine._adapter = adapter
|
|
97
|
+
_memory._propagator._adapter = adapter
|
|
98
|
+
|
|
99
|
+
return _memory
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _resolve_config() -> AMFSConfig:
|
|
103
|
+
"""Resolve AMFS configuration from environment or config files.
|
|
104
|
+
|
|
105
|
+
Priority:
|
|
106
|
+
1. AMFS_POSTGRES_DSN env var → Postgres adapter
|
|
107
|
+
2. AMFS_DATA_DIR env var → filesystem adapter at that path
|
|
108
|
+
3. amfs.yaml discovery → load from file
|
|
109
|
+
4. Default → filesystem adapter at .amfs/
|
|
110
|
+
"""
|
|
111
|
+
postgres_dsn = os.environ.get("AMFS_POSTGRES_DSN")
|
|
112
|
+
if postgres_dsn:
|
|
113
|
+
return AMFSConfig(
|
|
114
|
+
namespace="default",
|
|
115
|
+
layers={
|
|
116
|
+
"primary": LayerConfig(
|
|
117
|
+
adapter="postgres",
|
|
118
|
+
options={"dsn": postgres_dsn},
|
|
119
|
+
)
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
data_dir = os.environ.get("AMFS_DATA_DIR")
|
|
124
|
+
if data_dir:
|
|
125
|
+
return AMFSConfig(
|
|
126
|
+
namespace="default",
|
|
127
|
+
layers={
|
|
128
|
+
"primary": LayerConfig(
|
|
129
|
+
adapter="filesystem",
|
|
130
|
+
options={"root": data_dir},
|
|
131
|
+
)
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return load_config_or_default()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _serialize_entry(entry: Any) -> dict[str, Any]:
|
|
139
|
+
"""Convert a MemoryEntry to a JSON-safe dict for MCP responses."""
|
|
140
|
+
data = entry.model_dump(mode="json")
|
|
141
|
+
data.pop("embedding", None)
|
|
142
|
+
return data
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
146
|
+
# MCP Tools
|
|
147
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@mcp.tool
|
|
151
|
+
def amfs_read(entity_path: str, key: str) -> str:
|
|
152
|
+
"""Read a memory entry by entity path and key.
|
|
153
|
+
|
|
154
|
+
Returns the full entry as JSON including value, confidence, provenance,
|
|
155
|
+
and version. Returns a message if the entry does not exist.
|
|
156
|
+
|
|
157
|
+
Example: amfs_read("checkout-service", "retry-pattern")
|
|
158
|
+
"""
|
|
159
|
+
mem = _get_memory()
|
|
160
|
+
entry = mem.read(entity_path, key)
|
|
161
|
+
if entry is None:
|
|
162
|
+
return json.dumps({"status": "not_found", "entity_path": entity_path, "key": key})
|
|
163
|
+
return json.dumps(_serialize_entry(entry), default=str)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@mcp.tool
|
|
167
|
+
def amfs_write(
|
|
168
|
+
entity_path: str,
|
|
169
|
+
key: str,
|
|
170
|
+
value: str,
|
|
171
|
+
confidence: float = 1.0,
|
|
172
|
+
pattern_refs: list[str] | None = None,
|
|
173
|
+
memory_type: str = "fact",
|
|
174
|
+
artifact_refs: list[dict[str, Any]] | None = None,
|
|
175
|
+
shared: bool = True,
|
|
176
|
+
) -> str:
|
|
177
|
+
"""Write a memory entry with automatic provenance tracking.
|
|
178
|
+
|
|
179
|
+
The agent_id and session_id are auto-detected from the environment.
|
|
180
|
+
Use this after completing a task, discovering a pattern, or recording
|
|
181
|
+
a decision.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
entity_path: Hierarchical path like "repo/service" (e.g. "amfs/core-engine")
|
|
185
|
+
key: Name for this piece of knowledge (e.g. "retry-pattern", "risk-signals")
|
|
186
|
+
value: The knowledge to store — can be plain text or JSON string
|
|
187
|
+
confidence: How confident you are (0.0-1.0, default 1.0)
|
|
188
|
+
pattern_refs: Optional list of related pattern keys for cross-referencing
|
|
189
|
+
memory_type: One of "fact" (default), "belief", or "experience"
|
|
190
|
+
artifact_refs: Optional list of external artifact references. Each dict
|
|
191
|
+
should have "uri" (required), and optionally "media_type", "label",
|
|
192
|
+
"size_bytes".
|
|
193
|
+
shared: If True (default), other agents can read this entry. If False,
|
|
194
|
+
only the writing agent can access it — useful for internal reasoning,
|
|
195
|
+
scratchpad notes, or sensitive context.
|
|
196
|
+
|
|
197
|
+
Example: amfs_write("checkout-service", "retry-pattern", '{"max_retries": 3}')
|
|
198
|
+
Example private: amfs_write("checkout-service", "internal-notes", "...", shared=False)
|
|
199
|
+
"""
|
|
200
|
+
from amfs_core.models import ArtifactRef
|
|
201
|
+
|
|
202
|
+
mem = _get_memory()
|
|
203
|
+
|
|
204
|
+
parsed_value: Any = value
|
|
205
|
+
try:
|
|
206
|
+
parsed_value = json.loads(value)
|
|
207
|
+
except (json.JSONDecodeError, TypeError):
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
type_map = {"fact": MemoryType.FACT, "belief": MemoryType.BELIEF, "experience": MemoryType.EXPERIENCE}
|
|
211
|
+
mt = type_map.get(memory_type.lower(), MemoryType.FACT)
|
|
212
|
+
|
|
213
|
+
parsed_artifact_refs = [
|
|
214
|
+
ArtifactRef.model_validate(r) for r in (artifact_refs or [])
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
entry = mem.write(
|
|
218
|
+
entity_path,
|
|
219
|
+
key,
|
|
220
|
+
parsed_value,
|
|
221
|
+
confidence=confidence,
|
|
222
|
+
pattern_refs=pattern_refs,
|
|
223
|
+
memory_type=mt,
|
|
224
|
+
artifact_refs=parsed_artifact_refs,
|
|
225
|
+
shared=shared,
|
|
226
|
+
)
|
|
227
|
+
return json.dumps(_serialize_entry(entry), default=str)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@mcp.tool
|
|
231
|
+
def amfs_search(
|
|
232
|
+
query: str | None = None,
|
|
233
|
+
entity_path: str | None = None,
|
|
234
|
+
min_confidence: float = 0.0,
|
|
235
|
+
max_confidence: float | None = None,
|
|
236
|
+
agent_id: str | None = None,
|
|
237
|
+
since: str | None = None,
|
|
238
|
+
pattern_ref: str | None = None,
|
|
239
|
+
sort_by: str = "confidence",
|
|
240
|
+
limit: int = 20,
|
|
241
|
+
depth: int = 3,
|
|
242
|
+
) -> str:
|
|
243
|
+
"""Search across all memory entries with filters.
|
|
244
|
+
|
|
245
|
+
Use this before starting work to find context about the entity you're
|
|
246
|
+
modifying, or to check if another agent already solved a similar problem.
|
|
247
|
+
|
|
248
|
+
When a Postgres adapter with tsvector support is configured, the query
|
|
249
|
+
text is used for full-text search. Otherwise falls back to Python
|
|
250
|
+
substring matching on keys/values.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
query: Optional text to search for (full-text when available, substring fallback)
|
|
254
|
+
entity_path: Filter to a specific entity path
|
|
255
|
+
min_confidence: Minimum confidence threshold (0.0-1.0)
|
|
256
|
+
max_confidence: Maximum confidence threshold (0.0-1.0)
|
|
257
|
+
agent_id: Filter to entries from a specific agent
|
|
258
|
+
since: Optional ISO timestamp to filter entries written after this time
|
|
259
|
+
pattern_ref: Filter to entries tagged with this pattern reference
|
|
260
|
+
sort_by: Sort order — "confidence", "recency", or "version"
|
|
261
|
+
limit: Maximum results to return
|
|
262
|
+
depth: Tier depth (1=hot only, 2=hot+warm, 3=all tiers)
|
|
263
|
+
|
|
264
|
+
Example: amfs_search(entity_path="checkout-service", min_confidence=0.5)
|
|
265
|
+
"""
|
|
266
|
+
from datetime import datetime as dt
|
|
267
|
+
|
|
268
|
+
mem = _get_memory()
|
|
269
|
+
since_dt = dt.fromisoformat(since) if since else None
|
|
270
|
+
|
|
271
|
+
results = mem.search(
|
|
272
|
+
query=query,
|
|
273
|
+
entity_path=entity_path,
|
|
274
|
+
min_confidence=min_confidence,
|
|
275
|
+
max_confidence=max_confidence,
|
|
276
|
+
agent_id=agent_id,
|
|
277
|
+
since=since_dt,
|
|
278
|
+
pattern_ref=pattern_ref,
|
|
279
|
+
sort_by=sort_by,
|
|
280
|
+
limit=limit,
|
|
281
|
+
depth=depth,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
if query and not _adapter_supports_fts(mem):
|
|
285
|
+
query_lower = query.lower()
|
|
286
|
+
results = [
|
|
287
|
+
e
|
|
288
|
+
for e in results
|
|
289
|
+
if query_lower in e.key.lower()
|
|
290
|
+
or query_lower in str(e.value).lower()
|
|
291
|
+
or query_lower in e.entity_path.lower()
|
|
292
|
+
]
|
|
293
|
+
|
|
294
|
+
return json.dumps([_serialize_entry(e) for e in results], default=str)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _adapter_supports_fts(mem) -> bool:
|
|
298
|
+
"""Check if the adapter handles full-text search natively."""
|
|
299
|
+
adapter = getattr(mem, "_adapter", None) or getattr(mem, "adapter", None)
|
|
300
|
+
return getattr(adapter, "_has_search_tsv", False)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@mcp.tool
|
|
304
|
+
def amfs_retrieve(
|
|
305
|
+
query: str,
|
|
306
|
+
entity_path: str | None = None,
|
|
307
|
+
min_confidence: float = 0.0,
|
|
308
|
+
limit: int = 10,
|
|
309
|
+
semantic_weight: float = 0.5,
|
|
310
|
+
recency_weight: float = 0.3,
|
|
311
|
+
confidence_weight: float = 0.2,
|
|
312
|
+
depth: int = 3,
|
|
313
|
+
) -> str:
|
|
314
|
+
"""Find the most relevant memories for a natural language query.
|
|
315
|
+
|
|
316
|
+
Blends semantic similarity, recency, and confidence into a single
|
|
317
|
+
ranked list. Use this when you need to find memories by meaning,
|
|
318
|
+
not exact key/value match. Use amfs_search for structured filtering.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
query: Natural language query describing what you're looking for
|
|
322
|
+
entity_path: Optional entity path filter
|
|
323
|
+
min_confidence: Minimum confidence threshold (0.0-1.0)
|
|
324
|
+
limit: Maximum results to return
|
|
325
|
+
semantic_weight: Weight for semantic similarity (0.0-1.0)
|
|
326
|
+
recency_weight: Weight for recency (0.0-1.0)
|
|
327
|
+
confidence_weight: Weight for confidence (0.0-1.0)
|
|
328
|
+
depth: Tier depth (1=hot only, 2=hot+warm, 3=all tiers)
|
|
329
|
+
|
|
330
|
+
Returns ranked results with score breakdowns showing how each
|
|
331
|
+
signal contributed to the final ranking.
|
|
332
|
+
"""
|
|
333
|
+
from amfs_core.models import RecallConfig
|
|
334
|
+
|
|
335
|
+
mem = _get_memory()
|
|
336
|
+
recall_config = RecallConfig(
|
|
337
|
+
semantic_weight=semantic_weight,
|
|
338
|
+
recency_weight=recency_weight,
|
|
339
|
+
confidence_weight=confidence_weight,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
results = mem.search(
|
|
343
|
+
query=query,
|
|
344
|
+
entity_path=entity_path,
|
|
345
|
+
min_confidence=min_confidence,
|
|
346
|
+
limit=limit,
|
|
347
|
+
recall_config=recall_config,
|
|
348
|
+
depth=depth,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
serialized = []
|
|
352
|
+
for scored in results:
|
|
353
|
+
data = _serialize_entry(scored.entry)
|
|
354
|
+
data["_score"] = round(scored.score, 4)
|
|
355
|
+
data["_breakdown"] = {k: round(v, 4) for k, v in scored.breakdown.items()}
|
|
356
|
+
serialized.append(data)
|
|
357
|
+
|
|
358
|
+
return json.dumps(serialized, default=str)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@mcp.tool
|
|
362
|
+
def amfs_list(entity_path: str | None = None) -> str:
|
|
363
|
+
"""List all current memory entries, optionally filtered to an entity path.
|
|
364
|
+
|
|
365
|
+
Use to explore what knowledge exists for a given service or module.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
entity_path: Optional entity path to filter (e.g. "checkout-service")
|
|
369
|
+
|
|
370
|
+
Example: amfs_list("checkout-service")
|
|
371
|
+
"""
|
|
372
|
+
mem = _get_memory()
|
|
373
|
+
entries = mem.list(entity_path)
|
|
374
|
+
return json.dumps([_serialize_entry(e) for e in entries], default=str)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@mcp.tool
|
|
378
|
+
def amfs_graph_neighbors(
|
|
379
|
+
entity: str,
|
|
380
|
+
relation: str | None = None,
|
|
381
|
+
direction: str = "both",
|
|
382
|
+
min_confidence: float = 0.0,
|
|
383
|
+
depth: int = 1,
|
|
384
|
+
limit: int = 50,
|
|
385
|
+
) -> str:
|
|
386
|
+
"""Explore the knowledge graph around an entity.
|
|
387
|
+
|
|
388
|
+
Shows what services, agents, patterns, and outcomes are connected
|
|
389
|
+
to the given entity, with relationship types and confidence scores.
|
|
390
|
+
Use depth > 1 for multi-hop traversal.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
entity: The entity to explore (e.g. "checkout-service/retry-pattern")
|
|
394
|
+
relation: Optional filter by relation type (e.g. "references", "informed")
|
|
395
|
+
direction: Edge direction — "outgoing", "incoming", or "both"
|
|
396
|
+
min_confidence: Minimum edge confidence (0.0-1.0)
|
|
397
|
+
depth: Traversal depth (1 = direct neighbors, 2+ = multi-hop)
|
|
398
|
+
limit: Maximum edges to return
|
|
399
|
+
"""
|
|
400
|
+
mem = _get_memory()
|
|
401
|
+
edges = mem.graph_neighbors(
|
|
402
|
+
entity,
|
|
403
|
+
relation=relation,
|
|
404
|
+
direction=direction,
|
|
405
|
+
min_confidence=min_confidence,
|
|
406
|
+
depth=depth,
|
|
407
|
+
limit=limit,
|
|
408
|
+
)
|
|
409
|
+
return json.dumps([e.model_dump(mode="json") for e in edges], default=str)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@mcp.tool
|
|
413
|
+
def amfs_stats() -> str:
|
|
414
|
+
"""Get aggregate statistics about the memory store.
|
|
415
|
+
|
|
416
|
+
Returns total entries, entities, agents, confidence distribution,
|
|
417
|
+
and time range. Useful for understanding the current state of
|
|
418
|
+
shared knowledge.
|
|
419
|
+
"""
|
|
420
|
+
mem = _get_memory()
|
|
421
|
+
stats = mem.stats()
|
|
422
|
+
return json.dumps(stats.model_dump(mode="json"), default=str)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@mcp.tool
|
|
426
|
+
def amfs_commit_outcome(
|
|
427
|
+
outcome_ref: str,
|
|
428
|
+
outcome_type: str,
|
|
429
|
+
) -> str:
|
|
430
|
+
"""Record an outcome and auto-link it to everything read this session.
|
|
431
|
+
|
|
432
|
+
Call this when something significant happens — a deployment succeeds,
|
|
433
|
+
a bug is found, an incident occurs. The outcome automatically back-
|
|
434
|
+
propagates confidence changes to all entries that influenced the decision.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
outcome_ref: Reference identifier (e.g. "INC-2047", "task-42", "PR-456")
|
|
438
|
+
outcome_type: One of "success", "minor_failure", "failure", "critical_failure"
|
|
439
|
+
|
|
440
|
+
Example: amfs_commit_outcome("task-42", "success")
|
|
441
|
+
"""
|
|
442
|
+
mem = _get_memory()
|
|
443
|
+
|
|
444
|
+
type_map = {
|
|
445
|
+
"success": OutcomeType.SUCCESS,
|
|
446
|
+
"minor_failure": OutcomeType.MINOR_FAILURE,
|
|
447
|
+
"failure": OutcomeType.FAILURE,
|
|
448
|
+
"critical_failure": OutcomeType.CRITICAL_FAILURE,
|
|
449
|
+
"clean_deploy": OutcomeType.CLEAN_DEPLOY,
|
|
450
|
+
"regression": OutcomeType.REGRESSION,
|
|
451
|
+
"p2_incident": OutcomeType.P2_INCIDENT,
|
|
452
|
+
"p1_incident": OutcomeType.P1_INCIDENT,
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
otype = type_map.get(outcome_type.lower())
|
|
456
|
+
if otype is None:
|
|
457
|
+
valid = ", ".join(type_map.keys())
|
|
458
|
+
return json.dumps({
|
|
459
|
+
"error": f"Invalid outcome_type '{outcome_type}'. Must be one of: {valid}"
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
entries = mem.commit_outcome(outcome_ref, otype)
|
|
463
|
+
return json.dumps(
|
|
464
|
+
{
|
|
465
|
+
"outcome_ref": outcome_ref,
|
|
466
|
+
"outcome_type": outcome_type,
|
|
467
|
+
"affected_entries": len(entries),
|
|
468
|
+
"entries": [_serialize_entry(e) for e in entries],
|
|
469
|
+
},
|
|
470
|
+
default=str,
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@mcp.tool
|
|
475
|
+
def amfs_history(
|
|
476
|
+
entity_path: str,
|
|
477
|
+
key: str,
|
|
478
|
+
since: str | None = None,
|
|
479
|
+
until: str | None = None,
|
|
480
|
+
) -> str:
|
|
481
|
+
"""Get the full version history of a memory entry over time.
|
|
482
|
+
|
|
483
|
+
Returns all CoW versions of a key, showing how the value and confidence
|
|
484
|
+
evolved. Useful for temporal reasoning — "how did this decision change?"
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
entity_path: Entity path (e.g. "checkout-service")
|
|
488
|
+
key: Memory key to trace (e.g. "retry-pattern")
|
|
489
|
+
since: Optional ISO timestamp to filter versions after this time
|
|
490
|
+
until: Optional ISO timestamp to filter versions before this time
|
|
491
|
+
|
|
492
|
+
Example: amfs_history("checkout-service", "retry-pattern")
|
|
493
|
+
"""
|
|
494
|
+
from datetime import datetime as dt
|
|
495
|
+
|
|
496
|
+
mem = _get_memory()
|
|
497
|
+
since_dt = dt.fromisoformat(since) if since else None
|
|
498
|
+
until_dt = dt.fromisoformat(until) if until else None
|
|
499
|
+
|
|
500
|
+
versions = mem.history(entity_path, key, since=since_dt, until=until_dt)
|
|
501
|
+
return json.dumps(
|
|
502
|
+
{
|
|
503
|
+
"entity_path": entity_path,
|
|
504
|
+
"key": key,
|
|
505
|
+
"version_count": len(versions),
|
|
506
|
+
"versions": [_serialize_entry(e) for e in versions],
|
|
507
|
+
},
|
|
508
|
+
default=str,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
@mcp.tool
|
|
513
|
+
def amfs_record_context(
|
|
514
|
+
label: str,
|
|
515
|
+
summary: str,
|
|
516
|
+
source: str = "",
|
|
517
|
+
) -> str:
|
|
518
|
+
"""Record external context that influenced this session's decisions.
|
|
519
|
+
|
|
520
|
+
Call this after consulting an external tool, API, or data source.
|
|
521
|
+
The context is added to the causal chain returned by amfs_explain(),
|
|
522
|
+
making decision traces complete.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
label: Short name for the context (e.g. "pagerduty-incidents", "git-log")
|
|
526
|
+
summary: Brief summary of what was found
|
|
527
|
+
source: Optional source identifier (e.g. "PagerDuty API", "git")
|
|
528
|
+
|
|
529
|
+
Example: amfs_record_context("git-log", "15 commits since last deploy", "git")
|
|
530
|
+
"""
|
|
531
|
+
mem = _get_memory()
|
|
532
|
+
mem.record_context(label, summary, source=source or None)
|
|
533
|
+
return json.dumps({"recorded": label, "source": source or None})
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
@mcp.tool
|
|
537
|
+
def amfs_recall(entity_path: str, key: str) -> str:
|
|
538
|
+
"""Recall YOUR OWN memory for a key — what do I know about this?
|
|
539
|
+
|
|
540
|
+
Unlike amfs_read (which returns the latest version by any agent),
|
|
541
|
+
amfs_recall returns only entries written by you. Use this to check
|
|
542
|
+
your own knowledge before acting.
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
entity_path: Entity path (e.g. "checkout-service")
|
|
546
|
+
key: Memory key (e.g. "retry-pattern")
|
|
547
|
+
|
|
548
|
+
Example: amfs_recall("checkout-service", "retry-pattern")
|
|
549
|
+
"""
|
|
550
|
+
mem = _get_memory()
|
|
551
|
+
entry = mem.recall(entity_path, key)
|
|
552
|
+
if entry is None:
|
|
553
|
+
return json.dumps({"status": "not_found", "entity_path": entity_path, "key": key,
|
|
554
|
+
"hint": "You have not written this key. Try amfs_read() for shared knowledge."})
|
|
555
|
+
return json.dumps(_serialize_entry(entry), default=str)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
@mcp.tool
|
|
559
|
+
def amfs_my_entries(entity_path: str | None = None) -> str:
|
|
560
|
+
"""List all entries written by YOU — what's in my brain?
|
|
561
|
+
|
|
562
|
+
Returns only entries authored by this agent. Optionally filter to
|
|
563
|
+
a specific entity path.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
entity_path: Optional entity path filter
|
|
567
|
+
|
|
568
|
+
Example: amfs_my_entries("checkout-service")
|
|
569
|
+
"""
|
|
570
|
+
mem = _get_memory()
|
|
571
|
+
entries = mem.my_entries(entity_path)
|
|
572
|
+
return json.dumps({
|
|
573
|
+
"agent_id": mem.agent_id,
|
|
574
|
+
"count": len(entries),
|
|
575
|
+
"entries": [_serialize_entry(e) for e in entries],
|
|
576
|
+
}, default=str)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
@mcp.tool
|
|
580
|
+
def amfs_read_from(agent_id: str, entity_path: str, key: str) -> str:
|
|
581
|
+
"""Read a specific key from ANOTHER agent's memory.
|
|
582
|
+
|
|
583
|
+
Use this when you want to explicitly learn from another agent's
|
|
584
|
+
experience. The read is tracked for causal tracing.
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
agent_id: The agent whose memory to read from
|
|
588
|
+
entity_path: Entity path (e.g. "checkout-service")
|
|
589
|
+
key: Memory key (e.g. "retry-pattern")
|
|
590
|
+
|
|
591
|
+
Example: amfs_read_from("deploy-agent", "checkout-service", "deploy-config")
|
|
592
|
+
"""
|
|
593
|
+
mem = _get_memory()
|
|
594
|
+
entry = mem.read_from(agent_id, entity_path, key)
|
|
595
|
+
if entry is None:
|
|
596
|
+
return json.dumps({"status": "not_found", "agent_id": agent_id,
|
|
597
|
+
"entity_path": entity_path, "key": key})
|
|
598
|
+
return json.dumps(_serialize_entry(entry), default=str)
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
@mcp.tool
|
|
602
|
+
def amfs_cross_agent_reads() -> str:
|
|
603
|
+
"""Show which other agents' memory this agent has read.
|
|
604
|
+
|
|
605
|
+
Returns a mapping of other agent IDs to the specific entity/key pairs
|
|
606
|
+
read from them, with read counts. Use this to understand inter-agent
|
|
607
|
+
communication and memory sharing relationships.
|
|
608
|
+
|
|
609
|
+
Answers questions like:
|
|
610
|
+
- "Which agents have I talked to?"
|
|
611
|
+
- "What memory did I get from agent X?"
|
|
612
|
+
- "Who wrote the knowledge I'm relying on?"
|
|
613
|
+
|
|
614
|
+
Example response:
|
|
615
|
+
{
|
|
616
|
+
"agent_id": "review-agent",
|
|
617
|
+
"reads_from": {
|
|
618
|
+
"deploy-agent": [
|
|
619
|
+
{"entity_path": "checkout-service", "key": "retry-pattern", "read_count": 3}
|
|
620
|
+
]
|
|
621
|
+
},
|
|
622
|
+
"agents_read_from": ["deploy-agent"]
|
|
623
|
+
}
|
|
624
|
+
"""
|
|
625
|
+
mem = _get_memory()
|
|
626
|
+
cross_reads = mem.cross_agent_reads()
|
|
627
|
+
return json.dumps({
|
|
628
|
+
"agent_id": mem.agent_id,
|
|
629
|
+
"reads_from": cross_reads,
|
|
630
|
+
"agents_read_from": list(cross_reads.keys()),
|
|
631
|
+
"total_cross_agent_reads": sum(
|
|
632
|
+
r["read_count"] for reads in cross_reads.values() for r in reads
|
|
633
|
+
),
|
|
634
|
+
}, default=str)
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
@mcp.tool
|
|
638
|
+
def amfs_explain(outcome_ref: str | None = None) -> str:
|
|
639
|
+
"""Explain the causal chain — which memories influenced this session's decisions.
|
|
640
|
+
|
|
641
|
+
Shows every memory the agent read (in order) before committing an outcome.
|
|
642
|
+
This is production-grounded explainability: not what the LLM inferred,
|
|
643
|
+
but which stored knowledge actually drove the decision.
|
|
644
|
+
|
|
645
|
+
Args:
|
|
646
|
+
outcome_ref: Optional outcome reference to label the explanation
|
|
647
|
+
|
|
648
|
+
Example: amfs_explain("deploy-v1.2.3")
|
|
649
|
+
"""
|
|
650
|
+
mem = _get_memory()
|
|
651
|
+
explanation = mem.explain(outcome_ref)
|
|
652
|
+
return json.dumps(explanation, default=str)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
@mcp.tool
|
|
656
|
+
def amfs_briefing(
|
|
657
|
+
entity_path: str | None = None,
|
|
658
|
+
agent_id: str | None = None,
|
|
659
|
+
limit: int = 10,
|
|
660
|
+
) -> str:
|
|
661
|
+
"""Get a compiled knowledge briefing — what you should know right now.
|
|
662
|
+
|
|
663
|
+
Returns pre-compiled digests from the Memory Cortex, ranked by relevance.
|
|
664
|
+
Digests include entity summaries, agent brain briefs, and external source
|
|
665
|
+
summaries. Much faster and more complete than manual search.
|
|
666
|
+
|
|
667
|
+
Args:
|
|
668
|
+
entity_path: Focus on this entity (e.g. "checkout-service")
|
|
669
|
+
agent_id: Focus on this agent's context (defaults to current agent)
|
|
670
|
+
limit: Max digests to return (default 10)
|
|
671
|
+
|
|
672
|
+
Example: amfs_briefing(entity_path="checkout-service")
|
|
673
|
+
"""
|
|
674
|
+
mem = _get_memory()
|
|
675
|
+
digests = mem.briefing(
|
|
676
|
+
entity_path=entity_path,
|
|
677
|
+
agent_id=agent_id,
|
|
678
|
+
limit=limit,
|
|
679
|
+
)
|
|
680
|
+
return json.dumps(
|
|
681
|
+
[d.model_dump(mode="json") for d in digests],
|
|
682
|
+
default=str,
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
687
|
+
# Timeline (git log)
|
|
688
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
@mcp.tool
|
|
692
|
+
def amfs_timeline(
|
|
693
|
+
limit: int = 50,
|
|
694
|
+
event_type: str | None = None,
|
|
695
|
+
since: str | None = None,
|
|
696
|
+
) -> str:
|
|
697
|
+
"""View recent events on this agent's timeline (git commit log).
|
|
698
|
+
|
|
699
|
+
Every write, outcome, and cross-agent read is recorded as an event.
|
|
700
|
+
Use this to see the history of what happened to your agent's memory.
|
|
701
|
+
|
|
702
|
+
Args:
|
|
703
|
+
limit: Max events to return (default 50)
|
|
704
|
+
event_type: Filter by type (write, outcome, cross_agent_read, etc.)
|
|
705
|
+
since: ISO timestamp to get events after
|
|
706
|
+
|
|
707
|
+
Example: amfs_timeline(limit=20, event_type="write")
|
|
708
|
+
"""
|
|
709
|
+
from datetime import datetime as dt
|
|
710
|
+
mem = _get_memory()
|
|
711
|
+
since_dt = dt.fromisoformat(since) if since else None
|
|
712
|
+
events = mem._adapter.list_events(
|
|
713
|
+
mem.agent_id,
|
|
714
|
+
mem._config.namespace,
|
|
715
|
+
event_type=event_type,
|
|
716
|
+
since=since_dt,
|
|
717
|
+
limit=limit,
|
|
718
|
+
)
|
|
719
|
+
return json.dumps({
|
|
720
|
+
"events": [e.model_dump(mode="json") for e in events],
|
|
721
|
+
"count": len(events),
|
|
722
|
+
}, default=str)
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
726
|
+
# Entry point
|
|
727
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
728
|
+
|
|
729
|
+
_TRANSPORT_ALIASES: dict[str, str] = {
|
|
730
|
+
"stdio": "stdio",
|
|
731
|
+
"http": "streamable-http",
|
|
732
|
+
"streamable-http": "streamable-http",
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def _parse_args() -> argparse.Namespace:
|
|
737
|
+
parser = argparse.ArgumentParser(
|
|
738
|
+
prog="amfs-mcp-server",
|
|
739
|
+
description="AMFS MCP Server — shared agent memory over MCP",
|
|
740
|
+
)
|
|
741
|
+
parser.add_argument(
|
|
742
|
+
"--transport", "-t",
|
|
743
|
+
choices=["stdio", "http", "streamable-http"],
|
|
744
|
+
default=None,
|
|
745
|
+
help='Transport to use: "stdio" (default) or "http" / "streamable-http"',
|
|
746
|
+
)
|
|
747
|
+
parser.add_argument(
|
|
748
|
+
"--host",
|
|
749
|
+
default=None,
|
|
750
|
+
help="Host to bind for HTTP transport (default: 0.0.0.0)",
|
|
751
|
+
)
|
|
752
|
+
parser.add_argument(
|
|
753
|
+
"--port", "-p",
|
|
754
|
+
type=int,
|
|
755
|
+
default=None,
|
|
756
|
+
help="Port to bind for HTTP transport (default: 8000)",
|
|
757
|
+
)
|
|
758
|
+
parser.add_argument(
|
|
759
|
+
"--path",
|
|
760
|
+
default=None,
|
|
761
|
+
help="URL path for HTTP transport (default: /mcp)",
|
|
762
|
+
)
|
|
763
|
+
return parser.parse_args()
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def create_server() -> FastMCP:
|
|
767
|
+
"""Return the configured FastMCP server instance (for programmatic use)."""
|
|
768
|
+
return mcp
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def main() -> None:
|
|
772
|
+
"""Run the AMFS MCP server.
|
|
773
|
+
|
|
774
|
+
Transport is resolved in order:
|
|
775
|
+
1. ``--transport`` CLI flag
|
|
776
|
+
2. ``AMFS_TRANSPORT`` env var
|
|
777
|
+
3. Default: ``stdio``
|
|
778
|
+
"""
|
|
779
|
+
args = _parse_args()
|
|
780
|
+
|
|
781
|
+
raw_transport = (
|
|
782
|
+
args.transport
|
|
783
|
+
or os.environ.get("AMFS_TRANSPORT")
|
|
784
|
+
or "stdio"
|
|
785
|
+
)
|
|
786
|
+
transport = _TRANSPORT_ALIASES.get(raw_transport, raw_transport)
|
|
787
|
+
|
|
788
|
+
if transport == "streamable-http":
|
|
789
|
+
host = args.host or os.environ.get("AMFS_HOST", "0.0.0.0")
|
|
790
|
+
port = args.port or int(os.environ.get("AMFS_PORT", "8000"))
|
|
791
|
+
path = args.path or os.environ.get("AMFS_PATH", "/mcp")
|
|
792
|
+
logger.info("Starting AMFS MCP server — transport=%s %s:%d%s", transport, host, port, path)
|
|
793
|
+
mcp.run(transport=transport, host=host, port=port, path=path)
|
|
794
|
+
else:
|
|
795
|
+
logger.info("Starting AMFS MCP server — transport=stdio")
|
|
796
|
+
mcp.run(transport="stdio")
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
if __name__ == "__main__":
|
|
800
|
+
main()
|