honestcode 0.2.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.

Potentially problematic release.


This version of honestcode might be problematic. Click here for more details.

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