grp-mcp-plugin 0.81.0rc12__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.
- grp_mcp_plugin-0.81.0rc12/.env.example +49 -0
- grp_mcp_plugin-0.81.0rc12/.gitattributes +6 -0
- grp_mcp_plugin-0.81.0rc12/.githooks/pre-commit +5 -0
- grp_mcp_plugin-0.81.0rc12/.github/workflows/ci.yml +90 -0
- grp_mcp_plugin-0.81.0rc12/.gitignore +56 -0
- grp_mcp_plugin-0.81.0rc12/CLAUDE.md +199 -0
- grp_mcp_plugin-0.81.0rc12/Edit-Connections.bat +66 -0
- grp_mcp_plugin-0.81.0rc12/Installation-and-Setup-Guide.md +541 -0
- grp_mcp_plugin-0.81.0rc12/KNOWLEDGE.md +3981 -0
- grp_mcp_plugin-0.81.0rc12/PKG-INFO +1921 -0
- grp_mcp_plugin-0.81.0rc12/README.md +1900 -0
- grp_mcp_plugin-0.81.0rc12/connections.example.json +34 -0
- grp_mcp_plugin-0.81.0rc12/kb_server.example.json +30 -0
- grp_mcp_plugin-0.81.0rc12/packaging/BUILD.md +406 -0
- grp_mcp_plugin-0.81.0rc12/packaging/INSTALL-grp-mcp.md +431 -0
- grp_mcp_plugin-0.81.0rc12/packaging/extension_README.md +78 -0
- grp_mcp_plugin-0.81.0rc12/packaging/manifest.json +137 -0
- grp_mcp_plugin-0.81.0rc12/packaging/plugin_README.md +92 -0
- grp_mcp_plugin-0.81.0rc12/packaging/plugin_mcp.json +13 -0
- grp_mcp_plugin-0.81.0rc12/packaging/server_entry.py +31 -0
- grp_mcp_plugin-0.81.0rc12/packaging/setup_entry.py +13 -0
- grp_mcp_plugin-0.81.0rc12/packaging/test_frozen.py +133 -0
- grp_mcp_plugin-0.81.0rc12/playwright/EXTENDING_ENDPOINTS.md +267 -0
- grp_mcp_plugin-0.81.0rc12/playwright/access_rights_modern.js +56 -0
- grp_mcp_plugin-0.81.0rc12/playwright/add_endpoint_action.js +121 -0
- grp_mcp_plugin-0.81.0rc12/playwright/add_endpoint_action_modern.js +124 -0
- grp_mcp_plugin-0.81.0rc12/playwright/add_endpoint_entity.js +167 -0
- grp_mcp_plugin-0.81.0rc12/playwright/add_endpoint_entity_modern.js +146 -0
- grp_mcp_plugin-0.81.0rc12/playwright/add_endpoint_masterdetail_modern.js +119 -0
- grp_mcp_plugin-0.81.0rc12/playwright/change_company_type_modern.js +54 -0
- grp_mcp_plugin-0.81.0rc12/playwright/delete_endpoint_entity_modern.js +50 -0
- grp_mcp_plugin-0.81.0rc12/playwright/delete_finyear.js +39 -0
- grp_mcp_plugin-0.81.0rc12/playwright/inspect_company_fields.js +53 -0
- grp_mcp_plugin-0.81.0rc12/playwright/inspect_grid_fields.js +50 -0
- grp_mcp_plugin-0.81.0rc12/playwright/inspect_screen_fields.js +43 -0
- grp_mcp_plugin-0.81.0rc12/playwright/inspect_segkey.js +41 -0
- grp_mcp_plugin-0.81.0rc12/playwright/inspect_sm207060.js +36 -0
- grp_mcp_plugin-0.81.0rc12/playwright/inspect_tabs_cs101500.js +72 -0
- grp_mcp_plugin-0.81.0rc12/playwright/populate_all_views.js +104 -0
- grp_mcp_plugin-0.81.0rc12/playwright/set_features_modern.js +77 -0
- grp_mcp_plugin-0.81.0rc12/playwright/setup_finyear.js +120 -0
- grp_mcp_plugin-0.81.0rc12/pyproject.toml +108 -0
- grp_mcp_plugin-0.81.0rc12/scripts/build_plugin_dist.py +208 -0
- grp_mcp_plugin-0.81.0rc12/scripts/check_secrets.py +228 -0
- grp_mcp_plugin-0.81.0rc12/scripts/e2e_stdio_check.py +64 -0
- grp_mcp_plugin-0.81.0rc12/scripts/eval_find_tool.py +346 -0
- grp_mcp_plugin-0.81.0rc12/scripts/mutate_download_by_id.py +152 -0
- grp_mcp_plugin-0.81.0rc12/scripts/mutate_name_layer.py +128 -0
- grp_mcp_plugin-0.81.0rc12/scripts/mutate_sdk_seam.py +116 -0
- grp_mcp_plugin-0.81.0rc12/src/grp_mcp/__init__.py +45 -0
- grp_mcp_plugin-0.81.0rc12/src/grp_mcp/acumatica.py +790 -0
- grp_mcp_plugin-0.81.0rc12/src/grp_mcp/aspx.py +2285 -0
- grp_mcp_plugin-0.81.0rc12/src/grp_mcp/cli.py +65 -0
- grp_mcp_plugin-0.81.0rc12/src/grp_mcp/config.py +279 -0
- grp_mcp_plugin-0.81.0rc12/src/grp_mcp/customization.py +195 -0
- grp_mcp_plugin-0.81.0rc12/src/grp_mcp/enforcement.py +669 -0
- grp_mcp_plugin-0.81.0rc12/src/grp_mcp/guide_nudge.py +68 -0
- grp_mcp_plugin-0.81.0rc12/src/grp_mcp/kb.py +158 -0
- grp_mcp_plugin-0.81.0rc12/src/grp_mcp/kb_client.py +480 -0
- grp_mcp_plugin-0.81.0rc12/src/grp_mcp/loaders.py +75 -0
- grp_mcp_plugin-0.81.0rc12/src/grp_mcp/preflight.py +120 -0
- grp_mcp_plugin-0.81.0rc12/src/grp_mcp/report_params.py +135 -0
- grp_mcp_plugin-0.81.0rc12/src/grp_mcp/screen.py +5714 -0
- grp_mcp_plugin-0.81.0rc12/src/grp_mcp/server.py +12843 -0
- grp_mcp_plugin-0.81.0rc12/src/grp_mcp/setup_map.json +860 -0
- grp_mcp_plugin-0.81.0rc12/src/grp_mcp/setup_ui.py +100 -0
- grp_mcp_plugin-0.81.0rc12/src/grp_mcp/tool_search.py +411 -0
- grp_mcp_plugin-0.81.0rc12/src/grp_mcp/ui.py +759 -0
- grp_mcp_plugin-0.81.0rc12/src/grp_mcp/writeback.py +470 -0
- grp_mcp_plugin-0.81.0rc12/tests/test_aspx.py +1524 -0
- grp_mcp_plugin-0.81.0rc12/tests/test_defect_register.py +914 -0
- grp_mcp_plugin-0.81.0rc12/tests/test_kb.py +91 -0
- grp_mcp_plugin-0.81.0rc12/tests/test_module10_bugfixes.py +343 -0
- grp_mcp_plugin-0.81.0rc12/tests/test_smoke.py +10403 -0
- grp_mcp_plugin-0.81.0rc12/tests/test_tool_search.py +367 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Default Acumatica instance (used when a tool is called without an `instance` arg).
|
|
2
|
+
# For multiple instances, use connections.json instead (see connections.example.json).
|
|
3
|
+
|
|
4
|
+
GRP_MCP_BASE_URL=https://your-instance.acumatica.com
|
|
5
|
+
GRP_MCP_CLIENT_ID=YOUR_CLIENT_ID@CompanyLogin
|
|
6
|
+
GRP_MCP_CLIENT_SECRET=your_client_secret
|
|
7
|
+
GRP_MCP_USERNAME=admin
|
|
8
|
+
GRP_MCP_PASSWORD=your_password
|
|
9
|
+
|
|
10
|
+
# Optional. Defaults shown.
|
|
11
|
+
GRP_MCP_ENDPOINT_NAME=Default
|
|
12
|
+
GRP_MCP_ENDPOINT_VERSION=24.200.001
|
|
13
|
+
# Tenant/company login name, used for OData (Generic Inquiry) calls and
|
|
14
|
+
# Customization API cookie login.
|
|
15
|
+
GRP_MCP_TENANT=
|
|
16
|
+
# Optional login branch.
|
|
17
|
+
GRP_MCP_BRANCH=
|
|
18
|
+
# --- write gates (default read-only; opt in) ---
|
|
19
|
+
# Allow record mutations (create/update, load, actions, import-scenario, note, attach).
|
|
20
|
+
GRP_MCP_ALLOW_WRITE=false
|
|
21
|
+
# Allow record deletes (stricter than write).
|
|
22
|
+
GRP_MCP_ALLOW_DELETE=false
|
|
23
|
+
# Gate Customization API writes (publish/import/unpublish). Default off.
|
|
24
|
+
# WARNING: publishing is website-level and affects ALL tenants on the instance.
|
|
25
|
+
GRP_MCP_ALLOW_PUBLISH=false
|
|
26
|
+
# Filesystem sandbox (read_roots / write_roots / max_file_bytes) is configured via
|
|
27
|
+
# connections.json only — see connections.example.json. Empty roots confine
|
|
28
|
+
# filesystem tools to the process working directory; whole-disk access is the
|
|
29
|
+
# explicit `allow_unrestricted_fs: true` opt-in. (This comment said "empty roots =
|
|
30
|
+
# unrestricted" for several releases. It was wrong — config.py is authoritative.)
|
|
31
|
+
|
|
32
|
+
# --- tool surface (token cost) ---
|
|
33
|
+
# grp-mcp registers 100+ tools. Serving all of them costs roughly 58k tokens of
|
|
34
|
+
# context BEFORE the agent does anything — about 28% of a 200k window
|
|
35
|
+
# (measured 2026-08-17 against the live registry).
|
|
36
|
+
#
|
|
37
|
+
# Set this to 1 and only a small core is LISTED (~10k tokens, an 82% cut). The
|
|
38
|
+
# rest are not removed: they stay classified, gated and preflighted, and the
|
|
39
|
+
# agent reaches them with find_tool("what you want") then call_tool(name, args).
|
|
40
|
+
# Recommended for day-to-day use; leave it off if a client of yours calls a
|
|
41
|
+
# non-core tool by name, since a pruned tool is not directly callable.
|
|
42
|
+
GRP_MCP_CORE_ONLY=0
|
|
43
|
+
|
|
44
|
+
# Persisting connection-profile changes from inside the agent (add_instance /
|
|
45
|
+
# remove_instance / set_active_instance with persist) additionally requires:
|
|
46
|
+
# GRP_MCP_ALLOW_ADMIN=1
|
|
47
|
+
|
|
48
|
+
# Path to a JSON file holding multiple named instances (optional).
|
|
49
|
+
# GRP_MCP_CONNECTIONS=C:\Temp\grp-mcp\connections.json
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# core.hooksPath points at .githooks, so these run as shell scripts. Git on Windows
|
|
2
|
+
# checks text files out as CRLF by default, and /bin/sh refuses a script whose
|
|
3
|
+
# shebang line ends in \r ("bad interpreter: /bin/sh^M"). Pin them to LF so the
|
|
4
|
+
# hooks stay executable on every clone, including this one after a fresh checkout.
|
|
5
|
+
.githooks/** text eol=lf
|
|
6
|
+
scripts/*.py text eol=lf
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Content-based secret gate. See scripts/check_secrets.py for why this exists
|
|
3
|
+
# rather than relying on .gitignore alone (2026-07-27 leak).
|
|
4
|
+
# Enable once per clone: git config core.hooksPath .githooks
|
|
5
|
+
python "$(git rev-parse --show-toplevel)/scripts/check_secrets.py" --staged || exit 1
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["**"]
|
|
6
|
+
pull_request:
|
|
7
|
+
schedule:
|
|
8
|
+
# Weekly, for the mcp-latest job below. Nothing else here needs a timer.
|
|
9
|
+
- cron: "17 6 * * 1"
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
test:
|
|
17
|
+
# Windows is where this actually runs (the paths, the .exe launchers, the
|
|
18
|
+
# file-locking failure modes), so it is not an afterthought row here.
|
|
19
|
+
runs-on: ${{ matrix.os }}
|
|
20
|
+
strategy:
|
|
21
|
+
fail-fast: false
|
|
22
|
+
matrix:
|
|
23
|
+
include:
|
|
24
|
+
- { os: ubuntu-latest, python: "3.10" }
|
|
25
|
+
- { os: ubuntu-latest, python: "3.13" }
|
|
26
|
+
- { os: windows-latest, python: "3.13" }
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
- uses: actions/setup-python@v5
|
|
30
|
+
with:
|
|
31
|
+
python-version: ${{ matrix.python }}
|
|
32
|
+
cache: pip
|
|
33
|
+
# The `search` extra is load-bearing for the COUNT: without fastembed,
|
|
34
|
+
# tests/test_tool_search.py importorskips and the suite silently runs ~21
|
|
35
|
+
# tests short while still reporting all-green. Those tests build the corpus
|
|
36
|
+
# from the registry and never download the ONNX model.
|
|
37
|
+
- run: pip install -e ".[dev,search]"
|
|
38
|
+
- run: python -m pytest tests/ -q
|
|
39
|
+
|
|
40
|
+
lint:
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/checkout@v4
|
|
44
|
+
- uses: actions/setup-python@v5
|
|
45
|
+
with: { python-version: "3.13", cache: pip }
|
|
46
|
+
- run: pip install -e ".[dev]"
|
|
47
|
+
# Narrow by design (E9,F,BLE) — see the [tool.ruff] comment in pyproject.
|
|
48
|
+
- run: python -m ruff check src/ tests/ scripts/
|
|
49
|
+
|
|
50
|
+
package:
|
|
51
|
+
# The sdist is PUBLIC and permanent. Twice now something private reached a
|
|
52
|
+
# BUILT artifact while the working tree looked clean: machine-specific
|
|
53
|
+
# absolute paths (2026-08-17) and a live bearer token in a test (same day,
|
|
54
|
+
# caught by scanning the artifact, not the tree). Scanning the tree is not
|
|
55
|
+
# the same check, so this scans what would actually be uploaded.
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
steps:
|
|
58
|
+
- uses: actions/checkout@v4
|
|
59
|
+
- uses: actions/setup-python@v5
|
|
60
|
+
with: { python-version: "3.13", cache: pip }
|
|
61
|
+
- run: pip install build
|
|
62
|
+
- run: python -m build
|
|
63
|
+
- name: Scan the built sdist for credentials
|
|
64
|
+
run: python scripts/check_secrets.py --sdist dist/*.tar.gz
|
|
65
|
+
- name: Scan the built wheel for credentials
|
|
66
|
+
run: python scripts/check_secrets.py --sdist dist/*.whl
|
|
67
|
+
- uses: actions/upload-artifact@v4
|
|
68
|
+
with:
|
|
69
|
+
name: dist
|
|
70
|
+
path: dist/
|
|
71
|
+
|
|
72
|
+
mcp-latest:
|
|
73
|
+
# EARLY WARNING, not a gate. server.py reaches two private MCP-SDK attributes
|
|
74
|
+
# through the seam (_tool_manager._tools, _lowlevel_server.instructions).
|
|
75
|
+
# pyproject pins mcp>=2,<3, so a minor release cannot break an install — but
|
|
76
|
+
# it can break US, silently, at the next unpinned upgrade. This job finds out
|
|
77
|
+
# first. It is allowed to fail.
|
|
78
|
+
runs-on: ubuntu-latest
|
|
79
|
+
continue-on-error: true
|
|
80
|
+
steps:
|
|
81
|
+
- uses: actions/checkout@v4
|
|
82
|
+
- uses: actions/setup-python@v5
|
|
83
|
+
with: { python-version: "3.13", cache: pip }
|
|
84
|
+
- run: pip install -e ".[dev,search]"
|
|
85
|
+
- run: pip install --upgrade mcp
|
|
86
|
+
- run: python -c "import mcp; print('mcp', mcp.__version__ if hasattr(mcp,'__version__') else '?')"
|
|
87
|
+
# The three seam-degradation tests are the ones that matter here: the seam
|
|
88
|
+
# returns {} / "" / False rather than raising, so a moved attribute shows up
|
|
89
|
+
# as a silent no-op unless something asserts otherwise.
|
|
90
|
+
- run: python -m pytest tests/ -q
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
env/
|
|
11
|
+
|
|
12
|
+
# Secrets / local config
|
|
13
|
+
.env
|
|
14
|
+
connections.json
|
|
15
|
+
kb_server.json
|
|
16
|
+
|
|
17
|
+
# IDE
|
|
18
|
+
.vscode/
|
|
19
|
+
.idea/
|
|
20
|
+
.DS_Store
|
|
21
|
+
|
|
22
|
+
# Snapshot dumps (business data)
|
|
23
|
+
snapshots/
|
|
24
|
+
|
|
25
|
+
# Playwright debug screenshots
|
|
26
|
+
playwright/shots/
|
|
27
|
+
|
|
28
|
+
# Credential files — the bare name was not enough. A connections.json.bak-<date>
|
|
29
|
+
# backup (written by the config editor) slipped past `connections.json` and was
|
|
30
|
+
# committed to a PUBLIC repo on 2026-07-27. Match every variant, not the one name.
|
|
31
|
+
connections.json*
|
|
32
|
+
kb_server.json*
|
|
33
|
+
*.bak
|
|
34
|
+
*.bak-*
|
|
35
|
+
.env
|
|
36
|
+
|
|
37
|
+
# graphify is no longer used in this repo. The rule stays because the tool writes
|
|
38
|
+
# multi-MB generated graphs into these directories the moment anyone runs it, and
|
|
39
|
+
# `git status` collapses an untracked directory to a single line — which is exactly
|
|
40
|
+
# how a 2.8 MB graph.json gets swept into a `git add -A` unnoticed.
|
|
41
|
+
graphify-out/
|
|
42
|
+
graphify-out-code*/
|
|
43
|
+
|
|
44
|
+
# generated repowiki from an external tool — huge, and some paths exceed the Windows limit
|
|
45
|
+
.qoder/
|
|
46
|
+
|
|
47
|
+
# Frozen-build scratch (packaging/BUILD.md). buildenv/ already ignores itself via
|
|
48
|
+
# the .gitignore venv writes, but pybuild/ does not: only pybuild/dist matches the
|
|
49
|
+
# `dist/` rule above, so work/ and the generated .spec were left untracked and a
|
|
50
|
+
# `git add -A` would sweep in ~100MB of PyInstaller intermediates.
|
|
51
|
+
buildenv/
|
|
52
|
+
pybuild/
|
|
53
|
+
extbuild/
|
|
54
|
+
setupbuild/
|
|
55
|
+
ext/
|
|
56
|
+
*.mcpb
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
`grp-mcp` — an MCP server (stdio, `mcp.server.MCPServer`) that exposes an **Acumatica ERP** instance as ~120 agent
|
|
8
|
+
tools. Python 3.10+, `src/` layout, published to PyPI as `grp-mcp`. Entry points: `grp_mcp.server:main`
|
|
9
|
+
(the MCP server) and `grp_mcp.ui:main` (a localhost config page).
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install -e ".[dev]"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
./.venv/Scripts/python.exe -m pytest tests/ -q
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
711 tests, all pure logic — **no live Acumatica instance is needed or used**. The `search` extra is
|
|
22
|
+
required for the full count: without `fastembed`, `tests/test_tool_search.py` collects 0 and the suite
|
|
23
|
+
silently runs 21 tests short. Single test / file:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
./.venv/Scripts/python.exe -m pytest tests/test_smoke.py::test_delete_gate_is_independent_of_write -q
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Retrieval quality of `find_tool` (not part of pytest — these print a score, they don't assert):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
./.venv/Scripts/python.exe scripts/eval_find_tool.py
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
One runner, four datasets (`--dataset core|holdout|holdout2|holdout3`, `--list` to see them; all four
|
|
36
|
+
by default). `core` is the calibration set and the hold-outs are the anti-overfit checks written after
|
|
37
|
+
it was already passing — tuning against `core` alone overfits it. `holdout2` also measures the NOISE
|
|
38
|
+
CEILING: off-domain queries that must reach no tool, which is what the WEAK threshold has to clear.
|
|
39
|
+
`eval_find_tool_mined.py` stays separate — it mines intents from local Claude transcripts rather than
|
|
40
|
+
reading a fixed list, and reads machine-private files. `scripts/mutate_*.py` are mutation checks — they break a module on purpose,
|
|
41
|
+
run the matching test file, and report which tests went red (a test nobody has watched fail is a claim,
|
|
42
|
+
not a check). They restore the file in a `finally`.
|
|
43
|
+
|
|
44
|
+
Lint (narrow by design — `E9,F,BLE`; style rules are deliberately not selected):
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
./.venv/Scripts/python.exe -m ruff check src/ tests/ scripts/
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`BLE` is the point of enabling it at all: the package carries 150+ `# noqa: BLE001` suppressions, and
|
|
51
|
+
until 2026-08-17 they were written for a linter that never ran, so a deliberate blind except was
|
|
52
|
+
indistinguishable from an accidental one. Every suppression now carries a reason.
|
|
53
|
+
|
|
54
|
+
CI (`.github/workflows/ci.yml`) runs the suite on ubuntu 3.10/3.13 and windows 3.13, lints, and builds
|
|
55
|
+
the sdist+wheel and scans **the built artifacts** with `check_secrets.py` — scanning the tree is not the
|
|
56
|
+
same check, and twice something private reached an artifact while the tree looked clean. A
|
|
57
|
+
`continue-on-error` job tracks the latest `mcp` release as early warning for the SDK seam.
|
|
58
|
+
|
|
59
|
+
Enable the secret gate once per clone (content-based, scans staged diffs — see `scripts/check_secrets.py`
|
|
60
|
+
for the 2026-07-27 leak it exists for):
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
git config core.hooksPath .githooks
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Release (maintainers): bump `version` in `pyproject.toml`, `python -m build`, **audit the built sdist AND
|
|
67
|
+
wheel** (`python scripts/check_secrets.py --sdist dist/*.tar.gz`, plus grep for tenant/host names — the
|
|
68
|
+
sdist is public and permanent), then `twine upload dist/grp_mcp-<version>*`. Full checklist in
|
|
69
|
+
KNOWLEDGE.md §10.
|
|
70
|
+
|
|
71
|
+
## Architecture
|
|
72
|
+
|
|
73
|
+
### Five client planes
|
|
74
|
+
|
|
75
|
+
The whole design follows from one fact: **no single Acumatica API can drive every screen.** Each plane
|
|
76
|
+
has capability holes the others cover, so tools are grouped by plane, not by feature.
|
|
77
|
+
|
|
78
|
+
| Plane | Module | Reaches | Cannot |
|
|
79
|
+
|---|---|---|---|
|
|
80
|
+
| Contract REST (OAuth2) | `acumatica.py` | endpoint entities, bulk CRUD, actions, reports | context/master-detail screens |
|
|
81
|
+
| DAC / GI OData | `acumatica.py` | any table incl. config singletons, mandatory-field CSDL | read-only |
|
|
82
|
+
| Classic screen SOAP | `screen.py` (`ScreenClient`) | context/wizard screens by replaying UI commands | can't address an *existing* grid row by key; some action tags are silent no-ops |
|
|
83
|
+
| Modern UI-JSON | `screen.py` (`ui_*` methods) | grid CRUD, dialogs, enum values, live action state | `/structure` absent on older builds; one container per view name, so classic "ViewName: 2" duplicates are invisible |
|
|
84
|
+
| Classic ASPX callbacks | `aspx.py` | the REAL validation text behind a failed save; actions + confirms on keyed records | not a general write path |
|
|
85
|
+
|
|
86
|
+
Both classic SOAP and modern UI-JSON live in `screen.py` because they **share one login session**
|
|
87
|
+
(`_SESSION_CACHE`) — logins hold scarce "Web Services API Users" seats (trial = 2), so sessions are
|
|
88
|
+
pooled and logged out at shutdown. Never mix planes inside one session: they hold separate dirty-graph
|
|
89
|
+
state and collide.
|
|
90
|
+
|
|
91
|
+
KNOWLEDGE.md §1 is the plane routing table; §3 (classic command mechanics) and §4 (modern JSON protocol)
|
|
92
|
+
are the protocol references. Read them before touching `screen.py`.
|
|
93
|
+
|
|
94
|
+
### Tool registration and the core-only mode
|
|
95
|
+
|
|
96
|
+
Every tool is a `@mcp.tool()` in `server.py` (12.5k lines — one file by design; `_TOOL_NOTES` holds the
|
|
97
|
+
long prose that would otherwise bloat docstrings/schemas). At import time, after all registrations:
|
|
98
|
+
|
|
99
|
+
1. `_apply_core_only_gating()` snapshots the full registry into `_ALL_TOOLS`, then — **only if
|
|
100
|
+
`GRP_MCP_CORE_ONLY=1`** — deletes everything outside `CORE_TOOLS` from the SDK's registry.
|
|
101
|
+
2. `_sync_instruction_tool_count()` rewrites the served MCP `instructions` with the real count and, in
|
|
102
|
+
core-only mode, the sentence telling the client that the unlisted tail exists.
|
|
103
|
+
|
|
104
|
+
Pruning the registry (rather than skipping the decorator) keeps **one definition path**: a hidden tool is
|
|
105
|
+
still classified, still gated, still preflighted, and still reachable via `find_tool` (semantic search,
|
|
106
|
+
`tool_search.py`) → `call_tool(tool, args)`, which dispatches to the same function.
|
|
107
|
+
|
|
108
|
+
Both steps need MCP-SDK internals, so all of it goes through the **SDK seam** at the top of `server.py`
|
|
109
|
+
— `_registry()`, `_instructions()`, `_set_instructions()`. Never reach `mcp._tool_manager` /
|
|
110
|
+
`mcp._lowlevel_server` anywhere else; `test_sdk_private_access_stays_inside_the_seam` fails the build if
|
|
111
|
+
you do. `_registry()` returns the SDK's **live mutable dict, not a copy** — core-only prunes it in place,
|
|
112
|
+
so a copy silently turns pruning into a no-op that still reports a pruned count. The seam degrades (`{}`
|
|
113
|
+
/ `""` / `False`) rather than raising, and three tests plus `scripts/mutate_sdk_seam.py` keep that
|
|
114
|
+
degradation loud.
|
|
115
|
+
|
|
116
|
+
The seam paid for itself on the `mcp` 2.0 migration (`FastMCP` → `MCPServer`, `mcp.server.fastmcp`
|
|
117
|
+
deleted, low-level Server rebuilt): the whole cost inside `server.py` was the import, the constructor,
|
|
118
|
+
and one attribute name. Two measured facts worth keeping — `instructions` has **no setter in either
|
|
119
|
+
major** (`fset` is `None`), so the private write path is permanent rather than a v1 wart; and 2.0 still
|
|
120
|
+
exposes `_tool_manager._tools` as a dict of tools carrying `.description` and `.parameters`, which is
|
|
121
|
+
why `_registry()` and the `find_tool` corpus needed no change. Neither is promised by anything.
|
|
122
|
+
`pyproject.toml` now pins `mcp>=2,<3`; the sibling **grp-kb** still pins `<2`, which is fine — it runs
|
|
123
|
+
as a separate subprocess with its own environment, so the two majors never share an interpreter.
|
|
124
|
+
|
|
125
|
+
### Enforcement layer (`enforcement.py`) — the load-bearing invariant
|
|
126
|
+
|
|
127
|
+
`TOOL_CLASS` maps **every** registered tool to a risk class (`read`/`write`/`delete`/`publish`/`bg_job`/
|
|
128
|
+
`diagnostic_write`/`filesystem`/`admin`). It documents and checks; it does **not** re-route gates — the
|
|
129
|
+
real `_require_write` / `_require_delete` / `_require_publish` / `_require_admin` calls stay inline in
|
|
130
|
+
`server.py` (several are runtime branches — a tool that write-gates normally but delete-gates on a
|
|
131
|
+
destructive action). AST-based tests in `tests/test_smoke.py` cross-check the registry against the source
|
|
132
|
+
so an unguarded mutation cannot ship. Escape hatches are `GATELESS_MUTATION_ALLOW` and `DELEGATED_GATE`
|
|
133
|
+
— keep them tiny and justified.
|
|
134
|
+
|
|
135
|
+
### Adding or changing a tool — the checklist the tests enforce
|
|
136
|
+
|
|
137
|
+
1. `@mcp.tool()` in `server.py`.
|
|
138
|
+
2. Add it to `enforcement.TOOL_CLASS` (`test_every_registered_tool_is_classified` fails otherwise, in
|
|
139
|
+
both directions — a stale entry is also an error).
|
|
140
|
+
3. If it mutates ERP data, call a real `_require_*` gate **lexically in the tool body**
|
|
141
|
+
(`test_erp_mutation_tools_are_actually_gated` reads the AST, not the runtime).
|
|
142
|
+
4. Wire the KB write-preflight: `@_preflight_write(...)` below `@mcp.tool()`, or inline for the four
|
|
143
|
+
arteries in `PREFLIGHT_INLINE`. Coverage is tested.
|
|
144
|
+
5. If it's a read whose result should carry the nudge, `@_hint_only`.
|
|
145
|
+
|
|
146
|
+
### Write honesty
|
|
147
|
+
|
|
148
|
+
A clean `ok:true` from Acumatica proves nothing — success-shaped no-ops are routine. So:
|
|
149
|
+
|
|
150
|
+
- `writeback.py` (pure, no I/O) compares a read-back against what was sent. Its rule: **never report
|
|
151
|
+
`rejected` for something merely uncomparable** — a false "your write failed" sends the caller off to
|
|
152
|
+
redo work that already succeeded.
|
|
153
|
+
- `enforcement.verify_state()` / `normalize_verification()` collapse the scattered per-plane flags
|
|
154
|
+
(`save_verified`, `delete_verified`, `grid_rows_readable`, `graph_is_dirty`) into one
|
|
155
|
+
`verified`/`rejected`/`unverified` verdict, worst-case wins. There is deliberately **no rollback
|
|
156
|
+
state**: automatic cross-plane undo is unsafe, so a failure is surfaced, never reversed.
|
|
157
|
+
- Any non-verified mutation gets `_DIAGNOSE_ROUTE` stapled on — it names *instruments* (`aspx_screen_action`,
|
|
158
|
+
`diagnose_save_error`, `SMPerformanceInfoTraceMessages`), never a guessed cause. That wording is
|
|
159
|
+
deliberate; see the comment above it.
|
|
160
|
+
|
|
161
|
+
### KB-first policy
|
|
162
|
+
|
|
163
|
+
Before any write, the server itself consults the KB — it does not trust the agent to have done it.
|
|
164
|
+
`kb_client.py` spawns the sibling **grp-kb** package as a subprocess (launch spec from
|
|
165
|
+
`GRP_MCP_KB_SERVER` / `kb_server.json`) and runs its semantic search; `kb.py` digests the relevant
|
|
166
|
+
section of the **bundled** `KNOWLEDGE.md`. Server-produced evidence is unfakeable; caller-supplied
|
|
167
|
+
references are only an attestation. `preflight.py` applies the level from
|
|
168
|
+
`Instance.effective_enforcement()` — `off` (dev default) / `warn` (production default) / `enforce`
|
|
169
|
+
(blocks the write if grp-kb was unreachable). Preflight never raises: a broken preflight must not break
|
|
170
|
+
a write.
|
|
171
|
+
|
|
172
|
+
### Safety model (`config.py`)
|
|
173
|
+
|
|
174
|
+
Per-instance gates, **read-only by default**: `allow_write`, `allow_delete` (strictly stronger, enforced
|
|
175
|
+
across *all* planes), `allow_publish`. Persisting connection-profile changes additionally need
|
|
176
|
+
`GRP_MCP_ALLOW_ADMIN=1`. Every authenticated request is pinned to the profile's origin *and* base-URL
|
|
177
|
+
path prefix (SSRF / token-exfiltration guard, incl. sibling apps on the same host). Filesystem tools
|
|
178
|
+
resolve roots through `Instance.effective_roots()`: configured roots win, empty roots confine to the
|
|
179
|
+
process CWD, and whole-disk access is the explicit `allow_unrestricted_fs=true` opt-in. `config.py` is
|
|
180
|
+
authoritative here — the prose in README.md and Installation-and-Setup-Guide.md described the older
|
|
181
|
+
opt-in-sandbox behaviour for several releases before it was caught.
|
|
182
|
+
|
|
183
|
+
## Repo conventions
|
|
184
|
+
|
|
185
|
+
- **KNOWLEDGE.md is the engineering log, not decoration.** Each numbered section records a measured
|
|
186
|
+
finding (what was tried, what the instrument said, what was wrong). Tools quote it at runtime and the
|
|
187
|
+
preflight digests it. Headings must match `## N. Title` or `## §N Title` — sections 19+ use the `§`
|
|
188
|
+
form, and a heading that matches neither becomes invisible to `knowledge()` *and* to the write
|
|
189
|
+
preflight (this happened for five releases; see §26).
|
|
190
|
+
- Comments here explain **why**, usually with a date and a measurement, and often name the wrong
|
|
191
|
+
hypothesis that cost time. Match that register; don't strip those comments as noise.
|
|
192
|
+
- Commit subjects are imperative and state the *effect*, not the file touched — "delete_entity now
|
|
193
|
+
proves the record is gone", "Close a filesystem-sandbox existence oracle (authorize before stat)". A
|
|
194
|
+
release commit carries a trailing `(vX.Y.Z)` and pairs with a Status entry in README.md plus a
|
|
195
|
+
KNOWLEDGE.md section.
|
|
196
|
+
- `connections.json`, `kb_server.json`, `.env`, `*.bak*` and `snapshots/` are gitignored and must never
|
|
197
|
+
be committed or packaged — `pyproject.toml`'s sdist `exclude` list is the second line of defence.
|
|
198
|
+
- `graphify` was evaluated and removed on purpose (2026-07-29). Don't propose reinstating it; the
|
|
199
|
+
`.gitignore` rules for it stay because the tool writes multi-MB artifacts the moment anyone runs it.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
setlocal
|
|
3
|
+
title grp-mcp - launching config UI
|
|
4
|
+
|
|
5
|
+
rem Always use the connections.json sitting next to THIS script, no matter
|
|
6
|
+
rem what folder the double-click happened from. This is the fix for the
|
|
7
|
+
rem classic "config UI wrote to the wrong file" problem - it removes the
|
|
8
|
+
rem current-working-directory dependency entirely.
|
|
9
|
+
set "GRP_MCP_CONNECTIONS=%~dp0connections.json"
|
|
10
|
+
|
|
11
|
+
echo ============================================
|
|
12
|
+
echo grp-mcp Configuration UI
|
|
13
|
+
echo ============================================
|
|
14
|
+
echo Connections file: %GRP_MCP_CONNECTIONS%
|
|
15
|
+
echo ============================================
|
|
16
|
+
echo.
|
|
17
|
+
|
|
18
|
+
rem WHICH BUILD IS ABOUT TO RUN? This used to be invisible, and it bit us.
|
|
19
|
+
rem "uvx --from grp-mcp" resolves the PUBLISHED PyPI package, and --refresh
|
|
20
|
+
rem actively re-pulls it - so on a machine that HAS a source clone, this script
|
|
21
|
+
rem happily ignored the local code and served the released UI instead. Symptom:
|
|
22
|
+
rem you edit ui.py, re-run this, and get the old page with an old build number.
|
|
23
|
+
rem So: prefer a local install sitting next to this script, and fall back to
|
|
24
|
+
rem PyPI only when there is not one (the normal end-user case).
|
|
25
|
+
set "LOCAL_UI=%~dp0.venv\Scripts\grp-mcp-ui.exe"
|
|
26
|
+
|
|
27
|
+
if exist "%LOCAL_UI%" (
|
|
28
|
+
echo Build: LOCAL - %LOCAL_UI%
|
|
29
|
+
echo ^(rename .venv to force the published PyPI build instead^)
|
|
30
|
+
echo.
|
|
31
|
+
echo Starting the config server in a new window...
|
|
32
|
+
start "grp-mcp config UI - close this window when you are done editing" cmd /k "%LOCAL_UI%"
|
|
33
|
+
goto :opened
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
where uvx >nul 2>nul
|
|
37
|
+
if errorlevel 1 (
|
|
38
|
+
echo ERROR: "uvx" was not found on your PATH.
|
|
39
|
+
echo.
|
|
40
|
+
echo Install it first, then close ALL terminal windows and re-run this:
|
|
41
|
+
echo powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 ^| iex"
|
|
42
|
+
echo.
|
|
43
|
+
pause
|
|
44
|
+
exit /b 1
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
echo Build: PUBLISHED from PyPI ^(no local .venv beside this script^)
|
|
48
|
+
echo.
|
|
49
|
+
echo Starting the config server in a new window...
|
|
50
|
+
rem --refresh forces uvx to pull the LATEST grp-mcp from PyPI instead of a stale
|
|
51
|
+
rem cached wheel. Without it, uvx keeps serving whatever version it first cached,
|
|
52
|
+
rem so shipped UI fixes never reach you (symptom: the page hangs on "loading...").
|
|
53
|
+
start "grp-mcp config UI - close this window when you are done editing" cmd /k uvx --refresh --from grp-mcp grp-mcp-ui
|
|
54
|
+
|
|
55
|
+
:opened
|
|
56
|
+
echo Waiting for it to start...
|
|
57
|
+
timeout /t 3 /nobreak >nul
|
|
58
|
+
|
|
59
|
+
echo Opening your browser...
|
|
60
|
+
start "" http://127.0.0.1:8765
|
|
61
|
+
|
|
62
|
+
echo.
|
|
63
|
+
echo Done - edit your instance^(s^) in the browser tab that just opened.
|
|
64
|
+
echo The page header shows a build number; if it looks stale, hard-refresh with
|
|
65
|
+
echo Ctrl+Shift+R. When finished, close the "grp-mcp config UI" window.
|
|
66
|
+
timeout /t 4 >nul
|