sari 0.0.1__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.
- app/__init__.py +1 -0
- app/config.py +240 -0
- app/db.py +932 -0
- app/dedup_queue.py +77 -0
- app/engine_registry.py +56 -0
- app/engine_runtime.py +472 -0
- app/http_server.py +204 -0
- app/indexer.py +1532 -0
- app/main.py +147 -0
- app/models.py +39 -0
- app/queue_pipeline.py +65 -0
- app/ranking.py +144 -0
- app/registry.py +172 -0
- app/search_engine.py +572 -0
- app/watcher.py +124 -0
- app/workspace.py +286 -0
- deckard/__init__.py +3 -0
- deckard/__main__.py +4 -0
- deckard/main.py +345 -0
- deckard/version.py +1 -0
- mcp/__init__.py +1 -0
- mcp/__main__.py +19 -0
- mcp/cli.py +485 -0
- mcp/daemon.py +149 -0
- mcp/proxy.py +304 -0
- mcp/registry.py +218 -0
- mcp/server.py +519 -0
- mcp/session.py +234 -0
- mcp/telemetry.py +112 -0
- mcp/test_cli.py +89 -0
- mcp/test_daemon.py +124 -0
- mcp/test_server.py +197 -0
- mcp/tools/__init__.py +14 -0
- mcp/tools/_util.py +244 -0
- mcp/tools/deckard_guide.py +32 -0
- mcp/tools/doctor.py +208 -0
- mcp/tools/get_callers.py +60 -0
- mcp/tools/get_implementations.py +60 -0
- mcp/tools/index_file.py +75 -0
- mcp/tools/list_files.py +138 -0
- mcp/tools/read_file.py +48 -0
- mcp/tools/read_symbol.py +99 -0
- mcp/tools/registry.py +212 -0
- mcp/tools/repo_candidates.py +89 -0
- mcp/tools/rescan.py +46 -0
- mcp/tools/scan_once.py +54 -0
- mcp/tools/search.py +208 -0
- mcp/tools/search_api_endpoints.py +72 -0
- mcp/tools/search_symbols.py +63 -0
- mcp/tools/status.py +135 -0
- sari/__init__.py +1 -0
- sari/__main__.py +4 -0
- sari-0.0.1.dist-info/METADATA +521 -0
- sari-0.0.1.dist-info/RECORD +58 -0
- sari-0.0.1.dist-info/WHEEL +5 -0
- sari-0.0.1.dist-info/entry_points.txt +2 -0
- sari-0.0.1.dist-info/licenses/LICENSE +21 -0
- sari-0.0.1.dist-info/top_level.txt +4 -0
mcp/tools/get_callers.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, Dict, List
|
|
3
|
+
try:
|
|
4
|
+
from ._util import mcp_response, pack_header, pack_line, pack_encode_id, pack_encode_text, resolve_root_ids, pack_error, ErrorCode
|
|
5
|
+
except ImportError:
|
|
6
|
+
from _util import mcp_response, pack_header, pack_line, pack_encode_id, pack_encode_text, resolve_root_ids, pack_error, ErrorCode
|
|
7
|
+
|
|
8
|
+
def execute_get_callers(args: Dict[str, Any], db: Any, roots: List[str]) -> Dict[str, Any]:
|
|
9
|
+
"""Find symbols that call a specific symbol."""
|
|
10
|
+
target_symbol = args.get("name", "").strip()
|
|
11
|
+
if not target_symbol:
|
|
12
|
+
return mcp_response(
|
|
13
|
+
"get_callers",
|
|
14
|
+
lambda: pack_error("get_callers", ErrorCode.INVALID_ARGS, "Symbol name is required"),
|
|
15
|
+
lambda: {"error": {"code": ErrorCode.INVALID_ARGS.value, "message": "Symbol name is required"}, "isError": True},
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Search in symbol_relations table
|
|
19
|
+
sql = """
|
|
20
|
+
SELECT from_path, from_symbol, line, rel_type
|
|
21
|
+
FROM symbol_relations
|
|
22
|
+
WHERE to_symbol = ?
|
|
23
|
+
ORDER BY from_path, line
|
|
24
|
+
"""
|
|
25
|
+
params = [target_symbol]
|
|
26
|
+
root_ids = resolve_root_ids(roots)
|
|
27
|
+
if root_ids:
|
|
28
|
+
root_clause = " OR ".join(["from_path LIKE ?"] * len(root_ids))
|
|
29
|
+
sql = sql.replace("ORDER BY", f"AND ({root_clause}) ORDER BY")
|
|
30
|
+
params.extend([f"{rid}/%" for rid in root_ids])
|
|
31
|
+
|
|
32
|
+
with db._read_lock:
|
|
33
|
+
rows = db._read.execute(sql, params).fetchall()
|
|
34
|
+
|
|
35
|
+
results = []
|
|
36
|
+
for r in rows:
|
|
37
|
+
results.append({
|
|
38
|
+
"caller_path": r["from_path"],
|
|
39
|
+
"caller_symbol": r["from_symbol"],
|
|
40
|
+
"line": r["line"],
|
|
41
|
+
"rel_type": r["rel_type"]
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
def build_pack() -> str:
|
|
45
|
+
lines = [pack_header("get_callers", {"name": pack_encode_text(target_symbol)}, returned=len(results))]
|
|
46
|
+
for r in results:
|
|
47
|
+
kv = {
|
|
48
|
+
"caller_path": pack_encode_id(r["caller_path"]),
|
|
49
|
+
"caller_symbol": pack_encode_id(r["caller_symbol"]),
|
|
50
|
+
"line": str(r["line"]),
|
|
51
|
+
"rel_type": pack_encode_id(r["rel_type"]),
|
|
52
|
+
}
|
|
53
|
+
lines.append(pack_line("r", kv))
|
|
54
|
+
return "\n".join(lines)
|
|
55
|
+
|
|
56
|
+
return mcp_response(
|
|
57
|
+
"get_callers",
|
|
58
|
+
build_pack,
|
|
59
|
+
lambda: {"target": target_symbol, "results": results, "count": len(results)},
|
|
60
|
+
)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, Dict, List
|
|
3
|
+
try:
|
|
4
|
+
from ._util import mcp_response, pack_header, pack_line, pack_encode_id, pack_encode_text, resolve_root_ids, pack_error, ErrorCode
|
|
5
|
+
except ImportError:
|
|
6
|
+
from _util import mcp_response, pack_header, pack_line, pack_encode_id, pack_encode_text, resolve_root_ids, pack_error, ErrorCode
|
|
7
|
+
|
|
8
|
+
def execute_get_implementations(args: Dict[str, Any], db: Any, roots: List[str]) -> Dict[str, Any]:
|
|
9
|
+
"""Find symbols that implement or extend a specific symbol."""
|
|
10
|
+
target_symbol = args.get("name", "").strip()
|
|
11
|
+
if not target_symbol:
|
|
12
|
+
return mcp_response(
|
|
13
|
+
"get_implementations",
|
|
14
|
+
lambda: pack_error("get_implementations", ErrorCode.INVALID_ARGS, "Symbol name is required"),
|
|
15
|
+
lambda: {"error": {"code": ErrorCode.INVALID_ARGS.value, "message": "Symbol name is required"}, "isError": True},
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Search in symbol_relations table for implements and extends relations
|
|
19
|
+
sql = """
|
|
20
|
+
SELECT from_path, from_symbol, rel_type, line
|
|
21
|
+
FROM symbol_relations
|
|
22
|
+
WHERE to_symbol = ? AND (rel_type = 'implements' OR rel_type = 'extends')
|
|
23
|
+
ORDER BY from_path, line
|
|
24
|
+
"""
|
|
25
|
+
params = [target_symbol]
|
|
26
|
+
root_ids = resolve_root_ids(roots)
|
|
27
|
+
if root_ids:
|
|
28
|
+
root_clause = " OR ".join(["from_path LIKE ?"] * len(root_ids))
|
|
29
|
+
sql = sql.replace("ORDER BY", f"AND ({root_clause}) ORDER BY")
|
|
30
|
+
params.extend([f"{rid}/%" for rid in root_ids])
|
|
31
|
+
|
|
32
|
+
with db._read_lock:
|
|
33
|
+
rows = db._read.execute(sql, params).fetchall()
|
|
34
|
+
|
|
35
|
+
results = []
|
|
36
|
+
for r in rows:
|
|
37
|
+
results.append({
|
|
38
|
+
"implementer_path": r["from_path"],
|
|
39
|
+
"implementer_symbol": r["from_symbol"],
|
|
40
|
+
"rel_type": r["rel_type"],
|
|
41
|
+
"line": r["line"]
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
def build_pack() -> str:
|
|
45
|
+
lines = [pack_header("get_implementations", {"name": pack_encode_text(target_symbol)}, returned=len(results))]
|
|
46
|
+
for r in results:
|
|
47
|
+
kv = {
|
|
48
|
+
"implementer_path": pack_encode_id(r["implementer_path"]),
|
|
49
|
+
"implementer_symbol": pack_encode_id(r["implementer_symbol"]),
|
|
50
|
+
"rel_type": pack_encode_id(r["rel_type"]),
|
|
51
|
+
"line": str(r["line"]),
|
|
52
|
+
}
|
|
53
|
+
lines.append(pack_line("r", kv))
|
|
54
|
+
return "\n".join(lines)
|
|
55
|
+
|
|
56
|
+
return mcp_response(
|
|
57
|
+
"get_implementations",
|
|
58
|
+
build_pack,
|
|
59
|
+
lambda: {"target": target_symbol, "results": results, "count": len(results)},
|
|
60
|
+
)
|
mcp/tools/index_file.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Any, Dict, List
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from app.queue_pipeline import FsEvent, FsEventKind
|
|
6
|
+
except Exception:
|
|
7
|
+
FsEvent = None
|
|
8
|
+
FsEventKind = None
|
|
9
|
+
|
|
10
|
+
from mcp.tools._util import mcp_response, pack_error, ErrorCode, resolve_db_path, pack_header, pack_line, pack_encode_id
|
|
11
|
+
|
|
12
|
+
def execute_index_file(args: Dict[str, Any], indexer: Any, roots: List[str]) -> Dict[str, Any]:
|
|
13
|
+
"""Force immediate re-indexing of a specific file."""
|
|
14
|
+
path = args.get("path", "").strip()
|
|
15
|
+
if not path:
|
|
16
|
+
return mcp_response(
|
|
17
|
+
"index_file",
|
|
18
|
+
lambda: pack_error("index_file", ErrorCode.INVALID_ARGS, "File path is required"),
|
|
19
|
+
lambda: {"error": {"code": ErrorCode.INVALID_ARGS.value, "message": "File path is required"}, "isError": True},
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if not indexer:
|
|
23
|
+
return mcp_response(
|
|
24
|
+
"index_file",
|
|
25
|
+
lambda: pack_error("index_file", ErrorCode.INTERNAL, "Indexer not available"),
|
|
26
|
+
lambda: {"error": {"code": ErrorCode.INTERNAL.value, "message": "Indexer not available"}, "isError": True},
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if not getattr(indexer, "indexing_enabled", True):
|
|
30
|
+
mode = getattr(indexer, "indexer_mode", "off")
|
|
31
|
+
code = ErrorCode.ERR_INDEXER_DISABLED if mode == "off" else ErrorCode.ERR_INDEXER_FOLLOWER
|
|
32
|
+
return mcp_response(
|
|
33
|
+
"index_file",
|
|
34
|
+
lambda: pack_error("index_file", code, "Indexer is not available in follower/off mode", fields={"mode": mode}),
|
|
35
|
+
lambda: {"error": {"code": code.value, "message": "Indexer is not available in follower/off mode", "data": {"mode": mode}}, "isError": True},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
db_path = resolve_db_path(path, roots)
|
|
39
|
+
if not db_path:
|
|
40
|
+
return mcp_response(
|
|
41
|
+
"index_file",
|
|
42
|
+
lambda: pack_error("index_file", ErrorCode.ERR_ROOT_OUT_OF_SCOPE, f"Path out of scope: {path}", hints=["outside final_roots"]),
|
|
43
|
+
lambda: {"error": {"code": ErrorCode.ERR_ROOT_OUT_OF_SCOPE.value, "message": f"Path out of scope: {path}"}, "isError": True},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
fs_path = path
|
|
48
|
+
if hasattr(indexer, "_decode_db_path"):
|
|
49
|
+
decoded = indexer._decode_db_path(db_path) # type: ignore[attr-defined]
|
|
50
|
+
if decoded:
|
|
51
|
+
_, fs_path = decoded
|
|
52
|
+
fs_path = str(fs_path)
|
|
53
|
+
# Trigger watcher event logic which handles upsert/delete
|
|
54
|
+
if FsEvent and FsEventKind:
|
|
55
|
+
evt = FsEvent(kind=FsEventKind.MODIFIED, path=fs_path, dest_path=None, ts=time.time())
|
|
56
|
+
indexer._process_watcher_event(evt)
|
|
57
|
+
else:
|
|
58
|
+
indexer._process_watcher_event(fs_path)
|
|
59
|
+
|
|
60
|
+
def build_pack() -> str:
|
|
61
|
+
lines = [pack_header("index_file", {}, returned=1)]
|
|
62
|
+
lines.append(pack_line("m", {"path": pack_encode_id(db_path), "requested": "true"}))
|
|
63
|
+
return "\n".join(lines)
|
|
64
|
+
|
|
65
|
+
return mcp_response(
|
|
66
|
+
"index_file",
|
|
67
|
+
build_pack,
|
|
68
|
+
lambda: {"success": True, "path": db_path, "message": f"Successfully requested re-indexing for {db_path}"},
|
|
69
|
+
)
|
|
70
|
+
except Exception as e:
|
|
71
|
+
return mcp_response(
|
|
72
|
+
"index_file",
|
|
73
|
+
lambda: pack_error("index_file", ErrorCode.INTERNAL, str(e)),
|
|
74
|
+
lambda: {"error": {"code": ErrorCode.INTERNAL.value, "message": str(e)}, "isError": True},
|
|
75
|
+
)
|
mcp/tools/list_files.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
List files tool for Local Search MCP Server.
|
|
4
|
+
"""
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Dict, List
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from app.db import LocalSearchDB
|
|
10
|
+
from mcp.telemetry import TelemetryLogger
|
|
11
|
+
from mcp.tools._util import mcp_response, pack_header, pack_line, pack_truncated, pack_encode_id, resolve_root_ids
|
|
12
|
+
except ImportError:
|
|
13
|
+
# Fallback for direct script execution
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
17
|
+
from app.db import LocalSearchDB
|
|
18
|
+
from mcp.telemetry import TelemetryLogger
|
|
19
|
+
from mcp.tools._util import mcp_response, pack_header, pack_line, pack_truncated, pack_encode_id, resolve_root_ids
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def execute_list_files(args: Dict[str, Any], db: LocalSearchDB, logger: TelemetryLogger, roots: List[str]) -> Dict[str, Any]:
|
|
23
|
+
"""Execute list_files tool."""
|
|
24
|
+
start_ts = time.time()
|
|
25
|
+
root_ids = resolve_root_ids(roots)
|
|
26
|
+
|
|
27
|
+
# Parse args
|
|
28
|
+
repo = args.get("repo")
|
|
29
|
+
path_pattern = args.get("path_pattern")
|
|
30
|
+
file_types = args.get("file_types")
|
|
31
|
+
include_hidden = bool(args.get("include_hidden", False))
|
|
32
|
+
try:
|
|
33
|
+
offset = int(args.get("offset", 0))
|
|
34
|
+
except (ValueError, TypeError):
|
|
35
|
+
offset = 0
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
limit_arg = int(args.get("limit", 100))
|
|
39
|
+
except (ValueError, TypeError):
|
|
40
|
+
limit_arg = 100
|
|
41
|
+
|
|
42
|
+
# --- JSON Builder (Legacy) ---
|
|
43
|
+
def build_json() -> Dict[str, Any]:
|
|
44
|
+
summary_only = bool(args.get("summary", False)) or (not repo and not path_pattern and not file_types)
|
|
45
|
+
|
|
46
|
+
if summary_only:
|
|
47
|
+
repo_stats = db.get_repo_stats(root_ids=root_ids)
|
|
48
|
+
repos = [{"repo": k, "file_count": v} for k, v in repo_stats.items()]
|
|
49
|
+
repos.sort(key=lambda r: r["file_count"], reverse=True)
|
|
50
|
+
total = sum(repo_stats.values())
|
|
51
|
+
return {
|
|
52
|
+
"files": [],
|
|
53
|
+
"meta": {
|
|
54
|
+
"total": total,
|
|
55
|
+
"returned": 0,
|
|
56
|
+
"offset": 0,
|
|
57
|
+
"limit": 0,
|
|
58
|
+
"repos": repos,
|
|
59
|
+
"include_hidden": include_hidden,
|
|
60
|
+
"mode": "summary",
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
else:
|
|
64
|
+
files, meta = db.list_files(
|
|
65
|
+
repo=repo,
|
|
66
|
+
path_pattern=path_pattern,
|
|
67
|
+
file_types=file_types,
|
|
68
|
+
include_hidden=include_hidden,
|
|
69
|
+
limit=limit_arg,
|
|
70
|
+
offset=offset,
|
|
71
|
+
root_ids=root_ids,
|
|
72
|
+
)
|
|
73
|
+
return {
|
|
74
|
+
"files": files,
|
|
75
|
+
"meta": meta,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# --- PACK1 Builder ---
|
|
79
|
+
def build_pack() -> str:
|
|
80
|
+
# Hard limit for PACK1: 200
|
|
81
|
+
pack_limit = min(limit_arg, 200)
|
|
82
|
+
|
|
83
|
+
files, meta = db.list_files(
|
|
84
|
+
repo=repo,
|
|
85
|
+
path_pattern=path_pattern,
|
|
86
|
+
file_types=file_types,
|
|
87
|
+
include_hidden=include_hidden,
|
|
88
|
+
limit=pack_limit,
|
|
89
|
+
offset=offset,
|
|
90
|
+
root_ids=root_ids,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
total = meta.get("total", 0)
|
|
94
|
+
returned = len(files)
|
|
95
|
+
total_mode = "exact" # list_files usually returns exact counts via DB
|
|
96
|
+
|
|
97
|
+
# Header
|
|
98
|
+
kv = {
|
|
99
|
+
"offset": offset,
|
|
100
|
+
"limit": pack_limit
|
|
101
|
+
}
|
|
102
|
+
lines = [
|
|
103
|
+
pack_header("list_files", kv, returned=returned, total=total, total_mode=total_mode)
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
# Records
|
|
107
|
+
for f in files:
|
|
108
|
+
# p:<path> (ENC_ID)
|
|
109
|
+
path_enc = pack_encode_id(f["path"])
|
|
110
|
+
lines.append(pack_line("p", single_value=path_enc))
|
|
111
|
+
|
|
112
|
+
# Truncation
|
|
113
|
+
is_truncated = (offset + returned) < total
|
|
114
|
+
if is_truncated:
|
|
115
|
+
next_offset = offset + returned
|
|
116
|
+
lines.append(pack_truncated(next_offset, pack_limit, "true"))
|
|
117
|
+
|
|
118
|
+
return "\n".join(lines)
|
|
119
|
+
|
|
120
|
+
# Execute and Telemetry
|
|
121
|
+
response = mcp_response("list_files", build_pack, build_json)
|
|
122
|
+
|
|
123
|
+
# Telemetry logging
|
|
124
|
+
latency_ms = int((time.time() - start_ts) * 1000)
|
|
125
|
+
# Estimate payload size (rough)
|
|
126
|
+
payload_text = response["content"][0]["text"]
|
|
127
|
+
payload_bytes = len(payload_text.encode('utf-8'))
|
|
128
|
+
|
|
129
|
+
# Log simplified telemetry
|
|
130
|
+
repo_val = repo or "all"
|
|
131
|
+
item_count = payload_text.count('\n') if "PACK1" in payload_text else 0 # Approximation for PACK
|
|
132
|
+
if "PACK1" not in payload_text:
|
|
133
|
+
# Rough count for JSON without parsing
|
|
134
|
+
item_count = payload_text.count('"path":')
|
|
135
|
+
|
|
136
|
+
logger.log_telemetry(f"tool=list_files repo='{repo_val}' items={item_count} payload_bytes={payload_bytes} latency={latency_ms}ms")
|
|
137
|
+
|
|
138
|
+
return response
|
mcp/tools/read_file.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from typing import Any, Dict, List
|
|
2
|
+
from app.db import LocalSearchDB
|
|
3
|
+
from mcp.tools._util import mcp_response, pack_error, ErrorCode, resolve_db_path, pack_header, pack_line, pack_encode_text
|
|
4
|
+
|
|
5
|
+
def execute_read_file(args: Dict[str, Any], db: LocalSearchDB, roots: List[str]) -> Dict[str, Any]:
|
|
6
|
+
"""
|
|
7
|
+
Execute read_file tool.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
args: {"path": str}
|
|
11
|
+
db: LocalSearchDB instance
|
|
12
|
+
"""
|
|
13
|
+
path = args.get("path")
|
|
14
|
+
if not path:
|
|
15
|
+
return mcp_response(
|
|
16
|
+
"read_file",
|
|
17
|
+
lambda: pack_error("read_file", ErrorCode.INVALID_ARGS, "'path' is required"),
|
|
18
|
+
lambda: {"error": {"code": ErrorCode.INVALID_ARGS.value, "message": "'path' is required"}, "isError": True},
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
db_path = resolve_db_path(path, roots)
|
|
22
|
+
if not db_path and db.has_legacy_paths():
|
|
23
|
+
db_path = path
|
|
24
|
+
if not db_path:
|
|
25
|
+
return mcp_response(
|
|
26
|
+
"read_file",
|
|
27
|
+
lambda: pack_error("read_file", ErrorCode.ERR_ROOT_OUT_OF_SCOPE, f"Path out of scope: {path}", hints=["outside final_roots"]),
|
|
28
|
+
lambda: {"error": {"code": ErrorCode.ERR_ROOT_OUT_OF_SCOPE.value, "message": f"Path out of scope: {path}"}, "isError": True},
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
content = db.read_file(db_path)
|
|
32
|
+
if content is None:
|
|
33
|
+
return mcp_response(
|
|
34
|
+
"read_file",
|
|
35
|
+
lambda: pack_error("read_file", ErrorCode.NOT_INDEXED, f"File not found or not indexed: {db_path}"),
|
|
36
|
+
lambda: {"error": {"code": ErrorCode.NOT_INDEXED.value, "message": f"File not found or not indexed: {db_path}"}, "isError": True},
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def build_pack() -> str:
|
|
40
|
+
lines = [pack_header("read_file", {}, returned=1)]
|
|
41
|
+
lines.append(pack_line("t", single_value=pack_encode_text(content)))
|
|
42
|
+
return "\n".join(lines)
|
|
43
|
+
|
|
44
|
+
return mcp_response(
|
|
45
|
+
"read_file",
|
|
46
|
+
build_pack,
|
|
47
|
+
lambda: {"content": [{"type": "text", "text": content}]},
|
|
48
|
+
)
|
mcp/tools/read_symbol.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Read Symbol Tool for Local Search MCP Server.
|
|
4
|
+
Reads only the specific code block (function/class) of a symbol.
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any, Dict, List
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from app.db import LocalSearchDB
|
|
12
|
+
from mcp.telemetry import TelemetryLogger
|
|
13
|
+
from mcp.tools._util import mcp_response, pack_error, ErrorCode, resolve_db_path, pack_header, pack_line, pack_encode_text
|
|
14
|
+
except ImportError:
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
18
|
+
from app.db import LocalSearchDB
|
|
19
|
+
from mcp.telemetry import TelemetryLogger
|
|
20
|
+
from mcp.tools._util import mcp_response, pack_error, ErrorCode, resolve_db_path, pack_header, pack_line, pack_encode_text
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def execute_read_symbol(args: Dict[str, Any], db: LocalSearchDB, logger: TelemetryLogger, roots: List[str]) -> Dict[str, Any]:
|
|
24
|
+
"""Execute read_symbol tool (v2.7.0)."""
|
|
25
|
+
start_ts = time.time()
|
|
26
|
+
|
|
27
|
+
path = args.get("path")
|
|
28
|
+
symbol_name = args.get("name")
|
|
29
|
+
|
|
30
|
+
if not path or not symbol_name:
|
|
31
|
+
return mcp_response(
|
|
32
|
+
"read_symbol",
|
|
33
|
+
lambda: pack_error("read_symbol", ErrorCode.INVALID_ARGS, "'path' and 'name' are required."),
|
|
34
|
+
lambda: {"error": {"code": ErrorCode.INVALID_ARGS.value, "message": "'path' and 'name' are required."}, "isError": True},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
db_path = resolve_db_path(path, roots)
|
|
38
|
+
if not db_path and db.has_legacy_paths():
|
|
39
|
+
db_path = path
|
|
40
|
+
if not db_path:
|
|
41
|
+
return mcp_response(
|
|
42
|
+
"read_symbol",
|
|
43
|
+
lambda: pack_error("read_symbol", ErrorCode.ERR_ROOT_OUT_OF_SCOPE, f"Path out of scope: {path}", hints=["outside final_roots"]),
|
|
44
|
+
lambda: {"error": {"code": ErrorCode.ERR_ROOT_OUT_OF_SCOPE.value, "message": f"Path out of scope: {path}"}, "isError": True},
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
block = db.get_symbol_block(db_path, symbol_name)
|
|
48
|
+
|
|
49
|
+
latency_ms = int((time.time() - start_ts) * 1000)
|
|
50
|
+
logger.log_telemetry(f"tool=read_symbol path='{path}' name='{symbol_name}' found={bool(block)} latency={latency_ms}ms")
|
|
51
|
+
|
|
52
|
+
if not block:
|
|
53
|
+
return mcp_response(
|
|
54
|
+
"read_symbol",
|
|
55
|
+
lambda: pack_error("read_symbol", ErrorCode.NOT_INDEXED, f"Symbol '{symbol_name}' not found in '{db_path}' (or no block range available)."),
|
|
56
|
+
lambda: {"error": {"code": ErrorCode.NOT_INDEXED.value, "message": f"Symbol '{symbol_name}' not found in '{db_path}' (or no block range available)."}, "isError": True},
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Format output
|
|
60
|
+
doc = block.get('docstring', '')
|
|
61
|
+
meta = block.get('metadata', '{}')
|
|
62
|
+
|
|
63
|
+
header = [
|
|
64
|
+
f"File: {db_path}",
|
|
65
|
+
f"Symbol: {block['name']}",
|
|
66
|
+
f"Range: L{block['start_line']} - L{block['end_line']}"
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
m = json.loads(meta)
|
|
71
|
+
if m.get("annotations"):
|
|
72
|
+
header.append(f"Annotations: {', '.join(m['annotations'])}")
|
|
73
|
+
if m.get("http_path"):
|
|
74
|
+
header.append(f"API Endpoint: {m['http_path']}")
|
|
75
|
+
except: pass
|
|
76
|
+
|
|
77
|
+
output_lines = [
|
|
78
|
+
"\n".join(header),
|
|
79
|
+
"--------------------------------------------------"
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
if doc:
|
|
83
|
+
output_lines.append(f"/* DOCSTRING */\n{doc}\n")
|
|
84
|
+
|
|
85
|
+
output_lines.append(block['content'])
|
|
86
|
+
output_lines.append("--------------------------------------------------")
|
|
87
|
+
|
|
88
|
+
output = "\n".join(output_lines)
|
|
89
|
+
|
|
90
|
+
def build_pack() -> str:
|
|
91
|
+
lines = [pack_header("read_symbol", {}, returned=1)]
|
|
92
|
+
lines.append(pack_line("t", single_value=pack_encode_text(output)))
|
|
93
|
+
return "\n".join(lines)
|
|
94
|
+
|
|
95
|
+
return mcp_response(
|
|
96
|
+
"read_symbol",
|
|
97
|
+
build_pack,
|
|
98
|
+
lambda: {"content": [{"type": "text", "text": output}]},
|
|
99
|
+
)
|