d365fo-agent-developer 0.6.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.
- d365fo_agent_developer-0.6.0/LICENSE +21 -0
- d365fo_agent_developer-0.6.0/PKG-INFO +171 -0
- d365fo_agent_developer-0.6.0/README.md +140 -0
- d365fo_agent_developer-0.6.0/pyproject.toml +57 -0
- d365fo_agent_developer-0.6.0/setup.cfg +4 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/__init__.py +2 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/aot_relations.py +147 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/build.py +285 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/cli.py +651 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/data/aot-type-profiles.json +1836 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/data/x++-methodology.md +152 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/data/x++-rules.json +48 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/entity_derive.py +176 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/generator.py +1393 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/graph_query.py +107 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/graphify_runner.py +304 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/index_store.py +465 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/indexer.py +292 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/knowledge.py +393 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/knowledge_fetch.py +70 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/linter.py +369 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/mcp_server.py +842 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/models.py +48 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/packageslocal_export.py +253 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/rules.py +42 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/security_wiring.py +198 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/specs.py +651 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/sql_model.py +342 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/type_profile.py +113 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent/validate.py +180 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent_developer.egg-info/PKG-INFO +171 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent_developer.egg-info/SOURCES.txt +44 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent_developer.egg-info/dependency_links.txt +1 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent_developer.egg-info/entry_points.txt +3 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent_developer.egg-info/requires.txt +8 -0
- d365fo_agent_developer-0.6.0/src/d365fo_agent_developer.egg-info/top_level.txt +1 -0
- d365fo_agent_developer-0.6.0/tests/test_aot_relations.py +131 -0
- d365fo_agent_developer-0.6.0/tests/test_build.py +222 -0
- d365fo_agent_developer-0.6.0/tests/test_catalog.py +268 -0
- d365fo_agent_developer-0.6.0/tests/test_cli.py +64 -0
- d365fo_agent_developer-0.6.0/tests/test_entity_derive.py +108 -0
- d365fo_agent_developer-0.6.0/tests/test_linter.py +145 -0
- d365fo_agent_developer-0.6.0/tests/test_mcp_toolkit.py +599 -0
- d365fo_agent_developer-0.6.0/tests/test_security_wiring.py +130 -0
- d365fo_agent_developer-0.6.0/tests/test_spec_pipeline.py +1775 -0
- d365fo_agent_developer-0.6.0/tests/test_sql_model.py +237 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David Bru
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: d365fo-agent-developer
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Local D365 F&O knowledge toolkit: corpus indexing (SQLite FTS5), deterministic generation, and an MCP server that grounds coding agents (Claude Code, Codex) in real AOT facts.
|
|
5
|
+
Author-email: David Bru <dbru@fiveforty.fr>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dbru540/d365fo-agent
|
|
8
|
+
Project-URL: Repository, https://github.com/dbru540/d365fo-agent
|
|
9
|
+
Project-URL: Issues, https://github.com/dbru540/d365fo-agent/issues
|
|
10
|
+
Keywords: dynamics-365,d365fo,x++,aot,mcp,model-context-protocol,claude,claude-code,codex,ai,code-generation,rag
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
20
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Provides-Extra: graph
|
|
25
|
+
Requires-Dist: graphify; extra == "graph"
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff; extra == "dev"
|
|
29
|
+
Requires-Dist: build; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# d365fo-agent — D365 F&O X++ knowledge for Claude Code & Codex
|
|
33
|
+
|
|
34
|
+
A local **MCP server** that gives an AI coding agent (Claude Code, Codex, any MCP host) a grounded
|
|
35
|
+
**knowledge base of Dynamics 365 Finance & Operations X++** — so you can develop for D365 quickly
|
|
36
|
+
**without re-feeding a whole repo for analysis every time**.
|
|
37
|
+
|
|
38
|
+
It grounds the agent in real AOT facts: it verifies that a class/table/EDT/enum/entity actually
|
|
39
|
+
exists (anti-hallucination), searches the corpus, walks extension/security relationships, serves an
|
|
40
|
+
X++ engineering methodology, validates generated XML against learned per-type structure, scaffolds
|
|
41
|
+
or deterministically generates artifacts, and (on a Windows D365 host) compiles with the real X++
|
|
42
|
+
compiler.
|
|
43
|
+
|
|
44
|
+
- **Pure Python standard library** — zero runtime dependencies, runs anywhere Python 3.11+ runs.
|
|
45
|
+
- **Vendor-neutral** — Claude Code, Codex, Gemini CLI, or any MCP-speaking client over stdio.
|
|
46
|
+
- The standard D365 corpus is the same for everyone, so it is indexed **once** and used as a
|
|
47
|
+
portable knowledge base; your own custom code is **optional**.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install d365fo-agent-developer
|
|
53
|
+
# or, isolated: pipx install d365fo-agent-developer
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This installs two commands: `d365fo-mcp` (the server) and `d365fo-agent` (the CLI).
|
|
57
|
+
|
|
58
|
+
## Get the knowledge base (once)
|
|
59
|
+
|
|
60
|
+
Pick one. Both produce a local index at `~/.d365fo-agent/d365fo.db` that the server uses by default.
|
|
61
|
+
|
|
62
|
+
**A. Download the prebuilt standard-D365 index** (fastest, no D365 install needed):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
d365fo-agent fetch-knowledge # downloads + caches the standard knowledge index
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**B. Build it from your own D365 dev box** (no download; uses metadata you already have):
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
d365fo-agent build-index \
|
|
72
|
+
--db ~/.d365fo-agent/d365fo.db \
|
|
73
|
+
--packages-root "C:/AOSService/PackagesLocalDirectory" \
|
|
74
|
+
--rebuild
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
> The methodology, default lint rules, and a default learned type-profile ship **inside** the
|
|
78
|
+
> package, so validation and guidance work out of the box even before the index is built.
|
|
79
|
+
|
|
80
|
+
## Wire it into your agent
|
|
81
|
+
|
|
82
|
+
**Claude Code** — add to `.mcp.json` (project) or `~/.claude.json` (global):
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"mcpServers": {
|
|
87
|
+
"d365fo": { "command": "d365fo-mcp", "args": [] }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
…or one command: `claude mcp add d365fo d365fo-mcp`
|
|
93
|
+
|
|
94
|
+
**Codex** — add to `~/.codex/config.toml`:
|
|
95
|
+
|
|
96
|
+
```toml
|
|
97
|
+
[mcp_servers.d365fo]
|
|
98
|
+
command = "d365fo-mcp"
|
|
99
|
+
args = []
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
With no `--db`, the server uses the cached knowledge index automatically. That's it — ask the agent
|
|
103
|
+
to build something for D365 and it will verify elements, follow the methodology, and validate its
|
|
104
|
+
output instead of guessing.
|
|
105
|
+
|
|
106
|
+
## Add your custom code (optional)
|
|
107
|
+
|
|
108
|
+
Point the server at your D365 source repo so your **custom** classes/tables/EDTs/enums/extensions
|
|
109
|
+
are indexed too, and so the rich tools can read real signatures and clone real examples:
|
|
110
|
+
|
|
111
|
+
```toml
|
|
112
|
+
[mcp_servers.d365fo]
|
|
113
|
+
command = "d365fo-mcp"
|
|
114
|
+
args = ["--repo-root", "C:/path/to/your/D365Repo",
|
|
115
|
+
"--rules", "C:/path/to/your/rules.json",
|
|
116
|
+
"--packages-root", "C:/AOSService/PackagesLocalDirectory"]
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
| Capability | Knowledge index only | + a PackagesLocalDirectory / repo |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| Verify an element exists, search, relations, methodology, validation, scaffolding by template | ✅ | ✅ |
|
|
122
|
+
| Read a real signature, clone a real example (`get_signature`, `find_similar_examples`, `scaffold_object`) | needs source files | ✅ |
|
|
123
|
+
| Compile with the real X++ compiler (`compile_model`) | — | ✅ (Windows D365 host) |
|
|
124
|
+
|
|
125
|
+
## What the agent gets (MCP tools)
|
|
126
|
+
|
|
127
|
+
`element_exists`, `find_element`, `search_corpus`, `get_signature`, `get_extension_chain`,
|
|
128
|
+
`get_security_links`, `get_entity_exposure`, `find_similar_examples`, `scaffold_object`,
|
|
129
|
+
`find_references`, `find_reverse_references`, `analyze_spec`, `generate_from_spec`, `validate_xml`,
|
|
130
|
+
`lint_artifact`, `derive_entity`, `wire_security`, `compile_model`, `get_methodology`, `index_stats`.
|
|
131
|
+
|
|
132
|
+
See [docs/mcp-server.md](docs/mcp-server.md) for the verify-driven workflow and
|
|
133
|
+
[docs/x++-methodology.md](docs/x++-methodology.md) for the behavioural contract.
|
|
134
|
+
|
|
135
|
+
## Maintainer: publish the knowledge index
|
|
136
|
+
|
|
137
|
+
The wheel stays tiny; the ~100 MB standard index is distributed as a downloadable asset.
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# 1. Build a STANDARD-only index from a PackagesLocalDirectory (no custom repo)
|
|
141
|
+
d365fo-agent build-index --db d365fo-standard.db --packages-root <PLD> --rebuild
|
|
142
|
+
# 2. (optional) learn type profiles to ship as the default
|
|
143
|
+
d365fo-agent build-type-profiles --db d365fo-standard.db --packages-root <PLD> \
|
|
144
|
+
--out src/d365fo_agent/data/aot-type-profiles.json
|
|
145
|
+
# 3. Compress and attach to a GitHub release
|
|
146
|
+
python -c "import gzip,shutil; shutil.copyfileobj(open('d365fo-standard.db','rb'), gzip.open('d365fo-standard.db.gz','wb'))"
|
|
147
|
+
# 4. Point users at it: set DEFAULT_KNOWLEDGE_URL in knowledge_fetch.py (or pass --url)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
> **Note:** the index holds factual AOT metadata (element names, types, packages, labels,
|
|
151
|
+
> relations) — not Microsoft source. Confirm your redistribution position before publishing a
|
|
152
|
+
> prebuilt standard index; option **B** above lets each user build their own with zero redistribution.
|
|
153
|
+
|
|
154
|
+
To publish the package itself: `python -m build` then `python -m twine upload dist/*` (PyPI account
|
|
155
|
+
required).
|
|
156
|
+
|
|
157
|
+
## Develop / contribute
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
pip install -e ".[dev]"
|
|
161
|
+
PYTHONPATH=src python -m unittest discover -s tests # full test suite
|
|
162
|
+
ruff check src/d365fo_agent tests
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Docs: [Architecture](docs/architecture.md) · [MCP Server](docs/mcp-server.md) ·
|
|
166
|
+
[X++ Methodology](docs/x++-methodology.md) · [Specification Contract](docs/specification-contract.md) ·
|
|
167
|
+
[Metadata Schema](docs/metadata-schema.md) · [Tool Catalog](docs/tool-catalog.md)
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# d365fo-agent — D365 F&O X++ knowledge for Claude Code & Codex
|
|
2
|
+
|
|
3
|
+
A local **MCP server** that gives an AI coding agent (Claude Code, Codex, any MCP host) a grounded
|
|
4
|
+
**knowledge base of Dynamics 365 Finance & Operations X++** — so you can develop for D365 quickly
|
|
5
|
+
**without re-feeding a whole repo for analysis every time**.
|
|
6
|
+
|
|
7
|
+
It grounds the agent in real AOT facts: it verifies that a class/table/EDT/enum/entity actually
|
|
8
|
+
exists (anti-hallucination), searches the corpus, walks extension/security relationships, serves an
|
|
9
|
+
X++ engineering methodology, validates generated XML against learned per-type structure, scaffolds
|
|
10
|
+
or deterministically generates artifacts, and (on a Windows D365 host) compiles with the real X++
|
|
11
|
+
compiler.
|
|
12
|
+
|
|
13
|
+
- **Pure Python standard library** — zero runtime dependencies, runs anywhere Python 3.11+ runs.
|
|
14
|
+
- **Vendor-neutral** — Claude Code, Codex, Gemini CLI, or any MCP-speaking client over stdio.
|
|
15
|
+
- The standard D365 corpus is the same for everyone, so it is indexed **once** and used as a
|
|
16
|
+
portable knowledge base; your own custom code is **optional**.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install d365fo-agent-developer
|
|
22
|
+
# or, isolated: pipx install d365fo-agent-developer
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This installs two commands: `d365fo-mcp` (the server) and `d365fo-agent` (the CLI).
|
|
26
|
+
|
|
27
|
+
## Get the knowledge base (once)
|
|
28
|
+
|
|
29
|
+
Pick one. Both produce a local index at `~/.d365fo-agent/d365fo.db` that the server uses by default.
|
|
30
|
+
|
|
31
|
+
**A. Download the prebuilt standard-D365 index** (fastest, no D365 install needed):
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
d365fo-agent fetch-knowledge # downloads + caches the standard knowledge index
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**B. Build it from your own D365 dev box** (no download; uses metadata you already have):
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
d365fo-agent build-index \
|
|
41
|
+
--db ~/.d365fo-agent/d365fo.db \
|
|
42
|
+
--packages-root "C:/AOSService/PackagesLocalDirectory" \
|
|
43
|
+
--rebuild
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
> The methodology, default lint rules, and a default learned type-profile ship **inside** the
|
|
47
|
+
> package, so validation and guidance work out of the box even before the index is built.
|
|
48
|
+
|
|
49
|
+
## Wire it into your agent
|
|
50
|
+
|
|
51
|
+
**Claude Code** — add to `.mcp.json` (project) or `~/.claude.json` (global):
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"mcpServers": {
|
|
56
|
+
"d365fo": { "command": "d365fo-mcp", "args": [] }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
…or one command: `claude mcp add d365fo d365fo-mcp`
|
|
62
|
+
|
|
63
|
+
**Codex** — add to `~/.codex/config.toml`:
|
|
64
|
+
|
|
65
|
+
```toml
|
|
66
|
+
[mcp_servers.d365fo]
|
|
67
|
+
command = "d365fo-mcp"
|
|
68
|
+
args = []
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
With no `--db`, the server uses the cached knowledge index automatically. That's it — ask the agent
|
|
72
|
+
to build something for D365 and it will verify elements, follow the methodology, and validate its
|
|
73
|
+
output instead of guessing.
|
|
74
|
+
|
|
75
|
+
## Add your custom code (optional)
|
|
76
|
+
|
|
77
|
+
Point the server at your D365 source repo so your **custom** classes/tables/EDTs/enums/extensions
|
|
78
|
+
are indexed too, and so the rich tools can read real signatures and clone real examples:
|
|
79
|
+
|
|
80
|
+
```toml
|
|
81
|
+
[mcp_servers.d365fo]
|
|
82
|
+
command = "d365fo-mcp"
|
|
83
|
+
args = ["--repo-root", "C:/path/to/your/D365Repo",
|
|
84
|
+
"--rules", "C:/path/to/your/rules.json",
|
|
85
|
+
"--packages-root", "C:/AOSService/PackagesLocalDirectory"]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
| Capability | Knowledge index only | + a PackagesLocalDirectory / repo |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| Verify an element exists, search, relations, methodology, validation, scaffolding by template | ✅ | ✅ |
|
|
91
|
+
| Read a real signature, clone a real example (`get_signature`, `find_similar_examples`, `scaffold_object`) | needs source files | ✅ |
|
|
92
|
+
| Compile with the real X++ compiler (`compile_model`) | — | ✅ (Windows D365 host) |
|
|
93
|
+
|
|
94
|
+
## What the agent gets (MCP tools)
|
|
95
|
+
|
|
96
|
+
`element_exists`, `find_element`, `search_corpus`, `get_signature`, `get_extension_chain`,
|
|
97
|
+
`get_security_links`, `get_entity_exposure`, `find_similar_examples`, `scaffold_object`,
|
|
98
|
+
`find_references`, `find_reverse_references`, `analyze_spec`, `generate_from_spec`, `validate_xml`,
|
|
99
|
+
`lint_artifact`, `derive_entity`, `wire_security`, `compile_model`, `get_methodology`, `index_stats`.
|
|
100
|
+
|
|
101
|
+
See [docs/mcp-server.md](docs/mcp-server.md) for the verify-driven workflow and
|
|
102
|
+
[docs/x++-methodology.md](docs/x++-methodology.md) for the behavioural contract.
|
|
103
|
+
|
|
104
|
+
## Maintainer: publish the knowledge index
|
|
105
|
+
|
|
106
|
+
The wheel stays tiny; the ~100 MB standard index is distributed as a downloadable asset.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# 1. Build a STANDARD-only index from a PackagesLocalDirectory (no custom repo)
|
|
110
|
+
d365fo-agent build-index --db d365fo-standard.db --packages-root <PLD> --rebuild
|
|
111
|
+
# 2. (optional) learn type profiles to ship as the default
|
|
112
|
+
d365fo-agent build-type-profiles --db d365fo-standard.db --packages-root <PLD> \
|
|
113
|
+
--out src/d365fo_agent/data/aot-type-profiles.json
|
|
114
|
+
# 3. Compress and attach to a GitHub release
|
|
115
|
+
python -c "import gzip,shutil; shutil.copyfileobj(open('d365fo-standard.db','rb'), gzip.open('d365fo-standard.db.gz','wb'))"
|
|
116
|
+
# 4. Point users at it: set DEFAULT_KNOWLEDGE_URL in knowledge_fetch.py (or pass --url)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
> **Note:** the index holds factual AOT metadata (element names, types, packages, labels,
|
|
120
|
+
> relations) — not Microsoft source. Confirm your redistribution position before publishing a
|
|
121
|
+
> prebuilt standard index; option **B** above lets each user build their own with zero redistribution.
|
|
122
|
+
|
|
123
|
+
To publish the package itself: `python -m build` then `python -m twine upload dist/*` (PyPI account
|
|
124
|
+
required).
|
|
125
|
+
|
|
126
|
+
## Develop / contribute
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
pip install -e ".[dev]"
|
|
130
|
+
PYTHONPATH=src python -m unittest discover -s tests # full test suite
|
|
131
|
+
ruff check src/d365fo_agent tests
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Docs: [Architecture](docs/architecture.md) · [MCP Server](docs/mcp-server.md) ·
|
|
135
|
+
[X++ Methodology](docs/x++-methodology.md) · [Specification Contract](docs/specification-contract.md) ·
|
|
136
|
+
[Metadata Schema](docs/metadata-schema.md) · [Tool Catalog](docs/tool-catalog.md)
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "d365fo-agent-developer"
|
|
7
|
+
version = "0.6.0"
|
|
8
|
+
description = "Local D365 F&O knowledge toolkit: corpus indexing (SQLite FTS5), deterministic generation, and an MCP server that grounds coding agents (Claude Code, Codex) in real AOT facts."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "David Bru", email = "dbru@fiveforty.fr" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"dynamics-365", "d365fo", "x++", "aot", "mcp", "model-context-protocol",
|
|
15
|
+
"claude", "claude-code", "codex", "ai", "code-generation", "rag",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Software Development :: Code Generators",
|
|
27
|
+
"Topic :: Software Development :: Build Tools",
|
|
28
|
+
]
|
|
29
|
+
# Standard library only — nothing to install at runtime, which keeps the MCP server portable.
|
|
30
|
+
dependencies = []
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/dbru540/d365fo-agent"
|
|
34
|
+
Repository = "https://github.com/dbru540/d365fo-agent"
|
|
35
|
+
Issues = "https://github.com/dbru540/d365fo-agent/issues"
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
# The graph-building engine is the ONLY external dependency, and only the
|
|
39
|
+
# `run-graphify-staging` command needs it. Everything else (indexing, generation,
|
|
40
|
+
# MCP server, validation) is Python standard library only.
|
|
41
|
+
graph = ["graphify"]
|
|
42
|
+
dev = ["pytest", "ruff", "build"]
|
|
43
|
+
|
|
44
|
+
[project.scripts]
|
|
45
|
+
d365fo-agent = "d365fo_agent.cli:main"
|
|
46
|
+
d365fo-mcp = "d365fo_agent.mcp_server:main"
|
|
47
|
+
|
|
48
|
+
[tool.setuptools]
|
|
49
|
+
package-dir = { "" = "src" }
|
|
50
|
+
|
|
51
|
+
[tool.setuptools.packages.find]
|
|
52
|
+
where = ["src"]
|
|
53
|
+
|
|
54
|
+
[tool.setuptools.package-data]
|
|
55
|
+
# Methodology, default lint rules, and a default learned type-profile ship inside the package so
|
|
56
|
+
# the server works after a plain `pip install` (no source tree needed).
|
|
57
|
+
d365fo_agent = ["data/*"]
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Extract AOT table relations — the D365 equivalent of foreign keys.
|
|
2
|
+
|
|
3
|
+
AX business tables define no SQL foreign keys; the relational graph lives in the AOT
|
|
4
|
+
metadata: every ``AxTable`` (and ``AxTableExtension``) XML carries a ``<Relations>`` block
|
|
5
|
+
with the related table, the relationship type (Association/Composition), both cardinalities,
|
|
6
|
+
and the EXACT join fields (``<AxTableRelationConstraintField>``: Field ↔ RelatedField, plus
|
|
7
|
+
fixed-value constraints). This module walks one or more corpus roots (PackagesLocalDirectory
|
|
8
|
+
and/or editable source trees), parses those blocks, and stores them next to the SQL data
|
|
9
|
+
model so ``find_relations``/``get_sql_model`` can explain table relationships with the same
|
|
10
|
+
authority a foreign key would. Standard library only.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import sqlite3
|
|
16
|
+
import xml.etree.ElementTree as ET
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Iterator
|
|
19
|
+
|
|
20
|
+
_XSI_TYPE = "{http://www.w3.org/2001/XMLSchema-instance}type"
|
|
21
|
+
# Mirrors index_store._NON_AOT_DIRS: build outputs that contain compiled XML copies.
|
|
22
|
+
_SKIP_DIRS = {"bin", "Descriptor", "Resources", "XppMetadata", "AdditionalFiles", "obj"}
|
|
23
|
+
|
|
24
|
+
_SCHEMA = """
|
|
25
|
+
CREATE TABLE IF NOT EXISTS aot_relations(
|
|
26
|
+
table_name TEXT, relation_name TEXT, related_table TEXT,
|
|
27
|
+
relationship_type TEXT, cardinality TEXT, related_cardinality TEXT,
|
|
28
|
+
edt_relation INTEGER, source_element TEXT,
|
|
29
|
+
PRIMARY KEY(table_name, relation_name, source_element));
|
|
30
|
+
CREATE TABLE IF NOT EXISTS aot_relation_fields(
|
|
31
|
+
table_name TEXT, relation_name TEXT, kind TEXT,
|
|
32
|
+
field TEXT, related_field TEXT, fixed_value TEXT, source_edt TEXT);
|
|
33
|
+
CREATE INDEX IF NOT EXISTS ix_aot_rel_table ON aot_relations(table_name);
|
|
34
|
+
CREATE INDEX IF NOT EXISTS ix_aot_rel_related ON aot_relations(related_table);
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _text(node: ET.Element | None) -> str | None:
|
|
39
|
+
return node.text.strip() if node is not None and node.text else None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _iter_table_files(root: Path) -> Iterator[tuple[Path, str]]:
|
|
43
|
+
"""Yield (xml_path, element_kind) for every AxTable/AxTableExtension under a corpus root."""
|
|
44
|
+
for kind in ("AxTable", "AxTableExtension"):
|
|
45
|
+
for type_dir in root.rglob(kind):
|
|
46
|
+
if not type_dir.is_dir() or type_dir.name != kind:
|
|
47
|
+
continue
|
|
48
|
+
if any(part in _SKIP_DIRS for part in type_dir.relative_to(root).parts):
|
|
49
|
+
continue
|
|
50
|
+
for xml_file in type_dir.glob("*.xml"):
|
|
51
|
+
yield xml_file, kind
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_table_relations(xml_path: Path, kind: str) -> tuple[str, str, list[dict[str, object]]]:
|
|
55
|
+
"""Parse one AxTable/AxTableExtension XML. Returns (table_name, source_element, relations).
|
|
56
|
+
|
|
57
|
+
For an extension ``CustTable.MyModel``, the relations belong to ``CustTable``.
|
|
58
|
+
"""
|
|
59
|
+
tree = ET.parse(xml_path)
|
|
60
|
+
root = tree.getroot()
|
|
61
|
+
element_name = _text(root.find("Name")) or xml_path.stem
|
|
62
|
+
table_name = element_name.split(".")[0] if kind == "AxTableExtension" else element_name
|
|
63
|
+
|
|
64
|
+
relations: list[dict[str, object]] = []
|
|
65
|
+
for rel in root.iter("AxTableRelation"):
|
|
66
|
+
constraints: list[dict[str, object]] = []
|
|
67
|
+
for constraint in rel.iter("AxTableRelationConstraint"):
|
|
68
|
+
ctype = (constraint.get(_XSI_TYPE) or "AxTableRelationConstraintField")
|
|
69
|
+
constraints.append({
|
|
70
|
+
"kind": ctype.replace("AxTableRelationConstraint", "").lower() or "field",
|
|
71
|
+
"field": _text(constraint.find("Field")),
|
|
72
|
+
"related_field": _text(constraint.find("RelatedField")),
|
|
73
|
+
"fixed_value": _text(constraint.find("Value")),
|
|
74
|
+
"source_edt": _text(constraint.find("SourceEDT")),
|
|
75
|
+
})
|
|
76
|
+
relations.append({
|
|
77
|
+
"name": _text(rel.find("Name")),
|
|
78
|
+
"related_table": _text(rel.find("RelatedTable")),
|
|
79
|
+
"relationship_type": _text(rel.find("RelationshipType")),
|
|
80
|
+
"cardinality": _text(rel.find("Cardinality")),
|
|
81
|
+
"related_cardinality": _text(rel.find("RelatedTableCardinality")),
|
|
82
|
+
"edt_relation": 1 if _text(rel.find("EDTRelation")) == "Yes" else 0,
|
|
83
|
+
"constraints": constraints,
|
|
84
|
+
})
|
|
85
|
+
return table_name, element_name, relations
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def extract_aot_relations(
|
|
89
|
+
roots: "list[str | Path]",
|
|
90
|
+
db_path: str | Path,
|
|
91
|
+
*,
|
|
92
|
+
progress: "callable[[str, int], None] | None" = None,
|
|
93
|
+
) -> dict[str, int]:
|
|
94
|
+
"""Walk every AxTable/AxTableExtension under ``roots`` and persist their relations."""
|
|
95
|
+
conn = sqlite3.connect(Path(db_path))
|
|
96
|
+
conn.executescript("DELETE FROM aot_relations; DELETE FROM aot_relation_fields;"
|
|
97
|
+
if conn.execute("SELECT 1 FROM sqlite_master WHERE name='aot_relations'").fetchone()
|
|
98
|
+
else "SELECT 1;")
|
|
99
|
+
conn.executescript(_SCHEMA)
|
|
100
|
+
|
|
101
|
+
files = relations_count = 0
|
|
102
|
+
rel_rows: list[tuple] = []
|
|
103
|
+
field_rows: list[tuple] = []
|
|
104
|
+
for root in roots:
|
|
105
|
+
root = Path(root)
|
|
106
|
+
for xml_file, kind in _iter_table_files(root):
|
|
107
|
+
try:
|
|
108
|
+
table, element, relations = parse_table_relations(xml_file, kind)
|
|
109
|
+
except OSError:
|
|
110
|
+
# Windows MAX_PATH: deep AOT paths exceed 260 chars — retry extended-length.
|
|
111
|
+
try:
|
|
112
|
+
table, element, relations = parse_table_relations(
|
|
113
|
+
Path("\\\\?\\" + str(xml_file.resolve())), kind)
|
|
114
|
+
except (OSError, ET.ParseError):
|
|
115
|
+
continue
|
|
116
|
+
except ET.ParseError:
|
|
117
|
+
continue
|
|
118
|
+
files += 1
|
|
119
|
+
for rel in relations:
|
|
120
|
+
if not rel["related_table"]:
|
|
121
|
+
continue
|
|
122
|
+
relations_count += 1
|
|
123
|
+
rel_rows.append((table, rel["name"], rel["related_table"],
|
|
124
|
+
rel["relationship_type"], rel["cardinality"],
|
|
125
|
+
rel["related_cardinality"], rel["edt_relation"], element))
|
|
126
|
+
for c in rel["constraints"]:
|
|
127
|
+
field_rows.append((table, rel["name"], c["kind"], c["field"],
|
|
128
|
+
c["related_field"], c["fixed_value"], c["source_edt"]))
|
|
129
|
+
if len(rel_rows) >= 5000:
|
|
130
|
+
conn.executemany("INSERT OR REPLACE INTO aot_relations VALUES(?,?,?,?,?,?,?,?)", rel_rows)
|
|
131
|
+
conn.executemany("INSERT INTO aot_relation_fields VALUES(?,?,?,?,?,?,?)", field_rows)
|
|
132
|
+
conn.commit()
|
|
133
|
+
rel_rows, field_rows = [], []
|
|
134
|
+
if progress:
|
|
135
|
+
progress(str(root), relations_count)
|
|
136
|
+
conn.executemany("INSERT OR REPLACE INTO aot_relations VALUES(?,?,?,?,?,?,?,?)", rel_rows)
|
|
137
|
+
conn.executemany("INSERT INTO aot_relation_fields VALUES(?,?,?,?,?,?,?)", field_rows)
|
|
138
|
+
conn.commit()
|
|
139
|
+
stats = {
|
|
140
|
+
"files_parsed": files,
|
|
141
|
+
"relations": conn.execute("SELECT COUNT(*) FROM aot_relations").fetchone()[0],
|
|
142
|
+
"constraint_fields": conn.execute("SELECT COUNT(*) FROM aot_relation_fields").fetchone()[0],
|
|
143
|
+
"tables_with_relations": conn.execute(
|
|
144
|
+
"SELECT COUNT(DISTINCT table_name) FROM aot_relations").fetchone()[0],
|
|
145
|
+
}
|
|
146
|
+
conn.close()
|
|
147
|
+
return stats
|