kyp-mem 0.2.2 → 0.4.0

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.
package/kyp_mem/server.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """KYP-MEM MCP server — headless knowledge base for AI agents."""
2
2
 
3
3
  import json
4
+ from datetime import datetime
4
5
  from mcp.server.fastmcp import FastMCP
5
6
  from .config import get_vault_path
6
7
  from .vault import Vault
@@ -12,13 +13,18 @@ mcp = FastMCP("kyp-mem")
12
13
 
13
14
  @mcp.tool()
14
15
  def kyp_list(path: str = "") -> str:
15
- """List notes and folders in the vault. Pass a folder path to list its contents, or empty for root."""
16
+ """List notes and folders in the vault. Shows inline tags for quick navigation. Pass a folder path or empty for root."""
16
17
  tree = vault.list_tree(path)
17
18
  lines = []
18
19
  for f in tree["folders"]:
19
20
  lines.append(f" {f}/")
20
21
  for n in tree["notes"]:
21
- lines.append(f" {n}")
22
+ rel = f"{path}/{n}" if path else n
23
+ note = vault.index.notes.get(rel)
24
+ if note and note.tags:
25
+ lines.append(f" {n} [{', '.join(note.tags)}]")
26
+ else:
27
+ lines.append(f" {n}")
22
28
  if not lines:
23
29
  lines.append("(empty vault)")
24
30
  header = f"Vault: {path or '/'}"
@@ -26,24 +32,46 @@ def kyp_list(path: str = "") -> str:
26
32
 
27
33
 
28
34
  @mcp.tool()
29
- def kyp_read(path: str) -> str:
30
- """Read a note by path (e.g. 'Hedge Engine/Configuration.md'). Returns content + properties + backlinks + related notes."""
35
+ def kyp_read(path: str, full: bool = False) -> str:
36
+ """Read a note. Returns brief summary by default (title, tags, preview, links). Set full=True for complete content."""
31
37
  note = vault.read(path)
32
38
  if not note:
33
39
  return f"Not found: {path}"
34
40
 
41
+ if not full:
42
+ parts = [f"# {note.title}"]
43
+ if note.tags:
44
+ parts.append(f"tags: {', '.join(note.tags)}")
45
+ if note.created:
46
+ parts.append(f"created: {note.created}")
47
+
48
+ lines = [l for l in note.content.strip().split("\n") if l.strip() and not l.startswith("# ")]
49
+ preview = "\n".join(lines[:6])
50
+ if len(lines) > 6:
51
+ preview += "\n..."
52
+ parts.append("")
53
+ parts.append(preview)
54
+
55
+ backlinks = vault.get_backlinks(path)
56
+ outlinks = note.links
57
+ if outlinks:
58
+ parts.append(f"\nlinks: {', '.join(f'[[{l}]]' for l in outlinks)}")
59
+ if backlinks:
60
+ parts.append(f"backlinks: {', '.join(f'[[{b.replace('.md', '')}]]' for b in backlinks)}")
61
+
62
+ return "\n".join(parts)
63
+
35
64
  parts = [f"# {note.title}", ""]
36
65
 
37
66
  if note.tags or note.properties or note.created:
38
- parts.append("**Properties:**")
39
67
  if note.tags:
40
- parts.append(f" tags: {', '.join(note.tags)}")
68
+ parts.append(f"tags: {', '.join(note.tags)}")
41
69
  if note.created:
42
- parts.append(f" created: {note.created}")
70
+ parts.append(f"created: {note.created}")
43
71
  if note.updated:
44
- parts.append(f" updated: {note.updated}")
72
+ parts.append(f"updated: {note.updated}")
45
73
  for k, v in note.properties.items():
46
- parts.append(f" {k}: {v}")
74
+ parts.append(f"{k}: {v}")
47
75
  parts.append("")
48
76
 
49
77
  parts.append(note.content)
@@ -164,3 +192,106 @@ def kyp_stats() -> str:
164
192
  f" Links: {s['links']}\n"
165
193
  f" Backlinks: {s['backlinks']}"
166
194
  )
195
+
196
+
197
+ @mcp.tool()
198
+ def kyp_session_create(project: str, summary: str = "", investigated: str = "", learned: str = "", completed: str = "", next_steps: str = "") -> str:
199
+ """Create a structured session note. Project is required. Sections accept markdown text."""
200
+ session_id = datetime.now().strftime("%Y-%m-%d_%H%M%S")
201
+ parts = [f"# Session {session_id}", ""]
202
+ parts.append(f"**Project:** {project}")
203
+ parts.append("")
204
+ parts.append("## Summary")
205
+ parts.append(summary or "")
206
+ parts.append("")
207
+ parts.append("## INVESTIGATED")
208
+ parts.append(investigated or "")
209
+ parts.append("")
210
+ parts.append("## LEARNED")
211
+ parts.append(learned or "")
212
+ parts.append("")
213
+ parts.append("## COMPLETED")
214
+ parts.append(completed or "")
215
+ parts.append("")
216
+ parts.append("## NEXT STEPS")
217
+ parts.append(next_steps or "")
218
+
219
+ content = "\n".join(parts)
220
+ tags = ["session", "manual", project.lower().replace(" ", "-")]
221
+ path = f"{project}/Sessions/{session_id}.md"
222
+ vault.write_note(path, content, tags, {})
223
+ return f"Created session: {path}"
224
+
225
+
226
+ @mcp.tool()
227
+ def kyp_sessions(project: str = "", limit: int = 10) -> str:
228
+ """List sessions, optionally filtered by project. Shows most recent first."""
229
+ sessions = []
230
+ for path, note in vault.index.notes.items():
231
+ if "/Sessions/" not in path and not path.startswith("Sessions/"):
232
+ continue
233
+ if project and not path.lower().startswith(project.lower() + "/"):
234
+ continue
235
+ sessions.append((path, note))
236
+ sessions.sort(key=lambda s: s[0], reverse=True)
237
+ sessions = sessions[:limit]
238
+ if not sessions:
239
+ return "No sessions found." + (f" (project filter: {project})" if project else "")
240
+ lines = ["Sessions:", ""]
241
+ for path, note in sessions:
242
+ tags = f" [{', '.join(note.tags)}]" if note.tags else ""
243
+ date = note.created or note.updated or ""
244
+ lines.append(f" {date} — {note.title} ({path}){tags}")
245
+ return "\n".join(lines)
246
+
247
+
248
+ @mcp.tool()
249
+ def kyp_project_context(project: str) -> str:
250
+ """Get full project context: knowledge base + recent session summaries. Call this at session start to understand project history, avoid repeating past work, and prevent hallucination."""
251
+ parts = []
252
+
253
+ knowledge_path = f"{project}/Knowledge.md"
254
+ knowledge = vault.read(knowledge_path)
255
+ if knowledge:
256
+ parts.append("=== PROJECT KNOWLEDGE ===")
257
+ parts.append(knowledge.content)
258
+ parts.append("")
259
+
260
+ project_notes = []
261
+ for path, note in vault.index.notes.items():
262
+ if path.startswith(f"{project}/") and "/Sessions/" not in path and path != knowledge_path:
263
+ project_notes.append((path, note))
264
+
265
+ if project_notes:
266
+ parts.append("=== PROJECT NOTES ===")
267
+ for path, note in sorted(project_notes):
268
+ parts.append(f"\n--- {note.title} ({path}) ---")
269
+ preview = note.content.strip().split("\n")
270
+ parts.append("\n".join(preview[:10]))
271
+ if len(preview) > 10:
272
+ parts.append("...")
273
+ parts.append("")
274
+
275
+ sessions = []
276
+ for path, note in vault.index.notes.items():
277
+ if path.startswith(f"{project}/Sessions/"):
278
+ sessions.append((path, note))
279
+ sessions.sort(key=lambda s: s[0], reverse=True)
280
+ recent = sessions[:5]
281
+
282
+ if recent:
283
+ parts.append(f"=== RECENT SESSIONS ({len(recent)} of {len(sessions)}) ===")
284
+ for path, note in recent:
285
+ parts.append(f"\n--- {note.title} ---")
286
+ content = note.content
287
+ timeline_idx = content.find("## Timeline")
288
+ if timeline_idx > 0:
289
+ parts.append(content[:timeline_idx].strip())
290
+ else:
291
+ parts.append(content[:500])
292
+ parts.append("")
293
+
294
+ if not parts:
295
+ return f"No context found for project '{project}'. Create a Knowledge.md to get started."
296
+
297
+ return "\n".join(parts)