kyp-mem 0.2.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/LICENSE +21 -0
- package/README.md +147 -0
- package/bin/cli.mjs +33 -0
- package/bin/install.mjs +36 -0
- package/kyp_mem/__init__.py +3 -0
- package/kyp_mem/__pycache__/__init__.cpython-314.pyc +0 -0
- package/kyp_mem/__pycache__/cli.cpython-314.pyc +0 -0
- package/kyp_mem/__pycache__/config.cpython-314.pyc +0 -0
- package/kyp_mem/__pycache__/ui.cpython-314.pyc +0 -0
- package/kyp_mem/__pycache__/vault.cpython-314.pyc +0 -0
- package/kyp_mem/cli.py +263 -0
- package/kyp_mem/config.py +31 -0
- package/kyp_mem/server.py +166 -0
- package/kyp_mem/static/index.html +1293 -0
- package/kyp_mem/ui.py +91 -0
- package/kyp_mem/vault.py +319 -0
- package/package.json +35 -0
- package/pyproject.toml +44 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""KYP-MEM MCP server — headless knowledge base for AI agents."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from mcp.server.fastmcp import FastMCP
|
|
5
|
+
from .config import get_vault_path
|
|
6
|
+
from .vault import Vault
|
|
7
|
+
|
|
8
|
+
vault = Vault(get_vault_path())
|
|
9
|
+
|
|
10
|
+
mcp = FastMCP("kyp-mem", description="Know Your Project — headless knowledge base like Obsidian")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@mcp.tool()
|
|
14
|
+
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
|
+
tree = vault.list_tree(path)
|
|
17
|
+
lines = []
|
|
18
|
+
for f in tree["folders"]:
|
|
19
|
+
lines.append(f" {f}/")
|
|
20
|
+
for n in tree["notes"]:
|
|
21
|
+
lines.append(f" {n}")
|
|
22
|
+
if not lines:
|
|
23
|
+
lines.append("(empty vault)")
|
|
24
|
+
header = f"Vault: {path or '/'}"
|
|
25
|
+
return header + "\n" + "\n".join(lines)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@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."""
|
|
31
|
+
note = vault.read(path)
|
|
32
|
+
if not note:
|
|
33
|
+
return f"Not found: {path}"
|
|
34
|
+
|
|
35
|
+
parts = [f"# {note.title}", ""]
|
|
36
|
+
|
|
37
|
+
if note.tags or note.properties or note.created:
|
|
38
|
+
parts.append("**Properties:**")
|
|
39
|
+
if note.tags:
|
|
40
|
+
parts.append(f" tags: {', '.join(note.tags)}")
|
|
41
|
+
if note.created:
|
|
42
|
+
parts.append(f" created: {note.created}")
|
|
43
|
+
if note.updated:
|
|
44
|
+
parts.append(f" updated: {note.updated}")
|
|
45
|
+
for k, v in note.properties.items():
|
|
46
|
+
parts.append(f" {k}: {v}")
|
|
47
|
+
parts.append("")
|
|
48
|
+
|
|
49
|
+
parts.append(note.content)
|
|
50
|
+
|
|
51
|
+
backlinks = vault.get_backlinks(path)
|
|
52
|
+
if backlinks:
|
|
53
|
+
parts.append("\n---")
|
|
54
|
+
parts.append("**Backlinks:**")
|
|
55
|
+
for bl in backlinks:
|
|
56
|
+
parts.append(f" <- {bl}")
|
|
57
|
+
|
|
58
|
+
related = vault.get_related(path)
|
|
59
|
+
if related:
|
|
60
|
+
parts.append("\n**Related:**")
|
|
61
|
+
for rel_path, score in related[:8]:
|
|
62
|
+
rel_note = vault.index.notes.get(rel_path)
|
|
63
|
+
title = rel_note.title if rel_note else rel_path
|
|
64
|
+
parts.append(f" {score:.2f} > {title} ({rel_path})")
|
|
65
|
+
|
|
66
|
+
return "\n".join(parts)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@mcp.tool()
|
|
70
|
+
def kyp_write(path: str, content: str, tags: str = "", properties: str = "") -> str:
|
|
71
|
+
"""Create or update a note. Path like 'Project/Note.md'. Tags: comma-separated. Properties: JSON string."""
|
|
72
|
+
if not path.endswith(".md"):
|
|
73
|
+
path += ".md"
|
|
74
|
+
|
|
75
|
+
tag_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else []
|
|
76
|
+
props = json.loads(properties) if properties else {}
|
|
77
|
+
|
|
78
|
+
vault.write_note(path, content, tag_list, props)
|
|
79
|
+
|
|
80
|
+
note = vault.index.notes.get(path)
|
|
81
|
+
link_count = len(note.links) if note else 0
|
|
82
|
+
return f"Written: {path} ({len(tag_list)} tags, {link_count} links detected)"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@mcp.tool()
|
|
86
|
+
def kyp_delete(path: str) -> str:
|
|
87
|
+
"""Delete a note by path."""
|
|
88
|
+
if vault.delete(path):
|
|
89
|
+
return f"Deleted: {path}"
|
|
90
|
+
return f"Not found: {path}"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@mcp.tool()
|
|
94
|
+
def kyp_search(query: str, tag: str = "") -> str:
|
|
95
|
+
"""Full-text search across all notes. Optionally filter by tag."""
|
|
96
|
+
results = vault.search(query, tag or None)
|
|
97
|
+
if not results:
|
|
98
|
+
return "No results found."
|
|
99
|
+
|
|
100
|
+
lines = [f"Search: '{query}'" + (f" [tag: {tag}]" if tag else ""), ""]
|
|
101
|
+
for path, score, snippet in results:
|
|
102
|
+
note = vault.index.notes.get(path)
|
|
103
|
+
title = note.title if note else path
|
|
104
|
+
lines.append(f" {title} ({path}) — score: {score:.3f}")
|
|
105
|
+
if snippet:
|
|
106
|
+
lines.append(f" {snippet}")
|
|
107
|
+
return "\n".join(lines)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@mcp.tool()
|
|
111
|
+
def kyp_tags(tag: str = "") -> str:
|
|
112
|
+
"""List all tags with note counts, or get all notes with a specific tag."""
|
|
113
|
+
if tag:
|
|
114
|
+
notes = vault.get_notes_by_tag(tag)
|
|
115
|
+
if not notes:
|
|
116
|
+
return f"No notes tagged '{tag}'"
|
|
117
|
+
return f"Notes tagged '{tag}':\n" + "\n".join(f" {n}" for n in notes)
|
|
118
|
+
|
|
119
|
+
tags = vault.get_tags()
|
|
120
|
+
if not tags:
|
|
121
|
+
return "No tags in vault."
|
|
122
|
+
return "Tags:\n" + "\n".join(f" {t} ({c})" for t, c in tags.items())
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@mcp.tool()
|
|
126
|
+
def kyp_related(path: str) -> str:
|
|
127
|
+
"""Find notes related to the given note — by backlinks, shared tags, and folder proximity."""
|
|
128
|
+
related = vault.get_related(path)
|
|
129
|
+
if not related:
|
|
130
|
+
return f"No related notes for: {path}"
|
|
131
|
+
|
|
132
|
+
lines = [f"Related to {path}:", ""]
|
|
133
|
+
for rel_path, score in related:
|
|
134
|
+
note = vault.index.notes.get(rel_path)
|
|
135
|
+
title = note.title if note else rel_path
|
|
136
|
+
lines.append(f" {score:.2f} > {title} ({rel_path})")
|
|
137
|
+
return "\n".join(lines)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@mcp.tool()
|
|
141
|
+
def kyp_recent(limit: int = 10) -> str:
|
|
142
|
+
"""Get recently modified notes."""
|
|
143
|
+
notes = vault.get_recent(limit)
|
|
144
|
+
if not notes:
|
|
145
|
+
return "Vault is empty."
|
|
146
|
+
|
|
147
|
+
lines = ["Recent notes:", ""]
|
|
148
|
+
for note in notes:
|
|
149
|
+
date = note.updated or note.created or "?"
|
|
150
|
+
tags = f" [{', '.join(note.tags)}]" if note.tags else ""
|
|
151
|
+
lines.append(f" {date} — {note.title} ({note.path}){tags}")
|
|
152
|
+
return "\n".join(lines)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@mcp.tool()
|
|
156
|
+
def kyp_stats() -> str:
|
|
157
|
+
"""Get vault statistics — note count, folders, tags, links."""
|
|
158
|
+
s = vault.get_stats()
|
|
159
|
+
return (
|
|
160
|
+
f"Vault stats:\n"
|
|
161
|
+
f" Notes: {s['notes']}\n"
|
|
162
|
+
f" Folders: {s['folders']}\n"
|
|
163
|
+
f" Tags: {s['tags']}\n"
|
|
164
|
+
f" Links: {s['links']}\n"
|
|
165
|
+
f" Backlinks: {s['backlinks']}"
|
|
166
|
+
)
|