codevira 1.6.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.
- codevira-1.6.0.dist-info/LICENSE +21 -0
- codevira-1.6.0.dist-info/METADATA +477 -0
- codevira-1.6.0.dist-info/RECORD +58 -0
- codevira-1.6.0.dist-info/WHEEL +5 -0
- codevira-1.6.0.dist-info/entry_points.txt +2 -0
- codevira-1.6.0.dist-info/top_level.txt +2 -0
- indexer/__init__.py +1 -0
- indexer/chunker.py +428 -0
- indexer/global_db.py +197 -0
- indexer/graph_generator.py +380 -0
- indexer/index_codebase.py +588 -0
- indexer/outcome_tracker.py +172 -0
- indexer/rule_learner.py +186 -0
- indexer/sqlite_graph.py +640 -0
- indexer/treesitter_parser.py +423 -0
- mcp_server/__init__.py +1 -0
- mcp_server/__main__.py +20 -0
- mcp_server/auto_init.py +257 -0
- mcp_server/cli.py +622 -0
- mcp_server/crash_logger.py +236 -0
- mcp_server/data/__init__.py +1 -0
- mcp_server/data/agents/builder.md +84 -0
- mcp_server/data/agents/developer.md +111 -0
- mcp_server/data/agents/documenter.md +138 -0
- mcp_server/data/agents/orchestrator.md +96 -0
- mcp_server/data/agents/planner.md +106 -0
- mcp_server/data/agents/reviewer.md +82 -0
- mcp_server/data/agents/tester.md +83 -0
- mcp_server/data/config.example.yaml +33 -0
- mcp_server/data/rules/coding-standards.md +48 -0
- mcp_server/data/rules/engineering-excellence.md +28 -0
- mcp_server/data/rules/git-cicd-governance.md +32 -0
- mcp_server/data/rules/git_commits.md +130 -0
- mcp_server/data/rules/incremental-updates.md +5 -0
- mcp_server/data/rules/master_rule.md +187 -0
- mcp_server/data/rules/multi-language.md +19 -0
- mcp_server/data/rules/persistence.md +21 -0
- mcp_server/data/rules/resilience-observability.md +17 -0
- mcp_server/data/rules/smoke-testing.md +48 -0
- mcp_server/data/rules/testing-standards.md +23 -0
- mcp_server/detect.py +284 -0
- mcp_server/gitignore.py +284 -0
- mcp_server/global_sync.py +187 -0
- mcp_server/http_server.py +341 -0
- mcp_server/ide_inject.py +444 -0
- mcp_server/launchd.py +156 -0
- mcp_server/migrate.py +215 -0
- mcp_server/paths.py +256 -0
- mcp_server/prompts.py +136 -0
- mcp_server/server.py +1049 -0
- mcp_server/tools/__init__.py +0 -0
- mcp_server/tools/changesets.py +223 -0
- mcp_server/tools/code_reader.py +335 -0
- mcp_server/tools/graph.py +637 -0
- mcp_server/tools/learning.py +238 -0
- mcp_server/tools/playbook.py +89 -0
- mcp_server/tools/roadmap.py +599 -0
- mcp_server/tools/search.py +145 -0
mcp_server/server.py
ADDED
|
@@ -0,0 +1,1049 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent MCP Server
|
|
3
|
+
|
|
4
|
+
Exposes the project context graph, roadmap, code index, and changeset tracker
|
|
5
|
+
as MCP tools — usable by any MCP-compatible AI coding tool.
|
|
6
|
+
|
|
7
|
+
Tools:
|
|
8
|
+
get_node(file_path) → graph node: role, connections, rules
|
|
9
|
+
get_impact(file_path) → blast radius before touching a file
|
|
10
|
+
get_roadmap() → current phase, next action, open changesets
|
|
11
|
+
search_codebase(query, limit, layer) → semantic search via local ChromaDB
|
|
12
|
+
list_open_changesets() → check for unfinished multi-file work
|
|
13
|
+
start_changeset(id, desc, files) → begin tracking a multi-file fix
|
|
14
|
+
update_changeset_progress(id, file) → mark a file done within a changeset
|
|
15
|
+
complete_changeset(id, decisions) → mark changeset done, record decisions
|
|
16
|
+
update_node(file_path, changes) → update graph node after session
|
|
17
|
+
update_next_action(next_action) → update roadmap next action
|
|
18
|
+
refresh_graph(file_paths?) → auto-generate graph nodes for new files
|
|
19
|
+
get_signature(file_path) → skeleton: public symbols, signatures, line ranges
|
|
20
|
+
get_code(file_path, symbol?) → full source of one function or class from disk
|
|
21
|
+
|
|
22
|
+
Usage (Claude Code .claude/settings.json):
|
|
23
|
+
{
|
|
24
|
+
"mcpServers": {
|
|
25
|
+
"codevira": {
|
|
26
|
+
"command": "codevira",
|
|
27
|
+
"args": [],
|
|
28
|
+
"cwd": "/path/to/your-project"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
Usage (Cursor / Windsurf): configure via their MCP settings UI with same command.
|
|
34
|
+
"""
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import sys
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
import mcp.server.stdio
|
|
41
|
+
from mcp.server import Server
|
|
42
|
+
from mcp.types import Tool, TextContent
|
|
43
|
+
except ImportError:
|
|
44
|
+
print("ERROR: mcp package not installed.")
|
|
45
|
+
print("Run: pip install 'mcp>=1.0.0'")
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
import json
|
|
49
|
+
from mcp_server.tools.graph import (
|
|
50
|
+
get_node, get_impact, list_nodes, add_node, update_node, refresh_graph,
|
|
51
|
+
export_graph, get_graph_diff,
|
|
52
|
+
query_graph as query_graph_tool,
|
|
53
|
+
analyze_changes as analyze_changes_tool,
|
|
54
|
+
find_hotspots as find_hotspots_tool,
|
|
55
|
+
)
|
|
56
|
+
from mcp_server.tools.roadmap import (
|
|
57
|
+
get_roadmap, get_full_roadmap, get_phase,
|
|
58
|
+
add_phase, update_phase_status, defer_phase,
|
|
59
|
+
complete_phase, update_next_action,
|
|
60
|
+
add_open_changeset, remove_open_changeset,
|
|
61
|
+
)
|
|
62
|
+
from mcp_server.tools.search import search_codebase, refresh_index, search_decisions, get_history, write_session_log
|
|
63
|
+
from mcp_server.tools.changesets import (
|
|
64
|
+
start_changeset,
|
|
65
|
+
update_changeset_progress,
|
|
66
|
+
complete_changeset,
|
|
67
|
+
get_changeset,
|
|
68
|
+
list_open_changesets,
|
|
69
|
+
)
|
|
70
|
+
from mcp_server.tools.playbook import get_playbook
|
|
71
|
+
from mcp_server.tools.code_reader import get_signature, get_code
|
|
72
|
+
from mcp_server.tools.learning import (
|
|
73
|
+
get_decision_confidence as learning_get_decision_confidence,
|
|
74
|
+
get_preferences as learning_get_preferences,
|
|
75
|
+
get_learned_rules as learning_get_learned_rules,
|
|
76
|
+
get_project_maturity as learning_get_project_maturity,
|
|
77
|
+
get_session_context as learning_get_session_context,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
server = Server("codevira")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---- MCP Prompts (workflow templates) ----
|
|
84
|
+
@server.list_prompts()
|
|
85
|
+
async def handle_list_prompts():
|
|
86
|
+
from mcp_server.prompts import list_prompts as _list_prompts
|
|
87
|
+
from mcp.types import Prompt, PromptArgument
|
|
88
|
+
prompts = _list_prompts()
|
|
89
|
+
return [
|
|
90
|
+
Prompt(
|
|
91
|
+
name=p["name"],
|
|
92
|
+
description=p.get("description"),
|
|
93
|
+
arguments=[
|
|
94
|
+
PromptArgument(name=a["name"], description=a.get("description"), required=a.get("required", False))
|
|
95
|
+
for a in p.get("arguments", [])
|
|
96
|
+
],
|
|
97
|
+
)
|
|
98
|
+
for p in prompts
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@server.get_prompt()
|
|
103
|
+
async def handle_get_prompt(name: str, arguments: dict | None = None):
|
|
104
|
+
from mcp_server.prompts import get_prompt as _get_prompt
|
|
105
|
+
from mcp.types import GetPromptResult, PromptMessage, TextContent as PromptTextContent
|
|
106
|
+
result = _get_prompt(name, arguments)
|
|
107
|
+
if not result:
|
|
108
|
+
raise ValueError(f"Unknown prompt: {name}")
|
|
109
|
+
return GetPromptResult(
|
|
110
|
+
description=result["description"],
|
|
111
|
+
messages=[
|
|
112
|
+
PromptMessage(role=m["role"], content=PromptTextContent(type="text", text=m["content"]["text"]))
|
|
113
|
+
for m in result["messages"]
|
|
114
|
+
],
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@server.list_tools()
|
|
119
|
+
async def list_tools() -> list[Tool]:
|
|
120
|
+
return [
|
|
121
|
+
Tool(
|
|
122
|
+
name="get_node",
|
|
123
|
+
description=(
|
|
124
|
+
"Get the context graph node for a file. Returns role, connections, rules, "
|
|
125
|
+
"stability, tests, and do_not_revert flags. "
|
|
126
|
+
"Call this INSTEAD of reading the source file to understand what it does."
|
|
127
|
+
),
|
|
128
|
+
inputSchema={
|
|
129
|
+
"type": "object",
|
|
130
|
+
"properties": {
|
|
131
|
+
"file_path": {
|
|
132
|
+
"type": "string",
|
|
133
|
+
"description": "Relative file path (e.g. 'src/services/generator.py')",
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
"required": ["file_path"],
|
|
137
|
+
},
|
|
138
|
+
),
|
|
139
|
+
Tool(
|
|
140
|
+
name="get_impact",
|
|
141
|
+
description=(
|
|
142
|
+
"Get the full blast radius for a file before touching it. "
|
|
143
|
+
"BFS traversal of graph edges to find all downstream affected files. "
|
|
144
|
+
"ALWAYS call this before modifying any file."
|
|
145
|
+
),
|
|
146
|
+
inputSchema={
|
|
147
|
+
"type": "object",
|
|
148
|
+
"properties": {
|
|
149
|
+
"file_path": {
|
|
150
|
+
"type": "string",
|
|
151
|
+
"description": "File you are about to modify",
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"required": ["file_path"],
|
|
155
|
+
},
|
|
156
|
+
),
|
|
157
|
+
Tool(
|
|
158
|
+
name="get_roadmap",
|
|
159
|
+
description=(
|
|
160
|
+
"Get current project state: phase number, name, status, next action, "
|
|
161
|
+
"open changesets, and upcoming phases. Call at the START of every session."
|
|
162
|
+
),
|
|
163
|
+
inputSchema={"type": "object", "properties": {}},
|
|
164
|
+
),
|
|
165
|
+
Tool(
|
|
166
|
+
name="search_codebase",
|
|
167
|
+
description=(
|
|
168
|
+
"Semantic search over the codebase. Returns relevant functions, "
|
|
169
|
+
"classes, or module docs. Use to find implementation patterns before writing code."
|
|
170
|
+
),
|
|
171
|
+
inputSchema={
|
|
172
|
+
"type": "object",
|
|
173
|
+
"properties": {
|
|
174
|
+
"query": {
|
|
175
|
+
"type": "string",
|
|
176
|
+
"description": "Natural language or code query",
|
|
177
|
+
},
|
|
178
|
+
"limit": {
|
|
179
|
+
"type": "integer",
|
|
180
|
+
"description": "Number of results (default 5, max 10)",
|
|
181
|
+
"default": 5,
|
|
182
|
+
},
|
|
183
|
+
"layer": {
|
|
184
|
+
"type": "string",
|
|
185
|
+
"description": "Filter by architectural layer",
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
"required": ["query"],
|
|
189
|
+
},
|
|
190
|
+
),
|
|
191
|
+
Tool(
|
|
192
|
+
name="get_full_roadmap",
|
|
193
|
+
description=(
|
|
194
|
+
"Get the complete roadmap: all completed phases with key decisions, "
|
|
195
|
+
"current phase, all upcoming phases, and deferred items. "
|
|
196
|
+
"Use for planning sessions. More expensive than get_roadmap() — "
|
|
197
|
+
"only call when you need full project history."
|
|
198
|
+
),
|
|
199
|
+
inputSchema={"type": "object", "properties": {}},
|
|
200
|
+
),
|
|
201
|
+
Tool(
|
|
202
|
+
name="get_phase",
|
|
203
|
+
description="Get full details of any phase by number — completed, current, or upcoming.",
|
|
204
|
+
inputSchema={
|
|
205
|
+
"type": "object",
|
|
206
|
+
"properties": {
|
|
207
|
+
"phase_number": {
|
|
208
|
+
"type": ["integer", "string"],
|
|
209
|
+
"description": "Phase number (e.g. 19, '8R', '12A')",
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
"required": ["phase_number"],
|
|
213
|
+
},
|
|
214
|
+
),
|
|
215
|
+
Tool(
|
|
216
|
+
name="add_phase",
|
|
217
|
+
description=(
|
|
218
|
+
"Add a new upcoming phase to the roadmap. "
|
|
219
|
+
"Call when you identify new work during a session — gaps, refactors, follow-ups. "
|
|
220
|
+
"High-priority phases are inserted at the front of the queue."
|
|
221
|
+
),
|
|
222
|
+
inputSchema={
|
|
223
|
+
"type": "object",
|
|
224
|
+
"properties": {
|
|
225
|
+
"phase": {"type": ["integer", "string"], "description": "Phase number or label"},
|
|
226
|
+
"name": {"type": "string", "description": "Short phase name"},
|
|
227
|
+
"description": {"type": "string", "description": "What this phase does and why"},
|
|
228
|
+
"priority": {"type": "string", "description": "high | medium | low", "default": "medium"},
|
|
229
|
+
"depends_on": {"type": "array", "items": {"type": ["integer", "string"]}},
|
|
230
|
+
"files": {"type": "array", "items": {"type": "string"}, "description": "Key files that will be touched"},
|
|
231
|
+
"effort": {"type": "string", "description": "Rough estimate e.g. '~2 hours'"},
|
|
232
|
+
},
|
|
233
|
+
"required": ["phase", "name", "description"],
|
|
234
|
+
},
|
|
235
|
+
),
|
|
236
|
+
Tool(
|
|
237
|
+
name="update_phase_status",
|
|
238
|
+
description=(
|
|
239
|
+
"Update the current phase status: pending | in_progress | blocked. "
|
|
240
|
+
"Call when starting work on a phase (in_progress) or when blocked."
|
|
241
|
+
),
|
|
242
|
+
inputSchema={
|
|
243
|
+
"type": "object",
|
|
244
|
+
"properties": {
|
|
245
|
+
"status": {"type": "string", "description": "pending | in_progress | blocked"},
|
|
246
|
+
"blocker": {"type": "string", "description": "Required when status=blocked"},
|
|
247
|
+
"started": {"type": "string", "description": "ISO date override (defaults to today)"},
|
|
248
|
+
},
|
|
249
|
+
"required": ["status"],
|
|
250
|
+
},
|
|
251
|
+
),
|
|
252
|
+
Tool(
|
|
253
|
+
name="defer_phase",
|
|
254
|
+
description=(
|
|
255
|
+
"Move an upcoming phase to the deferred list. "
|
|
256
|
+
"Use when priorities shift or a phase depends on unavailable work."
|
|
257
|
+
),
|
|
258
|
+
inputSchema={
|
|
259
|
+
"type": "object",
|
|
260
|
+
"properties": {
|
|
261
|
+
"phase_number": {"type": ["integer", "string"]},
|
|
262
|
+
"reason": {"type": "string", "description": "Why this is being deferred"},
|
|
263
|
+
},
|
|
264
|
+
"required": ["phase_number", "reason"],
|
|
265
|
+
},
|
|
266
|
+
),
|
|
267
|
+
Tool(
|
|
268
|
+
name="complete_phase",
|
|
269
|
+
description=(
|
|
270
|
+
"Mark the current phase as complete and advance to the next upcoming phase. "
|
|
271
|
+
"Records key_decisions permanently. Requires phase_number to match current phase (safety check)."
|
|
272
|
+
),
|
|
273
|
+
inputSchema={
|
|
274
|
+
"type": "object",
|
|
275
|
+
"properties": {
|
|
276
|
+
"phase_number": {"type": ["integer", "string"], "description": "Must match current phase"},
|
|
277
|
+
"key_decisions": {
|
|
278
|
+
"type": "array",
|
|
279
|
+
"items": {"type": "string"},
|
|
280
|
+
"description": "Decisions made — preserved for all future agents",
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
"required": ["phase_number", "key_decisions"],
|
|
284
|
+
},
|
|
285
|
+
),
|
|
286
|
+
Tool(
|
|
287
|
+
name="list_open_changesets",
|
|
288
|
+
description=(
|
|
289
|
+
"List all in-progress multi-file changesets. "
|
|
290
|
+
"Call at session start to check for unfinished work from previous sessions."
|
|
291
|
+
),
|
|
292
|
+
inputSchema={"type": "object", "properties": {}},
|
|
293
|
+
),
|
|
294
|
+
Tool(
|
|
295
|
+
name="start_changeset",
|
|
296
|
+
description=(
|
|
297
|
+
"Begin tracking a multi-file fix. Call BEFORE touching any files. "
|
|
298
|
+
"Creates a changeset record for cross-session continuity."
|
|
299
|
+
),
|
|
300
|
+
inputSchema={
|
|
301
|
+
"type": "object",
|
|
302
|
+
"properties": {
|
|
303
|
+
"changeset_id": {
|
|
304
|
+
"type": "string",
|
|
305
|
+
"description": "Short slug (e.g. 'auth-refactor')",
|
|
306
|
+
},
|
|
307
|
+
"description": {"type": "string"},
|
|
308
|
+
"files": {
|
|
309
|
+
"type": "array",
|
|
310
|
+
"items": {"type": "string"},
|
|
311
|
+
"description": "All files that will be modified",
|
|
312
|
+
},
|
|
313
|
+
"trigger": {
|
|
314
|
+
"type": "string",
|
|
315
|
+
"enum": ["small_fix", "medium_change", "large_change"],
|
|
316
|
+
"default": "medium_change",
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
"required": ["changeset_id", "description", "files"],
|
|
320
|
+
},
|
|
321
|
+
),
|
|
322
|
+
Tool(
|
|
323
|
+
name="update_changeset_progress",
|
|
324
|
+
description="Mark a file as done within an active changeset.",
|
|
325
|
+
inputSchema={
|
|
326
|
+
"type": "object",
|
|
327
|
+
"properties": {
|
|
328
|
+
"changeset_id": {"type": "string"},
|
|
329
|
+
"file_done": {"type": "string", "description": "File path that was completed"},
|
|
330
|
+
"blocker": {"type": "string", "description": "Optional blocker note if session ending early"},
|
|
331
|
+
},
|
|
332
|
+
"required": ["changeset_id", "file_done"],
|
|
333
|
+
},
|
|
334
|
+
),
|
|
335
|
+
Tool(
|
|
336
|
+
name="complete_changeset",
|
|
337
|
+
description=(
|
|
338
|
+
"Mark a changeset as complete and record key decisions. "
|
|
339
|
+
"Call at session end after all files in the changeset are done."
|
|
340
|
+
),
|
|
341
|
+
inputSchema={
|
|
342
|
+
"type": "object",
|
|
343
|
+
"properties": {
|
|
344
|
+
"changeset_id": {"type": "string"},
|
|
345
|
+
"decisions": {
|
|
346
|
+
"type": "array",
|
|
347
|
+
"items": {"type": "string"},
|
|
348
|
+
"description": "Key decisions made (preserved for future agents)",
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
"required": ["changeset_id", "decisions"],
|
|
352
|
+
},
|
|
353
|
+
),
|
|
354
|
+
Tool(
|
|
355
|
+
name="update_node",
|
|
356
|
+
description=(
|
|
357
|
+
"Update a graph node after modifying a file. "
|
|
358
|
+
"Call at session end for each file you changed."
|
|
359
|
+
),
|
|
360
|
+
inputSchema={
|
|
361
|
+
"type": "object",
|
|
362
|
+
"properties": {
|
|
363
|
+
"file_path": {"type": "string"},
|
|
364
|
+
"changes": {
|
|
365
|
+
"type": "object",
|
|
366
|
+
"description": "Fields to update: last_changed_by (str), new_rules (list), do_not_revert (bool)",
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
"required": ["file_path", "changes"],
|
|
370
|
+
},
|
|
371
|
+
),
|
|
372
|
+
Tool(
|
|
373
|
+
name="list_nodes",
|
|
374
|
+
description=(
|
|
375
|
+
"List all nodes in the context graph with brief summaries. "
|
|
376
|
+
"Use at session start to discover what's in the graph and spot stale files. "
|
|
377
|
+
"Filter by layer, do_not_revert, or stability."
|
|
378
|
+
),
|
|
379
|
+
inputSchema={
|
|
380
|
+
"type": "object",
|
|
381
|
+
"properties": {
|
|
382
|
+
"layer": {
|
|
383
|
+
"type": "string",
|
|
384
|
+
"description": "Filter by architectural layer",
|
|
385
|
+
},
|
|
386
|
+
"do_not_revert": {
|
|
387
|
+
"type": "boolean",
|
|
388
|
+
"description": "If true, return only protected nodes",
|
|
389
|
+
},
|
|
390
|
+
"stability": {
|
|
391
|
+
"type": "string",
|
|
392
|
+
"description": "Filter by stability: low | medium | high",
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
),
|
|
397
|
+
Tool(
|
|
398
|
+
name="add_node",
|
|
399
|
+
description=(
|
|
400
|
+
"Add a new node to the context graph for a newly created file. "
|
|
401
|
+
"Call this after creating a new file. Graph file is auto-inferred from path."
|
|
402
|
+
),
|
|
403
|
+
inputSchema={
|
|
404
|
+
"type": "object",
|
|
405
|
+
"properties": {
|
|
406
|
+
"file_path": {"type": "string", "description": "Relative file path"},
|
|
407
|
+
"role": {"type": "string", "description": "One-line description of what the file does"},
|
|
408
|
+
"layer": {"type": "string", "description": "Architectural layer"},
|
|
409
|
+
"stability": {"type": "string", "description": "low | medium | high", "default": "medium"},
|
|
410
|
+
"node_type": {"type": "string", "description": "file | service | schema | event", "default": "file"},
|
|
411
|
+
"key_functions": {"type": "array", "items": {"type": "string"}},
|
|
412
|
+
"connects_to": {
|
|
413
|
+
"type": "array",
|
|
414
|
+
"items": {"type": "object"},
|
|
415
|
+
"description": "Edge list: [{target, edge, via}]",
|
|
416
|
+
},
|
|
417
|
+
"rules": {"type": "array", "items": {"type": "string"}},
|
|
418
|
+
"do_not_revert": {"type": "boolean", "default": False},
|
|
419
|
+
"tests": {"type": "array", "items": {"type": "string"}},
|
|
420
|
+
},
|
|
421
|
+
"required": ["file_path", "role", "layer"],
|
|
422
|
+
},
|
|
423
|
+
),
|
|
424
|
+
Tool(
|
|
425
|
+
name="search_decisions",
|
|
426
|
+
description=(
|
|
427
|
+
"Search past decisions across all completed changesets, roadmap phases, and session logs. "
|
|
428
|
+
"Answers 'has anyone decided this before?' — gives agents institutional memory."
|
|
429
|
+
),
|
|
430
|
+
inputSchema={
|
|
431
|
+
"type": "object",
|
|
432
|
+
"properties": {
|
|
433
|
+
"query": {"type": "string", "description": "Keywords to search (e.g. 'threshold', 'uuid', 'retry')"},
|
|
434
|
+
"limit": {"type": "integer", "description": "Max results (default 10)", "default": 10},
|
|
435
|
+
"session_id": {"type": "string", "description": "Optional — filter results to a specific session only"},
|
|
436
|
+
},
|
|
437
|
+
"required": ["query"],
|
|
438
|
+
},
|
|
439
|
+
),
|
|
440
|
+
Tool(
|
|
441
|
+
name="get_history",
|
|
442
|
+
description=(
|
|
443
|
+
"Get the last N git commits that touched a file. "
|
|
444
|
+
"Links graph node last_changed_by to actual commits."
|
|
445
|
+
),
|
|
446
|
+
inputSchema={
|
|
447
|
+
"type": "object",
|
|
448
|
+
"properties": {
|
|
449
|
+
"file_path": {"type": "string", "description": "Relative file path"},
|
|
450
|
+
},
|
|
451
|
+
"required": ["file_path"],
|
|
452
|
+
},
|
|
453
|
+
),
|
|
454
|
+
Tool(
|
|
455
|
+
name="write_session_log",
|
|
456
|
+
description=(
|
|
457
|
+
"Write a structured session log to .agents/logs/YYYY-MM-DD/. "
|
|
458
|
+
"Called by the Documenter at the end of every session. "
|
|
459
|
+
"Feeds search_decisions() with institutional memory."
|
|
460
|
+
),
|
|
461
|
+
inputSchema={
|
|
462
|
+
"type": "object",
|
|
463
|
+
"properties": {
|
|
464
|
+
"session_id": {"type": "string", "description": "Short ID (8-char slug)"},
|
|
465
|
+
"task": {"type": "string", "description": "Original developer prompt"},
|
|
466
|
+
"phase": {"type": "string", "description": "phase"},
|
|
467
|
+
"files_changed": {"type": "array", "items": {"type": "string"}},
|
|
468
|
+
"decisions": {
|
|
469
|
+
"type": "array",
|
|
470
|
+
"items": {
|
|
471
|
+
"type": "object",
|
|
472
|
+
"properties": {
|
|
473
|
+
"file_path": {"type": "string"},
|
|
474
|
+
"decision": {"type": "string"},
|
|
475
|
+
"context": {"type": "string"}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
"next_steps": {"type": "array", "items": {"type": "string"}}
|
|
480
|
+
},
|
|
481
|
+
"required": ["session_id", "task", "phase", "files_changed", "decisions", "next_steps"],
|
|
482
|
+
},
|
|
483
|
+
),
|
|
484
|
+
Tool(
|
|
485
|
+
name="refresh_index",
|
|
486
|
+
description=(
|
|
487
|
+
"Trigger an incremental reindex of changed files. "
|
|
488
|
+
"Call when get_node() returns index_status.stale=true, or before searching "
|
|
489
|
+
"files you know have changed. Pass file_paths to reindex specific files only."
|
|
490
|
+
),
|
|
491
|
+
inputSchema={
|
|
492
|
+
"type": "object",
|
|
493
|
+
"properties": {
|
|
494
|
+
"file_paths": {
|
|
495
|
+
"type": "array",
|
|
496
|
+
"items": {"type": "string"},
|
|
497
|
+
"description": "Specific files to reindex. Omit to reindex all changed files.",
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
),
|
|
502
|
+
Tool(
|
|
503
|
+
name="get_playbook",
|
|
504
|
+
description=(
|
|
505
|
+
"Get curated architectural rules for a specific task type. "
|
|
506
|
+
"Returns only the 2-3 relevant rule files — not all of them. "
|
|
507
|
+
"Use for: add_route | add_service | add_schema | "
|
|
508
|
+
"debug_pipeline | commit | write_test"
|
|
509
|
+
),
|
|
510
|
+
inputSchema={
|
|
511
|
+
"type": "object",
|
|
512
|
+
"properties": {
|
|
513
|
+
"task_type": {
|
|
514
|
+
"type": "string",
|
|
515
|
+
"description": "Task type (e.g. 'add_route', 'debug_pipeline', 'commit')",
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
"required": ["task_type"],
|
|
519
|
+
},
|
|
520
|
+
),
|
|
521
|
+
Tool(
|
|
522
|
+
name="update_next_action",
|
|
523
|
+
description="Update the roadmap's next_action field. Call at session end.",
|
|
524
|
+
inputSchema={
|
|
525
|
+
"type": "object",
|
|
526
|
+
"properties": {
|
|
527
|
+
"next_action": {
|
|
528
|
+
"type": "string",
|
|
529
|
+
"description": "Exact description of what the next agent should do",
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
"required": ["next_action"],
|
|
533
|
+
},
|
|
534
|
+
),
|
|
535
|
+
Tool(
|
|
536
|
+
name="refresh_graph",
|
|
537
|
+
description=(
|
|
538
|
+
"Auto-generate context graph nodes for new or unregistered files. "
|
|
539
|
+
"Call after creating a new file to get a graph stub immediately — "
|
|
540
|
+
"no CLI needed. Safe merge: existing enriched nodes are never overwritten. "
|
|
541
|
+
"Pass file_paths to target specific files, or omit to scan all new files."
|
|
542
|
+
),
|
|
543
|
+
inputSchema={
|
|
544
|
+
"type": "object",
|
|
545
|
+
"properties": {
|
|
546
|
+
"file_paths": {
|
|
547
|
+
"type": "array",
|
|
548
|
+
"items": {"type": "string"},
|
|
549
|
+
"description": (
|
|
550
|
+
"Relative file paths to generate nodes for. "
|
|
551
|
+
"Omit to auto-detect all Python files not yet in the graph."
|
|
552
|
+
),
|
|
553
|
+
}
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
),
|
|
557
|
+
Tool(
|
|
558
|
+
name="get_signature",
|
|
559
|
+
description=(
|
|
560
|
+
"Get the skeleton of a Python file — all public function and class names, "
|
|
561
|
+
"their signatures, docstrings, and line ranges. "
|
|
562
|
+
"Call this after get_node() to understand file structure before deciding "
|
|
563
|
+
"which symbol to read with get_code(). Much cheaper than reading the full file. "
|
|
564
|
+
"Note: Python files only. For other languages, read the file directly."
|
|
565
|
+
),
|
|
566
|
+
inputSchema={
|
|
567
|
+
"type": "object",
|
|
568
|
+
"properties": {
|
|
569
|
+
"file_path": {
|
|
570
|
+
"type": "string",
|
|
571
|
+
"description": "Relative file path (e.g. 'src/services/generator.py')",
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
"required": ["file_path"],
|
|
575
|
+
},
|
|
576
|
+
),
|
|
577
|
+
Tool(
|
|
578
|
+
name="get_code",
|
|
579
|
+
description=(
|
|
580
|
+
"Get the full source of a single function or class by name. "
|
|
581
|
+
"Always reads from disk — always current, never stale. "
|
|
582
|
+
"Call get_signature() first to discover available symbol names and line ranges. "
|
|
583
|
+
"Omit symbol to get module-level constants and assignments only. "
|
|
584
|
+
"Note: Python files only. For other languages, read the file directly."
|
|
585
|
+
),
|
|
586
|
+
inputSchema={
|
|
587
|
+
"type": "object",
|
|
588
|
+
"properties": {
|
|
589
|
+
"file_path": {
|
|
590
|
+
"type": "string",
|
|
591
|
+
"description": "Relative file path",
|
|
592
|
+
},
|
|
593
|
+
"symbol": {
|
|
594
|
+
"type": "string",
|
|
595
|
+
"description": "Function or class name to retrieve. Omit for module-level constants.",
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
"required": ["file_path"],
|
|
599
|
+
},
|
|
600
|
+
),
|
|
601
|
+
# ---- v1.4: Graph Visualization & Diff ----
|
|
602
|
+
Tool(
|
|
603
|
+
name="export_graph",
|
|
604
|
+
description=(
|
|
605
|
+
"Export the dependency graph as a Mermaid or DOT diagram. "
|
|
606
|
+
"Use for documentation, PR descriptions, or onboarding. "
|
|
607
|
+
"Pass scope to limit to a directory (e.g. 'src/services/')."
|
|
608
|
+
),
|
|
609
|
+
inputSchema={
|
|
610
|
+
"type": "object",
|
|
611
|
+
"properties": {
|
|
612
|
+
"format": {
|
|
613
|
+
"type": "string",
|
|
614
|
+
"enum": ["mermaid", "dot"],
|
|
615
|
+
"description": "Output format: 'mermaid' or 'dot'",
|
|
616
|
+
"default": "mermaid",
|
|
617
|
+
},
|
|
618
|
+
"scope": {
|
|
619
|
+
"type": "string",
|
|
620
|
+
"description": "Filter to files under this directory prefix",
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
),
|
|
625
|
+
Tool(
|
|
626
|
+
name="get_graph_diff",
|
|
627
|
+
description=(
|
|
628
|
+
"Show which graph nodes changed between two git refs and their blast radius. "
|
|
629
|
+
"Use before opening a PR to understand the impact of your changes. "
|
|
630
|
+
"Defaults to comparing main...HEAD."
|
|
631
|
+
),
|
|
632
|
+
inputSchema={
|
|
633
|
+
"type": "object",
|
|
634
|
+
"properties": {
|
|
635
|
+
"base_ref": {
|
|
636
|
+
"type": "string",
|
|
637
|
+
"description": "Base git ref (default: 'main')",
|
|
638
|
+
"default": "main",
|
|
639
|
+
},
|
|
640
|
+
"head_ref": {
|
|
641
|
+
"type": "string",
|
|
642
|
+
"description": "Head git ref (default: 'HEAD')",
|
|
643
|
+
"default": "HEAD",
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
),
|
|
648
|
+
# ---- v1.4: Learning & Adaptive Memory ----
|
|
649
|
+
Tool(
|
|
650
|
+
name="get_decision_confidence",
|
|
651
|
+
description=(
|
|
652
|
+
"Get confidence scores for a file or pattern based on outcome history. "
|
|
653
|
+
"Returns how often past decisions were kept, modified, or reverted. "
|
|
654
|
+
"Call this before making decisions in an area to gauge reliability."
|
|
655
|
+
),
|
|
656
|
+
inputSchema={
|
|
657
|
+
"type": "object",
|
|
658
|
+
"properties": {
|
|
659
|
+
"file_path": {
|
|
660
|
+
"type": "string",
|
|
661
|
+
"description": "Specific file to check confidence for",
|
|
662
|
+
},
|
|
663
|
+
"pattern": {
|
|
664
|
+
"type": "string",
|
|
665
|
+
"description": "Directory or file pattern to check (e.g. 'src/api/')",
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
),
|
|
670
|
+
Tool(
|
|
671
|
+
name="get_preferences",
|
|
672
|
+
description=(
|
|
673
|
+
"Get learned developer preferences from past correction patterns. "
|
|
674
|
+
"Returns coding style signals: naming conventions, structural preferences, patterns. "
|
|
675
|
+
"Call this before writing code to match the developer's style."
|
|
676
|
+
),
|
|
677
|
+
inputSchema={
|
|
678
|
+
"type": "object",
|
|
679
|
+
"properties": {
|
|
680
|
+
"category": {
|
|
681
|
+
"type": "string",
|
|
682
|
+
"description": "Filter by category: 'naming' | 'structure' | 'patterns' | 'formatting'",
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
),
|
|
687
|
+
Tool(
|
|
688
|
+
name="get_learned_rules",
|
|
689
|
+
description=(
|
|
690
|
+
"Get auto-generated rules from observed patterns across sessions. "
|
|
691
|
+
"These rules are learned from what works — test pairing patterns, import rules, "
|
|
692
|
+
"co-change patterns. Higher confidence = more reliable. "
|
|
693
|
+
"Use alongside get_playbook() for comprehensive guidance."
|
|
694
|
+
),
|
|
695
|
+
inputSchema={
|
|
696
|
+
"type": "object",
|
|
697
|
+
"properties": {
|
|
698
|
+
"file_path": {
|
|
699
|
+
"type": "string",
|
|
700
|
+
"description": "File path to get rules for (matches by pattern)",
|
|
701
|
+
},
|
|
702
|
+
"category": {
|
|
703
|
+
"type": "string",
|
|
704
|
+
"description": "Filter: 'testing' | 'imports' | 'structure' | 'patterns' | 'naming'",
|
|
705
|
+
},
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
),
|
|
709
|
+
Tool(
|
|
710
|
+
name="get_project_maturity",
|
|
711
|
+
description=(
|
|
712
|
+
"Get overall project intelligence and maturity metrics. "
|
|
713
|
+
"Shows session count, file coverage, confidence score, learned rules, "
|
|
714
|
+
"and preference signals. The higher the score, the less ambiguous agent decisions are."
|
|
715
|
+
),
|
|
716
|
+
inputSchema={"type": "object", "properties": {}},
|
|
717
|
+
),
|
|
718
|
+
Tool(
|
|
719
|
+
name="get_session_context",
|
|
720
|
+
description=(
|
|
721
|
+
"Single 'catch me up' call for cross-tool continuity. "
|
|
722
|
+
"Returns current roadmap phase, open changesets, recent decisions with confidence, "
|
|
723
|
+
"learned preferences, and active rules — everything a new session needs. "
|
|
724
|
+
"Call this at the START of every session instead of multiple separate calls. "
|
|
725
|
+
"Works seamlessly across AI tools: Cursor, Claude Code, Windsurf, Antigravity."
|
|
726
|
+
),
|
|
727
|
+
inputSchema={"type": "object", "properties": {}},
|
|
728
|
+
),
|
|
729
|
+
# ---- v1.5: Deep Graph Intelligence Tools ----
|
|
730
|
+
Tool(
|
|
731
|
+
name="query_graph",
|
|
732
|
+
description=(
|
|
733
|
+
"Query the function-level call graph. Find callers, callees, tests, or dependents "
|
|
734
|
+
"for a specific symbol. Use query_type='symbols' to list all functions in a file."
|
|
735
|
+
),
|
|
736
|
+
inputSchema={
|
|
737
|
+
"type": "object",
|
|
738
|
+
"properties": {
|
|
739
|
+
"file_path": {"type": "string", "description": "Relative file path"},
|
|
740
|
+
"symbol": {"type": "string", "description": "Function or class name to query"},
|
|
741
|
+
"query_type": {
|
|
742
|
+
"type": "string",
|
|
743
|
+
"description": "callers | callees | tests | dependents | symbols",
|
|
744
|
+
"default": "callees",
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
"required": ["file_path"],
|
|
748
|
+
},
|
|
749
|
+
),
|
|
750
|
+
Tool(
|
|
751
|
+
name="analyze_changes",
|
|
752
|
+
description=(
|
|
753
|
+
"Function-level risk-scored change analysis. Maps git diff to affected functions, "
|
|
754
|
+
"counts callers, flags test coverage gaps, assigns risk scores (high/medium/low). "
|
|
755
|
+
"Use before code review or pre-commit."
|
|
756
|
+
),
|
|
757
|
+
inputSchema={
|
|
758
|
+
"type": "object",
|
|
759
|
+
"properties": {
|
|
760
|
+
"base_ref": {"type": "string", "description": "Base git ref (default: main)", "default": "main"},
|
|
761
|
+
"head_ref": {"type": "string", "description": "Head git ref (default: HEAD)", "default": "HEAD"},
|
|
762
|
+
},
|
|
763
|
+
},
|
|
764
|
+
),
|
|
765
|
+
Tool(
|
|
766
|
+
name="find_hotspots",
|
|
767
|
+
description=(
|
|
768
|
+
"Find complexity and risk hotspots: large functions (exceeding line threshold), "
|
|
769
|
+
"high fan-in symbols (many callers = risky to change), and high fan-out files "
|
|
770
|
+
"(many dependencies = fragile)."
|
|
771
|
+
),
|
|
772
|
+
inputSchema={
|
|
773
|
+
"type": "object",
|
|
774
|
+
"properties": {
|
|
775
|
+
"threshold": {"type": "integer", "description": "Min lines for large function (default: 50)", "default": 50},
|
|
776
|
+
},
|
|
777
|
+
},
|
|
778
|
+
),
|
|
779
|
+
]
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
@server.call_tool()
|
|
783
|
+
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
784
|
+
# v1.6: Auto-init check — triggers background init on first call if needed.
|
|
785
|
+
# This is a no-op (<1ms) on every subsequent call after initialization.
|
|
786
|
+
try:
|
|
787
|
+
from mcp_server.auto_init import ensure_project_initialized
|
|
788
|
+
ensure_project_initialized()
|
|
789
|
+
except Exception:
|
|
790
|
+
pass # auto-init must never block tool dispatch
|
|
791
|
+
|
|
792
|
+
try:
|
|
793
|
+
if name == "get_node":
|
|
794
|
+
result = get_node(arguments["file_path"])
|
|
795
|
+
elif name == "get_impact":
|
|
796
|
+
result = get_impact(arguments["file_path"])
|
|
797
|
+
elif name == "get_roadmap":
|
|
798
|
+
result = get_roadmap()
|
|
799
|
+
elif name == "get_full_roadmap":
|
|
800
|
+
result = get_full_roadmap()
|
|
801
|
+
elif name == "get_phase":
|
|
802
|
+
result = get_phase(arguments["phase_number"])
|
|
803
|
+
elif name == "add_phase":
|
|
804
|
+
result = add_phase(
|
|
805
|
+
phase=arguments["phase"],
|
|
806
|
+
name=arguments["name"],
|
|
807
|
+
description=arguments["description"],
|
|
808
|
+
priority=arguments.get("priority", "medium"),
|
|
809
|
+
depends_on=arguments.get("depends_on"),
|
|
810
|
+
files=arguments.get("files"),
|
|
811
|
+
effort=arguments.get("effort"),
|
|
812
|
+
)
|
|
813
|
+
elif name == "update_phase_status":
|
|
814
|
+
result = update_phase_status(
|
|
815
|
+
status=arguments["status"],
|
|
816
|
+
blocker=arguments.get("blocker"),
|
|
817
|
+
started=arguments.get("started"),
|
|
818
|
+
)
|
|
819
|
+
elif name == "defer_phase":
|
|
820
|
+
result = defer_phase(
|
|
821
|
+
phase_number=arguments["phase_number"],
|
|
822
|
+
reason=arguments["reason"],
|
|
823
|
+
)
|
|
824
|
+
elif name == "complete_phase":
|
|
825
|
+
result = complete_phase(
|
|
826
|
+
phase_number=arguments["phase_number"],
|
|
827
|
+
key_decisions=arguments["key_decisions"],
|
|
828
|
+
)
|
|
829
|
+
elif name == "search_codebase":
|
|
830
|
+
result = search_codebase(
|
|
831
|
+
arguments["query"],
|
|
832
|
+
top_k=arguments.get("limit", 5),
|
|
833
|
+
)
|
|
834
|
+
elif name == "list_open_changesets":
|
|
835
|
+
result = list_open_changesets()
|
|
836
|
+
elif name == "start_changeset":
|
|
837
|
+
result = start_changeset(
|
|
838
|
+
arguments["changeset_id"],
|
|
839
|
+
arguments["description"],
|
|
840
|
+
arguments["files"],
|
|
841
|
+
trigger=arguments.get("trigger", "medium_change"),
|
|
842
|
+
)
|
|
843
|
+
elif name == "update_changeset_progress":
|
|
844
|
+
result = update_changeset_progress(
|
|
845
|
+
arguments["changeset_id"],
|
|
846
|
+
arguments["file_done"],
|
|
847
|
+
blocker=arguments.get("blocker"),
|
|
848
|
+
)
|
|
849
|
+
elif name == "complete_changeset":
|
|
850
|
+
result = complete_changeset(
|
|
851
|
+
arguments["changeset_id"],
|
|
852
|
+
arguments["decisions"],
|
|
853
|
+
)
|
|
854
|
+
elif name == "update_node":
|
|
855
|
+
result = update_node(
|
|
856
|
+
arguments["file_path"],
|
|
857
|
+
arguments["changes"],
|
|
858
|
+
)
|
|
859
|
+
elif name == "list_nodes":
|
|
860
|
+
result = list_nodes(
|
|
861
|
+
layer=arguments.get("layer"),
|
|
862
|
+
do_not_revert=arguments.get("do_not_revert"),
|
|
863
|
+
stability=arguments.get("stability"),
|
|
864
|
+
)
|
|
865
|
+
elif name == "add_node":
|
|
866
|
+
result = add_node(
|
|
867
|
+
file_path=arguments["file_path"],
|
|
868
|
+
role=arguments["role"],
|
|
869
|
+
layer=arguments["layer"],
|
|
870
|
+
stability=arguments.get("stability", "medium"),
|
|
871
|
+
node_type=arguments.get("node_type", "file"),
|
|
872
|
+
key_functions=arguments.get("key_functions"),
|
|
873
|
+
connects_to=arguments.get("connects_to"),
|
|
874
|
+
rules=arguments.get("rules"),
|
|
875
|
+
do_not_revert=arguments.get("do_not_revert", False),
|
|
876
|
+
tests=arguments.get("tests"),
|
|
877
|
+
)
|
|
878
|
+
elif name == "search_decisions":
|
|
879
|
+
result = search_decisions(
|
|
880
|
+
arguments["query"],
|
|
881
|
+
limit=arguments.get("limit", 10),
|
|
882
|
+
session_id=arguments.get("session_id"),
|
|
883
|
+
)
|
|
884
|
+
elif name == "get_history":
|
|
885
|
+
result = get_history(arguments["file_path"])
|
|
886
|
+
elif name == "write_session_log":
|
|
887
|
+
result = write_session_log(
|
|
888
|
+
session_id=arguments["session_id"],
|
|
889
|
+
task=arguments["task"],
|
|
890
|
+
phase=arguments["phase"],
|
|
891
|
+
files_changed=arguments["files_changed"],
|
|
892
|
+
decisions=arguments["decisions"],
|
|
893
|
+
next_steps=arguments["next_steps"],
|
|
894
|
+
)
|
|
895
|
+
elif name == "refresh_index":
|
|
896
|
+
result = refresh_index(file_paths=arguments.get("file_paths") or [])
|
|
897
|
+
elif name == "get_playbook":
|
|
898
|
+
result = get_playbook(arguments["task_type"])
|
|
899
|
+
elif name == "update_next_action":
|
|
900
|
+
result = update_next_action(arguments["next_action"])
|
|
901
|
+
elif name == "refresh_graph":
|
|
902
|
+
result = refresh_graph(file_paths=arguments.get("file_paths"))
|
|
903
|
+
elif name == "get_signature":
|
|
904
|
+
result = get_signature(arguments["file_path"])
|
|
905
|
+
elif name == "get_code":
|
|
906
|
+
result = get_code(arguments["file_path"], symbol=arguments.get("symbol"))
|
|
907
|
+
# ---- v1.4: Graph Visualization & Diff ----
|
|
908
|
+
elif name == "export_graph":
|
|
909
|
+
result = export_graph(
|
|
910
|
+
format=arguments.get("format", "mermaid"),
|
|
911
|
+
scope=arguments.get("scope"),
|
|
912
|
+
)
|
|
913
|
+
elif name == "get_graph_diff":
|
|
914
|
+
result = get_graph_diff(
|
|
915
|
+
base_ref=arguments.get("base_ref", "main"),
|
|
916
|
+
head_ref=arguments.get("head_ref", "HEAD"),
|
|
917
|
+
)
|
|
918
|
+
# ---- v1.4: Learning & Adaptive Memory ----
|
|
919
|
+
elif name == "get_decision_confidence":
|
|
920
|
+
result = learning_get_decision_confidence(
|
|
921
|
+
file_path=arguments.get("file_path"),
|
|
922
|
+
pattern=arguments.get("pattern"),
|
|
923
|
+
)
|
|
924
|
+
elif name == "get_preferences":
|
|
925
|
+
result = learning_get_preferences(category=arguments.get("category"))
|
|
926
|
+
elif name == "get_learned_rules":
|
|
927
|
+
result = learning_get_learned_rules(
|
|
928
|
+
file_path=arguments.get("file_path"),
|
|
929
|
+
category=arguments.get("category"),
|
|
930
|
+
)
|
|
931
|
+
elif name == "get_project_maturity":
|
|
932
|
+
result = learning_get_project_maturity()
|
|
933
|
+
elif name == "get_session_context":
|
|
934
|
+
result = learning_get_session_context()
|
|
935
|
+
# ---- v1.5: Deep Graph Intelligence ----
|
|
936
|
+
elif name == "query_graph":
|
|
937
|
+
result = query_graph_tool(
|
|
938
|
+
file_path=arguments["file_path"],
|
|
939
|
+
symbol=arguments.get("symbol"),
|
|
940
|
+
query_type=arguments.get("query_type", "callees"),
|
|
941
|
+
)
|
|
942
|
+
elif name == "analyze_changes":
|
|
943
|
+
result = analyze_changes_tool(
|
|
944
|
+
base_ref=arguments.get("base_ref", "main"),
|
|
945
|
+
head_ref=arguments.get("head_ref", "HEAD"),
|
|
946
|
+
)
|
|
947
|
+
elif name == "find_hotspots":
|
|
948
|
+
result = find_hotspots_tool(
|
|
949
|
+
threshold=arguments.get("threshold", 50),
|
|
950
|
+
)
|
|
951
|
+
else:
|
|
952
|
+
result = {"error": f"Unknown tool: {name}"}
|
|
953
|
+
|
|
954
|
+
except Exception as e:
|
|
955
|
+
result = {"error": str(e), "tool": name}
|
|
956
|
+
# Log crashes with full traceback (sanitized, no secrets)
|
|
957
|
+
try:
|
|
958
|
+
from mcp_server.crash_logger import log_crash
|
|
959
|
+
from mcp_server.paths import get_project_root
|
|
960
|
+
log_crash(e, context="tool dispatch", tool_name=name,
|
|
961
|
+
project_path=str(get_project_root()))
|
|
962
|
+
except Exception:
|
|
963
|
+
pass # crash logger must never break the server
|
|
964
|
+
|
|
965
|
+
return [TextContent(type="text", text=json.dumps(result, indent=2))]
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def main():
|
|
969
|
+
import asyncio
|
|
970
|
+
import logging
|
|
971
|
+
|
|
972
|
+
logger = logging.getLogger("codevira.server")
|
|
973
|
+
|
|
974
|
+
# Install global crash handler — catches unhandled exceptions
|
|
975
|
+
try:
|
|
976
|
+
from mcp_server.crash_logger import install_global_handler, log_crash
|
|
977
|
+
install_global_handler()
|
|
978
|
+
except Exception as e:
|
|
979
|
+
logger.warning("Could not install crash handler: %s", e)
|
|
980
|
+
|
|
981
|
+
# v1.6: Auto-migrate legacy .codevira/ → ~/.codevira/projects/<key>/
|
|
982
|
+
try:
|
|
983
|
+
from mcp_server.migrate import detect_migration_needed, migrate_to_centralized
|
|
984
|
+
from mcp_server.paths import get_project_root
|
|
985
|
+
_proj_root = get_project_root()
|
|
986
|
+
if detect_migration_needed(_proj_root):
|
|
987
|
+
logger.info("Migrating legacy .codevira/ to centralized storage...")
|
|
988
|
+
result = migrate_to_centralized(_proj_root)
|
|
989
|
+
if result.get("migrated"):
|
|
990
|
+
logger.info("Migration complete: %d files moved to %s",
|
|
991
|
+
result.get("files_copied", 0), result.get("new_path", ""))
|
|
992
|
+
except Exception as e:
|
|
993
|
+
logger.warning("Could not run storage migration: %s", e)
|
|
994
|
+
|
|
995
|
+
# Auto-start background file watcher so the index stays fresh
|
|
996
|
+
# on every file save — no manual trigger or git commit needed.
|
|
997
|
+
watcher = None
|
|
998
|
+
try:
|
|
999
|
+
from indexer.index_codebase import start_background_watcher
|
|
1000
|
+
watcher = start_background_watcher(quiet=True)
|
|
1001
|
+
logger.info("Live file watcher active — index updates on save")
|
|
1002
|
+
except Exception as e:
|
|
1003
|
+
# Watcher is best-effort; don't block server startup
|
|
1004
|
+
logger.warning("Could not start background watcher: %s", e)
|
|
1005
|
+
try: log_crash(e, context="background watcher startup")
|
|
1006
|
+
except Exception: pass
|
|
1007
|
+
|
|
1008
|
+
# v1.4: Run outcome analysis and rule inference on startup
|
|
1009
|
+
# This processes any sessions that haven't been analyzed yet.
|
|
1010
|
+
try:
|
|
1011
|
+
from indexer.outcome_tracker import analyze_session_outcomes
|
|
1012
|
+
from indexer.rule_learner import run_rule_inference
|
|
1013
|
+
analyze_session_outcomes()
|
|
1014
|
+
run_rule_inference()
|
|
1015
|
+
logger.info("Outcome analysis and rule inference complete")
|
|
1016
|
+
except Exception as e:
|
|
1017
|
+
logger.warning("Could not run startup learning: %s", e)
|
|
1018
|
+
try: log_crash(e, context="startup learning pipeline")
|
|
1019
|
+
except Exception: pass
|
|
1020
|
+
|
|
1021
|
+
# v1.5: Import global intelligence from cross-project memory
|
|
1022
|
+
try:
|
|
1023
|
+
from mcp_server.global_sync import import_global_to_project
|
|
1024
|
+
stats = import_global_to_project()
|
|
1025
|
+
if stats.get("preferences_imported") or stats.get("rules_imported"):
|
|
1026
|
+
logger.info("Global memory: imported %d preferences, %d rules",
|
|
1027
|
+
stats["preferences_imported"], stats["rules_imported"])
|
|
1028
|
+
except Exception as e:
|
|
1029
|
+
logger.warning("Could not sync global memory: %s", e)
|
|
1030
|
+
try: log_crash(e, context="global memory import")
|
|
1031
|
+
except Exception: pass
|
|
1032
|
+
|
|
1033
|
+
async def _run():
|
|
1034
|
+
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
|
1035
|
+
await server.run(
|
|
1036
|
+
read_stream,
|
|
1037
|
+
write_stream,
|
|
1038
|
+
server.create_initialization_options(),
|
|
1039
|
+
)
|
|
1040
|
+
|
|
1041
|
+
try:
|
|
1042
|
+
asyncio.run(_run())
|
|
1043
|
+
finally:
|
|
1044
|
+
if watcher is not None:
|
|
1045
|
+
watcher.stop()
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
if __name__ == "__main__":
|
|
1049
|
+
main()
|