simconnect-mcp 1.0.0__tar.gz
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.
- simconnect_mcp-1.0.0/.github/workflows/ci.yml +40 -0
- simconnect_mcp-1.0.0/.github/workflows/release.yml +113 -0
- simconnect_mcp-1.0.0/.gitignore +9 -0
- simconnect_mcp-1.0.0/CLAUDE.md +275 -0
- simconnect_mcp-1.0.0/LICENSE.txt +662 -0
- simconnect_mcp-1.0.0/PKG-INFO +464 -0
- simconnect_mcp-1.0.0/README.md +437 -0
- simconnect_mcp-1.0.0/docs/extending-catalogs.md +258 -0
- simconnect_mcp-1.0.0/docs/superpowers/plans/2026-03-26-pmdg-client-data.md +1537 -0
- simconnect_mcp-1.0.0/docs/superpowers/plans/2026-08-29-mcp-modernization-phase0-correctness.md +3202 -0
- simconnect_mcp-1.0.0/docs/superpowers/plans/2026-08-29-mcp-modernization-phase1-mcp-surface.md +1971 -0
- simconnect_mcp-1.0.0/docs/superpowers/plans/2026-08-29-mcp-modernization-phase2-capability.md +1860 -0
- simconnect_mcp-1.0.0/docs/superpowers/plans/2026-08-31-pypi-publishing.md +1187 -0
- simconnect_mcp-1.0.0/docs/superpowers/specs/2026-03-26-pmdg-client-data-design.md +121 -0
- simconnect_mcp-1.0.0/docs/superpowers/specs/2026-08-29-mcp-modernization-design.md +327 -0
- simconnect_mcp-1.0.0/pyproject.toml +84 -0
- simconnect_mcp-1.0.0/scripts/parse_pmdg_sdk.py +1046 -0
- simconnect_mcp-1.0.0/scripts/set_version.py +77 -0
- simconnect_mcp-1.0.0/server.json +22 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/__init__.py +10 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/__main__.py +5 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/connection.py +620 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/data/__init__.py +1 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/data/catalog.py +240 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/data/hubhop.py +647 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/data/pmdg_737.json +22835 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/data/pmdg_777.json +20669 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/data/simvar_catalog.py +162 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/data/simvars_catalog.json +6538 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/dispatch.py +427 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/docs/best_practices.md +118 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/docs/events.md +119 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/docs/lvars.md +102 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/docs/overview.md +65 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/docs/pmdg_737.md +89 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/docs/pmdg_777.md +492 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/docs/rpn.md +150 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/docs/simvars.md +124 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/facilities.py +305 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/pmdg.py +908 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/pmdg_detect.py +194 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/pmdg_ng3.py +907 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/prompts/__init__.py +1 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/prompts/templates.py +191 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/resources/__init__.py +1 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/resources/documentation.py +119 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/resources/state.py +57 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/server.py +264 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/simvar_access.py +622 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/tools/__init__.py +130 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/tools/aircraft.py +164 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/tools/connection_tools.py +59 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/tools/events.py +541 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/tools/facilities.py +444 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/tools/flight.py +602 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/tools/formatting.py +118 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/tools/hubhop.py +294 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/tools/lvars.py +897 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/tools/models.py +482 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/tools/pmdg.py +533 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/tools/simvars.py +455 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/tools/utilities.py +294 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/vendor/MOBIFLIGHT_LICENSE +21 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/vendor/__init__.py +6 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/vendor/mobiflight_variable_requests.py +204 -0
- simconnect_mcp-1.0.0/src/simconnect_mcp/vendor/simconnect_mobiflight.py +39 -0
- simconnect_mcp-1.0.0/tests/conftest.py +218 -0
- simconnect_mcp-1.0.0/tests/fixtures/facilities/airport_list.hex +1 -0
- simconnect_mcp-1.0.0/tests/fixtures/facilities/ndb_list.hex +1 -0
- simconnect_mcp-1.0.0/tests/fixtures/facilities/vor_list.hex +1 -0
- simconnect_mcp-1.0.0/tests/fixtures/facilities/waypoint_list.hex +1 -0
- simconnect_mcp-1.0.0/tests/live/__init__.py +0 -0
- simconnect_mcp-1.0.0/tests/live/conftest.py +154 -0
- simconnect_mcp-1.0.0/tests/live/test_live_events.py +115 -0
- simconnect_mcp-1.0.0/tests/live/test_live_flight.py +175 -0
- simconnect_mcp-1.0.0/tests/live/test_live_lvars.py +231 -0
- simconnect_mcp-1.0.0/tests/live/test_live_mobiflight_responses.py +115 -0
- simconnect_mcp-1.0.0/tests/live/test_live_pmdg.py +112 -0
- simconnect_mcp-1.0.0/tests/live/test_live_simvars.py +138 -0
- simconnect_mcp-1.0.0/tests/live/test_pmdg_gate.py +66 -0
- simconnect_mcp-1.0.0/tests/test_aircraft.py +138 -0
- simconnect_mcp-1.0.0/tests/test_connection.py +128 -0
- simconnect_mcp-1.0.0/tests/test_connection_tools.py +121 -0
- simconnect_mcp-1.0.0/tests/test_dispatch.py +689 -0
- simconnect_mcp-1.0.0/tests/test_documentation.py +298 -0
- simconnect_mcp-1.0.0/tests/test_error_suggestions.py +85 -0
- simconnect_mcp-1.0.0/tests/test_events.py +390 -0
- simconnect_mcp-1.0.0/tests/test_facilities_parsing.py +587 -0
- simconnect_mcp-1.0.0/tests/test_facilities_tools.py +610 -0
- simconnect_mcp-1.0.0/tests/test_flight.py +629 -0
- simconnect_mcp-1.0.0/tests/test_formatting.py +77 -0
- simconnect_mcp-1.0.0/tests/test_hubhop.py +367 -0
- simconnect_mcp-1.0.0/tests/test_hubhop_tool.py +300 -0
- simconnect_mcp-1.0.0/tests/test_logging.py +102 -0
- simconnect_mcp-1.0.0/tests/test_lvar_listing.py +529 -0
- simconnect_mcp-1.0.0/tests/test_lvars.py +371 -0
- simconnect_mcp-1.0.0/tests/test_mobiflight_responses.py +78 -0
- simconnect_mcp-1.0.0/tests/test_mobiflight_unavailable_code.py +90 -0
- simconnect_mcp-1.0.0/tests/test_models.py +51 -0
- simconnect_mcp-1.0.0/tests/test_packaging.py +100 -0
- simconnect_mcp-1.0.0/tests/test_pmdg.py +911 -0
- simconnect_mcp-1.0.0/tests/test_pmdg_ng3.py +569 -0
- simconnect_mcp-1.0.0/tests/test_registration.py +375 -0
- simconnect_mcp-1.0.0/tests/test_search.py +210 -0
- simconnect_mcp-1.0.0/tests/test_set_version.py +94 -0
- simconnect_mcp-1.0.0/tests/test_simvar_access.py +1072 -0
- simconnect_mcp-1.0.0/tests/test_simvar_catalog.py +108 -0
- simconnect_mcp-1.0.0/tests/test_simvars.py +570 -0
- simconnect_mcp-1.0.0/tests/test_title_detection.py +893 -0
- simconnect_mcp-1.0.0/tests/test_tools_init.py +214 -0
- simconnect_mcp-1.0.0/tests/test_utilities.py +295 -0
- simconnect_mcp-1.0.0/uv.lock +1007 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
name: Tests (Python ${{ matrix.python-version }})
|
|
12
|
+
# Windows is required, not preferred: dispatch.py and facilities.py import
|
|
13
|
+
# ctypes.wintypes at module level, which does not import on Linux.
|
|
14
|
+
runs-on: windows-latest
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
matrix:
|
|
18
|
+
# Floor from requires-python, plus a current release.
|
|
19
|
+
python-version: ["3.10", "3.13"]
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout
|
|
23
|
+
uses: actions/checkout@v7
|
|
24
|
+
|
|
25
|
+
- name: Install uv
|
|
26
|
+
uses: astral-sh/setup-uv@v10.0.1
|
|
27
|
+
with:
|
|
28
|
+
python-version: ${{ matrix.python-version }}
|
|
29
|
+
enable-cache: true
|
|
30
|
+
|
|
31
|
+
- name: Install dependencies
|
|
32
|
+
run: uv sync --locked
|
|
33
|
+
|
|
34
|
+
- name: Lint
|
|
35
|
+
run: uv run ruff check src tests scripts/set_version.py
|
|
36
|
+
|
|
37
|
+
- name: Run mocked test suite
|
|
38
|
+
# pyproject sets addopts = "-m 'not live'", so the 28 live tests that
|
|
39
|
+
# need a running MSFS are deselected here.
|
|
40
|
+
run: uv run pytest -q
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
name: Tests (Python ${{ matrix.python-version }})
|
|
10
|
+
runs-on: windows-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.13"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout
|
|
18
|
+
uses: actions/checkout@v7
|
|
19
|
+
|
|
20
|
+
- name: Install uv
|
|
21
|
+
uses: astral-sh/setup-uv@v10.0.1
|
|
22
|
+
with:
|
|
23
|
+
python-version: ${{ matrix.python-version }}
|
|
24
|
+
enable-cache: true
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: uv sync --locked
|
|
28
|
+
|
|
29
|
+
- name: Lint
|
|
30
|
+
run: uv run ruff check src tests scripts/set_version.py
|
|
31
|
+
|
|
32
|
+
- name: Run mocked test suite
|
|
33
|
+
run: uv run pytest -q
|
|
34
|
+
|
|
35
|
+
publish:
|
|
36
|
+
name: Publish to PyPI and the MCP Registry
|
|
37
|
+
needs: test
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
permissions:
|
|
40
|
+
# PyPI trusted publishing and `mcp-publisher login github-oidc` both
|
|
41
|
+
# mint short-lived OIDC tokens; neither uses a stored secret.
|
|
42
|
+
id-token: write
|
|
43
|
+
contents: read
|
|
44
|
+
|
|
45
|
+
steps:
|
|
46
|
+
- name: Checkout the tagged commit
|
|
47
|
+
uses: actions/checkout@v7
|
|
48
|
+
|
|
49
|
+
- name: Install uv
|
|
50
|
+
uses: astral-sh/setup-uv@v10.0.1
|
|
51
|
+
with:
|
|
52
|
+
enable-cache: true
|
|
53
|
+
|
|
54
|
+
- name: Derive the version from the tag
|
|
55
|
+
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"
|
|
56
|
+
|
|
57
|
+
- name: Set the version in pyproject.toml and server.json
|
|
58
|
+
run: python scripts/set_version.py "$VERSION"
|
|
59
|
+
|
|
60
|
+
- name: Build the distributions
|
|
61
|
+
# Pure-Python py3-none-any wheel, so building on Linux is fine even
|
|
62
|
+
# though the package only runs on Windows.
|
|
63
|
+
run: uv build
|
|
64
|
+
|
|
65
|
+
- name: Publish to PyPI
|
|
66
|
+
run: uv publish --trusted-publishing always
|
|
67
|
+
|
|
68
|
+
- name: Install mcp-publisher
|
|
69
|
+
run: |
|
|
70
|
+
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
|
|
71
|
+
|
|
72
|
+
- name: Authenticate to the MCP Registry
|
|
73
|
+
run: ./mcp-publisher login github-oidc
|
|
74
|
+
|
|
75
|
+
- name: Publish to the MCP Registry
|
|
76
|
+
run: ./mcp-publisher publish
|
|
77
|
+
|
|
78
|
+
sync-version:
|
|
79
|
+
name: Commit the version bump back to main
|
|
80
|
+
needs: publish
|
|
81
|
+
runs-on: ubuntu-latest
|
|
82
|
+
permissions:
|
|
83
|
+
contents: write
|
|
84
|
+
|
|
85
|
+
steps:
|
|
86
|
+
- name: Checkout main
|
|
87
|
+
uses: actions/checkout@v7
|
|
88
|
+
with:
|
|
89
|
+
ref: main
|
|
90
|
+
|
|
91
|
+
- name: Install uv
|
|
92
|
+
uses: astral-sh/setup-uv@v10.0.1
|
|
93
|
+
|
|
94
|
+
- name: Derive the version from the tag
|
|
95
|
+
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"
|
|
96
|
+
|
|
97
|
+
- name: Set the version in pyproject.toml and server.json
|
|
98
|
+
run: python scripts/set_version.py "$VERSION"
|
|
99
|
+
|
|
100
|
+
- name: Sync the lockfile
|
|
101
|
+
run: uv lock
|
|
102
|
+
|
|
103
|
+
- name: Commit and push
|
|
104
|
+
run: |
|
|
105
|
+
git config user.name "github-actions[bot]"
|
|
106
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
107
|
+
git add pyproject.toml server.json uv.lock
|
|
108
|
+
if git diff --staged --quiet; then
|
|
109
|
+
echo "main is already at $VERSION; nothing to commit"
|
|
110
|
+
else
|
|
111
|
+
git commit -m "chore: set version to $VERSION"
|
|
112
|
+
git push origin HEAD:main
|
|
113
|
+
fi
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
SimConnect MCP Server — an MCP server that gives AI agents full read/write access to Microsoft Flight Simulator via SimConnect. Built for the add-on development use case, not consumer flight assistance.
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
MCP Protocol (FastMCP, stdio)
|
|
11
|
+
→ server.py (tool/resource/prompt registration, lifespan)
|
|
12
|
+
→ tools/*.py (domain modules)
|
|
13
|
+
→ connection.py (SimConnectManager singleton)
|
|
14
|
+
→ simvar_access.py (SimVarAccessor — generic SimVar reads/writes, replaces AircraftRequests)
|
|
15
|
+
→ facilities.py (facility *_LIST message parsing/accumulation, feeds tools/facilities.py)
|
|
16
|
+
→ dispatch.py (SimConnectDispatcher — owns the dispatch loop; both of the above depend on it)
|
|
17
|
+
→ vendored SimConnectMobiFlight (client-data support for the WASM bridge)
|
|
18
|
+
→ SimConnect DLL / MSFS
|
|
19
|
+
→ MobiFlight WASM Module (L-vars, calculator code)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Key Design Decisions
|
|
23
|
+
|
|
24
|
+
- **Singleton connection** — SimConnect allows one connection per process. `SimConnectManager` is a thread-safe singleton with lazy-connect.
|
|
25
|
+
- **`run_in_executor` + `threading.Lock`** — all SimConnect DLL calls are blocking and not thread-safe. Every call goes through `run_sync()` which acquires a lock and runs in an executor to avoid blocking the async MCP event loop.
|
|
26
|
+
- **MobiFlight optional** — the vendored `SimConnectMobiFlight` (from [Koseng/MSFSPythonSimConnectMobiFlightExtension](https://github.com/Koseng/MSFSPythonSimConnectMobiFlightExtension)) is a drop-in subclass of `SimConnect` that adds client-data support for the WASM bridge. If it fails to load, core SimVar/event tools still work; only L-var tools degrade.
|
|
27
|
+
- **`SimVarAccessor` replaced `AircraftRequests`** — SimVar access no longer goes through `SimConnect.AircraftRequests`, which holds a hardcoded table of ~828 variables each bound to one fixed unit and signals failure by returning `None`/`False`. `simvar_access.py`'s `SimVarAccessor` builds data definitions directly (`AddToDataDefinition` + `RequestDataOnSimObject`/`SetDataOnSimObject`), which is what makes unit selection, reading variables outside the table, string variables, and honest write failures possible. `SimConnectManager.set_lvar` routes through the same accessor rather than a hand-rolled copy of the pattern.
|
|
28
|
+
- **`SimConnectDispatcher` owns `my_dispatch_proc`.** This is the single most important invariant in the codebase. `dispatch.py`'s `SimConnectDispatcher` (a subclass of the vendored `SimConnectMobiFlight`) takes over the SimConnect dispatch loop so SimVar reads/writes and exceptions can be correlated back to the call that caused them, and so facility (`*_LIST`) messages can be parsed directly. **Anything added to the dispatch loop must never fall through to the library's `SYSTEM_STATE` or `*_LIST` branches** — both call `print()` (`handle_state_event`, and `dump()` on the facility objects) and would corrupt the JSON-RPC stream this server speaks over stdio.
|
|
29
|
+
- **The vendored bridge has exactly two documented local changes** — see the header comment in `vendor/mobiflight_variable_requests.py`: per-call logging demoted from INFO to DEBUG, and response-channel strings (definition ID 0) routed to registered handlers instead of being logged as "DefinitionID not found" and dropped. Both must survive a re-sync from upstream — the header comment is the only thing standing between a re-sync and silently reverting them. `vendor/simconnect_mobiflight.py` carries no local changes at all.
|
|
30
|
+
- **Native L-var writes** — `SimConnectManager.set_lvar()` (which the `msfs_set_lvar` tool calls) uses `AddToDataDefinition` + `SetDataOnSimObject` (the native SimConnect API, via `SimVarAccessor`), NOT the MobiFlight RPN `set()` command. This is critical because proprietary aircraft like the Fenix ignore MobiFlight RPN writes but respond to native SimConnect data writes.
|
|
31
|
+
- **`clear_sim_variables()` on connect** — the MobiFlight WASM module retains stale variable registrations from prior sessions. Without clearing on connect, all reads return 0.
|
|
32
|
+
- **Tool naming and contract** — every tool is registered in `server.py` with the `msfs_` prefix and explicit `ToolAnnotations` (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `title`); there is no annotation-free registration path. Every tool function returns `SomeResult | ToolError`, never a bare dict and never a fabricated success — a new tool must follow both conventions.
|
|
33
|
+
|
|
34
|
+
## Project Structure
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
src/simconnect_mcp/
|
|
38
|
+
├── server.py # FastMCP instance, lifespan, tool registration
|
|
39
|
+
├── connection.py # SimConnectManager singleton + native set_lvar
|
|
40
|
+
├── dispatch.py # SimConnectDispatcher — owns the SimConnect dispatch loop
|
|
41
|
+
├── simvar_access.py # SimVarAccessor — generic SimVar reads/writes via data definitions
|
|
42
|
+
├── facilities.py # Facility (*_LIST) message parsing and accumulation
|
|
43
|
+
├── pmdg.py # PMDG 777 SDK structs, CDU rendering, data manager
|
|
44
|
+
├── pmdg_ng3.py # PMDG 737 NG3 SDK structs, CDU rendering, data manager
|
|
45
|
+
├── pmdg_detect.py # PMDG variant detection/probe, shared by tools/pmdg.py and tools/lvars.py
|
|
46
|
+
├── tools/
|
|
47
|
+
│ ├── __init__.py # @handle_simconnect_errors, @require_connection decorators
|
|
48
|
+
│ ├── connection_tools.py # connect_to_sim, disconnect_from_sim, get_connection_status
|
|
49
|
+
│ ├── simvars.py # SimVar CRUD via SimVarAccessor (1,080+ vars in the catalog)
|
|
50
|
+
│ ├── events.py # Event trigger/search + built-in catalog
|
|
51
|
+
│ ├── lvars.py # L-var read/write/search/enumerate/browse catalogs/calculator code
|
|
52
|
+
│ ├── pmdg.py # PMDG tools — auto-dispatch to 777 or 737 NG3
|
|
53
|
+
│ ├── aircraft.py # get_aircraft_snapshot — combined state snapshot
|
|
54
|
+
│ ├── facilities.py # Airport/navaid lookup on top of facilities.py
|
|
55
|
+
│ ├── flight.py # load/save flight, load flight plan, spawn AI object
|
|
56
|
+
│ ├── hubhop.py # HubHop preset search, exposed as MCP tools
|
|
57
|
+
│ ├── utilities.py # send_sim_text, set_aircraft_position
|
|
58
|
+
│ ├── models.py # Shared Pydantic result/error models
|
|
59
|
+
│ └── formatting.py # Pagination and markdown-table helpers
|
|
60
|
+
├── resources/
|
|
61
|
+
│ ├── documentation.py # Embedded docs served as MCP resources
|
|
62
|
+
│ └── state.py # Live connection/aircraft state resources
|
|
63
|
+
├── prompts/
|
|
64
|
+
│ └── templates.py # debug_simvar, analyze_aircraft_vars, rpn_helper, etc.
|
|
65
|
+
├── data/
|
|
66
|
+
│ ├── catalog.py # L-var catalog loader and search engine
|
|
67
|
+
│ ├── simvar_catalog.py # SimVar catalog loader, unit resolution, string-var detection
|
|
68
|
+
│ ├── hubhop.py # MobiFlight HubHop API client (CLI + library), in-memory cache
|
|
69
|
+
│ ├── pmdg_777.json # PMDG 777 catalog (1,607 vars, 28 panels)
|
|
70
|
+
│ ├── pmdg_737.json # PMDG 737 NG3 catalog (1,861 vars, 27 panels)
|
|
71
|
+
│ └── simvars_catalog.json # Built-in SimVar catalog (1,080+ vars, 25 categories)
|
|
72
|
+
├── vendor/ # Byte-faithful to upstream except two documented local changes
|
|
73
|
+
│ ├── simconnect_mobiflight.py # unmodified
|
|
74
|
+
│ └── mobiflight_variable_requests.py # log-level demotion + response-channel routing
|
|
75
|
+
└── docs/ # Embedded markdown documentation, served as MCP resources
|
|
76
|
+
├── overview.md, simvars.md, events.md, rpn.md, lvars.md, best_practices.md
|
|
77
|
+
└── pmdg_777.md, pmdg_737.md
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Known Sim Behaviours
|
|
81
|
+
|
|
82
|
+
Measured against a live sim during this project. Each cost real investigation to establish, so they are recorded here rather than left to be rediscovered. This is a reference, not a narrative.
|
|
83
|
+
|
|
84
|
+
- **MSFS 2024 packs facility records on the wire.** The installed `SimConnect.Enum` bindings for `SIMCONNECT_DATA_FACILITY_AIRPORT/WAYPOINT/NDB/VOR` are stale in two independent ways: they declare a single `Icao[9]` field where MSFS 2024 actually sends `Ident[6]` + `Region[3]`, and they assume ordinary 8-byte-aligned structs where the wire records are packed with no padding. `facilities.py` defines its own structs with `_pack_ = 1` and the Ident/Region split; verified strides are AIRPORT 33, WAYPOINT 37, NDB 41, VOR 77. Committed fixtures in `tests/fixtures/facilities/` replay real captured bytes, so a regression back to 8-byte alignment fails the suite immediately.
|
|
85
|
+
- **Only the airport facility list is world-wide** (85,249 records, unrelated to the aircraft's position). Waypoints, NDBs and VORs are a "reality bubble" scoped to wherever the aircraft currently is — all measured within ~193 nm of it — so they must not be cached across a reposition. Only AIRPORT is cached (`SimConnectManager._facility_cache`); the other three are recollected on every call.
|
|
86
|
+
- **`MF.LVars.List` is capped at 1000 names and still sends its end sentinel**, so a truncated response is indistinguishable from a complete one at the protocol level. `msfs_list_lvars` reports `truncated: true` when the raw pre-filter wire count hits the cap. Treat any one source — a catalog or a live listing — as a starting point, not a guaranteed inventory: `msfs_get_lvar` reads any name you supply, whether or not it surfaced there.
|
|
87
|
+
- **The WASM module ignores a repeated identical command.** Sending `MF.LVars.List` twice in a row returns nothing the second time. A trailing space does not help — this is not a byte-level dedupe check — and the state survives reconnection, so it lives in the WASM module itself, not the client. The fix that works reliably: send a different, zero-side-effect RPN command (a bare `MF.SimVars.Set.1` literal, which stores nothing) immediately before `MF.LVars.List` to re-arm it. See the call site in `tools/lvars.py` before "simplifying" this away.
|
|
88
|
+
- **PMDG aircraft ignore default key events.** A `(>K:PARKING_BRAKES)` that does nothing on a loaded PMDG aircraft is the aircraft's own behaviour — PMDG reimplements most default events internally rather than responding to them — not a broken mechanism. Use `msfs_send_pmdg_event` for PMDG control surfaces instead of `msfs_trigger_event`.
|
|
89
|
+
|
|
90
|
+
- **Saving a flight freezes SimConnect for several seconds.** `FlightSave` writes its `.FLT` almost immediately (~0.13s for a 69 KB file) and returns `S_OK`, but MSFS then stops answering SimConnect requests entirely while it finishes — measured from 0.7s up to 14.5s on the same aircraft and session, with no identified cause for the variance. During that window every read fails with a timeout whose message blames a paused or loading sim, which is wrong. `msfs_save_flight` therefore polls until the sim answers again before returning, so its contract is "when this returns, the sim is usable"; `msfs_load_flight` and `msfs_load_flight_plan` use the same wait (a load's own stall measured only ~0.9s). Do not remove that wait, and do not "fix" it by reconnecting — the connection is fine, the sim is busy.
|
|
91
|
+
|
|
92
|
+
- **PMDG event parameters are not always the documented switch positions.** `EVT_OH_ELEC_BATTERY_SWITCH` with `parameter=1` behaves as a *toggle* (0 → 1, then 1 → 0); `parameter=0`, `2` and no parameter all do nothing, even though the catalog's `values` map for `ELEC_BatSelector` reads `{"0": "OFF 1", "2": "ON"}` (itself visibly malformed). Guarded switches also need their guard lifted first. When a PMDG event appears not to work, read the field back rather than trusting the catalog's value map — the tool reports the send honestly as unconfirmed precisely because the sim gives no acknowledgement.
|
|
93
|
+
|
|
94
|
+
## Extending the Aircraft L-Var Catalog
|
|
95
|
+
|
|
96
|
+
The catalog system provides searchable, human-readable L-var databases per aircraft. When `msfs_search_lvars("seatbelt")` is called, the server auto-detects the loaded aircraft in two steps: PMDG's own client-data-area probe first (`pmdg_detect.detect_or_probe_pmdg_catalog` — the same authoritative probe `msfs_get_pmdg_var`/`msfs_get_pmdg_cdu` use, confirming which SDK is actually loaded regardless of what `TITLE`/`ATC_MODEL` say), then every catalog's own `title_pattern` matched against `TITLE`/`ATC_MODEL` as the fallback. The fallback is the *only* mechanism available to a third-party catalog dropped into `data/` (e.g. a regenerated Fenix catalog — see "Fenix A320/A321 Notes" below), since the probe only knows about PMDG. Both `msfs_search_lvars` and `msfs_browse_lvar_catalog` report which of the two resolved the catalog in `message`, so a caller can tell a live-confirmed detection apart from a plain text match — and when neither finds anything, the search spans every catalog and says so rather than guessing.
|
|
97
|
+
|
|
98
|
+
A real, live-verified gap this two-step order fixes: a PMDG 737-800's `TITLE` can read `'737-800 PAX SSW TC'` — no "PMDG" substring at all — which fails every bundled catalog's `title_pattern` on its own. Before the probe was added to catalog auto-detection, `msfs_search_lvars` on that aircraft silently searched every bundled catalog (all PMDG) instead of scoping to the one actually loaded, with only an easy-to-miss footer note distinguishing the two.
|
|
99
|
+
|
|
100
|
+
### Adding a New Aircraft
|
|
101
|
+
|
|
102
|
+
Create a JSON file in `src/simconnect_mcp/data/` (e.g., `fbw_a320.json`). All `*.json` files in this directory are auto-discovered on startup.
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"aircraft": "FlyByWire A320neo",
|
|
107
|
+
"title_pattern": "FlyByWire",
|
|
108
|
+
"variables": [
|
|
109
|
+
{
|
|
110
|
+
"name": "A32NX_AUTOPILOT_1_ACTIVE",
|
|
111
|
+
"display_name": "Autopilot 1 Active",
|
|
112
|
+
"category": "Autopilot",
|
|
113
|
+
"prefix": "A32NX",
|
|
114
|
+
"writable": true,
|
|
115
|
+
"values": {"0": "Off", "1": "On"}
|
|
116
|
+
}
|
|
117
|
+
],
|
|
118
|
+
"panels": {
|
|
119
|
+
"Autopilot": [
|
|
120
|
+
"A32NX_AUTOPILOT_1_ACTIVE",
|
|
121
|
+
"A32NX_AUTOPILOT_2_ACTIVE"
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Field Reference
|
|
128
|
+
|
|
129
|
+
| Field | Required | Description |
|
|
130
|
+
|-------|----------|-------------|
|
|
131
|
+
| `aircraft` | Yes | Human-readable aircraft name |
|
|
132
|
+
| `title_pattern` | Yes | Substring matched against `TITLE` SimVar for auto-detection (case-insensitive) |
|
|
133
|
+
| `variables` | Yes | Array of variable definitions |
|
|
134
|
+
| `variables[].name` | Yes | L-var name (without `L:` prefix) |
|
|
135
|
+
| `variables[].display_name` | Yes | Human-readable name for search and display |
|
|
136
|
+
| `variables[].category` | No | System/panel category for grouping |
|
|
137
|
+
| `variables[].prefix` | No | Variable prefix (for filtering) |
|
|
138
|
+
| `variables[].writable` | No | Whether the variable can be written (default: false) |
|
|
139
|
+
| `variables[].values` | No | Map of numeric values to descriptions (e.g., `{"0": "Off", "1": "On"}`) |
|
|
140
|
+
| `panels` | No | Groups variable names by physical panel/section |
|
|
141
|
+
|
|
142
|
+
### Tips for Building Catalogs
|
|
143
|
+
|
|
144
|
+
- **From existing tools:** export variable lists from FSUIPC, MobiFlight Connector, or SPAD.neXt
|
|
145
|
+
- **From source code:** if the aircraft is open source (e.g., FlyByWire), extract variable names from the codebase
|
|
146
|
+
- **By discovery:** use `msfs_list_lvars` to enumerate variables on a loaded aircraft (capped at 1000 names — see "Known Sim Behaviours" above), then categorize by prefix patterns
|
|
147
|
+
- **The `title_pattern`** should match a unique substring from `msfs_get_simvar("TITLE")`
|
|
148
|
+
|
|
149
|
+
## Fenix A320/A321 Notes
|
|
150
|
+
|
|
151
|
+
The Fenix uses a proprietary internal system with specific patterns you need to know when interacting with it.
|
|
152
|
+
|
|
153
|
+
**No catalog is bundled for the Fenix.** `data/fenix_a320.json` (1,433 plain L-vars, no SDK struct fields) was removed once HubHop's own `FenixSim` coverage overtook it (2,273 presets, community-maintained, current the moment Fenix ships an update). Search it live with `msfs_search_hubhop(vendor="FenixSim")`, or regenerate a local catalog file with `data/hubhop.py`'s CLI --
|
|
154
|
+
`python -m simconnect_mcp.data.hubhop --vendor FenixSim --aircraft-name "Fenix A320/A321" --title-pattern Fenix -o fenix_a320.json` -- and drop it into `src/simconnect_mcp/data/`; every `*.json` there is auto-discovered on startup, so it starts working immediately with no code change. The prefix/FCU/button knowledge below still applies regardless of whether a catalog is loaded, since it describes the sim's behaviour, not catalog data.
|
|
155
|
+
|
|
156
|
+
### Variable Prefix Convention
|
|
157
|
+
|
|
158
|
+
| Prefix | Type | Writable | Description |
|
|
159
|
+
|--------|------|----------|-------------|
|
|
160
|
+
| `S_` | Switch | Yes | Toggle switches, selector positions |
|
|
161
|
+
| `A_` | Analog | Yes | Rotary knobs with numeric positions |
|
|
162
|
+
| `E_` | Event counter | Yes | Rotary encoders (increment/decrement by changing value) |
|
|
163
|
+
| `N_` | Numeric | No | Display readouts, computed values |
|
|
164
|
+
| `I_` | Indicator | No | Status lights, switch position indicators |
|
|
165
|
+
| `B_` | Boolean | No | On/off indicator lights |
|
|
166
|
+
|
|
167
|
+
### Counter-Based FCU Controls
|
|
168
|
+
|
|
169
|
+
Fenix FCU controls (altitude, heading, speed, V/S) do **NOT** accept direct value writes. They use a counter-based pattern:
|
|
170
|
+
|
|
171
|
+
1. Read the current counter value from the `E_` variable (e.g., `E_FCU_ALTITUDE`)
|
|
172
|
+
2. Read the current display value from the `N_` variable (e.g., `N_FCU_ALTITUDE`)
|
|
173
|
+
3. Calculate the number of steps needed
|
|
174
|
+
4. For altitude: set `S_FCU_ALTITUDE_SCALE` to `0` (force 100ft mode) first
|
|
175
|
+
5. Increment/decrement the `E_` counter by 1 for each step, with 15ms delay between steps
|
|
176
|
+
6. Restore the scale mode after
|
|
177
|
+
|
|
178
|
+
Each change of ±1 in the counter triggers one knob click in the sim. The Fenix detects the **direction of change**, not the absolute value.
|
|
179
|
+
|
|
180
|
+
### Button Transitions
|
|
181
|
+
|
|
182
|
+
Some Fenix switches are momentary buttons that need a press-release cycle: set to 1, wait 200ms, set to 0.
|
|
183
|
+
|
|
184
|
+
### Write Method
|
|
185
|
+
|
|
186
|
+
The Fenix responds to native SimConnect `SetDataOnSimObject` for L-var writes, NOT to MobiFlight RPN `set()` commands. The `msfs_set_lvar` tool already uses the native method.
|
|
187
|
+
|
|
188
|
+
## Running Tests
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
uv run pytest # all tests (mocked; no MSFS required)
|
|
192
|
+
uv run pytest -v # verbose
|
|
193
|
+
uv run pytest -k search # only search tests
|
|
194
|
+
uv run pytest -m live # live suite against a real, running MSFS instance
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Tests mock SimConnect so the default run happens without MSFS. The `conftest.py` fixture provides a mock SimConnect with realistic SimVar values.
|
|
198
|
+
|
|
199
|
+
### What the live suite is for
|
|
200
|
+
|
|
201
|
+
`tests/live/` is deliberately small, and stays that way by one rule: **a
|
|
202
|
+
live test earns its place only if a self-consistent mock could agree with
|
|
203
|
+
itself regardless of whether the code is right.** A round trip on one L-var
|
|
204
|
+
name proves nothing about encoding, because a mangled datum name would make
|
|
205
|
+
the write and the read-back agree with each other on the wrong variable —
|
|
206
|
+
see `test_two_distinct_lvars_do_not_collide` in `test_live_lvars.py` for the
|
|
207
|
+
test that actually closes that gap, and its docstring for the full
|
|
208
|
+
reasoning. Anything a mock *can* settle — pure Python logic, cache
|
|
209
|
+
bookkeeping, message wording, string matching against a catalog — belongs
|
|
210
|
+
in the mocked suite instead, however tempting it is to also check it live.
|
|
211
|
+
|
|
212
|
+
What's left after that filter is a short, specific list: does the real DLL's
|
|
213
|
+
own unit conversion match the physical constant we assume; does a real
|
|
214
|
+
SimConnect wire reply decode correctly through structs this project has
|
|
215
|
+
already caught the installed package mis-declaring once (`RecvException` in
|
|
216
|
+
`dispatch.py`); does a real airframe's calculated variables actually reject
|
|
217
|
+
a write (or silently ignore it, which is the bug this layer replaces); does
|
|
218
|
+
the real MobiFlight WASM module's undocumented quirks (repeated-command
|
|
219
|
+
drop, definition-ID-0 routing) behave the way the workaround assumes; and
|
|
220
|
+
whether a real PMDG's binary client-data area answers a probe the way its
|
|
221
|
+
struct decode expects. None of that can be established by a mock inventing
|
|
222
|
+
its own answer and checking it against itself.
|
|
223
|
+
|
|
224
|
+
Prefer freezing a live finding into a committed fixture over adding another
|
|
225
|
+
live test. `tests/fixtures/facilities/` holds real SimConnect wire bytes
|
|
226
|
+
captured once from a live session, replayed by the *mocked*
|
|
227
|
+
`tests/test_facilities_parsing.py` on every run, with no simulator and no
|
|
228
|
+
flakiness — strictly better than a live test for anything it can cover,
|
|
229
|
+
because it pins the exact discovery deterministically instead of
|
|
230
|
+
re-discovering it (or failing to) on whatever aircraft happens to be loaded
|
|
231
|
+
that day. Reach for that pattern first; reach for `tests/live/` only for the
|
|
232
|
+
residue no fixture can freeze because the claim is about behaviour, not
|
|
233
|
+
about a fixed byte layout.
|
|
234
|
+
|
|
235
|
+
`tests/live/test_live_pmdg.py`'s tests need a real PMDG 737/777 loaded and
|
|
236
|
+
skip -- rather than fail -- when the loaded aircraft doesn't look like one
|
|
237
|
+
(see that file's gate in `conftest.py`); a connection failure skips the
|
|
238
|
+
whole suite the same way.
|
|
239
|
+
|
|
240
|
+
The live suite under `tests/live/` is marked `@pytest.mark.live` and deselected by default (`pyproject.toml`'s `addopts = "-m 'not live'"`), so it never runs on a machine without MSFS — including CI. It requires MSFS running with an aircraft loaded; a connection failure is skipped rather than failed, but some files assume a specific aircraft is loaded (check that file's own module docstring before running it against an arbitrary airframe).
|
|
241
|
+
|
|
242
|
+
## Releasing
|
|
243
|
+
|
|
244
|
+
Pushing a `v*` tag is the whole release. `.github/workflows/release.yml` runs
|
|
245
|
+
the mocked suite on Windows, rewrites the version from the tag (minus its `v`)
|
|
246
|
+
into `pyproject.toml` and `server.json` via `scripts/set_version.py`, builds,
|
|
247
|
+
publishes to PyPI, publishes to the MCP Registry, and commits the bump back to
|
|
248
|
+
`main` in a separate job.
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
git tag v0.3.0
|
|
252
|
+
git push origin v0.3.0
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Notes for anyone changing this:
|
|
256
|
+
|
|
257
|
+
- **Do not hand-edit the version in more than one place.** `pyproject.toml` is
|
|
258
|
+
authoritative; `src/simconnect_mcp/__init__.py` resolves its own version from
|
|
259
|
+
installed metadata, and `server.json` is rewritten by the release. Tests in
|
|
260
|
+
`tests/test_packaging.py` fail if these drift apart.
|
|
261
|
+
- **`scripts/set_version.py` is unit-tested and pins its writes to LF** so it
|
|
262
|
+
produces identical bytes on the Linux runner and a Windows checkout. It is
|
|
263
|
+
covered by CI's lint step for the same reason: a bug there ships a wrong
|
|
264
|
+
version number to PyPI.
|
|
265
|
+
- **Both publish steps authenticate via OIDC, not stored tokens** — hence
|
|
266
|
+
`id-token: write` on the `publish` job. PyPI additionally requires a trusted
|
|
267
|
+
publisher registered once through the PyPI web UI (project `simconnect-mcp`,
|
|
268
|
+
owner `robin24`, repo `simconnect-mcp`, workflow `release.yml`, no
|
|
269
|
+
environment). Without it, `uv publish` fails with an authentication error.
|
|
270
|
+
- **The registry verifies PyPI ownership by finding `mcp-name:
|
|
271
|
+
io.github.robin24/simconnect-mcp` in the package description**, which is
|
|
272
|
+
`README.md`. Removing that marker from the README breaks registry publishing
|
|
273
|
+
even though nothing else notices.
|
|
274
|
+
- **CI must stay on `windows-latest`** — `dispatch.py` and `facilities.py`
|
|
275
|
+
import `ctypes.wintypes` at module level, which does not import on Linux.
|