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.
- pythonparts_mcp-0.2.0.dist-info/METADATA +179 -0
- pythonparts_mcp-0.2.0.dist-info/RECORD +20 -0
- pythonparts_mcp-0.2.0.dist-info/WHEEL +4 -0
- pythonparts_mcp-0.2.0.dist-info/entry_points.txt +2 -0
- server/__init__.py +0 -0
- server/__main__.py +101 -0
- server/config.py +58 -0
- server/paths.py +87 -0
- server/pyright_config.py +67 -0
- server/registry.py +59 -0
- server/tools/__init__.py +23 -0
- server/tools/_introspect_worker.py +537 -0
- server/tools/_sandbox_worker.py +196 -0
- server/tools/connector.py +81 -0
- server/tools/inspect_symbol.py +75 -0
- server/tools/run_snippet_in_allplan.py +85 -0
- server/tools/run_snippet_in_sandbox.py +91 -0
- server/tools/runtime_context.py +157 -0
- server/tools/typecheck.py +140 -0
- server/worker.py +184 -0
|
@@ -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,,
|
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]
|
server/pyright_config.py
ADDED
|
@@ -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
|
server/tools/__init__.py
ADDED
|
@@ -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
|
+
|