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,16 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .venv/
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ node_modules/
12
+ .next/
13
+ !uv.lock
14
+ !pnpm-lock.yaml
15
+ .amfs/
16
+ test.py
@@ -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,5 @@
1
+ """AMFS MCP Server — expose Agent Memory as MCP tools."""
2
+
3
+ from amfs_mcp.server import create_server
4
+
5
+ __all__ = ["create_server"]
@@ -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()