pythonparts-mcp 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.
@@ -0,0 +1,179 @@
1
+ Metadata-Version: 2.5
2
+ Name: pythonparts-mcp
3
+ Version: 0.2.0
4
+ Summary: MCP server exposing the ALLPLAN Python runtime to AI coding assistants
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: fastmcp>=3.0
7
+ Requires-Dist: pyright>=1.1
8
+ Requires-Dist: pywin32>=306; sys_platform == 'win32'
9
+ Description-Content-Type: text/markdown
10
+
11
+ # PythonParts Local MCP Server
12
+
13
+ An MCP server that exposes the local ALLPLAN Python runtime to AI coding assistants.
14
+ It is started once per ALLPLAN version and gives any MCP-compatible agent accurate,
15
+ live information about the interpreter, import paths, installed packages, and type
16
+ correctness — without guessing or hallucinating.
17
+
18
+ Transport: **stdio** (spawned as a subprocess by the MCP client).
19
+ Protocol: [Model Context Protocol](https://modelcontextprotocol.io/) via `FastMCP`.
20
+ Release history: [CHANGELOG.md](CHANGELOG.md).
21
+
22
+ ---
23
+
24
+ ## Prerequisites
25
+
26
+ - [**uv**](https://docs.astral.sh/uv/getting-started/installation/) — Python package manager used to run the server without a manual install step.
27
+
28
+ ```powershell
29
+ winget install astral-sh.uv
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Attaching to your AI agent
35
+
36
+ Add the following entry to your MCP configuration:
37
+
38
+ ```json
39
+ "pythonparts-local-mcp": {
40
+ "type": "stdio",
41
+ "command": "uvx",
42
+ "args": [
43
+ "--index", "https://test.pypi.org/simple",
44
+ "--index-strategy", "unsafe-best-match",
45
+ "pythonparts-mcp",
46
+ "--allplan-version", "2026",
47
+ "--std-path", "X:\\Path\\To\\Allplan\\Std",
48
+ "--usr-path", "X:\\Path\\To\\Allplan\\Usr\\Local"
49
+ ]
50
+ }
51
+ ```
52
+
53
+ **Arguments:**
54
+
55
+ | Argument | Description | Example |
56
+ |---|---|---|
57
+ | `--allplan-version` | Installed ALLPLAN version | `2026` |
58
+ | `--std-path` | Path to ALLPLAN's `Std` directory | `C:\Data\Allplan\Allplan 2026\Std` |
59
+ | `--usr-path` | Path to ALLPLAN's user directory. Located under `Usr\<username>` or `Usr\Local` depending on your ALLPLAN configuration. | `C:\Users\Jane\Nemetschek\Allplan\2026\Usr\Local` |
60
+
61
+ The `prg` and `etc` paths are resolved automatically from the Windows registry.
62
+
63
+ ---
64
+
65
+ ## Available Tools
66
+
67
+ ### `get_allplan_info()`
68
+
69
+ Returns the installed ALLPLAN version string and the four resolved file-system paths (`prg`, `etc`, `std`, `usr`). No input required.
70
+
71
+ ```json
72
+ {
73
+ "ok": true,
74
+ "data": {
75
+ "version": "2026.0.0",
76
+ "paths": {
77
+ "prg": "C:\\Program Files\\Allplan\\Allplan 2026\\Prg",
78
+ "etc": "C:\\ProgramData\\Nemetschek\\Allplan\\2026\\etc",
79
+ "std": "X:\\Path\\To\\Allplan\\Std",
80
+ "usr": "X:\\Path\\To\\Allplan\\Usr\\Local"
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ ### `get_python_env_info()`
87
+
88
+ Returns the path to the ALLPLAN-embedded Python interpreter, its version, and the ordered `sys.path` ALLPLAN uses when executing PythonParts scripts. No input required.
89
+
90
+ ```json
91
+ {
92
+ "ok": true,
93
+ "data": {
94
+ "interpreter_path": "C:\\...\\Prg\\Python\\python.exe",
95
+ "python_version": "3.11.9",
96
+ "sys_path": ["C:\\...\\Prg\\Python", "..."]
97
+ }
98
+ }
99
+ ```
100
+
101
+ ### `list_site_packages()`
102
+
103
+ Lists all Python packages installed in the ALLPLAN-embedded Python environment, deduplicated and sorted alphabetically. No input required.
104
+
105
+ ```json
106
+ {
107
+ "ok": true,
108
+ "data": [
109
+ {"name": "requests", "version": "2.31.0"},
110
+ {"name": "shapely", "version": null}
111
+ ]
112
+ }
113
+ ```
114
+
115
+ ### `typecheck_file(file_path: str)`
116
+
117
+ Type-checks a `.py` file against the ALLPLAN runtime using Pyright. Input: absolute path to the file.
118
+
119
+ ```json
120
+ {
121
+ "ok": true,
122
+ "data": {
123
+ "error_count": 2,
124
+ "warning_count": 0,
125
+ "diagnostics": [
126
+ {
127
+ "severity": "error",
128
+ "message": "...",
129
+ "file": "C:\\...\\main.py",
130
+ "line": 12,
131
+ "column": 5,
132
+ "rule": "reportMissingImports"
133
+ }
134
+ ]
135
+ }
136
+ }
137
+ ```
138
+
139
+ ### `inspect_symbol(symbol: str)`
140
+
141
+ Inspects a Python symbol in the live ALLPLAN runtime — returns its category, signature, docstring, and public members. Use this to verify a symbol exists and discover its API before writing code against it.
142
+
143
+ Input: dotted Python path to the symbol.
144
+
145
+ ```json
146
+ {
147
+ "ok": true,
148
+ "data": {
149
+ "symbol": "NemAll_Python_Geometry.Point3D",
150
+ "category": "class",
151
+ "module": "NemAll_Python_Geometry",
152
+ "docstring": "...",
153
+ "signature": null,
154
+ "members": ["X", "Y", "Z", "..."]
155
+
156
+
157
+
158
+ }
159
+ }
160
+ ```
161
+
162
+ ### `run_snippet_in_sandbox(code: str)`
163
+
164
+ Runs a Python snippet against the ALLPLAN libraries in a **separate process, outside ALLPLAN**, and returns its `print()` output. ALLPLAN does not need to be running, and the snippet cannot touch the open drawing or project data. This is the tool to reach for when verifying geometry math or API behaviour.
165
+
166
+ The ALLPLAN interface modules are pre-imported as globals (`AllplanGeometry`, `AllplanBasisElements`, …). There is no document and no coordinate input, so `document` and `coord_input` are **not** available — anything that needs the open drawing, the project or user settings will fail or return a meaningless value.
167
+
168
+ Globals do not persist between calls, so each snippet must be self-contained. The first call spends a second or two loading the ALLPLAN DLLs; the process is then reused. Snippets are abandoned after 60 seconds.
169
+
170
+ Snippets must not show a user interface — `AllplanUtility.ShowMessageBox`, file dialogs and the like. There is no ALLPLAN window to host them, so the call never returns and the snippet runs into the timeout.
171
+
172
+ ```json
173
+ {
174
+ "ok": true,
175
+ "data": {
176
+ "output": "(30000.0, NemAll_Python_Geometry.eServiceResult.POSITIVE_ORIENTATION)\n"
177
+ }
178
+ }
179
+ ```
@@ -0,0 +1,20 @@
1
+ server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ server/__main__.py,sha256=4tG2TUYNKgY-ZslQIWiDKkJESe5eptLI99k5bYct8YU,3078
3
+ server/config.py,sha256=2GHe6kmP4ngv3lXvt1aQZb734uRoj3IPqbiSIXneSd8,1455
4
+ server/paths.py,sha256=JImoZD92bdLy4g9LLdoIGMLF6gSzzN2X1DvWlztCv3A,2720
5
+ server/pyright_config.py,sha256=ADKEeDibT0m28JcgRc91VFHxS9EQqOX38RSvRLJvNfo,2241
6
+ server/registry.py,sha256=l0en2fqnjaV7ksFepqFayMJ0WMn4JqjaQFTxab0bOYs,2127
7
+ server/worker.py,sha256=krjPZvMQI6NfXzZidmP5vLCZlBUlj7GZJmjgQ9g18Pk,6645
8
+ server/tools/__init__.py,sha256=7o6-OuNXkQUFWNpHvEGKc8Yh2cu2PrPrTpFmhTdiAIY,718
9
+ server/tools/_introspect_worker.py,sha256=-g_AzB7lclXRJUhcnb-rBzq5A2OeFAjm6lhTpXVCpeU,18914
10
+ server/tools/_sandbox_worker.py,sha256=6Og6yDXgaQZmnZWh6_spP7jbBoorUwI1m0BhZ55TQo4,7357
11
+ server/tools/connector.py,sha256=ugAvhWShzYahFwJLTt-q-bZ1s3lOm5JSQzmhxspr28o,2892
12
+ server/tools/inspect_symbol.py,sha256=XB5OZu_NHd74FEnevX8wj9l21FCMTK19qeVyj3FguD4,2825
13
+ server/tools/run_snippet_in_allplan.py,sha256=0FM9htz7O5mhPyv1Fd3MwuHg-9_9nQFbfhNyAvGJrv4,3668
14
+ server/tools/run_snippet_in_sandbox.py,sha256=SlVQ8X2tuY2303TUzbpr9nexcp3ctaNSimNfDMx6CPA,3594
15
+ server/tools/runtime_context.py,sha256=09avnd85iyjydJ2PGqowfCR0bdKVFDZ_KtpT67en_r4,5516
16
+ server/tools/typecheck.py,sha256=uSFpoGdNG1HiDB0ZB1zBRSG74pWPndn801YQYEfpvsY,4879
17
+ pythonparts_mcp-0.2.0.dist-info/METADATA,sha256=zY4dLDJiMzXRCZMBXdaSIi44JBc2MlzE5LOfaNQ4KzU,5402
18
+ pythonparts_mcp-0.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
19
+ pythonparts_mcp-0.2.0.dist-info/entry_points.txt,sha256=Z0Y7jP5IsuOEXyhbwWRZKMyqKwunlM-pbdThqFwUjXs,57
20
+ pythonparts_mcp-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pythonparts-mcp = server.__main__:main
server/__init__.py ADDED
File without changes
server/__main__.py ADDED
@@ -0,0 +1,101 @@
1
+ """
2
+ PythonParts Local MCP Server — entry point.
3
+
4
+ Usage:
5
+ python -m server --allplan-version 2026 --std-path "C:\\..." --usr-path "C:\\..."
6
+ python -m server --allplan-version 2026 --product-name Allplan --std-path "C:\\..." --usr-path "C:\\..."
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import logging
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ from fastmcp import FastMCP
17
+
18
+ from .config import get_config, init_config
19
+ from .pyright_config import setup_pyright_config
20
+ from .registry import AllplanNotInstalledError, read_install_paths
21
+ from .tools.inspect_symbol import register_inspect_symbol_tools
22
+ # from .tools.run_snippet_in_allplan import register_run_snippet_in_allplan_tools
23
+ from .tools.run_snippet_in_sandbox import register_run_snippet_in_sandbox_tools
24
+ from .tools.runtime_context import register_runtime_context_tools
25
+ from .tools.typecheck import register_typecheck_tools
26
+
27
+
28
+ def parse_args() -> argparse.Namespace:
29
+ parser = argparse.ArgumentParser(description="PythonParts Local MCP Server")
30
+ parser.add_argument(
31
+ "--allplan-version",
32
+ required=True,
33
+ help="ALLPLAN version, e.g. 2026",
34
+ )
35
+ parser.add_argument(
36
+ "--product-name",
37
+ default="Allplan",
38
+ help="Registry product name (default: Allplan)",
39
+ )
40
+ parser.add_argument(
41
+ "--std-path",
42
+ required=True,
43
+ type=Path,
44
+ help="ALLPLAN 'std' directory path",
45
+ )
46
+ parser.add_argument(
47
+ "--usr-path",
48
+ required=True,
49
+ type=Path,
50
+ help="ALLPLAN 'usr' directory path",
51
+ )
52
+ return parser.parse_args()
53
+
54
+
55
+ def main() -> None:
56
+ # stdout is the MCP stdio transport — never log there. Log everything to a file instead.
57
+ log_path = Path(__file__).resolve().parent.parent / "server.log"
58
+ logging.basicConfig(
59
+ filename=log_path,
60
+ encoding="utf-8",
61
+ level=logging.DEBUG,
62
+ )
63
+
64
+ logger = logging.getLogger(__name__)
65
+ logger.info("Starting PythonParts Local MCP Server")
66
+
67
+ args = parse_args()
68
+
69
+ try:
70
+ prg, etc, program_name = read_install_paths(
71
+ product_name=args.product_name,
72
+ allplan_version=args.allplan_version,
73
+ )
74
+ except AllplanNotInstalledError as exc:
75
+ print(f"ERROR: {exc}", file=sys.stderr)
76
+ sys.exit(1)
77
+
78
+ init_config(
79
+ allplan_version=args.allplan_version,
80
+ product_name=args.product_name,
81
+ prg=prg,
82
+ etc=etc,
83
+ std=args.std_path,
84
+ usr=args.usr_path,
85
+ program_name=program_name,
86
+ )
87
+
88
+ setup_pyright_config(get_config())
89
+
90
+ mcp = FastMCP("PythonParts local MCP")
91
+ register_runtime_context_tools(mcp)
92
+ register_typecheck_tools(mcp)
93
+ register_inspect_symbol_tools(mcp)
94
+ # Withheld from the public release: it executes code in the user's live ALLPLAN session.
95
+ # register_run_snippet_in_allplan_tools(mcp)
96
+ register_run_snippet_in_sandbox_tools(mcp)
97
+ mcp.run()
98
+
99
+
100
+ if __name__ == "__main__":
101
+ main()
server/config.py ADDED
@@ -0,0 +1,58 @@
1
+ """
2
+ AllplanConfig dataclass and module-level singleton.
3
+
4
+ All tools access the runtime configuration through `get_config()`.
5
+ `init_config()` must be called once at server startup before any tool is invoked.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+
13
+ # Fixed port owned by the Vibe Coding Connector PythonPart running inside ALLPLAN.
14
+ CONNECTOR_BASE_URL = "http://localhost:9731"
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class AllplanConfig:
19
+ allplan_version: str
20
+ product_name: str
21
+ prg: Path
22
+ etc: Path
23
+ std: Path
24
+ usr: Path
25
+ program_name: str
26
+
27
+
28
+ _config: AllplanConfig | None = None
29
+
30
+
31
+ def init_config(
32
+ allplan_version: str,
33
+ product_name: str,
34
+ prg: Path,
35
+ etc: Path,
36
+ std: Path,
37
+ usr: Path,
38
+ program_name: str,
39
+ ) -> AllplanConfig:
40
+ """Initialize the global config singleton. Call once at startup."""
41
+ global _config
42
+ _config = AllplanConfig(
43
+ allplan_version=allplan_version,
44
+ product_name=product_name,
45
+ prg=prg,
46
+ etc=etc,
47
+ std=std,
48
+ usr=usr,
49
+ program_name=program_name,
50
+ )
51
+ return _config
52
+
53
+
54
+ def get_config() -> AllplanConfig:
55
+ """Return the active config. Raises RuntimeError if not yet initialized."""
56
+ if _config is None:
57
+ raise RuntimeError("Server config has not been initialized. Call init_config() first.")
58
+ return _config
server/paths.py ADDED
@@ -0,0 +1,87 @@
1
+ """
2
+ Path helpers derived from the ALLPLAN installation config.
3
+
4
+ Shared by multiple tools — import from here rather than duplicating.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ from .config import AllplanConfig
12
+
13
+
14
+ def interpreter_path(prg: Path) -> Path:
15
+ """Return the path to the ALLPLAN-embedded Python interpreter."""
16
+ return prg / "Python" / "python.exe"
17
+
18
+
19
+ def framework_version(cfg: AllplanConfig) -> str:
20
+ """Return the ALLPLAN's product version from the executable."""
21
+ import win32api # stdlib-level on Windows; lazy-imported for non-Windows testability
22
+
23
+ program_path = cfg.prg / cfg.program_name
24
+
25
+ # Get available language code
26
+ translation = win32api.GetFileVersionInfo(
27
+ str(program_path), "\\VarFileInfo\\Translation")
28
+ lang, codepage = translation[0] if translation else (1033, 1252)
29
+
30
+ # Format as hex string
31
+ lang_code = f"{lang:04x}{codepage:04x}"
32
+
33
+ # Access StringFileInfo with the correct language code
34
+ product_version = win32api.GetFileVersionInfo(
35
+ str(program_path),
36
+ f"\\StringFileInfo\\{lang_code}\\ProductVersion"
37
+ )
38
+ return product_version
39
+
40
+
41
+ def build_sys_path(cfg: AllplanConfig) -> list[str]:
42
+ """Reconstruct the ``sys.path`` that ALLPLAN injects into its embedded Python runtime.
43
+
44
+ The order follows the observed path order on a live installation.
45
+ Per-project (prj) entries are intentionally omitted.
46
+
47
+ Args:
48
+ cfg: Active ALLPLAN configuration.
49
+
50
+ Returns:
51
+ Ordered list of path strings matching ALLPLAN's runtime ``sys.path``.
52
+ """
53
+ prg = cfg.prg
54
+ etc = cfg.etc
55
+ std = cfg.std
56
+ usr = cfg.usr
57
+
58
+ paths: list[Path] = [
59
+ # Interpreter roots
60
+ prg / "Python",
61
+ prg / "Python" / "Lib",
62
+ # Site-packages per hierarchy
63
+ std / "PythonParts-site-packages",
64
+ usr / "PythonParts-site-packages",
65
+ etc / "PythonParts-site-packages",
66
+ # Standard Python lib paths
67
+ prg / "Python" / "Lib" / "site-packages",
68
+ prg / "Python" / "DLLs",
69
+ # Framework paths (etc)
70
+ etc / "PythonPartsFramework",
71
+ etc / "PythonPartsFramework" / "GeneralScripts",
72
+ etc / "PythonPartsScripts",
73
+ etc / "PythonPartsExampleScripts",
74
+ etc / "Scripts",
75
+ # Script paths per hierarchy (std / usr)
76
+ std / "PythonPartsScripts",
77
+ std / "VisualScripts",
78
+ std / "Scripts",
79
+ usr / "PythonPartsScripts",
80
+ usr / "PythonPartsExampleScripts",
81
+ usr / "VisualScripts",
82
+ usr / "Scripts",
83
+ # Prg root
84
+ prg,
85
+ ]
86
+
87
+ return [str(p) for p in paths]
@@ -0,0 +1,67 @@
1
+ """
2
+ Manages the temporary pyrightconfig.json used for type-checking against the
3
+ ALLPLAN runtime. The config directory is created at server startup and
4
+ deleted automatically on shutdown via atexit.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import atexit
10
+ import json
11
+ import shutil
12
+ import tempfile
13
+ from pathlib import Path
14
+
15
+ from .config import AllplanConfig
16
+ from .paths import build_sys_path, interpreter_path
17
+
18
+ _pyright_config_dir: Path | None = None
19
+ _python_interpreter: str | None = None
20
+
21
+
22
+ def get_python_interpreter() -> str:
23
+ """Return the ALLPLAN interpreter path for use as the pyright --pythonpath argument."""
24
+ if _python_interpreter is None:
25
+ raise RuntimeError("Pyright config has not been set up. Call setup_pyright_config() first.")
26
+ return _python_interpreter
27
+
28
+
29
+ def setup_pyright_config(cfg: AllplanConfig) -> Path:
30
+ """Create a temp directory with a ``pyrightconfig.json`` for the given ALLPLAN install.
31
+
32
+ Registers cleanup of the temp directory via ``atexit`` so it is removed on
33
+ server shutdown without requiring explicit teardown in tool code.
34
+
35
+ Args:
36
+ cfg: Active ALLPLAN configuration.
37
+
38
+ Returns:
39
+ Path to the temp directory. Pass this to ``--project`` when invoking pyright.
40
+ """
41
+ global _pyright_config_dir, _python_interpreter
42
+
43
+ _python_interpreter = str(interpreter_path(cfg.prg))
44
+ tmpdir = Path(tempfile.mkdtemp(prefix="pythonparts-mcp-pyright-"))
45
+ atexit.register(shutil.rmtree, str(tmpdir), True)
46
+
47
+ config = {
48
+ "stubPath": str(cfg.etc / "PythonPartsFramework" / "InterfaceStubs"),
49
+ "extraPaths": build_sys_path(cfg),
50
+ "typeCheckingMode": "basic",
51
+ "reportMissingModuleSource": "none",
52
+ }
53
+
54
+ (tmpdir / "pyrightconfig.json").write_text(
55
+ json.dumps(config, indent=2),
56
+ encoding="utf-8",
57
+ )
58
+
59
+ _pyright_config_dir = tmpdir
60
+ return tmpdir
61
+
62
+
63
+ def get_pyright_config_dir() -> Path:
64
+ """Return the temp project dir. Raises if ``setup_pyright_config`` was not called."""
65
+ if _pyright_config_dir is None:
66
+ raise RuntimeError("Pyright config has not been set up. Call setup_pyright_config() first.")
67
+ return _pyright_config_dir
server/registry.py ADDED
@@ -0,0 +1,59 @@
1
+ """
2
+ Windows registry reader for ALLPLAN installation paths.
3
+
4
+ Reads `prg` and `etc` paths from:
5
+ HKLM\\SOFTWARE\\NEMETSCHEK\\{product_name}\\{version}.0\\InstallRoot
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+
12
+
13
+ class AllplanNotInstalledError(Exception):
14
+ """Raised when the requested ALLPLAN version is not found in the registry."""
15
+
16
+
17
+ def _format_version(allplan_version: str) -> str:
18
+ """Convert a version string like '2026' to '2026.0' for registry lookup."""
19
+ return f"{allplan_version}.0"
20
+
21
+
22
+ def read_install_paths(product_name: str, allplan_version: str) -> tuple[Path, Path, str]:
23
+ """Read the ``prg``, ``etc``, and program name from the Windows registry.
24
+
25
+ Args:
26
+ product_name: Registry product name, e.g. ``"Allplan"``.
27
+ allplan_version: Version string as supplied by the user, e.g. ``"2026"``.
28
+
29
+ Returns:
30
+ A ``(prg_path, etc_path, program_name)`` tuple.
31
+
32
+ Raises:
33
+ AllplanNotInstalledError: If the registry key does not exist (product not installed).
34
+ """
35
+ import winreg # stdlib on Windows; imported here to allow import on non-Windows for testing
36
+
37
+ version_str = _format_version(allplan_version)
38
+ key_path = f"SOFTWARE\\NEMETSCHEK\\{product_name}\\{version_str}\\InstallRoot"
39
+
40
+ try:
41
+ key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path)
42
+ except FileNotFoundError:
43
+ raise AllplanNotInstalledError(
44
+ f"{product_name} {version_str} is not installed"
45
+ ) from None
46
+
47
+ try:
48
+ program_data_drive, _ = winreg.QueryValueEx(key, "ProgramDataDrive")
49
+ program_data_path, _ = winreg.QueryValueEx(key, "ProgramDataPath")
50
+ program_drive, _ = winreg.QueryValueEx(key, "ProgramDrive")
51
+ program_path, _ = winreg.QueryValueEx(key, "ProgramPath")
52
+ program_name, _ = winreg.QueryValueEx(key, "ProgramName")
53
+ finally:
54
+ winreg.CloseKey(key)
55
+
56
+ etc_path = Path(program_data_drive + program_data_path) / "etc"
57
+ prg_path = Path(program_drive + program_path)
58
+
59
+ return prg_path, etc_path, program_name
@@ -0,0 +1,23 @@
1
+ """Shared helpers and constants used across all tool modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from mcp.types import ToolAnnotations
6
+
7
+ # Shared annotations for read-only, idempotent, non-destructive tools.
8
+ # Import this constant instead of duplicating the ToolAnnotations call in each module.
9
+ READ_ONLY_TOOL_ANNOTATIONS = ToolAnnotations(
10
+ readOnlyHint=True,
11
+ destructiveHint=False,
12
+ idempotentHint=True,
13
+ openWorldHint=True,
14
+ )
15
+
16
+ # Shared annotations for tools that can mutate the open ALLPLAN document (e.g. run arbitrary code).
17
+ MUTATING_TOOL_ANNOTATIONS = ToolAnnotations(
18
+ readOnlyHint=False,
19
+ destructiveHint=True,
20
+ idempotentHint=False,
21
+ openWorldHint=True,
22
+ )
23
+