d365fo-mcp 1.13.0 → 1.15.0

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.
Files changed (234) hide show
  1. package/.github/copilot-instructions.md +4 -4
  2. package/README.md +4 -4
  3. package/bridge/D365MetadataBridge/Models/Models.cs +20 -0
  4. package/bridge/D365MetadataBridge/Program.cs +128 -20
  5. package/bridge/D365MetadataBridge/Protocol/RequestDispatcher.cs +37 -0
  6. package/bridge/D365MetadataBridge/Services/MetadataReadService.cs +536 -72
  7. package/bridge/D365MetadataBridge/Services/MetadataWriteService.cs +37 -1
  8. package/dist/bridge/bridgeAdapter.d.ts +105 -1
  9. package/dist/bridge/bridgeAdapter.js +290 -56
  10. package/dist/bridge/bridgeClient.d.ts +2 -0
  11. package/dist/bridge/bridgeClient.js +20 -0
  12. package/dist/bridge/bridgeFailure.d.ts +3 -1
  13. package/dist/bridge/bridgeFailure.js +15 -6
  14. package/dist/bridge/bridgeTypes.d.ts +14 -0
  15. package/dist/cli/session/analyze.js +6 -1
  16. package/dist/config/settings.js +49 -5
  17. package/dist/index.js +22 -9
  18. package/dist/knowledge/compilerFacts.d.ts +50 -0
  19. package/dist/knowledge/compilerFacts.generated.d.ts +27 -0
  20. package/dist/knowledge/compilerFacts.generated.js +915 -0
  21. package/dist/knowledge/compilerFacts.js +67 -0
  22. package/dist/knowledge/kernelEnums.d.ts +64 -0
  23. package/dist/knowledge/kernelEnums.js +104 -0
  24. package/dist/knowledge/mobileAppPatterns/catalog.d.ts +20 -0
  25. package/dist/knowledge/mobileAppPatterns/catalog.js +548 -0
  26. package/dist/knowledge/mobileAppPatterns/index.d.ts +19 -0
  27. package/dist/knowledge/mobileAppPatterns/index.js +115 -0
  28. package/dist/knowledge/mobileAppPatterns/types.d.ts +62 -0
  29. package/dist/knowledge/mobileAppPatterns/types.js +22 -0
  30. package/dist/knowledge/reportPatterns/catalog.d.ts +16 -0
  31. package/dist/knowledge/reportPatterns/catalog.js +411 -0
  32. package/dist/knowledge/reportPatterns/index.d.ts +13 -0
  33. package/dist/knowledge/reportPatterns/index.js +73 -0
  34. package/dist/knowledge/reportPatterns/types.d.ts +43 -0
  35. package/dist/knowledge/reportPatterns/types.js +11 -0
  36. package/dist/metadata/indexWarmup.d.ts +43 -0
  37. package/dist/metadata/indexWarmup.js +62 -0
  38. package/dist/metadata/indexWarmupWorker.d.ts +67 -0
  39. package/dist/metadata/indexWarmupWorker.js +117 -0
  40. package/dist/metadata/symbolIndex.d.ts +23 -0
  41. package/dist/metadata/symbolIndex.js +72 -5
  42. package/dist/prompts/codeReview.js +5 -0
  43. package/dist/prompts/systemInstructions.js +5 -2
  44. package/dist/resources/index.d.ts +1 -0
  45. package/dist/resources/index.js +43 -14
  46. package/dist/scripts/build-database.js +66 -9
  47. package/dist/scripts/build-fts.js +66 -9
  48. package/dist/scripts/extract-metadata.js +38 -4
  49. package/dist/server/serverMode.d.ts +23 -3
  50. package/dist/server/serverMode.js +46 -12
  51. package/dist/server/toolAnnotations.js +1 -4
  52. package/dist/server/toolSchemas/buildD365foProject.d.ts +14 -5
  53. package/dist/server/toolSchemas/buildD365foProject.js +16 -6
  54. package/dist/server/toolSchemas/d365foFile.d.ts +4 -1
  55. package/dist/server/toolSchemas/d365foFile.js +26 -18
  56. package/dist/server/toolSchemas/extensionInfo.js +2 -2
  57. package/dist/server/toolSchemas/findReferences.js +1 -1
  58. package/dist/server/toolSchemas/generateObject.d.ts +0 -1
  59. package/dist/server/toolSchemas/generateObject.js +14 -5
  60. package/dist/server/toolSchemas/getKnowledge.d.ts +8 -1
  61. package/dist/server/toolSchemas/getKnowledge.js +12 -4
  62. package/dist/server/toolSchemas/getObjectInfo.d.ts +0 -1
  63. package/dist/server/toolSchemas/getObjectInfo.js +8 -5
  64. package/dist/server/toolSchemas/getWorkspaceInfo.d.ts +11 -0
  65. package/dist/server/toolSchemas/getWorkspaceInfo.js +14 -3
  66. package/dist/server/toolSchemas/index.d.ts +189 -148
  67. package/dist/server/toolSchemas/index.js +0 -6
  68. package/dist/server/toolSchemas/labels.d.ts +0 -23
  69. package/dist/server/toolSchemas/labels.js +16 -27
  70. package/dist/server/toolSchemas/objectPatterns.js +15 -13
  71. package/dist/server/toolSchemas/prepare.d.ts +0 -1
  72. package/dist/server/toolSchemas/prepare.js +11 -4
  73. package/dist/server/toolSchemas/runBpCheck.d.ts +0 -8
  74. package/dist/server/toolSchemas/runBpCheck.js +10 -6
  75. package/dist/server/toolSchemas/runSystestClass.js +3 -3
  76. package/dist/server/toolSchemas/search.d.ts +0 -36
  77. package/dist/server/toolSchemas/search.js +6 -46
  78. package/dist/server/toolSchemas/securityInfo.d.ts +0 -1
  79. package/dist/server/toolSchemas/securityInfo.js +1 -1
  80. package/dist/server/toolSchemas/updateSymbolIndex.js +5 -3
  81. package/dist/server/toolSchemas/validateCode.js +6 -6
  82. package/dist/server/toolSchemas/validateObjectNaming.js +1 -1
  83. package/dist/server/toolSchemas/verifyD365foProject.d.ts +0 -8
  84. package/dist/server/toolSchemas/verifyD365foProject.js +7 -4
  85. package/dist/server/transport.d.ts +19 -0
  86. package/dist/server/transport.js +21 -8
  87. package/dist/tools/analysis/batchSearch.js +17 -23
  88. package/dist/tools/analysis/findReferences.js +107 -7
  89. package/dist/tools/analysis/search.d.ts +24 -0
  90. package/dist/tools/analysis/search.js +169 -15
  91. package/dist/tools/analysis/searchLabels.d.ts +4 -0
  92. package/dist/tools/analysis/searchLabels.js +115 -12
  93. package/dist/tools/analysis/validateCode.d.ts +1 -1
  94. package/dist/tools/analysis/validateCode.js +237 -42
  95. package/dist/tools/analysis/validateObjectNaming.d.ts +4 -3
  96. package/dist/tools/analysis/validateObjectNaming.js +13 -355
  97. package/dist/tools/analysis/validateXpp.d.ts +27 -6
  98. package/dist/tools/analysis/validateXpp.js +1078 -146
  99. package/dist/tools/d365foFile.d.ts +2 -1
  100. package/dist/tools/d365foFile.js +255 -8
  101. package/dist/tools/knowledge/getKnowledge.d.ts +29 -0
  102. package/dist/tools/knowledge/getKnowledge.js +118 -0
  103. package/dist/tools/knowledge/getMobileAppPatterns.d.ts +22 -0
  104. package/dist/tools/knowledge/getMobileAppPatterns.js +29 -0
  105. package/dist/tools/knowledge/getReportPatterns.d.ts +22 -0
  106. package/dist/tools/knowledge/getReportPatterns.js +29 -0
  107. package/dist/tools/knowledge/objectPatterns.d.ts +1 -0
  108. package/dist/tools/knowledge/objectPatterns.js +42 -4
  109. package/dist/tools/knowledge/xppKnowledge.js +1171 -39
  110. package/dist/tools/labels.js +23 -1
  111. package/dist/tools/prepare/prepare.d.ts +4 -2
  112. package/dist/tools/prepare/prepare.js +76 -7
  113. package/dist/tools/prepare/prepareChange.d.ts +22 -0
  114. package/dist/tools/prepare/prepareChange.js +264 -44
  115. package/dist/tools/prepare/prepareCreate.js +83 -26
  116. package/dist/tools/prepare/prepareTest.d.ts +3 -0
  117. package/dist/tools/prepare/prepareTest.js +196 -0
  118. package/dist/tools/readers/edtInfo.js +91 -16
  119. package/dist/tools/readers/enumInfo.d.ts +1 -6
  120. package/dist/tools/readers/enumInfo.js +9 -0
  121. package/dist/tools/readers/formInfo.js +8 -2
  122. package/dist/tools/readers/getObjectInfo.js +135 -3
  123. package/dist/tools/readers/getWorkspaceInfo.js +68 -25
  124. package/dist/tools/readers/tableInfo.d.ts +1 -13
  125. package/dist/tools/readers/tableInfo.js +39 -2
  126. package/dist/tools/responseCaps.d.ts +11 -0
  127. package/dist/tools/responseCaps.js +116 -0
  128. package/dist/tools/sdlc/buildProject.d.ts +39 -0
  129. package/dist/tools/sdlc/buildProject.js +201 -11
  130. package/dist/tools/sdlc/dbSync.js +5 -5
  131. package/dist/tools/sdlc/reviewWorkspaceChanges.d.ts +1 -1
  132. package/dist/tools/sdlc/reviewWorkspaceChanges.js +78 -9
  133. package/dist/tools/sdlc/runBpCheck.d.ts +13 -0
  134. package/dist/tools/sdlc/runBpCheck.js +104 -9
  135. package/dist/tools/sdlc/sysTestRunner.d.ts +17 -0
  136. package/dist/tools/sdlc/sysTestRunner.js +180 -10
  137. package/dist/tools/sdlc/undoLastModification.js +4 -4
  138. package/dist/tools/sdlc/updateSymbolIndex.js +22 -1
  139. package/dist/tools/smart/codeGen.d.ts +8 -0
  140. package/dist/tools/smart/codeGen.js +437 -18
  141. package/dist/tools/smart/generateSmartForm.js +7 -2
  142. package/dist/tools/smart/generateSmartReport.d.ts +10 -2
  143. package/dist/tools/smart/generateSmartReport.js +143 -47
  144. package/dist/tools/smart/generateSmartTable.d.ts +21 -3
  145. package/dist/tools/smart/generateSmartTable.js +101 -7
  146. package/dist/tools/specs/d365foFileOpSpecs.d.ts +42 -0
  147. package/dist/tools/specs/d365foFileOpSpecs.js +140 -20
  148. package/dist/tools/specs/generateObjectOpSpecs.js +61 -7
  149. package/dist/tools/specs/labelsOpSpecs.js +16 -2
  150. package/dist/tools/specs/opSpecs.d.ts +1 -15
  151. package/dist/tools/specs/opSpecs.js +95 -9
  152. package/dist/tools/toolHandler.d.ts +0 -1
  153. package/dist/tools/toolHandler.js +119 -66
  154. package/dist/tools/write/createD365File.d.ts +42 -452
  155. package/dist/tools/write/createD365File.js +357 -2663
  156. package/dist/tools/write/createLabel.d.ts +95 -0
  157. package/dist/tools/write/createLabel.js +267 -4
  158. package/dist/tools/write/deleteD365File.js +8 -6
  159. package/dist/tools/write/directXmlWriters.d.ts +378 -0
  160. package/dist/tools/write/directXmlWriters.js +1570 -0
  161. package/dist/tools/write/inlineWriteVerification.d.ts +15 -0
  162. package/dist/tools/write/inlineWriteVerification.js +20 -0
  163. package/dist/tools/write/modifyD365File.d.ts +276 -66
  164. package/dist/tools/write/modifyD365File.js +684 -1579
  165. package/dist/tools/write/renameLabel.js +4 -3
  166. package/dist/tools/write/resolveReferences.d.ts +7 -0
  167. package/dist/tools/write/resolveReferences.js +179 -55
  168. package/dist/tools/xml/createTablePropertyHonesty.js +9 -6
  169. package/dist/tools/xml/formXml.js +54 -2
  170. package/dist/tools/xml/generateD365Xml.d.ts +6 -152
  171. package/dist/tools/xml/generateD365Xml.js +6 -1214
  172. package/dist/tools/xml/generateTableFields.d.ts +1 -1
  173. package/dist/tools/xml/generateTableFields.js +4 -4
  174. package/dist/tools/xml/queryViewXml.d.ts +1 -1
  175. package/dist/tools/xml/queryViewXml.js +2 -2
  176. package/dist/tools/xml/securityDutyRoleXml.d.ts +1 -1
  177. package/dist/tools/xml/securityDutyRoleXml.js +1 -1
  178. package/dist/tools/xml/securityPrivilegeXml.d.ts +56 -0
  179. package/dist/tools/xml/securityPrivilegeXml.js +121 -7
  180. package/dist/tools/xml/tableXml.d.ts +42 -0
  181. package/dist/tools/xml/tableXml.js +112 -24
  182. package/dist/tools/xml/xmlTemplateGenerator.d.ts +481 -0
  183. package/dist/tools/xml/xmlTemplateGenerator.js +2608 -0
  184. package/dist/utils/axTablePropertyOrder.d.ts +1 -1
  185. package/dist/utils/axTablePropertyOrder.js +1 -1
  186. package/dist/utils/baseObjectXml.d.ts +22 -0
  187. package/dist/utils/baseObjectXml.js +88 -0
  188. package/dist/utils/callDedup.d.ts +31 -2
  189. package/dist/utils/callDedup.js +73 -4
  190. package/dist/utils/configManager.d.ts +14 -0
  191. package/dist/utils/configManager.js +69 -5
  192. package/dist/utils/fieldControlTypes.d.ts +35 -1
  193. package/dist/utils/fieldControlTypes.js +118 -2
  194. package/dist/utils/formExtensionControlModifications.d.ts +92 -0
  195. package/dist/utils/formExtensionControlModifications.js +289 -0
  196. package/dist/utils/formPatternTemplates.d.ts +24 -2
  197. package/dist/utils/formPatternTemplates.js +15 -9
  198. package/dist/utils/indexStaleness.d.ts +35 -2
  199. package/dist/utils/indexStaleness.js +58 -2
  200. package/dist/utils/labelDiskCheck.d.ts +13 -0
  201. package/dist/utils/labelDiskCheck.js +46 -13
  202. package/dist/utils/labelReference.d.ts +40 -0
  203. package/dist/utils/labelReference.js +75 -0
  204. package/dist/utils/loopLag.d.ts +38 -0
  205. package/dist/utils/loopLag.js +81 -0
  206. package/dist/utils/objectNamingRules.d.ts +60 -0
  207. package/dist/utils/objectNamingRules.js +410 -0
  208. package/dist/utils/packageResolver.d.ts +22 -0
  209. package/dist/utils/packageResolver.js +65 -0
  210. package/dist/utils/phaseTimer.js +36 -0
  211. package/dist/utils/repeatedNotes.d.ts +29 -0
  212. package/dist/utils/repeatedNotes.js +38 -0
  213. package/dist/utils/smartXmlBuilder.js +1 -1
  214. package/dist/utils/toolMetrics.d.ts +18 -1
  215. package/dist/utils/toolMetrics.js +23 -2
  216. package/dist/utils/workspaceDetector.d.ts +5 -0
  217. package/dist/utils/workspaceDetector.js +1 -1
  218. package/dist/utils/writeSideEffects.d.ts +43 -0
  219. package/dist/utils/writeSideEffects.js +56 -0
  220. package/dist/utils/xppLexer.d.ts +59 -0
  221. package/dist/utils/xppLexer.js +123 -0
  222. package/dist/utils/xppSelectLint.js +8 -36
  223. package/dist/validation/formPatternValidator.js +15 -0
  224. package/dist/workspace/contextSnapshot.d.ts +17 -2
  225. package/dist/workspace/contextSnapshot.js +105 -24
  226. package/dist/workspace/workspaceScanner.d.ts +1 -1
  227. package/dist/workspace/workspaceScanner.js +1 -1
  228. package/package.json +1 -1
  229. package/dist/server/toolSchemas/reviewWorkspaceChanges.d.ts +0 -20
  230. package/dist/server/toolSchemas/reviewWorkspaceChanges.js +0 -19
  231. package/dist/server/toolSchemas/triggerDbSync.d.ts +0 -47
  232. package/dist/server/toolSchemas/triggerDbSync.js +0 -31
  233. package/dist/server/toolSchemas/undoLastModification.d.ts +0 -20
  234. package/dist/server/toolSchemas/undoLastModification.js +0 -17
@@ -32,7 +32,7 @@ PowerShell / any terminal command **WILL HANG** in VS 2022 / VS 2026 MCP integra
32
32
  | Plan a new object before creating it | `prepare(mode="create", goal, objectName, objectType)` — returns collision check, naming, EDT/label hints + `groundingToken` |
33
33
  | Create a D365FO object | `d365fo_file(action="create")` (never `create_file`) |
34
34
  | Edit an existing object | `d365fo_file(action="modify")` (applies immediately — confirm in chat first) |
35
- | Revert the last write | `undo_last_modification` |
35
+ | Revert the last write | `d365fo_file(action="undo", filePath)` — git-tracked → checkout HEAD (discards ALL uncommitted changes to that file); untracked → deleted |
36
36
  | Search objects | `search` — multiple via `search(queries[])`, custom-only via `search(scope="extensions")` |
37
37
  | Read any object's metadata | `get_object_info(objectType, name, options?)` — objectType ∈ class/table/form/query/view/enum/edt/report/data-entity/menu-item/service/map/config-key/security-policy/macro. 2+ known names: `get_object_info(objects=[{objectType,objectName},…])` — ONE call, never a loop |
38
38
  | Method signature for CoC | `get_object_info(objectType="class", name, options={method, include:"signature"})` (already returned by `prepare(mode="change")`) |
@@ -41,7 +41,7 @@ PowerShell / any terminal command **WILL HANG** in VS 2022 / VS 2026 MCP integra
41
41
  | Create a NEW form | `object_patterns(domain="form", action="analyze", recommend={...})` → `object_patterns(domain="form", action="spec", pattern)` → `generate_object(mode="scaffold", objectType="form", cloneFrom=referenceForm, tableMapping={...})` → `object_patterns(domain="form", action="validate", xml)` |
42
42
  | Validate form XML against its pattern | `object_patterns(domain="form", action="validate", xml \| formName \| filePath)` — structural errors block form writes (FORM_PATTERN_ENFORCE) |
43
43
  | Resolve label / EDT / class refs | `validate_code(mode="references", code)` |
44
- | Build / BP / Sync | `build_d365fo_project` / `run_bp_check` / `trigger_db_sync` |
44
+ | Build / BP / Sync | `build_d365fo_project(bpCheck: true, dbSync: true)` — ONE call compiles, runs the best-practice check and syncs AxDB |
45
45
  | Error diagnosis | `get_knowledge(kind="error", errorText)` |
46
46
  | Parameters for a `d365fo_file` operation / `generate_object` mode | `get_knowledge(kind="op-spec", topic="add-index" \| "table" \| "scaffold:form")` — those two tools keep their parameters OUT of the tool schema; look the contract up once for the operation you picked, then nest the values in `params` (`properties` for `action="create"`) |
47
47
 
@@ -53,7 +53,7 @@ PowerShell / any terminal command **WILL HANG** in VS 2022 / VS 2026 MCP integra
53
53
 
54
54
  ### Writes & file editing
55
55
 
56
- 2. **`d365fo_file` (action=create/modify) applies immediately** (no dry-run / preview). Describe the change in chat and wait for explicit user confirmation ("apply", "ok", "yes") before calling. Revert with `undo_last_modification` (or pass `createBackup=true` to keep a `.bak`).
56
+ 2. **`d365fo_file` (action=create/modify) applies immediately** (no dry-run / preview). Describe the change in chat and wait for explicit user confirmation ("apply", "ok", "yes") before calling. Revert with `d365fo_file(action="undo")` (or pass `createBackup=true` to keep a `.bak`).
57
57
  3. **Never** use `replace_string_in_file`, `edit_file`, `apply_patch`, or any built-in file-write tool on `.xml` or `.xpp` files — **not even as a fallback** when `d365fo_file(action="modify")` fails. These bypass `IMetadataProvider` and corrupt VS 2022's in-memory model. If `d365fo_file(action="modify")` errors, STOP and report the error verbatim.
58
58
 
59
59
  ### Build automation
@@ -78,7 +78,7 @@ PowerShell / any terminal command **WILL HANG** in VS 2022 / VS 2026 MCP integra
78
78
  ### Reuse & diff safety
79
79
 
80
80
  10. **Reuse before creating** — `prepare(mode="change")` lists existing CoC wrappers and event handlers. If an extension or handler class in the custom model already owns the target, add the new method there. Never create a parallel feature-named class (`<Target>_<Feature>_Extension`, `<Form>_<Feature>_EH`) unless the user explicitly asks for a separate class. The suffix comes from `EXTENSION_NAMING_STYLE` / existing artifacts — never from feature, ticket, or customer names; if it cannot be derived, ask.
81
- 11. **The post-write diff must be additive or narrowly targeted** — verify via `review_workspace_changes` (or re-read with `get_*_info`) that no unrelated XML nodes (`<DataSources>`, `<Controls>`, methods, pattern metadata) disappeared. If they did, the edit failed: `undo_last_modification`.
81
+ 11. **The post-write diff must be additive or narrowly targeted** — verify via `get_workspace_info(changes=true)` (or re-read with `get_object_info`) that no unrelated XML nodes (`<DataSources>`, `<Controls>`, methods, pattern metadata) disappeared. If they did, the edit failed: `d365fo_file(action="undo")`.
82
82
  12. **An example form named by the user is a pattern contract** — keep its pattern family and required scaffolding (datasources, ActionPane/Tab/grid/QuickFilter); missing pattern elements are a failed generation even if the XML is well-formed.
83
83
 
84
84
  ### Spending tool calls
package/README.md CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  <div align="center">
4
4
 
5
- **23 AI tools that know every X++ class, table, form, and EDT in your D365FO codebase**
5
+ **20 AI tools that know every X++ class, table, form, and EDT in your D365FO codebase**
6
6
 
7
7
  [![npm](https://img.shields.io/npm/v/d365fo-mcp.svg?logo=npm&color=cb3837)](https://www.npmjs.com/package/d365fo-mcp)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
9
  [![Node.js](https://img.shields.io/badge/node-%3E%3D24.0.0-brightgreen.svg)](https://nodejs.org/)
10
10
  [![TypeScript](https://img.shields.io/badge/TypeScript-7.0-blue.svg)](https://www.typescriptlang.org/)
11
- [![Tests](https://img.shields.io/badge/tests-2800%2B-brightgreen.svg)](docs/TESTING.md)
11
+ [![Tests](https://img.shields.io/badge/tests-5000%2B-brightgreen.svg)](docs/TESTING.md)
12
12
  <!-- coverage-badge:start -->
13
13
  [![Core coverage](https://img.shields.io/badge/core_coverage-100%25-brightgreen.svg)](eval/COVERAGE.md) [![Total coverage](https://img.shields.io/badge/total_coverage-100%25-lightgrey.svg)](eval/COVERAGE.md)
14
14
  <!-- coverage-badge:end -->
@@ -29,7 +29,7 @@
29
29
 
30
30
  AI assistants excel at C#, Python, and JavaScript. X++ is different: your D365FO codebase is private, deeply customized, and invisible to every model — so AI confidently generates code that doesn't compile.
31
31
 
32
- This server pre-indexes your entire D365FO installation (580 000+ symbols across standard, ISV, and custom models) and exposes it as 23 specialized MCP tools. Every signature, every CoC wrapper, every label, every form pattern — verified against your real metadata **before** the AI writes a single line.
32
+ This server pre-indexes your entire D365FO installation (580 000+ symbols across standard, ISV, and custom models) and exposes it as 20 specialized MCP tools. Every signature, every CoC wrapper, every label, every form pattern — verified against your real metadata **before** the AI writes a single line.
33
33
 
34
34
  ![Solution Architecture](docs/img/solution-architecture-diagram.svg)
35
35
 
@@ -115,7 +115,7 @@ Deployment guide: [docs/SETUP_AZURE.md](docs/SETUP_AZURE.md) — includes CI/CD
115
115
 
116
116
  | Getting started | Reference | Operations |
117
117
  |-----------------|-----------|------------|
118
- | [Quick Start](docs/QUICK_START.md) — connect or install | [All 23 tools](docs/MCP_TOOLS.md) | [Azure deployment](docs/SETUP_AZURE.md) |
118
+ | [Quick Start](docs/QUICK_START.md) — connect or install | [All 20 tools](docs/MCP_TOOLS.md) | [Azure deployment](docs/SETUP_AZURE.md) |
119
119
  | [Setup scenarios A–F](docs/SETUP.md) | [`.mcp.json` reference](docs/MCP_CONFIG.md) | [DevOps pipelines](docs/SETUP_AZURE.md#azure-devops-pipelines) |
120
120
  | [Claude Code setup](docs/SETUP.md#claude-code-cli) | [Configuration](docs/CONFIGURATION.md) | [Testing](docs/TESTING.md) |
121
121
  | [Usage examples](docs/USAGE_EXAMPLES.md) — real tool chains | [Architecture](docs/ARCHITECTURE.md) | [Custom / ISV models](docs/CUSTOM_EXTENSIONS.md) |
@@ -255,6 +255,18 @@ namespace D365MetadataBridge.Models
255
255
  [JsonPropertyName("extends")]
256
256
  public string? Extends { get; set; }
257
257
 
258
+ /// <summary>
259
+ /// Root of the Extends chain. Reported for orientation in a multi-level hierarchy; it is
260
+ /// not necessarily the element that declares the StringSize -- see StringSizeInheritedFrom
261
+ /// for that.
262
+ ///
263
+ /// Null for a root EDT, and also null when the chain could not be walked to its end -- a
264
+ /// dangling Extends or a cycle -- because the last ancestor reached is not the root and
265
+ /// naming it as one would be a wrong answer rather than a missing one.
266
+ /// </summary>
267
+ [JsonPropertyName("rootEdt")]
268
+ public string? RootEdt { get; set; }
269
+
258
270
  [JsonPropertyName("label")]
259
271
  public string? Label { get; set; }
260
272
 
@@ -264,6 +276,14 @@ namespace D365MetadataBridge.Models
264
276
  [JsonPropertyName("stringSize")]
265
277
  public int? StringSize { get; set; }
266
278
 
279
+ /// <summary>
280
+ /// Ancestor the reported StringSize came from. Null when the number is what this EDT's
281
+ /// own XML declares. Set both when the EDT declares nothing (the common case) and when
282
+ /// an ancestor's value overrode one the EDT did declare.
283
+ /// </summary>
284
+ [JsonPropertyName("stringSizeInheritedFrom")]
285
+ public string? StringSizeInheritedFrom { get; set; }
286
+
267
287
  [JsonPropertyName("referenceTable")]
268
288
  public string? ReferenceTable { get; set; }
269
289
 
@@ -1,9 +1,11 @@
1
1
  using System;
2
+ using System.Collections.Generic;
2
3
  using System.Diagnostics;
3
4
  using System.IO;
4
5
  using System.Reflection;
5
6
  using System.Runtime.CompilerServices;
6
7
  using System.Text.Json;
8
+ using System.Threading;
7
9
  using System.Threading.Tasks;
8
10
  using D365MetadataBridge.Protocol;
9
11
 
@@ -219,9 +221,33 @@ namespace D365MetadataBridge
219
221
  }
220
222
  }
221
223
 
224
+ /// <summary>
225
+ /// How many reads may be inside the provider at once. Bounded on purpose: the
226
+ /// win is in overlapping a handful of fan-out reads, not in handing an unbounded
227
+ /// number of threads to a metadata provider whose thread-safety is undocumented.
228
+ /// </summary>
229
+ private const int ReadSlots = 6;
230
+
231
+ /// <summary>
232
+ /// A read takes one slot; anything else takes ALL of them, which is what makes a
233
+ /// write or a provider refresh exclusive against every in-flight read. Simple
234
+ /// enough to reason about without an async reader/writer lock.
235
+ ///
236
+ /// A writer waits for the slots, so a continuous stream of reads could delay it.
237
+ /// In practice requests come from one MCP client that awaits its own calls, so
238
+ /// there is no such stream.
239
+ /// </summary>
240
+ private static readonly SemaphoreSlim _slots = new SemaphoreSlim(ReadSlots, ReadSlots);
241
+
242
+ /// <summary>stdout is one stream: concurrent handlers must not interleave lines on it.</summary>
243
+ private static readonly SemaphoreSlim _stdoutLock = new SemaphoreSlim(1, 1);
244
+ private static readonly StreamWriter _stdout =
245
+ new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true };
246
+
222
247
  private static async Task<int> RunStdioLoop(RequestDispatcher dispatcher)
223
248
  {
224
249
  var reader = new StreamReader(Console.OpenStandardInput());
250
+ var inFlight = new List<Task>();
225
251
 
226
252
  string? line;
227
253
  while ((line = await reader.ReadLineAsync()) != null)
@@ -229,45 +255,127 @@ namespace D365MetadataBridge
229
255
  if (string.IsNullOrWhiteSpace(line))
230
256
  continue;
231
257
 
232
- BridgeResponse response;
258
+ BridgeRequest? request = null;
233
259
  try
234
260
  {
235
- var request = JsonSerializer.Deserialize<BridgeRequest>(line, JsonOptions.Default);
236
- if (request == null || string.IsNullOrEmpty(request.Method))
237
- {
238
- response = BridgeResponse.CreateError("?", -32600, "Invalid request");
239
- }
240
- else
241
- {
242
- Log.WriteLine($"[DEBUG] → {request.Method} (id={request.Id})");
243
- response = await dispatcher.Dispatch(request);
244
- Log.WriteLine($"[DEBUG] ← {request.Method} OK (id={request.Id})");
245
- }
261
+ request = JsonSerializer.Deserialize<BridgeRequest>(line, JsonOptions.Default);
246
262
  }
247
263
  catch (JsonException ex)
248
264
  {
249
265
  Log.WriteLine($"[ERROR] JSON parse error: {ex.Message}");
250
- response = BridgeResponse.CreateError("?", -32700, $"Parse error: {ex.Message}");
266
+ await WriteResponse(BridgeResponse.CreateError("?", -32700, $"Parse error: {ex.Message}"));
267
+ continue;
251
268
  }
252
- catch (Exception ex)
269
+
270
+ if (request == null || string.IsNullOrEmpty(request.Method))
253
271
  {
254
- Log.WriteLine($"[ERROR] Unhandled: {ex.Message}\n{ex.StackTrace}");
255
- response = BridgeResponse.CreateError("?", -32603, $"Internal error: {ex.Message}");
272
+ await WriteResponse(BridgeResponse.CreateError("?", -32600, "Invalid request"));
273
+ continue;
256
274
  }
257
275
 
258
- await WriteResponse(response);
276
+ // Reads go to the thread pool so the loop can read the next line at once;
277
+ // everything else keeps the original behaviour of running to completion
278
+ // before the next request is even parsed.
279
+ if (RequestDispatcher.IsConcurrentSafeRead(request.Method))
280
+ {
281
+ // Touch Exception on a faulted task before dropping it, so the fault
282
+ // is observed rather than left for the finalizer — a task pruned here
283
+ // is never awaited anywhere else.
284
+ inFlight.RemoveAll(t =>
285
+ {
286
+ if (t.IsFaulted) { var _ = t.Exception; }
287
+ return t.IsCompleted;
288
+ });
289
+ inFlight.Add(HandleConcurrently(dispatcher, request));
290
+ }
291
+ else
292
+ {
293
+ // Drain the reads already running, then take every slot: a write or a
294
+ // provider rebuild must not overlap a read of the provider it replaces.
295
+ if (inFlight.Count > 0)
296
+ {
297
+ // Guarded. DispatchGuarded turns any handler escape into a
298
+ // response, so the only realistic thrower left is WriteResponse on
299
+ // a broken stdout — and an unguarded WhenAll rethrows that HERE,
300
+ // out of RunStdioLoop and out of Main, killing the bridge because
301
+ // an unrelated read could not print. The final drain below was
302
+ // already written this way; this one was not.
303
+ try { await Task.WhenAll(inFlight); }
304
+ catch { /* each read already answered, or failed on its own */ }
305
+ inFlight.Clear();
306
+ }
307
+ for (var i = 0; i < ReadSlots; i++) await _slots.WaitAsync();
308
+ try
309
+ {
310
+ await WriteResponse(await DispatchGuarded(dispatcher, request));
311
+ }
312
+ finally
313
+ {
314
+ _slots.Release(ReadSlots);
315
+ }
316
+ }
259
317
  }
260
318
 
319
+ if (inFlight.Count > 0)
320
+ {
321
+ try { await Task.WhenAll(inFlight); } catch { /* each response was already sent */ }
322
+ }
261
323
  Log.WriteLine("[INFO] stdin closed, bridge exiting");
262
324
  return 0;
263
325
  }
264
326
 
327
+ /// <summary>One read: take a slot, dispatch, answer. Never throws back into the loop.</summary>
328
+ private static async Task HandleConcurrently(RequestDispatcher dispatcher, BridgeRequest request)
329
+ {
330
+ await _slots.WaitAsync();
331
+ try
332
+ {
333
+ // Task.Run, not a bare await: every read handler returns
334
+ // Task.FromResult(...), so Dispatch runs to completion SYNCHRONOUSLY on
335
+ // whichever thread calls it. Awaiting it here left the work on the stdio
336
+ // loop thread and measured 1.06x against 1.04x for the serial build —
337
+ // the change did nothing until the work actually left this thread.
338
+ var response = await Task.Run(() => DispatchGuarded(dispatcher, request));
339
+ await WriteResponse(response);
340
+ }
341
+ finally
342
+ {
343
+ _slots.Release();
344
+ }
345
+ }
346
+
347
+ /// <summary>Dispatch, turning any escape into a response rather than a lost request.</summary>
348
+ private static async Task<BridgeResponse> DispatchGuarded(RequestDispatcher dispatcher, BridgeRequest request)
349
+ {
350
+ try
351
+ {
352
+ Log.WriteLine($"[DEBUG] → {request.Method} (id={request.Id})");
353
+ var response = await dispatcher.Dispatch(request);
354
+ Log.WriteLine($"[DEBUG] ← {request.Method} OK (id={request.Id})");
355
+ return response;
356
+ }
357
+ catch (Exception ex)
358
+ {
359
+ Log.WriteLine($"[ERROR] Unhandled: {ex.Message}\n{ex.StackTrace}");
360
+ return BridgeResponse.CreateError(request.Id, -32603, $"Internal error: {ex.Message}");
361
+ }
362
+ }
363
+
265
364
  private static async Task WriteResponse(BridgeResponse response)
266
365
  {
267
366
  var json = JsonSerializer.Serialize(response, JsonOptions.Default);
268
- var stdout = new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true };
269
- await stdout.WriteLineAsync(json);
270
- await stdout.FlushAsync();
367
+ // Serialized, and on ONE cached writer: concurrent handlers each opening their
368
+ // own StreamWriter over the same handle is how a JSON-RPC stream gets interleaved.
369
+ await _stdoutLock.WaitAsync();
370
+ try
371
+ {
372
+ await _stdout.WriteLineAsync(json);
373
+ await _stdout.FlushAsync();
374
+ }
375
+ finally
376
+ {
377
+ _stdoutLock.Release();
378
+ }
271
379
  }
272
380
 
273
381
  /// <summary>
@@ -813,6 +813,43 @@ namespace D365MetadataBridge.Protocol
813
813
  }
814
814
  }
815
815
 
816
+ /// <summary>
817
+ /// Methods that only READ metadata, and may therefore run concurrently with
818
+ /// each other.
819
+ ///
820
+ /// Measured on this VM: six reads issued together took 674 ms against 689 ms
821
+ /// issued one at a time — a 1.02x "speedup", because the stdio loop awaited each
822
+ /// Dispatch before reading the next line. Every fan-out the TS side performs
823
+ /// (get_object_info objects[], search queries[], prepare's five probes) was
824
+ /// therefore pipelining into a queue of one.
825
+ ///
826
+ /// The list is explicit and the DEFAULT IS FALSE: anything not named here —
827
+ /// including a method added later — keeps the old exclusive behaviour. A write
828
+ /// misfiled as a read would corrupt metadata; a read misfiled as a write is
829
+ /// merely as slow as it is today.
830
+ /// </summary>
831
+ private static readonly HashSet<string> ConcurrentSafeReads =
832
+ new HashSet<string>(StringComparer.OrdinalIgnoreCase)
833
+ {
834
+ "ping", "getcapabilities", "getinfo", "getxrefschema",
835
+ // Typed readers
836
+ "readtable", "readclass", "readenum", "readedt", "readform", "readquery",
837
+ "readview", "readreport", "readdataentity", "readmenuitem",
838
+ "readsecurityprivilege", "readsecurityduty", "readsecurityrole",
839
+ "readtableextensions",
840
+ // Short aliases of the same readers
841
+ "table", "class", "enum", "edt", "form", "query", "view", "menu",
842
+ // Lookup / search
843
+ "searchobjects", "listobjects", "resolveobjectinfo", "getcompletionmembers",
844
+ "getmethodsource", "validateobject", "discoverformpatterns",
845
+ // Cross-reference queries
846
+ "findreferences", "findeventsubscribers", "findextensionclasses",
847
+ "findapiusagecallers", "samplexrefrows",
848
+ };
849
+
850
+ /// <summary>True when <paramref name="method"/> may run alongside other reads.</summary>
851
+ public static bool IsConcurrentSafeRead(string? method)
852
+ => !string.IsNullOrEmpty(method) && ConcurrentSafeReads.Contains(method!);
816
853
  private Task<BridgeResponse> HandleMetadata(BridgeRequest request, Func<object?> handler)
817
854
  {
818
855
  if (_metadataService == null)