honestcode 0.5.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.
- honestcode/__init__.py +19 -0
- honestcode/cli.py +428 -0
- honestcode/graph/__init__.py +13 -0
- honestcode/graph/graph_store.py +276 -0
- honestcode/graph/watcher.py +116 -0
- honestcode/guards/__init__.py +104 -0
- honestcode/guards/guard.py +777 -0
- honestcode/guards/store.py +457 -0
- honestcode/guards/taxonomy.py +228 -0
- honestcode/honest/__init__.py +19 -0
- honestcode/honest/project_binding.py +200 -0
- honestcode/honest/router.py +205 -0
- honestcode/honest/symbol_index.py +446 -0
- honestcode/integrations/__init__.py +14 -0
- honestcode/integrations/pydantic_ai.py +304 -0
- honestcode/mcp/__init__.py +46 -0
- honestcode/mcp/knowledge_base.py +290 -0
- honestcode/mcp/server.py +357 -0
- honestcode/mcp/tools.py +2002 -0
- honestcode/py.typed +0 -0
- honestcode/sandbox/__init__.py +334 -0
- honestcode/structure/__init__.py +6 -0
- honestcode/structure/extractor.py +353 -0
- honestcode/structure/multi_lang.py +267 -0
- honestcode/structure/relations.py +47 -0
- honestcode/structure/utils.py +262 -0
- honestcode/verify/__init__.py +19 -0
- honestcode/verify/evidence.py +195 -0
- honestcode/verify/surface.py +497 -0
- honestcode/verify/verify.py +1074 -0
- honestcode-0.5.0.dist-info/METADATA +669 -0
- honestcode-0.5.0.dist-info/RECORD +36 -0
- honestcode-0.5.0.dist-info/WHEEL +5 -0
- honestcode-0.5.0.dist-info/entry_points.txt +3 -0
- honestcode-0.5.0.dist-info/licenses/LICENSE +21 -0
- honestcode-0.5.0.dist-info/top_level.txt +1 -0
honestcode/mcp/server.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HonestCode MCP Server.
|
|
3
|
+
|
|
4
|
+
A Model Context Protocol server that exposes hallucination-detection tools
|
|
5
|
+
so any MCP-compatible client (Cursor, Claude Desktop, VS Code, etc.) can
|
|
6
|
+
verify generated code against the real project structure and dependencies.
|
|
7
|
+
|
|
8
|
+
Run with:
|
|
9
|
+
python -m honestcode.mcp.server # stdio (default)
|
|
10
|
+
python -m honestcode.mcp.server --sse # SSE transport
|
|
11
|
+
|
|
12
|
+
Tool visibility is controlled by the ``HONESTCODE_TOOLS`` environment variable.
|
|
13
|
+
By default ``scan_file`` and ``verify_file`` are exposed; set
|
|
14
|
+
``HONESTCODE_TOOLS`` to a comma-separated list of tool names (or ``all``) to
|
|
15
|
+
expose more:
|
|
16
|
+
|
|
17
|
+
export HONESTCODE_TOOLS=index,check_symbol,check_api,validate_types
|
|
18
|
+
export HONESTCODE_TOOLS=all
|
|
19
|
+
|
|
20
|
+
The SSE transport has no authentication and the sandbox is an accident guard,
|
|
21
|
+
not a containment boundary. Binding it to a non-loopback address therefore
|
|
22
|
+
requires an explicit opt-in (``HONESTCODE_ALLOW_NETWORK=1``) and should be
|
|
23
|
+
fronted by a real access-control layer; a remote caller with ``execute_code``
|
|
24
|
+
can run code as the current user.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import argparse
|
|
30
|
+
import logging
|
|
31
|
+
import os
|
|
32
|
+
import sys
|
|
33
|
+
from typing import TYPE_CHECKING
|
|
34
|
+
|
|
35
|
+
from mcp.server.fastmcp import FastMCP
|
|
36
|
+
|
|
37
|
+
from honestcode.mcp import tools as _tools
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from collections.abc import Callable
|
|
41
|
+
|
|
42
|
+
logging.basicConfig(level=os.environ.get("HONESTCODE_LOG_LEVEL", "INFO"))
|
|
43
|
+
logger = logging.getLogger("honestcode.mcp")
|
|
44
|
+
|
|
45
|
+
mcp = FastMCP("honestcode")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _mcp_index_project(root_path: str, force_rebuild: bool = False, watch: bool = False) -> dict:
|
|
49
|
+
"""Build (or reuse) the symbol index for a project directory.
|
|
50
|
+
|
|
51
|
+
Call this once before other tools. Subsequent calls return a cached index
|
|
52
|
+
unless force_rebuild=True.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
root_path: Absolute path to the project root.
|
|
56
|
+
force_rebuild: If True, discard the cache and rebuild the index.
|
|
57
|
+
watch: If True, start a background watcher that re-indexes on changes.
|
|
58
|
+
"""
|
|
59
|
+
logger.info("index_project(%s, force_rebuild=%s, watch=%s)", root_path, force_rebuild, watch)
|
|
60
|
+
return _tools.index_project(root_path, force_rebuild=force_rebuild, watch=watch)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _mcp_load_project_deps(root_path: str) -> dict:
|
|
64
|
+
"""Load dependency APIs declared in requirements.txt / pyproject.toml
|
|
65
|
+
so library calls can be validated.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
root_path: Absolute path to the project root.
|
|
69
|
+
"""
|
|
70
|
+
return _tools.load_project_deps(root_path)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _mcp_check_symbol(symbol_name: str, file_path: str = "") -> dict:
|
|
74
|
+
"""Check whether a symbol is actually defined in the project.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
symbol_name: The identifier to verify (function/class/variable name).
|
|
78
|
+
file_path: Optional file the symbol appears in.
|
|
79
|
+
"""
|
|
80
|
+
return _tools.check_symbol(symbol_name, file_path or None)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _mcp_check_api(api_name: str) -> dict:
|
|
84
|
+
"""Verify a library API call is correct, e.g. pd.read_exel -> read_excel.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
api_name: Fully qualified API name, e.g. "pandas.read_csv".
|
|
88
|
+
"""
|
|
89
|
+
return _tools.check_api(api_name)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _mcp_execute_code(code: str, prelude: str = "", known_names: list[str] | None = None) -> dict:
|
|
93
|
+
"""Execute code in a sandbox and return stdout/stderr plus structured
|
|
94
|
+
error analysis with fix suggestions.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
code: Python source to execute.
|
|
98
|
+
prelude: Optional setup code (imports) run before `code`.
|
|
99
|
+
known_names: Optional list of known symbol names for typo suggestions.
|
|
100
|
+
"""
|
|
101
|
+
return _tools.execute_code(code, prelude, known_names)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _mcp_scan_file(file_path: str) -> dict:
|
|
105
|
+
"""Scan a file for potential hallucinations: undefined symbols, missing
|
|
106
|
+
imports, and incorrect API calls.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
file_path: Absolute path to the Python file.
|
|
110
|
+
"""
|
|
111
|
+
return _tools.scan_file(file_path)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _mcp_verify_file(file_path: str) -> dict:
|
|
115
|
+
"""Verify a file and return structured evidence for the agent.
|
|
116
|
+
|
|
117
|
+
Returns ``status``, ``findings`` with ``evidence``, and a ``text`` summary.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
file_path: Absolute path to the Python file.
|
|
121
|
+
"""
|
|
122
|
+
return _tools.verify_file(file_path)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _mcp_load_package_apis(package_name: str) -> dict:
|
|
126
|
+
"""Load (and cache) API signatures for a specific installed package.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
package_name: Importable package name, e.g. "numpy".
|
|
130
|
+
"""
|
|
131
|
+
return _tools.load_package_apis(package_name)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _mcp_get_project_stats() -> dict:
|
|
135
|
+
"""Return statistics about the currently indexed project."""
|
|
136
|
+
return _tools.get_project_stats()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _mcp_validate_types(code: str) -> dict:
|
|
140
|
+
"""Run a lightweight AST-based structural check on code.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
code: Python source to analyze.
|
|
144
|
+
"""
|
|
145
|
+
return _tools.validate_types(code)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _mcp_find_dead_code(
|
|
149
|
+
entrypoints: list[str] | None = None,
|
|
150
|
+
ignore_patterns: list[str] | None = None,
|
|
151
|
+
include_tests: bool = True,
|
|
152
|
+
) -> dict:
|
|
153
|
+
"""Find symbols that appear to be unused in the indexed project.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
entrypoints: Symbol names to always treat as alive (e.g. ["main", "cli"]).
|
|
157
|
+
ignore_patterns: Regex patterns for files to exclude from dead-code results.
|
|
158
|
+
include_tests: Treat symbols in test files as alive.
|
|
159
|
+
"""
|
|
160
|
+
return _tools.find_dead_code(entrypoints, ignore_patterns, include_tests)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _mcp_find_similar_code(threshold: float = 0.85) -> dict:
|
|
164
|
+
"""Find function-level code clones across the project.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
threshold: Similarity ratio above which two functions are reported (0-1).
|
|
168
|
+
"""
|
|
169
|
+
return _tools.find_similar_code(threshold)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _mcp_explore_call_graph(symbol_name: str) -> dict:
|
|
173
|
+
"""Explore callers and callees of a symbol across the project.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
symbol_name: Name of the symbol to explore.
|
|
177
|
+
"""
|
|
178
|
+
return _tools.explore_call_graph(symbol_name)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _mcp_explore_impact(symbol_name: str, max_depth: int = 3) -> dict:
|
|
182
|
+
"""Compute the blast radius of a symbol: transitively impacted symbols/files.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
symbol_name: Name of the symbol to analyze.
|
|
186
|
+
max_depth: Maximum number of call-graph hops to follow.
|
|
187
|
+
"""
|
|
188
|
+
return _tools.explore_impact(symbol_name, max_depth)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _mcp_stop_watching() -> dict:
|
|
192
|
+
"""Stop the background file watcher, if any."""
|
|
193
|
+
return _tools.stop_watching()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _mcp_affected_files(base: str = "HEAD", head: str | None = None, max_depth: int = 4) -> dict:
|
|
197
|
+
"""Find files (especially tests) that may be affected by a git diff.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
base: Git ref to diff against (default: HEAD).
|
|
201
|
+
head: Optional second ref; when given, diffs base..head.
|
|
202
|
+
max_depth: Maximum call-graph hops to trace.
|
|
203
|
+
"""
|
|
204
|
+
return _tools.affected_files(base, head, max_depth)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _mcp_search_code(pattern: str, glob: str = "*.py") -> dict:
|
|
208
|
+
"""Search project source code with a regex pattern.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
pattern: Regular expression to search for.
|
|
212
|
+
glob: Glob pattern for files to search.
|
|
213
|
+
"""
|
|
214
|
+
return _tools.search_code(pattern, glob)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _mcp_choose_tool(query: str) -> dict:
|
|
218
|
+
"""For debugging: choose which tool a natural-language query maps to."""
|
|
219
|
+
return _tools.choose_tool(query)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# ── Tool registry table ────────────────────────────────────────────────
|
|
223
|
+
# Maps the public tool name (used by MCP clients and the HONESTCODE_TOOLS
|
|
224
|
+
# env var) to its wrapper function and short description.
|
|
225
|
+
_ALL_TOOLS: dict[str, tuple[Callable, str]] = {
|
|
226
|
+
"scan_file": (_mcp_scan_file, "Scan a file for undefined calls and API issues."),
|
|
227
|
+
"verify_file": (_mcp_verify_file, "Verify a file and return structured agent evidence."),
|
|
228
|
+
"index": (_mcp_index_project, "Build the project symbol index."),
|
|
229
|
+
"deps": (_mcp_load_project_deps, "Load dependency APIs."),
|
|
230
|
+
"check_symbol": (_mcp_check_symbol, "Verify a symbol is defined in the project."),
|
|
231
|
+
"check_api": (_mcp_check_api, "Verify a library API call exists."),
|
|
232
|
+
"execute_code": (_mcp_execute_code, "Run code in a sandbox."),
|
|
233
|
+
"validate_types": (_mcp_validate_types, "Structural type checks."),
|
|
234
|
+
"find_dead_code": (_mcp_find_dead_code, "Find unused symbols."),
|
|
235
|
+
"find_similar_code": (_mcp_find_similar_code, "Find code clones."),
|
|
236
|
+
"explore_call_graph": (_mcp_explore_call_graph, "Explore callers/callees."),
|
|
237
|
+
"explore_impact": (_mcp_explore_impact, "Blast radius of a symbol."),
|
|
238
|
+
"affected_files": (_mcp_affected_files, "Files/tests affected by a git diff."),
|
|
239
|
+
"stop_watching": (_mcp_stop_watching, "Stop the background file watcher."),
|
|
240
|
+
"search_code": (_mcp_search_code, "Regex search across project files."),
|
|
241
|
+
"load_package_apis": (_mcp_load_package_apis, "Load a package's API signatures."),
|
|
242
|
+
"get_project_stats": (_mcp_get_project_stats, "Return index statistics."),
|
|
243
|
+
"choose_tool": (_mcp_choose_tool, "Map a query to the best tool."),
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# ``scan_file`` is always exposed; ``verify_file`` is the agent-facing tool and
|
|
247
|
+
# is part of the default surface. The remaining tools are opt-in.
|
|
248
|
+
_PRIMARY_TOOL = "scan_file"
|
|
249
|
+
_DEFAULT_TOOLS = frozenset({"scan_file", "verify_file"})
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _resolve_tool_whitelist() -> set[str]:
|
|
253
|
+
"""Resolve which tool names to expose on this server.
|
|
254
|
+
|
|
255
|
+
Reads ``HONESTCODE_TOOLS``. If unset, the two verification tools
|
|
256
|
+
(``scan_file`` and ``verify_file``) are exposed. ``all`` exposes every
|
|
257
|
+
tool. Otherwise the value is treated as a comma-separated list of names;
|
|
258
|
+
unknown names are ignored with a warning.
|
|
259
|
+
"""
|
|
260
|
+
raw = os.environ.get("HONESTCODE_TOOLS", "").strip()
|
|
261
|
+
if not raw:
|
|
262
|
+
return set(_DEFAULT_TOOLS)
|
|
263
|
+
|
|
264
|
+
if raw.lower() == "all":
|
|
265
|
+
return set(_ALL_TOOLS.keys())
|
|
266
|
+
|
|
267
|
+
requested = {t.strip() for t in raw.split(",") if t.strip()}
|
|
268
|
+
unknown = requested - set(_ALL_TOOLS.keys())
|
|
269
|
+
for name in unknown:
|
|
270
|
+
logger.warning("HONESTCODE_TOOLS: unknown tool '%s' ignored", name)
|
|
271
|
+
# The primary tool is always included even if not explicitly listed.
|
|
272
|
+
requested.add(_PRIMARY_TOOL)
|
|
273
|
+
return requested & set(_ALL_TOOLS.keys())
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _register_tools() -> None:
|
|
277
|
+
"""Register the whitelisted tools into FastMCP.
|
|
278
|
+
|
|
279
|
+
Idempotent: safe to call multiple times; already-registered tools are
|
|
280
|
+
skipped to avoid duplicate registration errors.
|
|
281
|
+
"""
|
|
282
|
+
already = set(getattr(mcp, "_registered_tool_names", set())) # type: ignore[attr-defined]
|
|
283
|
+
whitelist = _resolve_tool_whitelist()
|
|
284
|
+
for name in whitelist:
|
|
285
|
+
if name in already:
|
|
286
|
+
continue
|
|
287
|
+
func, _desc = _ALL_TOOLS[name]
|
|
288
|
+
# FastMCP infers the schema from the function signature; pass the
|
|
289
|
+
# public tool name explicitly so it shows up as `name`.
|
|
290
|
+
mcp.tool(name=name)(func)
|
|
291
|
+
already.add(name)
|
|
292
|
+
mcp._registered_tool_names = already # type: ignore[attr-defined]
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def register_tools() -> None:
|
|
296
|
+
"""Public entry point for registering the whitelisted tools.
|
|
297
|
+
|
|
298
|
+
``scripts/run_mcp_server.py`` imports the FastMCP object directly; without
|
|
299
|
+
this call it used to start a server with an empty tool registry.
|
|
300
|
+
"""
|
|
301
|
+
_register_tools()
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _is_loopback(host: str) -> bool:
|
|
305
|
+
if host in {"localhost", "::1"}:
|
|
306
|
+
return True
|
|
307
|
+
try:
|
|
308
|
+
import ipaddress
|
|
309
|
+
|
|
310
|
+
return ipaddress.ip_address(host).is_loopback
|
|
311
|
+
except ValueError:
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def main(argv: list[str] | None = None) -> None:
|
|
316
|
+
parser = argparse.ArgumentParser(
|
|
317
|
+
prog="honestcode-mcp",
|
|
318
|
+
description="HonestCode MCP server.",
|
|
319
|
+
)
|
|
320
|
+
parser.add_argument(
|
|
321
|
+
"--transport",
|
|
322
|
+
choices=["stdio", "sse"],
|
|
323
|
+
default="stdio",
|
|
324
|
+
help="MCP transport (default: stdio).",
|
|
325
|
+
)
|
|
326
|
+
parser.add_argument("--host", default="127.0.0.1", help="SSE host (default: 127.0.0.1).")
|
|
327
|
+
parser.add_argument("--port", type=int, default=8000, help="SSE port (default: 8000).")
|
|
328
|
+
args = parser.parse_args(argv)
|
|
329
|
+
|
|
330
|
+
if (
|
|
331
|
+
args.transport == "sse"
|
|
332
|
+
and not _is_loopback(args.host)
|
|
333
|
+
and os.environ.get("HONESTCODE_ALLOW_NETWORK") != "1"
|
|
334
|
+
):
|
|
335
|
+
logger.error(
|
|
336
|
+
"Refusing to expose the unauthenticated MCP server on non-loopback host %s. "
|
|
337
|
+
"Set HONESTCODE_ALLOW_NETWORK=1 only behind a real access-control layer.",
|
|
338
|
+
args.host,
|
|
339
|
+
)
|
|
340
|
+
raise SystemExit(2)
|
|
341
|
+
|
|
342
|
+
register_tools()
|
|
343
|
+
|
|
344
|
+
whitelist = _resolve_tool_whitelist()
|
|
345
|
+
logger.info(
|
|
346
|
+
"Starting HonestCode MCP server (transport=%s, tools=%s)",
|
|
347
|
+
args.transport,
|
|
348
|
+
sorted(whitelist),
|
|
349
|
+
)
|
|
350
|
+
if args.transport == "sse":
|
|
351
|
+
mcp.run(transport="sse", host=args.host, port=args.port)
|
|
352
|
+
else:
|
|
353
|
+
mcp.run(transport="stdio")
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
if __name__ == "__main__":
|
|
357
|
+
main(sys.argv[1:])
|