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.
- package/.github/copilot-instructions.md +4 -4
- package/README.md +4 -4
- package/bridge/D365MetadataBridge/Models/Models.cs +20 -0
- package/bridge/D365MetadataBridge/Program.cs +128 -20
- package/bridge/D365MetadataBridge/Protocol/RequestDispatcher.cs +37 -0
- package/bridge/D365MetadataBridge/Services/MetadataReadService.cs +536 -72
- package/bridge/D365MetadataBridge/Services/MetadataWriteService.cs +37 -1
- package/dist/bridge/bridgeAdapter.d.ts +105 -1
- package/dist/bridge/bridgeAdapter.js +290 -56
- package/dist/bridge/bridgeClient.d.ts +2 -0
- package/dist/bridge/bridgeClient.js +20 -0
- package/dist/bridge/bridgeFailure.d.ts +3 -1
- package/dist/bridge/bridgeFailure.js +15 -6
- package/dist/bridge/bridgeTypes.d.ts +14 -0
- package/dist/cli/session/analyze.js +6 -1
- package/dist/config/settings.js +49 -5
- package/dist/index.js +22 -9
- package/dist/knowledge/compilerFacts.d.ts +50 -0
- package/dist/knowledge/compilerFacts.generated.d.ts +27 -0
- package/dist/knowledge/compilerFacts.generated.js +915 -0
- package/dist/knowledge/compilerFacts.js +67 -0
- package/dist/knowledge/kernelEnums.d.ts +64 -0
- package/dist/knowledge/kernelEnums.js +104 -0
- package/dist/knowledge/mobileAppPatterns/catalog.d.ts +20 -0
- package/dist/knowledge/mobileAppPatterns/catalog.js +548 -0
- package/dist/knowledge/mobileAppPatterns/index.d.ts +19 -0
- package/dist/knowledge/mobileAppPatterns/index.js +115 -0
- package/dist/knowledge/mobileAppPatterns/types.d.ts +62 -0
- package/dist/knowledge/mobileAppPatterns/types.js +22 -0
- package/dist/knowledge/reportPatterns/catalog.d.ts +16 -0
- package/dist/knowledge/reportPatterns/catalog.js +411 -0
- package/dist/knowledge/reportPatterns/index.d.ts +13 -0
- package/dist/knowledge/reportPatterns/index.js +73 -0
- package/dist/knowledge/reportPatterns/types.d.ts +43 -0
- package/dist/knowledge/reportPatterns/types.js +11 -0
- package/dist/metadata/indexWarmup.d.ts +43 -0
- package/dist/metadata/indexWarmup.js +62 -0
- package/dist/metadata/indexWarmupWorker.d.ts +67 -0
- package/dist/metadata/indexWarmupWorker.js +117 -0
- package/dist/metadata/symbolIndex.d.ts +23 -0
- package/dist/metadata/symbolIndex.js +72 -5
- package/dist/prompts/codeReview.js +5 -0
- package/dist/prompts/systemInstructions.js +5 -2
- package/dist/resources/index.d.ts +1 -0
- package/dist/resources/index.js +43 -14
- package/dist/scripts/build-database.js +66 -9
- package/dist/scripts/build-fts.js +66 -9
- package/dist/scripts/extract-metadata.js +38 -4
- package/dist/server/serverMode.d.ts +23 -3
- package/dist/server/serverMode.js +46 -12
- package/dist/server/toolAnnotations.js +1 -4
- package/dist/server/toolSchemas/buildD365foProject.d.ts +14 -5
- package/dist/server/toolSchemas/buildD365foProject.js +16 -6
- package/dist/server/toolSchemas/d365foFile.d.ts +4 -1
- package/dist/server/toolSchemas/d365foFile.js +26 -18
- package/dist/server/toolSchemas/extensionInfo.js +2 -2
- package/dist/server/toolSchemas/findReferences.js +1 -1
- package/dist/server/toolSchemas/generateObject.d.ts +0 -1
- package/dist/server/toolSchemas/generateObject.js +14 -5
- package/dist/server/toolSchemas/getKnowledge.d.ts +8 -1
- package/dist/server/toolSchemas/getKnowledge.js +12 -4
- package/dist/server/toolSchemas/getObjectInfo.d.ts +0 -1
- package/dist/server/toolSchemas/getObjectInfo.js +8 -5
- package/dist/server/toolSchemas/getWorkspaceInfo.d.ts +11 -0
- package/dist/server/toolSchemas/getWorkspaceInfo.js +14 -3
- package/dist/server/toolSchemas/index.d.ts +189 -148
- package/dist/server/toolSchemas/index.js +0 -6
- package/dist/server/toolSchemas/labels.d.ts +0 -23
- package/dist/server/toolSchemas/labels.js +16 -27
- package/dist/server/toolSchemas/objectPatterns.js +15 -13
- package/dist/server/toolSchemas/prepare.d.ts +0 -1
- package/dist/server/toolSchemas/prepare.js +11 -4
- package/dist/server/toolSchemas/runBpCheck.d.ts +0 -8
- package/dist/server/toolSchemas/runBpCheck.js +10 -6
- package/dist/server/toolSchemas/runSystestClass.js +3 -3
- package/dist/server/toolSchemas/search.d.ts +0 -36
- package/dist/server/toolSchemas/search.js +6 -46
- package/dist/server/toolSchemas/securityInfo.d.ts +0 -1
- package/dist/server/toolSchemas/securityInfo.js +1 -1
- package/dist/server/toolSchemas/updateSymbolIndex.js +5 -3
- package/dist/server/toolSchemas/validateCode.js +6 -6
- package/dist/server/toolSchemas/validateObjectNaming.js +1 -1
- package/dist/server/toolSchemas/verifyD365foProject.d.ts +0 -8
- package/dist/server/toolSchemas/verifyD365foProject.js +7 -4
- package/dist/server/transport.d.ts +19 -0
- package/dist/server/transport.js +21 -8
- package/dist/tools/analysis/batchSearch.js +17 -23
- package/dist/tools/analysis/findReferences.js +107 -7
- package/dist/tools/analysis/search.d.ts +24 -0
- package/dist/tools/analysis/search.js +169 -15
- package/dist/tools/analysis/searchLabels.d.ts +4 -0
- package/dist/tools/analysis/searchLabels.js +115 -12
- package/dist/tools/analysis/validateCode.d.ts +1 -1
- package/dist/tools/analysis/validateCode.js +237 -42
- package/dist/tools/analysis/validateObjectNaming.d.ts +4 -3
- package/dist/tools/analysis/validateObjectNaming.js +13 -355
- package/dist/tools/analysis/validateXpp.d.ts +27 -6
- package/dist/tools/analysis/validateXpp.js +1078 -146
- package/dist/tools/d365foFile.d.ts +2 -1
- package/dist/tools/d365foFile.js +255 -8
- package/dist/tools/knowledge/getKnowledge.d.ts +29 -0
- package/dist/tools/knowledge/getKnowledge.js +118 -0
- package/dist/tools/knowledge/getMobileAppPatterns.d.ts +22 -0
- package/dist/tools/knowledge/getMobileAppPatterns.js +29 -0
- package/dist/tools/knowledge/getReportPatterns.d.ts +22 -0
- package/dist/tools/knowledge/getReportPatterns.js +29 -0
- package/dist/tools/knowledge/objectPatterns.d.ts +1 -0
- package/dist/tools/knowledge/objectPatterns.js +42 -4
- package/dist/tools/knowledge/xppKnowledge.js +1171 -39
- package/dist/tools/labels.js +23 -1
- package/dist/tools/prepare/prepare.d.ts +4 -2
- package/dist/tools/prepare/prepare.js +76 -7
- package/dist/tools/prepare/prepareChange.d.ts +22 -0
- package/dist/tools/prepare/prepareChange.js +264 -44
- package/dist/tools/prepare/prepareCreate.js +83 -26
- package/dist/tools/prepare/prepareTest.d.ts +3 -0
- package/dist/tools/prepare/prepareTest.js +196 -0
- package/dist/tools/readers/edtInfo.js +91 -16
- package/dist/tools/readers/enumInfo.d.ts +1 -6
- package/dist/tools/readers/enumInfo.js +9 -0
- package/dist/tools/readers/formInfo.js +8 -2
- package/dist/tools/readers/getObjectInfo.js +135 -3
- package/dist/tools/readers/getWorkspaceInfo.js +68 -25
- package/dist/tools/readers/tableInfo.d.ts +1 -13
- package/dist/tools/readers/tableInfo.js +39 -2
- package/dist/tools/responseCaps.d.ts +11 -0
- package/dist/tools/responseCaps.js +116 -0
- package/dist/tools/sdlc/buildProject.d.ts +39 -0
- package/dist/tools/sdlc/buildProject.js +201 -11
- package/dist/tools/sdlc/dbSync.js +5 -5
- package/dist/tools/sdlc/reviewWorkspaceChanges.d.ts +1 -1
- package/dist/tools/sdlc/reviewWorkspaceChanges.js +78 -9
- package/dist/tools/sdlc/runBpCheck.d.ts +13 -0
- package/dist/tools/sdlc/runBpCheck.js +104 -9
- package/dist/tools/sdlc/sysTestRunner.d.ts +17 -0
- package/dist/tools/sdlc/sysTestRunner.js +180 -10
- package/dist/tools/sdlc/undoLastModification.js +4 -4
- package/dist/tools/sdlc/updateSymbolIndex.js +22 -1
- package/dist/tools/smart/codeGen.d.ts +8 -0
- package/dist/tools/smart/codeGen.js +437 -18
- package/dist/tools/smart/generateSmartForm.js +7 -2
- package/dist/tools/smart/generateSmartReport.d.ts +10 -2
- package/dist/tools/smart/generateSmartReport.js +143 -47
- package/dist/tools/smart/generateSmartTable.d.ts +21 -3
- package/dist/tools/smart/generateSmartTable.js +101 -7
- package/dist/tools/specs/d365foFileOpSpecs.d.ts +42 -0
- package/dist/tools/specs/d365foFileOpSpecs.js +140 -20
- package/dist/tools/specs/generateObjectOpSpecs.js +61 -7
- package/dist/tools/specs/labelsOpSpecs.js +16 -2
- package/dist/tools/specs/opSpecs.d.ts +1 -15
- package/dist/tools/specs/opSpecs.js +95 -9
- package/dist/tools/toolHandler.d.ts +0 -1
- package/dist/tools/toolHandler.js +119 -66
- package/dist/tools/write/createD365File.d.ts +42 -452
- package/dist/tools/write/createD365File.js +357 -2663
- package/dist/tools/write/createLabel.d.ts +95 -0
- package/dist/tools/write/createLabel.js +267 -4
- package/dist/tools/write/deleteD365File.js +8 -6
- package/dist/tools/write/directXmlWriters.d.ts +378 -0
- package/dist/tools/write/directXmlWriters.js +1570 -0
- package/dist/tools/write/inlineWriteVerification.d.ts +15 -0
- package/dist/tools/write/inlineWriteVerification.js +20 -0
- package/dist/tools/write/modifyD365File.d.ts +276 -66
- package/dist/tools/write/modifyD365File.js +684 -1579
- package/dist/tools/write/renameLabel.js +4 -3
- package/dist/tools/write/resolveReferences.d.ts +7 -0
- package/dist/tools/write/resolveReferences.js +179 -55
- package/dist/tools/xml/createTablePropertyHonesty.js +9 -6
- package/dist/tools/xml/formXml.js +54 -2
- package/dist/tools/xml/generateD365Xml.d.ts +6 -152
- package/dist/tools/xml/generateD365Xml.js +6 -1214
- package/dist/tools/xml/generateTableFields.d.ts +1 -1
- package/dist/tools/xml/generateTableFields.js +4 -4
- package/dist/tools/xml/queryViewXml.d.ts +1 -1
- package/dist/tools/xml/queryViewXml.js +2 -2
- package/dist/tools/xml/securityDutyRoleXml.d.ts +1 -1
- package/dist/tools/xml/securityDutyRoleXml.js +1 -1
- package/dist/tools/xml/securityPrivilegeXml.d.ts +56 -0
- package/dist/tools/xml/securityPrivilegeXml.js +121 -7
- package/dist/tools/xml/tableXml.d.ts +42 -0
- package/dist/tools/xml/tableXml.js +112 -24
- package/dist/tools/xml/xmlTemplateGenerator.d.ts +481 -0
- package/dist/tools/xml/xmlTemplateGenerator.js +2608 -0
- package/dist/utils/axTablePropertyOrder.d.ts +1 -1
- package/dist/utils/axTablePropertyOrder.js +1 -1
- package/dist/utils/baseObjectXml.d.ts +22 -0
- package/dist/utils/baseObjectXml.js +88 -0
- package/dist/utils/callDedup.d.ts +31 -2
- package/dist/utils/callDedup.js +73 -4
- package/dist/utils/configManager.d.ts +14 -0
- package/dist/utils/configManager.js +69 -5
- package/dist/utils/fieldControlTypes.d.ts +35 -1
- package/dist/utils/fieldControlTypes.js +118 -2
- package/dist/utils/formExtensionControlModifications.d.ts +92 -0
- package/dist/utils/formExtensionControlModifications.js +289 -0
- package/dist/utils/formPatternTemplates.d.ts +24 -2
- package/dist/utils/formPatternTemplates.js +15 -9
- package/dist/utils/indexStaleness.d.ts +35 -2
- package/dist/utils/indexStaleness.js +58 -2
- package/dist/utils/labelDiskCheck.d.ts +13 -0
- package/dist/utils/labelDiskCheck.js +46 -13
- package/dist/utils/labelReference.d.ts +40 -0
- package/dist/utils/labelReference.js +75 -0
- package/dist/utils/loopLag.d.ts +38 -0
- package/dist/utils/loopLag.js +81 -0
- package/dist/utils/objectNamingRules.d.ts +60 -0
- package/dist/utils/objectNamingRules.js +410 -0
- package/dist/utils/packageResolver.d.ts +22 -0
- package/dist/utils/packageResolver.js +65 -0
- package/dist/utils/phaseTimer.js +36 -0
- package/dist/utils/repeatedNotes.d.ts +29 -0
- package/dist/utils/repeatedNotes.js +38 -0
- package/dist/utils/smartXmlBuilder.js +1 -1
- package/dist/utils/toolMetrics.d.ts +18 -1
- package/dist/utils/toolMetrics.js +23 -2
- package/dist/utils/workspaceDetector.d.ts +5 -0
- package/dist/utils/workspaceDetector.js +1 -1
- package/dist/utils/writeSideEffects.d.ts +43 -0
- package/dist/utils/writeSideEffects.js +56 -0
- package/dist/utils/xppLexer.d.ts +59 -0
- package/dist/utils/xppLexer.js +123 -0
- package/dist/utils/xppSelectLint.js +8 -36
- package/dist/validation/formPatternValidator.js +15 -0
- package/dist/workspace/contextSnapshot.d.ts +17 -2
- package/dist/workspace/contextSnapshot.js +105 -24
- package/dist/workspace/workspaceScanner.d.ts +1 -1
- package/dist/workspace/workspaceScanner.js +1 -1
- package/package.json +1 -1
- package/dist/server/toolSchemas/reviewWorkspaceChanges.d.ts +0 -20
- package/dist/server/toolSchemas/reviewWorkspaceChanges.js +0 -19
- package/dist/server/toolSchemas/triggerDbSync.d.ts +0 -47
- package/dist/server/toolSchemas/triggerDbSync.js +0 -31
- package/dist/server/toolSchemas/undoLastModification.d.ts +0 -20
- 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 | `
|
|
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
|
|
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 `
|
|
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 `
|
|
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
|
-
**
|
|
5
|
+
**20 AI tools that know every X++ class, table, form, and EDT in your D365FO codebase**
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/d365fo-mcp)
|
|
8
8
|
[](https://opensource.org/licenses/MIT)
|
|
9
9
|
[](https://nodejs.org/)
|
|
10
10
|
[](https://www.typescriptlang.org/)
|
|
11
|
-
[](docs/TESTING.md)
|
|
12
12
|
<!-- coverage-badge:start -->
|
|
13
13
|
[](eval/COVERAGE.md) [](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
|
|
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
|

|
|
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
|
|
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
|
-
|
|
258
|
+
BridgeRequest? request = null;
|
|
233
259
|
try
|
|
234
260
|
{
|
|
235
|
-
|
|
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
|
-
|
|
266
|
+
await WriteResponse(BridgeResponse.CreateError("?", -32700, $"Parse error: {ex.Message}"));
|
|
267
|
+
continue;
|
|
251
268
|
}
|
|
252
|
-
|
|
269
|
+
|
|
270
|
+
if (request == null || string.IsNullOrEmpty(request.Method))
|
|
253
271
|
{
|
|
254
|
-
|
|
255
|
-
|
|
272
|
+
await WriteResponse(BridgeResponse.CreateError("?", -32600, "Invalid request"));
|
|
273
|
+
continue;
|
|
256
274
|
}
|
|
257
275
|
|
|
258
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
await
|
|
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)
|