mnemo-dev 0.1.0__py3-none-any.whl

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,118 @@
1
+ """Knowledge MCP — team knowledge base from markdown files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from ..chunking import markdown_heading_chunks
8
+ from ..retrieval import index_chunks, semantic_query
9
+ from ..config import mnemo_path
10
+
11
+ KNOWLEDGE_DIR = "knowledge"
12
+
13
+
14
+ def _knowledge_path(repo_root: Path) -> Path:
15
+ return mnemo_path(repo_root) / KNOWLEDGE_DIR
16
+
17
+
18
+ def init_knowledge(repo_root: Path) -> Path:
19
+ """Create the knowledge directory with a README."""
20
+ kdir = _knowledge_path(repo_root)
21
+ kdir.mkdir(parents=True, exist_ok=True)
22
+ readme = kdir / "README.md"
23
+ if not readme.exists():
24
+ readme.write_text(
25
+ "# Project Knowledge Base\n\n"
26
+ "Add markdown files here for team knowledge that Amazon Q should know about.\n\n"
27
+ "Examples:\n"
28
+ "- `runbooks.md` — deployment and debugging procedures\n"
29
+ "- `architecture.md` — system design decisions\n"
30
+ "- `standards.md` — coding conventions and requirements\n"
31
+ "- `onboarding.md` — project overview for new team members\n"
32
+ "- `gotchas.md` — common pitfalls and workarounds\n"
33
+ )
34
+ return kdir
35
+
36
+
37
+ def search_knowledge(repo_root: Path, query: str) -> str:
38
+ """Search knowledge base for relevant content."""
39
+ kdir = _knowledge_path(repo_root)
40
+ if not kdir.exists():
41
+ return "No knowledge base found. Create markdown files in `.mnemo/knowledge/`."
42
+
43
+ query_lower = query.lower()
44
+ results: list[tuple[str, str]] = []
45
+ chunks = []
46
+
47
+ for md_file in kdir.rglob("*.md"):
48
+ chunks.extend(markdown_heading_chunks(kdir, md_file))
49
+ try:
50
+ content = md_file.read_text()
51
+ except (OSError, PermissionError):
52
+ continue
53
+
54
+ # Score by how many query words appear
55
+ score = sum(1 for word in query_lower.split() if word in content.lower())
56
+ if score > 0:
57
+ # Extract relevant section
58
+ lines = content.splitlines()
59
+ relevant_lines = []
60
+ for i, line in enumerate(lines):
61
+ if any(word in line.lower() for word in query_lower.split()):
62
+ # Get surrounding context (3 lines before/after)
63
+ start = max(0, i - 3)
64
+ end = min(len(lines), i + 4)
65
+ relevant_lines.extend(lines[start:end])
66
+ relevant_lines.append("---")
67
+
68
+ if relevant_lines:
69
+ name = md_file.relative_to(kdir)
70
+ excerpt = "\n".join(relevant_lines[:50])
71
+ results.append((str(name), excerpt))
72
+
73
+ if chunks:
74
+ index_chunks(repo_root, "knowledge", chunks)
75
+ semantic_results = semantic_query(repo_root, "knowledge", query, limit=5)
76
+ if semantic_results:
77
+ lines = [f"# Knowledge: '{query}'\n", "## Semantic Matches"]
78
+ for result in semantic_results:
79
+ meta = result.get("metadata", {})
80
+ lines.append(f"- **{meta.get('path', 'unknown')}** ({meta.get('symbol', 'section')})")
81
+ lines.append(result.get("content", "")[:600])
82
+ lines.append("")
83
+ return "\n".join(lines)
84
+
85
+ if not results:
86
+ # Return all file names as suggestions
87
+ files = [str(f.relative_to(kdir)) for f in kdir.rglob("*.md")]
88
+ return f"No results for '{query}'. Available knowledge files: {', '.join(files)}"
89
+
90
+ lines = [f"# Knowledge: '{query}'\n"]
91
+ for name, excerpt in results[:5]:
92
+ lines.append(f"## {name}")
93
+ lines.append(excerpt)
94
+ lines.append("")
95
+
96
+ return "\n".join(lines)
97
+
98
+
99
+ def list_knowledge(repo_root: Path) -> str:
100
+ """List all knowledge base files with their headings."""
101
+ kdir = _knowledge_path(repo_root)
102
+ if not kdir.exists():
103
+ return "No knowledge base. Create `.mnemo/knowledge/` with markdown files."
104
+
105
+ lines = ["# Knowledge Base\n"]
106
+ for md_file in sorted(kdir.rglob("*.md")):
107
+ name = md_file.relative_to(kdir)
108
+ try:
109
+ content = md_file.read_text()
110
+ # Get first heading or first line
111
+ for line in content.splitlines():
112
+ if line.strip():
113
+ lines.append(f"- **{name}** — {line.strip('#').strip()}")
114
+ break
115
+ except (OSError, PermissionError):
116
+ lines.append(f"- **{name}**")
117
+
118
+ return "\n".join(lines)
mnemo/mcp_server.py ADDED
@@ -0,0 +1,458 @@
1
+ """MCP Server – Exposes Mnemo tools to Amazon Q."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from .init import init
11
+ from .memory import add_memory, add_decision, save_context, recall, lookup
12
+ from .repo_map import save_repo_map
13
+ from .intelligence import generate_intelligence, find_similar, context_for_active_task
14
+ from .workspace import cross_repo_semantic_query, cross_repo_impact, format_links, get_linked_repos
15
+ from .knowledge import search_knowledge, list_knowledge, init_knowledge
16
+ from .api_discovery import discover_apis, search_api
17
+ from .code_review import add_review, format_reviews
18
+ from .errors import add_error, search_errors
19
+ from .dependency_graph import format_graph, impact_analysis
20
+ from .onboarding import generate_onboarding
21
+ from .sprint import set_current_task, complete_task, get_current_task, format_tasks
22
+ from .test_intel import get_tests_for_file, get_coverage_summary
23
+ from .health import calculate_health
24
+ from .team_graph import get_experts, who_last_touched
25
+ from .incidents import add_incident, search_incidents, format_incidents
26
+ from .config import MNEMO_DIR
27
+ from . import __version__
28
+
29
+
30
+ def _find_repo_root(start: Path | None = None) -> Path | None:
31
+ """Walk up from start to find a directory containing .mnemo/."""
32
+ current = start or Path.cwd()
33
+ for parent in [current, *current.parents]:
34
+ if (parent / MNEMO_DIR).exists():
35
+ return parent
36
+ return None
37
+
38
+
39
+ def handle_tool_call(tool_name: str, arguments: dict) -> dict:
40
+ """Route MCP tool calls."""
41
+ # Auto-detect repo root, fallback to explicit path
42
+ repo_path = arguments.get("repo_path")
43
+ if repo_path:
44
+ repo_root = Path(repo_path).resolve()
45
+ else:
46
+ repo_root = _find_repo_root()
47
+
48
+ if tool_name == "mnemo_init":
49
+ target = Path(repo_path).resolve() if repo_path else Path.cwd()
50
+ try:
51
+ msg = init(target, client=arguments.get("client", "amazonq"))
52
+ except ValueError as exc:
53
+ return {"content": [{"type": "text", "text": str(exc)}], "isError": True}
54
+ return {"content": [{"type": "text", "text": msg}]}
55
+
56
+ if not repo_root:
57
+ return {"content": [{"type": "text", "text": "No .mnemo/ found. Run `mnemo init` in your repo first."}], "isError": True}
58
+
59
+ if tool_name == "mnemo_recall":
60
+ data = recall(repo_root)
61
+ if not data:
62
+ return {"content": [{"type": "text", "text": "Memory is empty. Run mnemo_init first."}]}
63
+ return {"content": [{"type": "text", "text": data}]}
64
+
65
+ elif tool_name == "mnemo_lookup":
66
+ query = arguments.get("query", "")
67
+ if not query:
68
+ return {"content": [{"type": "text", "text": "Please provide a file or folder name to look up."}], "isError": True}
69
+ result = lookup(repo_root, query)
70
+ return {"content": [{"type": "text", "text": result}]}
71
+
72
+ elif tool_name == "mnemo_remember":
73
+ entry = add_memory(repo_root, arguments["content"], arguments.get("category", "general"))
74
+ return {"content": [{"type": "text", "text": f"Stored memory #{entry['id']}: {entry['content']}"}]}
75
+
76
+ elif tool_name == "mnemo_decide":
77
+ entry = add_decision(repo_root, arguments["decision"], arguments.get("reasoning", ""))
78
+ return {"content": [{"type": "text", "text": f"Decision #{entry['id']} recorded: {entry['decision']}"}]}
79
+
80
+ elif tool_name == "mnemo_context":
81
+ save_context(repo_root, arguments.get("context", {}))
82
+ return {"content": [{"type": "text", "text": "Context updated."}]}
83
+
84
+ elif tool_name == "mnemo_map":
85
+ out = save_repo_map(repo_root)
86
+ return {"content": [{"type": "text", "text": f"Repo map regenerated: {out}"}]}
87
+
88
+ elif tool_name == "mnemo_intelligence":
89
+ report = generate_intelligence(repo_root)
90
+ return {"content": [{"type": "text", "text": report}]}
91
+
92
+ elif tool_name == "mnemo_similar":
93
+ query = arguments.get("query", "")
94
+ if not query:
95
+ return {"content": [{"type": "text", "text": "Provide a pattern name (e.g. 'Handler', 'Service')"}], "isError": True}
96
+ results = find_similar(repo_root, query)
97
+ if not results:
98
+ return {"content": [{"type": "text", "text": f"No similar implementations for '{query}'"}]}
99
+ lines = [f"# Similar to '{query}'\n"]
100
+ for r in results:
101
+ lines.append(f"- **{r['file']}** — `{r['class']}`")
102
+ if r.get("content"):
103
+ lines.append(f" ```\n {r['content']}\n ```")
104
+ return {"content": [{"type": "text", "text": "\n".join(lines)}]}
105
+
106
+ elif tool_name == "mnemo_context_for_task":
107
+ query = arguments.get("query", "")
108
+ result = context_for_active_task(repo_root, query)
109
+ return {"content": [{"type": "text", "text": result}]}
110
+
111
+ elif tool_name == "mnemo_knowledge":
112
+ query = arguments.get("query", "")
113
+ if query:
114
+ result = search_knowledge(repo_root, query)
115
+ else:
116
+ result = list_knowledge(repo_root)
117
+ return {"content": [{"type": "text", "text": result}]}
118
+
119
+ elif tool_name == "mnemo_discover_apis":
120
+ report = discover_apis(repo_root)
121
+ return {"content": [{"type": "text", "text": report}]}
122
+
123
+ elif tool_name == "mnemo_search_api":
124
+ query = arguments.get("query", "")
125
+ if not query:
126
+ return {"content": [{"type": "text", "text": "Provide an endpoint or schema name to search."}], "isError": True}
127
+ result = search_api(repo_root, query)
128
+ return {"content": [{"type": "text", "text": result}]}
129
+
130
+ # --- Code Review ---
131
+ elif tool_name == "mnemo_add_review":
132
+ entry = add_review(repo_root, arguments["summary"],
133
+ arguments.get("files", []),
134
+ arguments.get("feedback", ""),
135
+ arguments.get("outcome", "approved"))
136
+ return {"content": [{"type": "text", "text": f"Review #{entry['id']} stored."}]}
137
+
138
+ elif tool_name == "mnemo_reviews":
139
+ return {"content": [{"type": "text", "text": format_reviews(repo_root)}]}
140
+
141
+ # --- Error Memory ---
142
+ elif tool_name == "mnemo_add_error":
143
+ entry = add_error(repo_root, arguments["error"], arguments["cause"],
144
+ arguments["fix"], arguments.get("file", ""),
145
+ arguments.get("tags", []))
146
+ return {"content": [{"type": "text", "text": f"Error #{entry['id']} stored."}]}
147
+
148
+ elif tool_name == "mnemo_search_errors":
149
+ return {"content": [{"type": "text", "text": search_errors(repo_root, arguments.get("query", ""))}]}
150
+
151
+ # --- Dependency Graph ---
152
+ elif tool_name == "mnemo_dependencies":
153
+ return {"content": [{"type": "text", "text": format_graph(repo_root)}]}
154
+
155
+ elif tool_name == "mnemo_impact":
156
+ return {"content": [{"type": "text", "text": impact_analysis(repo_root, arguments.get("query", ""))}]}
157
+
158
+ # --- Onboarding ---
159
+ elif tool_name == "mnemo_onboarding":
160
+ return {"content": [{"type": "text", "text": generate_onboarding(repo_root)}]}
161
+
162
+ # --- Sprint/Task ---
163
+ elif tool_name == "mnemo_task":
164
+ task_id = arguments.get("task_id", "")
165
+ if not task_id:
166
+ return {"content": [{"type": "text", "text": get_current_task(repo_root)}]}
167
+ entry = set_current_task(repo_root, task_id,
168
+ arguments.get("description", ""),
169
+ arguments.get("files", []),
170
+ arguments.get("notes", ""))
171
+ return {"content": [{"type": "text", "text": f"Task {task_id} set as active."}]}
172
+
173
+ elif tool_name == "mnemo_task_done":
174
+ result = complete_task(repo_root, arguments.get("task_id", ""), arguments.get("summary", ""))
175
+ return {"content": [{"type": "text", "text": result}]}
176
+
177
+ # --- Test Intelligence ---
178
+ elif tool_name == "mnemo_tests":
179
+ query = arguments.get("query", "")
180
+ if query:
181
+ return {"content": [{"type": "text", "text": get_tests_for_file(repo_root, query)}]}
182
+ return {"content": [{"type": "text", "text": get_coverage_summary(repo_root)}]}
183
+
184
+ # --- Code Health ---
185
+ elif tool_name == "mnemo_health":
186
+ return {"content": [{"type": "text", "text": calculate_health(repo_root)}]}
187
+
188
+ # --- Team Graph ---
189
+ elif tool_name == "mnemo_team":
190
+ query = arguments.get("query", "")
191
+ return {"content": [{"type": "text", "text": get_experts(repo_root, query)}]}
192
+
193
+ elif tool_name == "mnemo_who_touched":
194
+ return {"content": [{"type": "text", "text": who_last_touched(repo_root, arguments.get("query", ""))}]}
195
+
196
+ # --- Incidents ---
197
+ elif tool_name == "mnemo_add_incident":
198
+ entry = add_incident(repo_root, arguments["title"],
199
+ arguments["what_happened"], arguments["root_cause"],
200
+ arguments["fix"], arguments.get("prevention", ""),
201
+ arguments.get("severity", "medium"),
202
+ arguments.get("services", []))
203
+ return {"content": [{"type": "text", "text": f"Incident #{entry['id']} recorded."}]}
204
+
205
+ elif tool_name == "mnemo_incidents":
206
+ query = arguments.get("query", "")
207
+ if query:
208
+ return {"content": [{"type": "text", "text": search_incidents(repo_root, query)}]}
209
+ return {"content": [{"type": "text", "text": format_incidents(repo_root)}]}
210
+
211
+ # --- Multi-Repo Workspace ---
212
+ elif tool_name == "mnemo_links":
213
+ return {"content": [{"type": "text", "text": format_links(repo_root)}]}
214
+
215
+ elif tool_name == "mnemo_cross_search":
216
+ query = arguments.get("query", "")
217
+ namespace = arguments.get("namespace", "code")
218
+ if not query:
219
+ return {"content": [{"type": "text", "text": "Provide a search query."}], "isError": True}
220
+ results = cross_repo_semantic_query(repo_root, namespace, query, limit=15)
221
+ if not results:
222
+ return {"content": [{"type": "text", "text": f"No cross-repo results for '{query}'"}]}
223
+ lines = [f"# Cross-Repo Search: '{query}'\n"]
224
+ for r in results:
225
+ meta = r.get("metadata", {})
226
+ lines.append(f"- **[{r.get('repo', '?')}]** `{meta.get('path', '')}` :: `{meta.get('symbol', '')}`")
227
+ if r.get("content"):
228
+ lines.append(f" {r['content'][:200]}")
229
+ return {"content": [{"type": "text", "text": "\n".join(lines)}]}
230
+
231
+ elif tool_name == "mnemo_cross_impact":
232
+ query = arguments.get("query", "")
233
+ if not query:
234
+ return {"content": [{"type": "text", "text": "Provide a service or file name."}], "isError": True}
235
+ return {"content": [{"type": "text", "text": cross_repo_impact(repo_root, query)}]}
236
+
237
+ return {"content": [{"type": "text", "text": f"Unknown tool: {tool_name}"}], "isError": True}
238
+
239
+
240
+ TOOLS = [
241
+ {
242
+ "name": "mnemo_init",
243
+ "description": "Initialize Mnemo in a repository. Creates .mnemo/ folder, generates repo map, and bootstraps memory.",
244
+ "inputSchema": {
245
+ "type": "object",
246
+ "properties": {
247
+ "repo_path": {"type": "string", "description": "Path to the repository root"},
248
+ "client": {
249
+ "type": "string",
250
+ "description": "AI client to configure: amazonq, cursor, claude-code, kiro, copilot, generic, or all",
251
+ },
252
+ },
253
+ "required": ["repo_path"],
254
+ },
255
+ },
256
+ {
257
+ "name": "mnemo_recall",
258
+ "description": "Recall all stored memory, decisions, context, and repo map. YOU MUST call this at the START of every new chat to load project context before answering any questions.",
259
+ "inputSchema": {
260
+ "type": "object",
261
+ "properties": {
262
+ "repo_path": {"type": "string", "description": "Path to the repository root (auto-detected if omitted)"}
263
+ },
264
+ },
265
+ },
266
+ {
267
+ "name": "mnemo_lookup",
268
+ "description": "Look up detailed code structure for a specific file or folder. Returns full method signatures, imports, and class details. Use this when you need to understand a specific part of the codebase in depth.",
269
+ "inputSchema": {
270
+ "type": "object",
271
+ "properties": {
272
+ "repo_path": {"type": "string", "description": "Path to the repository root (auto-detected if omitted)"},
273
+ "query": {"type": "string", "description": "File name, folder name, or path fragment to search for (e.g. 'AuthService', 'Controllers', 'UserController.cs')"},
274
+ },
275
+ "required": ["query"],
276
+ },
277
+ },
278
+ {
279
+ "name": "mnemo_remember",
280
+ "description": "Store important information in persistent memory. Use this to save context, user preferences, patterns, or anything that should be remembered across chat sessions.",
281
+ "inputSchema": {
282
+ "type": "object",
283
+ "properties": {
284
+ "repo_path": {"type": "string", "description": "Path to the repository root (auto-detected if omitted)"},
285
+ "content": {"type": "string", "description": "The information to remember"},
286
+ "category": {"type": "string", "description": "Category: general, architecture, preference, pattern, bug, todo"},
287
+ },
288
+ "required": ["content"],
289
+ },
290
+ },
291
+ {
292
+ "name": "mnemo_decide",
293
+ "description": "Record an architectural or design decision with reasoning. Use this whenever a significant technical choice is made.",
294
+ "inputSchema": {
295
+ "type": "object",
296
+ "properties": {
297
+ "repo_path": {"type": "string", "description": "Path to the repository root (auto-detected if omitted)"},
298
+ "decision": {"type": "string", "description": "The decision that was made"},
299
+ "reasoning": {"type": "string", "description": "Why this decision was made"},
300
+ },
301
+ "required": ["decision"],
302
+ },
303
+ },
304
+ {
305
+ "name": "mnemo_context",
306
+ "description": "Save or update project context (tech stack, conventions, preferences). Merges with existing context.",
307
+ "inputSchema": {
308
+ "type": "object",
309
+ "properties": {
310
+ "repo_path": {"type": "string", "description": "Path to the repository root (auto-detected if omitted)"},
311
+ "context": {"type": "object", "description": "Key-value pairs of project context to store"},
312
+ },
313
+ "required": ["context"],
314
+ },
315
+ },
316
+ {
317
+ "name": "mnemo_map",
318
+ "description": "Regenerate the repo map. Use this after significant code changes to keep the structural map up to date.",
319
+ "inputSchema": {
320
+ "type": "object",
321
+ "properties": {
322
+ "repo_path": {"type": "string", "description": "Path to the repository root (auto-detected if omitted)"}
323
+ },
324
+ },
325
+ },
326
+ {
327
+ "name": "mnemo_intelligence",
328
+ "description": "Generate a code intelligence report: architecture graph (service-to-service calls), dependency map, detected patterns and conventions, code ownership.",
329
+ "inputSchema": {
330
+ "type": "object",
331
+ "properties": {
332
+ "repo_path": {"type": "string", "description": "Path to the repository root (auto-detected if omitted)"}
333
+ },
334
+ },
335
+ },
336
+ {
337
+ "name": "mnemo_similar",
338
+ "description": "Find similar implementations in the codebase. Use this when implementing something new to find existing patterns to follow (e.g. 'Handler' finds all handler implementations).",
339
+ "inputSchema": {
340
+ "type": "object",
341
+ "properties": {
342
+ "repo_path": {"type": "string", "description": "Path to the repository root (auto-detected if omitted)"},
343
+ "query": {"type": "string", "description": "Pattern name to search for (e.g. 'Handler', 'Service', 'Controller')"},
344
+ },
345
+ "required": ["query"],
346
+ },
347
+ },
348
+ {
349
+ "name": "mnemo_context_for_task",
350
+ "description": "Return context relevant to the active mnemo_task using semantic retrieval with fallback behavior.",
351
+ "inputSchema": {
352
+ "type": "object",
353
+ "properties": {
354
+ "repo_path": {"type": "string", "description": "Path to repository root (auto-detected if omitted)"},
355
+ "query": {"type": "string", "description": "Optional extra focus query (e.g. endpoint, module, feature)"},
356
+ },
357
+ },
358
+ },
359
+ {
360
+ "name": "mnemo_knowledge",
361
+ "description": "Search the project knowledge base (runbooks, architecture docs, standards, gotchas). Without a query, lists all available knowledge files.",
362
+ "inputSchema": {
363
+ "type": "object",
364
+ "properties": {
365
+ "repo_path": {"type": "string", "description": "Path to the repository root (auto-detected if omitted)"},
366
+ "query": {"type": "string", "description": "Search term (optional — omit to list all knowledge files)"},
367
+ },
368
+ },
369
+ },
370
+ {
371
+ "name": "mnemo_discover_apis",
372
+ "description": "Discover all API endpoints in the project. Parses OpenAPI/Swagger specs and controller annotations to build a complete API catalog.",
373
+ "inputSchema": {
374
+ "type": "object",
375
+ "properties": {
376
+ "repo_path": {"type": "string", "description": "Path to the repository root (auto-detected if omitted)"}
377
+ },
378
+ },
379
+ },
380
+ {
381
+ "name": "mnemo_search_api",
382
+ "description": "Search for a specific API endpoint, schema, or service in the API catalog.",
383
+ "inputSchema": {
384
+ "type": "object",
385
+ "properties": {
386
+ "repo_path": {"type": "string", "description": "Path to the repository root (auto-detected if omitted)"},
387
+ "query": {"type": "string", "description": "Endpoint path, method name, or schema to search for"},
388
+ },
389
+ "required": ["query"],
390
+ },
391
+ },
392
+ {"name": "mnemo_add_review", "description": "Store a code review summary with feedback and outcome.", "inputSchema": {"type": "object", "properties": {"repo_path": {"type": "string"}, "summary": {"type": "string", "description": "What was reviewed"}, "files": {"type": "array", "items": {"type": "string"}, "description": "Files involved"}, "feedback": {"type": "string", "description": "Review feedback"}, "outcome": {"type": "string", "description": "approved, rejected, or changes_requested"}}, "required": ["summary"]}},
393
+ {"name": "mnemo_reviews", "description": "Show code review history.", "inputSchema": {"type": "object", "properties": {"repo_path": {"type": "string"}}}},
394
+ {"name": "mnemo_add_error", "description": "Store an error → cause → fix mapping for future reference.", "inputSchema": {"type": "object", "properties": {"repo_path": {"type": "string"}, "error": {"type": "string", "description": "The error message or symptom"}, "cause": {"type": "string", "description": "Root cause"}, "fix": {"type": "string", "description": "How it was fixed"}, "file": {"type": "string"}, "tags": {"type": "array", "items": {"type": "string"}}}, "required": ["error", "cause", "fix"]}},
395
+ {"name": "mnemo_search_errors", "description": "Search known errors for a matching issue. Use when user hits an error to check if it's been seen before.", "inputSchema": {"type": "object", "properties": {"repo_path": {"type": "string"}, "query": {"type": "string", "description": "Error message or keyword"}}, "required": ["query"]}},
396
+ {"name": "mnemo_dependencies", "description": "Show the full service dependency graph — which service depends on which.", "inputSchema": {"type": "object", "properties": {"repo_path": {"type": "string"}}}},
397
+ {"name": "mnemo_impact", "description": "Impact analysis — what breaks if you change a specific service or file.", "inputSchema": {"type": "object", "properties": {"repo_path": {"type": "string"}, "query": {"type": "string", "description": "Service or file name to analyze"}}, "required": ["query"]}},
398
+ {"name": "mnemo_onboarding", "description": "Generate a complete project onboarding guide for new team members.", "inputSchema": {"type": "object", "properties": {"repo_path": {"type": "string"}}}},
399
+ {"name": "mnemo_task", "description": "Set or get the current task/ticket being worked on. Without task_id, shows active tasks.", "inputSchema": {"type": "object", "properties": {"repo_path": {"type": "string"}, "task_id": {"type": "string", "description": "Ticket ID (e.g. JIRA-123)"}, "description": {"type": "string"}, "files": {"type": "array", "items": {"type": "string"}}, "notes": {"type": "string"}}}},
400
+ {"name": "mnemo_task_done", "description": "Mark a task as completed.", "inputSchema": {"type": "object", "properties": {"repo_path": {"type": "string"}, "task_id": {"type": "string"}, "summary": {"type": "string"}}, "required": ["task_id"]}},
401
+ {"name": "mnemo_tests", "description": "Show which tests cover a file, or get overall test coverage summary. Use when modifying code to know what tests to run.", "inputSchema": {"type": "object", "properties": {"repo_path": {"type": "string"}, "query": {"type": "string", "description": "File name to find tests for (omit for coverage summary)"}}}},
402
+ {"name": "mnemo_health", "description": "Code health report — complexity hotspots, large files, potential god classes.", "inputSchema": {"type": "object", "properties": {"repo_path": {"type": "string"}}}},
403
+ {"name": "mnemo_team", "description": "Show team expertise map — who knows what based on git history.", "inputSchema": {"type": "object", "properties": {"repo_path": {"type": "string"}, "query": {"type": "string", "description": "Service or area to find experts for (omit for full map)"}}}},
404
+ {"name": "mnemo_who_touched", "description": "Find who last modified a specific file.", "inputSchema": {"type": "object", "properties": {"repo_path": {"type": "string"}, "query": {"type": "string", "description": "File path or name"}}, "required": ["query"]}},
405
+ {"name": "mnemo_add_incident", "description": "Record a production incident with root cause and fix.", "inputSchema": {"type": "object", "properties": {"repo_path": {"type": "string"}, "title": {"type": "string"}, "what_happened": {"type": "string"}, "root_cause": {"type": "string"}, "fix": {"type": "string"}, "prevention": {"type": "string"}, "severity": {"type": "string", "description": "low, medium, high, critical"}, "services": {"type": "array", "items": {"type": "string"}}}, "required": ["title", "what_happened", "root_cause", "fix"]}},
406
+ {"name": "mnemo_incidents", "description": "Search or list production incidents.", "inputSchema": {"type": "object", "properties": {"repo_path": {"type": "string"}, "query": {"type": "string", "description": "Search term (omit to list all)"}}}},
407
+ {"name": "mnemo_links", "description": "Show all linked repos in the multi-repo workspace.", "inputSchema": {"type": "object", "properties": {"repo_path": {"type": "string"}}}},
408
+ {"name": "mnemo_cross_search", "description": "Search across this repo AND all linked repos. Use when looking for code, APIs, or patterns that may live in sibling services.", "inputSchema": {"type": "object", "properties": {"repo_path": {"type": "string"}, "query": {"type": "string", "description": "What to search for across all repos"}, "namespace": {"type": "string", "description": "Search namespace: code, api, or knowledge (default: code)"}}, "required": ["query"]}},
409
+ {"name": "mnemo_cross_impact", "description": "Cross-repo impact analysis — find what breaks across ALL linked repos if you change a service, file, or API.", "inputSchema": {"type": "object", "properties": {"repo_path": {"type": "string"}, "query": {"type": "string", "description": "Service, file, or API to analyze impact for"}}, "required": ["query"]}},
410
+ ]
411
+
412
+
413
+ def run_stdio():
414
+ """Run as MCP server over stdio (JSON-RPC)."""
415
+ for line in sys.stdin:
416
+ try:
417
+ request = json.loads(line)
418
+ except json.JSONDecodeError:
419
+ continue
420
+
421
+ method = request.get("method")
422
+ req_id = request.get("id")
423
+
424
+ if method == "initialize":
425
+ response = {
426
+ "jsonrpc": "2.0",
427
+ "id": req_id,
428
+ "result": {
429
+ "protocolVersion": "2024-11-05",
430
+ "capabilities": {"tools": {"listChanged": False}},
431
+ "serverInfo": {"name": "mnemo", "version": __version__},
432
+ },
433
+ }
434
+ elif method == "tools/list":
435
+ response = {
436
+ "jsonrpc": "2.0",
437
+ "id": req_id,
438
+ "result": {"tools": TOOLS},
439
+ }
440
+ elif method == "tools/call":
441
+ params = request.get("params", {})
442
+ result = handle_tool_call(params["name"], params.get("arguments", {}))
443
+ response = {"jsonrpc": "2.0", "id": req_id, "result": result}
444
+ elif method == "notifications/initialized":
445
+ continue
446
+ else:
447
+ response = {
448
+ "jsonrpc": "2.0",
449
+ "id": req_id,
450
+ "error": {"code": -32601, "message": f"Unknown method: {method}"},
451
+ }
452
+
453
+ sys.stdout.write(json.dumps(response) + "\n")
454
+ sys.stdout.flush()
455
+
456
+
457
+ if __name__ == "__main__":
458
+ run_stdio()