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
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sqlite3
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
try:
|
|
5
|
+
from ._util import mcp_response, pack_header, pack_line, pack_encode_id, pack_encode_text, resolve_root_ids, pack_error, ErrorCode
|
|
6
|
+
except ImportError:
|
|
7
|
+
from _util import mcp_response, pack_header, pack_line, pack_encode_id, pack_encode_text, resolve_root_ids, pack_error, ErrorCode
|
|
8
|
+
|
|
9
|
+
def execute_search_api_endpoints(args: Dict[str, Any], db: Any, roots: List[str]) -> Dict[str, Any]:
|
|
10
|
+
"""Search for API endpoints by URL path pattern."""
|
|
11
|
+
path_query = args.get("path", "").strip()
|
|
12
|
+
if not path_query:
|
|
13
|
+
return mcp_response(
|
|
14
|
+
"search_api_endpoints",
|
|
15
|
+
lambda: pack_error("search_api_endpoints", ErrorCode.INVALID_ARGS, "Path query is required"),
|
|
16
|
+
lambda: {"error": {"code": ErrorCode.INVALID_ARGS.value, "message": "Path query is required"}, "isError": True},
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Search in symbols where metadata contains the path
|
|
20
|
+
# SQLite JSON support is limited in older versions, so we use LIKE on metadata TEXT
|
|
21
|
+
sql = """
|
|
22
|
+
SELECT path, name, kind, line, metadata, content
|
|
23
|
+
FROM symbols
|
|
24
|
+
WHERE metadata LIKE ? AND (kind = 'method' OR kind = 'function' OR kind = 'class')
|
|
25
|
+
"""
|
|
26
|
+
# Look for partial matches in metadata (looser LIKE, filter in Python)
|
|
27
|
+
params = [f'%{path_query}%']
|
|
28
|
+
root_ids = resolve_root_ids(roots)
|
|
29
|
+
if root_ids:
|
|
30
|
+
root_clause = " OR ".join(["path LIKE ?"] * len(root_ids))
|
|
31
|
+
sql += f" AND ({root_clause})"
|
|
32
|
+
params.extend([f"{rid}/%" for rid in root_ids])
|
|
33
|
+
|
|
34
|
+
with db._read_lock:
|
|
35
|
+
rows = db._read.execute(sql, params).fetchall()
|
|
36
|
+
|
|
37
|
+
results = []
|
|
38
|
+
for r in rows:
|
|
39
|
+
try:
|
|
40
|
+
meta = json.loads(r["metadata"])
|
|
41
|
+
http_path = meta.get("http_path", "")
|
|
42
|
+
if path_query in http_path or path_query == http_path:
|
|
43
|
+
results.append({
|
|
44
|
+
"path": r["path"],
|
|
45
|
+
"name": r["name"],
|
|
46
|
+
"kind": r["kind"],
|
|
47
|
+
"line": r["line"],
|
|
48
|
+
"http_path": http_path,
|
|
49
|
+
"annotations": meta.get("annotations", []),
|
|
50
|
+
"snippet": r["content"]
|
|
51
|
+
})
|
|
52
|
+
except:
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
def build_pack() -> str:
|
|
56
|
+
lines = [pack_header("search_api_endpoints", {"q": pack_encode_text(path_query)}, returned=len(results))]
|
|
57
|
+
for r in results:
|
|
58
|
+
kv = {
|
|
59
|
+
"path": pack_encode_id(r["path"]),
|
|
60
|
+
"name": pack_encode_id(r["name"]),
|
|
61
|
+
"kind": pack_encode_id(r["kind"]),
|
|
62
|
+
"line": str(r["line"]),
|
|
63
|
+
"http_path": pack_encode_text(r["http_path"]),
|
|
64
|
+
}
|
|
65
|
+
lines.append(pack_line("r", kv))
|
|
66
|
+
return "\n".join(lines)
|
|
67
|
+
|
|
68
|
+
return mcp_response(
|
|
69
|
+
"search_api_endpoints",
|
|
70
|
+
build_pack,
|
|
71
|
+
lambda: {"query": path_query, "results": results, "count": len(results)},
|
|
72
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
from app.db import LocalSearchDB
|
|
3
|
+
from mcp.tools._util import mcp_response, pack_header, pack_line, pack_truncated, pack_encode_id, pack_encode_text, resolve_root_ids
|
|
4
|
+
|
|
5
|
+
def execute_search_symbols(args: Dict[str, Any], db: LocalSearchDB, roots: Optional[List[str]] = None) -> Dict[str, Any]:
|
|
6
|
+
"""
|
|
7
|
+
Execute search_symbols tool.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
args: {"query": str, "limit": int}
|
|
11
|
+
db: LocalSearchDB instance
|
|
12
|
+
"""
|
|
13
|
+
query = args.get("query", "")
|
|
14
|
+
limit_arg = int(args.get("limit", 20))
|
|
15
|
+
root_ids = resolve_root_ids(list(roots or []))
|
|
16
|
+
|
|
17
|
+
# --- JSON Builder (Legacy/Debug) ---
|
|
18
|
+
def build_json() -> Dict[str, Any]:
|
|
19
|
+
results = db.search_symbols(query, limit=limit_arg, root_ids=root_ids)
|
|
20
|
+
return {
|
|
21
|
+
"query": query,
|
|
22
|
+
"count": len(results),
|
|
23
|
+
"symbols": results
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# --- PACK1 Builder ---
|
|
27
|
+
def build_pack() -> str:
|
|
28
|
+
# Hard limit for PACK1: 50
|
|
29
|
+
pack_limit = min(limit_arg, 50)
|
|
30
|
+
|
|
31
|
+
results = db.search_symbols(query, limit=pack_limit, root_ids=root_ids)
|
|
32
|
+
returned = len(results)
|
|
33
|
+
|
|
34
|
+
# Header
|
|
35
|
+
# Note: search_symbols DB query typically doesn't return total count currently
|
|
36
|
+
kv = {"q": pack_encode_text(query), "limit": pack_limit}
|
|
37
|
+
lines = [
|
|
38
|
+
pack_header("search_symbols", kv, returned=returned, total_mode="none")
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
# Records
|
|
42
|
+
for r in results:
|
|
43
|
+
# h:repo=<repo> path=<path> line=<line> kind=<kind> name=<name>
|
|
44
|
+
# repo, path, name, kind => ENC_ID (identifiers)
|
|
45
|
+
kv_line = {
|
|
46
|
+
"repo": pack_encode_id(r["repo"]),
|
|
47
|
+
"path": pack_encode_id(r["path"]),
|
|
48
|
+
"line": str(r["line"]),
|
|
49
|
+
"kind": pack_encode_id(r["kind"]),
|
|
50
|
+
"name": pack_encode_id(r["name"])
|
|
51
|
+
}
|
|
52
|
+
lines.append(pack_line("h", kv_line))
|
|
53
|
+
|
|
54
|
+
# Truncation
|
|
55
|
+
# Since we don't know total, if we hit the limit, we say truncated=maybe
|
|
56
|
+
if returned >= pack_limit:
|
|
57
|
+
# next offset is unknown/not supported by simple symbol search usually,
|
|
58
|
+
# but we follow the format. offset=returned is best guess if paginated.
|
|
59
|
+
lines.append(pack_truncated(returned, pack_limit, "maybe"))
|
|
60
|
+
|
|
61
|
+
return "\n".join(lines)
|
|
62
|
+
|
|
63
|
+
return mcp_response("search_symbols", build_pack, build_json)
|
mcp/tools/status.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Status tool for Local Search MCP Server.
|
|
4
|
+
"""
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
from mcp.tools._util import mcp_response, pack_header, pack_line, resolve_root_ids
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from app.db import LocalSearchDB
|
|
10
|
+
from app.indexer import Indexer
|
|
11
|
+
from app.config import Config
|
|
12
|
+
from app.registry import ServerRegistry
|
|
13
|
+
from mcp.telemetry import TelemetryLogger
|
|
14
|
+
except ImportError:
|
|
15
|
+
# Fallback for direct script execution
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
19
|
+
from app.db import LocalSearchDB
|
|
20
|
+
from app.indexer import Indexer
|
|
21
|
+
from app.config import Config
|
|
22
|
+
from app.registry import ServerRegistry
|
|
23
|
+
from mcp.telemetry import TelemetryLogger
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def execute_status(args: Dict[str, Any], indexer: Optional[Indexer], db: Optional[LocalSearchDB], cfg: Optional[Config], workspace_root: str, server_version: str, logger: Optional[TelemetryLogger] = None) -> Dict[str, Any]:
|
|
27
|
+
"""Execute status tool."""
|
|
28
|
+
details = bool(args.get("details", False))
|
|
29
|
+
|
|
30
|
+
# 1. Gather status data
|
|
31
|
+
actual_http_port = None
|
|
32
|
+
try:
|
|
33
|
+
inst = ServerRegistry().get_instance(workspace_root)
|
|
34
|
+
if inst and inst.get("port"):
|
|
35
|
+
actual_http_port = int(inst.get("port"))
|
|
36
|
+
except Exception:
|
|
37
|
+
actual_http_port = None
|
|
38
|
+
|
|
39
|
+
config_http_port = cfg.http_api_port if cfg else 0
|
|
40
|
+
status_data = {
|
|
41
|
+
"index_ready": indexer.status.index_ready if indexer else False,
|
|
42
|
+
"last_scan_ts": indexer.status.last_scan_ts if indexer else 0,
|
|
43
|
+
"last_commit_ts": indexer.get_last_commit_ts() if indexer and hasattr(indexer, "get_last_commit_ts") else 0,
|
|
44
|
+
"scanned_files": indexer.status.scanned_files if indexer else 0,
|
|
45
|
+
"indexed_files": indexer.status.indexed_files if indexer else 0,
|
|
46
|
+
"errors": indexer.status.errors if indexer else 0,
|
|
47
|
+
"fts_enabled": db.fts_enabled if db else False,
|
|
48
|
+
"workspace_root": workspace_root,
|
|
49
|
+
"server_version": server_version,
|
|
50
|
+
"http_api_port": actual_http_port if actual_http_port is not None else config_http_port,
|
|
51
|
+
"http_api_port_config": config_http_port,
|
|
52
|
+
"indexer_mode": getattr(indexer, "indexer_mode", "auto") if indexer else "off",
|
|
53
|
+
}
|
|
54
|
+
if db and hasattr(db, "engine") and hasattr(db.engine, "status"):
|
|
55
|
+
try:
|
|
56
|
+
st = db.engine.status()
|
|
57
|
+
status_data.update({
|
|
58
|
+
"engine_mode": st.engine_mode,
|
|
59
|
+
"engine_ready": st.engine_ready,
|
|
60
|
+
"engine_version": st.engine_version,
|
|
61
|
+
"index_docs": st.doc_count,
|
|
62
|
+
"index_size_bytes": st.index_size_bytes,
|
|
63
|
+
"last_build_ts": st.last_build_ts,
|
|
64
|
+
"engine_reason": st.reason,
|
|
65
|
+
"engine_hint": st.hint,
|
|
66
|
+
"engine_mem_mb": getattr(st, "engine_mem_mb", 0),
|
|
67
|
+
"index_mem_mb": getattr(st, "index_mem_mb", 0),
|
|
68
|
+
"engine_threads": getattr(st, "engine_threads", 0),
|
|
69
|
+
})
|
|
70
|
+
except Exception:
|
|
71
|
+
status_data.update({
|
|
72
|
+
"engine_mode": "embedded",
|
|
73
|
+
"engine_ready": False,
|
|
74
|
+
})
|
|
75
|
+
if indexer and hasattr(indexer, "get_queue_depths"):
|
|
76
|
+
status_data["queue_depths"] = indexer.get_queue_depths()
|
|
77
|
+
|
|
78
|
+
if cfg:
|
|
79
|
+
status_data["config"] = {
|
|
80
|
+
"include_ext": cfg.include_ext,
|
|
81
|
+
"exclude_dirs": cfg.exclude_dirs,
|
|
82
|
+
"exclude_globs": getattr(cfg, "exclude_globs", []),
|
|
83
|
+
"max_file_bytes": cfg.max_file_bytes,
|
|
84
|
+
"http_api_port": cfg.http_api_port,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
repo_stats = None
|
|
88
|
+
if details and db:
|
|
89
|
+
root_ids = resolve_root_ids(cfg.workspace_roots if cfg else [])
|
|
90
|
+
repo_stats = db.get_repo_stats(root_ids=root_ids)
|
|
91
|
+
status_data["repo_stats"] = repo_stats
|
|
92
|
+
|
|
93
|
+
if logger:
|
|
94
|
+
logger.log_telemetry(f"tool=status details={details} scanned={status_data['scanned_files']} indexed={status_data['indexed_files']}")
|
|
95
|
+
|
|
96
|
+
# --- JSON Builder ---
|
|
97
|
+
def build_json() -> Dict[str, Any]:
|
|
98
|
+
return status_data
|
|
99
|
+
|
|
100
|
+
# --- PACK1 Builder ---
|
|
101
|
+
def build_pack() -> str:
|
|
102
|
+
metrics = []
|
|
103
|
+
|
|
104
|
+
# Base status
|
|
105
|
+
for k, v in status_data.items():
|
|
106
|
+
if k in {"config", "repo_stats", "queue_depths"}:
|
|
107
|
+
continue
|
|
108
|
+
val = str(v).lower() if isinstance(v, bool) else str(v)
|
|
109
|
+
metrics.append((k, val))
|
|
110
|
+
|
|
111
|
+
# Config (if exists)
|
|
112
|
+
if "config" in status_data:
|
|
113
|
+
c = status_data["config"]
|
|
114
|
+
metrics.append(("cfg_include_ext", ",".join(c.get("include_ext", []))))
|
|
115
|
+
metrics.append(("cfg_max_file_bytes", str(c.get("max_file_bytes", 0))))
|
|
116
|
+
|
|
117
|
+
if "queue_depths" in status_data:
|
|
118
|
+
q = status_data["queue_depths"]
|
|
119
|
+
metrics.append(("queue_watcher", str(q.get("watcher", 0))))
|
|
120
|
+
metrics.append(("queue_db_writer", str(q.get("db_writer", 0))))
|
|
121
|
+
metrics.append(("queue_telemetry", str(q.get("telemetry", 0))))
|
|
122
|
+
|
|
123
|
+
# Repo stats (if exists)
|
|
124
|
+
if repo_stats:
|
|
125
|
+
for r_name, r_count in repo_stats.items():
|
|
126
|
+
metrics.append((f"repo_{r_name}", str(r_count)))
|
|
127
|
+
|
|
128
|
+
# Build lines
|
|
129
|
+
lines = [pack_header("status", {}, returned=len(metrics))]
|
|
130
|
+
for k, v in metrics:
|
|
131
|
+
lines.append(pack_line("m", kv={k: v}))
|
|
132
|
+
|
|
133
|
+
return "\n".join(lines)
|
|
134
|
+
|
|
135
|
+
return mcp_response("status", build_pack, build_json)
|
sari/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from deckard.version import __version__
|
sari/__main__.py
ADDED