d365fo-mcp 1.5.2 → 1.7.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 (87) hide show
  1. package/README.md +1 -1
  2. package/bridge/D365MetadataBridge/D365MetadataBridge.csproj +33 -3
  3. package/bridge/D365MetadataBridge/Program.cs +1 -1
  4. package/bridge/D365MetadataBridge/Services/MetadataWriteService.cs +54 -1
  5. package/dist/bridge/bridgeAdapter.d.ts +1 -1
  6. package/dist/bridge/bridgeAdapter.js +34 -3
  7. package/dist/bridge/bridgeClient.d.ts +1 -1
  8. package/dist/bridge/bridgeClient.js +3 -5
  9. package/dist/bridge/bridgeTypes.d.ts +15 -0
  10. package/dist/cli/commands/doctor.js +39 -0
  11. package/dist/cli/commands/instance.js +7 -4
  12. package/dist/cli/commands/setup.js +11 -7
  13. package/dist/cli/copilotFiles.d.ts +3 -9
  14. package/dist/cli/copilotFiles.js +13 -24
  15. package/dist/cli/settingsPrompt.js +4 -1
  16. package/dist/config/settings.js +3 -1
  17. package/dist/metadata/modelDescriptor.d.ts +53 -0
  18. package/dist/metadata/modelDescriptor.js +129 -0
  19. package/dist/metadata/symbolIndex.js +4 -6
  20. package/dist/metadata/types.d.ts +8 -0
  21. package/dist/metadata/xmlParser.d.ts +5 -1
  22. package/dist/metadata/xmlParser.js +10 -2
  23. package/dist/metadata/xppDeclaration.d.ts +22 -0
  24. package/dist/metadata/xppDeclaration.js +36 -2
  25. package/dist/scripts/build-database.js +76 -10
  26. package/dist/scripts/build-fts.js +76 -10
  27. package/dist/scripts/extract-metadata.js +141 -66
  28. package/dist/server/toolSchemas/d365foFile.js +6 -6
  29. package/dist/server/toolSchemas/verifyD365foProject.js +1 -1
  30. package/dist/tools/analyzeExtensionPoints.js +141 -5
  31. package/dist/tools/buildProject.d.ts +58 -0
  32. package/dist/tools/buildProject.js +147 -55
  33. package/dist/tools/compileLabels.d.ts +62 -0
  34. package/dist/tools/compileLabels.js +239 -0
  35. package/dist/tools/completion.js +11 -2
  36. package/dist/tools/createD365File.d.ts +15 -7
  37. package/dist/tools/createD365File.js +51 -24
  38. package/dist/tools/createLabel.js +5 -4
  39. package/dist/tools/dataEntityInfo.d.ts +9 -12
  40. package/dist/tools/dataEntityInfo.js +12 -3
  41. package/dist/tools/dataEntityViewExtensionXml.d.ts +74 -0
  42. package/dist/tools/dataEntityViewExtensionXml.js +142 -0
  43. package/dist/tools/dataEntityXml.d.ts +60 -0
  44. package/dist/tools/dataEntityXml.js +165 -21
  45. package/dist/tools/dbSync.js +2 -1
  46. package/dist/tools/enumInfo.d.ts +10 -3
  47. package/dist/tools/enumInfo.js +69 -10
  48. package/dist/tools/formInfo.js +11 -0
  49. package/dist/tools/generateD365Xml.d.ts +157 -0
  50. package/dist/tools/generateD365Xml.js +51 -15
  51. package/dist/tools/generateSmart.js +1 -1
  52. package/dist/tools/generateSmartForm.js +2 -1
  53. package/dist/tools/generateSmartReport.d.ts +2 -1
  54. package/dist/tools/generateSmartReport.js +119 -117
  55. package/dist/tools/generateSmartTable.js +5 -4
  56. package/dist/tools/getMethodSource.js +49 -1
  57. package/dist/tools/menuItemExtensionXml.d.ts +39 -0
  58. package/dist/tools/menuItemExtensionXml.js +91 -0
  59. package/dist/tools/methodSignature.js +71 -25
  60. package/dist/tools/prepareChange.js +70 -34
  61. package/dist/tools/queryInfo.d.ts +10 -3
  62. package/dist/tools/queryInfo.js +123 -10
  63. package/dist/tools/renameLabel.js +5 -4
  64. package/dist/tools/reportInfo.js +42 -14
  65. package/dist/tools/resolveReferences.d.ts +14 -1
  66. package/dist/tools/resolveReferences.js +160 -14
  67. package/dist/tools/runBpCheck.js +23 -16
  68. package/dist/tools/sysTestRunner.js +2 -1
  69. package/dist/tools/updateSymbolIndex.js +3 -4
  70. package/dist/tools/validateCode.js +2 -1
  71. package/dist/tools/verifyD365Project.js +6 -5
  72. package/dist/tools/viewInfo.d.ts +15 -4
  73. package/dist/tools/viewInfo.js +162 -9
  74. package/dist/tools/xppKnowledge.js +160 -12
  75. package/dist/utils/axTablePropertyOrder.d.ts +1 -1
  76. package/dist/utils/axTablePropertyOrder.js +5 -0
  77. package/dist/utils/configManager.d.ts +7 -0
  78. package/dist/utils/configManager.js +17 -25
  79. package/dist/utils/frameworkBin.d.ts +19 -0
  80. package/dist/utils/frameworkBin.js +48 -0
  81. package/dist/utils/indexedXmlLookup.d.ts +54 -0
  82. package/dist/utils/indexedXmlLookup.js +103 -0
  83. package/dist/utils/inheritanceChain.d.ts +44 -0
  84. package/dist/utils/inheritanceChain.js +99 -0
  85. package/dist/utils/packagesRoot.d.ts +55 -0
  86. package/dist/utils/packagesRoot.js +134 -0
  87. package/package.json +1 -1
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
  [![TypeScript](https://img.shields.io/badge/TypeScript-6.0-blue.svg)](https://www.typescriptlang.org/)
11
11
  [![Tests](https://img.shields.io/badge/tests-1400%2B-brightgreen.svg)](docs/TESTING.md)
12
12
  <!-- coverage-badge:start -->
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-72.7%25-lightgrey.svg)](eval/COVERAGE.md)
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-91%25-lightgrey.svg)](eval/COVERAGE.md)
14
14
  <!-- coverage-badge:end -->
15
15
 
16
16
  *Grounded AI development for Dynamics 365 Finance & Operations — works with GitHub Copilot and Claude Code*
@@ -23,10 +23,18 @@
23
23
 
24
24
  <!--
25
25
  D365FO bin directory — contains all Microsoft.Dynamics.*.dll assemblies.
26
- Every D365FO development desktop has this directory available.
26
+ Every D365FO development desktop has this directory available, but WHICH
27
+ volume it sits on is decided by the VM image: K: on cloud-hosted
28
+ environments, C: on the downloadable VHD, J: on newer images (#769), and
29
+ nothing stops the next image from using another letter again.
30
+
31
+ So the drive is swept rather than guessed: the first
32
+ <drive>:\AosService\PackagesLocalDirectory\bin that exists wins, with the
33
+ historically standard letters tried first and the rest of the alphabet
34
+ after them. MSBuild has no loop at evaluation time, and the property has to
35
+ be resolved before Reference items are globbed — hence one line per letter.
36
+ A: and B: are left out on purpose (floppy letters stall the probe).
27
37
 
28
- Traditional VM: K:\AosService\PackagesLocalDirectory\bin
29
- Cloud-hosted: C:\AosService\PackagesLocalDirectory\bin
30
38
  UDE (Unified Developer Experience): path from .mcp.json microsoftPackagesPath/bin
31
39
 
32
40
  Override per-machine via:
@@ -36,6 +44,28 @@
36
44
  <PropertyGroup>
37
45
  <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('K:\AosService\PackagesLocalDirectory\bin')">K:\AosService\PackagesLocalDirectory\bin</D365BinPath>
38
46
  <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('C:\AosService\PackagesLocalDirectory\bin')">C:\AosService\PackagesLocalDirectory\bin</D365BinPath>
47
+ <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('J:\AosService\PackagesLocalDirectory\bin')">J:\AosService\PackagesLocalDirectory\bin</D365BinPath>
48
+ <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('I:\AosService\PackagesLocalDirectory\bin')">I:\AosService\PackagesLocalDirectory\bin</D365BinPath>
49
+ <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('D:\AosService\PackagesLocalDirectory\bin')">D:\AosService\PackagesLocalDirectory\bin</D365BinPath>
50
+ <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('E:\AosService\PackagesLocalDirectory\bin')">E:\AosService\PackagesLocalDirectory\bin</D365BinPath>
51
+ <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('F:\AosService\PackagesLocalDirectory\bin')">F:\AosService\PackagesLocalDirectory\bin</D365BinPath>
52
+ <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('G:\AosService\PackagesLocalDirectory\bin')">G:\AosService\PackagesLocalDirectory\bin</D365BinPath>
53
+ <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('H:\AosService\PackagesLocalDirectory\bin')">H:\AosService\PackagesLocalDirectory\bin</D365BinPath>
54
+ <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('L:\AosService\PackagesLocalDirectory\bin')">L:\AosService\PackagesLocalDirectory\bin</D365BinPath>
55
+ <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('M:\AosService\PackagesLocalDirectory\bin')">M:\AosService\PackagesLocalDirectory\bin</D365BinPath>
56
+ <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('N:\AosService\PackagesLocalDirectory\bin')">N:\AosService\PackagesLocalDirectory\bin</D365BinPath>
57
+ <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('O:\AosService\PackagesLocalDirectory\bin')">O:\AosService\PackagesLocalDirectory\bin</D365BinPath>
58
+ <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('P:\AosService\PackagesLocalDirectory\bin')">P:\AosService\PackagesLocalDirectory\bin</D365BinPath>
59
+ <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('Q:\AosService\PackagesLocalDirectory\bin')">Q:\AosService\PackagesLocalDirectory\bin</D365BinPath>
60
+ <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('R:\AosService\PackagesLocalDirectory\bin')">R:\AosService\PackagesLocalDirectory\bin</D365BinPath>
61
+ <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('S:\AosService\PackagesLocalDirectory\bin')">S:\AosService\PackagesLocalDirectory\bin</D365BinPath>
62
+ <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('T:\AosService\PackagesLocalDirectory\bin')">T:\AosService\PackagesLocalDirectory\bin</D365BinPath>
63
+ <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('U:\AosService\PackagesLocalDirectory\bin')">U:\AosService\PackagesLocalDirectory\bin</D365BinPath>
64
+ <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('V:\AosService\PackagesLocalDirectory\bin')">V:\AosService\PackagesLocalDirectory\bin</D365BinPath>
65
+ <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('W:\AosService\PackagesLocalDirectory\bin')">W:\AosService\PackagesLocalDirectory\bin</D365BinPath>
66
+ <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('X:\AosService\PackagesLocalDirectory\bin')">X:\AosService\PackagesLocalDirectory\bin</D365BinPath>
67
+ <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('Y:\AosService\PackagesLocalDirectory\bin')">Y:\AosService\PackagesLocalDirectory\bin</D365BinPath>
68
+ <D365BinPath Condition="'$(D365BinPath)' == '' And Exists('Z:\AosService\PackagesLocalDirectory\bin')">Z:\AosService\PackagesLocalDirectory\bin</D365BinPath>
39
69
  </PropertyGroup>
40
70
 
41
71
  <!-- NuGet packages -->
@@ -416,7 +416,7 @@ Usage:
416
416
  D365MetadataBridge.exe [options]
417
417
 
418
418
  Options:
419
- --packages-path <path> Path to primary PackagesLocalDirectory (default: K:\AosService\PackagesLocalDirectory)
419
+ --packages-path <path> Path to primary PackagesLocalDirectory (required; the server detects it per machine)
420
420
  --reference-packages-path <path> UDE: secondary packages path (Microsoft FrameworkDirectory). Objects not found in
421
421
  the primary path are looked up here, enabling resolution of both custom and
422
422
  Microsoft-shipped metadata in UDE environments.
@@ -1504,7 +1504,7 @@ namespace D365MetadataBridge.Services
1504
1504
  ?? throw new ArgumentException($"Table '{objectName}' not found");
1505
1505
  var msi = GetModelSaveInfoForObject(_provider.Tables, objectName);
1506
1506
  if (!SetAxTableProperty(obj, propertyPath, propertyValue))
1507
- throw new ArgumentException($"Unknown AxTable property '{propertyPath}' — nothing was written. Supported: label, developerDocumentation, configurationKey, formRef, tableGroup, cacheLookup, clusteredIndex, primaryIndex, replacementKey, saveDataPerCompany, tableType, supportInheritance, instanceRelationType, extends, titleField1, titleField2.");
1507
+ throw new ArgumentException($"Unknown AxTable property '{propertyPath}' — nothing was written. Supported: label, developerDocumentation, configurationKey, formRef, tableGroup, cacheLookup, clusteredIndex, primaryIndex, replacementKey, saveDataPerCompany, allowRowVersionChangeTracking, createdBy, createdDateTime, createdTransactionId, modifiedBy, modifiedDateTime, modifiedTransactionId, tableType, supportInheritance, instanceRelationType, extends, titleField1, titleField2.");
1508
1508
  ((IMetaTableProvider)_provider.Tables).Update(obj, msi);
1509
1509
  return new { success = true, operation = "modify-property", objectType, objectName, propertyPath, propertyValue, api = "Update" };
1510
1510
  }
@@ -1548,6 +1548,16 @@ namespace D365MetadataBridge.Services
1548
1548
  ((IMetaViewProvider)_provider.Views).Update(obj, msi);
1549
1549
  return new { success = true, operation = "modify-property", objectType, objectName, propertyPath, propertyValue, api = "Update" };
1550
1550
  }
1551
+ case "data-entity":
1552
+ {
1553
+ var obj = _provider.DataEntityViews.Read(objectName)
1554
+ ?? throw new ArgumentException($"Data entity '{objectName}' not found");
1555
+ var msi = GetModelSaveInfoForObject(_provider.DataEntityViews, objectName);
1556
+ if (!SetAxDataEntityViewProperty(obj, propertyPath, propertyValue))
1557
+ throw new ArgumentException($"Unknown AxDataEntityView property '{propertyPath}' — nothing was written. Supported: label, developerDocumentation, primaryKey, isPublic, publicEntityName, publicCollectionName, dataManagementEnabled, dataManagementStagingTable, entityCategory, allowRowVersionChangeTracking, allowRetention.");
1558
+ ((IMetaDataEntityViewProvider)_provider.DataEntityViews).Update(obj, msi);
1559
+ return new { success = true, operation = "modify-property", objectType, objectName, propertyPath, propertyValue, api = "Update" };
1560
+ }
1551
1561
  case "menu-item-action":
1552
1562
  {
1553
1563
  var obj = _provider.MenuItemActions.Read(objectName)
@@ -2642,6 +2652,16 @@ namespace D365MetadataBridge.Services
2642
2652
  case "savedatapercompany":
2643
2653
  tbl.SaveDataPerCompany = ParseNoYes(value);
2644
2654
  break;
2655
+ // Dual-write's table-side change-tracking prerequisite.
2656
+ case "allowrowversionchangetracking":
2657
+ tbl.AllowRowVersionChangeTracking = ParseNoYes(value);
2658
+ break;
2659
+ case "createdby": tbl.CreatedBy = ParseNoYes(value); break;
2660
+ case "createddatetime": tbl.CreatedDateTime = ParseNoYes(value); break;
2661
+ case "createdtransactionid": tbl.CreatedTransactionId = ParseNoYes(value); break;
2662
+ case "modifiedby": tbl.ModifiedBy = ParseNoYes(value); break;
2663
+ case "modifieddatetime": tbl.ModifiedDateTime = ParseNoYes(value); break;
2664
+ case "modifiedtransactionid": tbl.ModifiedTransactionId = ParseNoYes(value); break;
2645
2665
  case "tabletype":
2646
2666
  if (!Enum.TryParse<Microsoft.Dynamics.AX.Metadata.Core.MetaModel.TableType>(value, true, out var tt))
2647
2667
  throw new ArgumentException(
@@ -2725,6 +2745,39 @@ namespace D365MetadataBridge.Services
2725
2745
  return true;
2726
2746
  }
2727
2747
 
2748
+ /// <summary>
2749
+ /// AxDataEntityView property setter. AllowRowVersionChangeTracking is
2750
+ /// dual-write's change-tracking switch and is also required on every
2751
+ /// source table.
2752
+ /// </summary>
2753
+ private bool SetAxDataEntityViewProperty(AxDataEntityView e, string prop, string value)
2754
+ {
2755
+ switch (prop.ToLowerInvariant())
2756
+ {
2757
+ case "label": e.Label = value; break;
2758
+ case "developerdocumentation": e.DeveloperDocumentation = value; break;
2759
+ case "primarykey": e.PrimaryKey = value; break;
2760
+ case "publicentityname": e.PublicEntityName = value; break;
2761
+ case "publiccollectionname": e.PublicCollectionName = value; break;
2762
+ case "datamanagementstagingtable": e.DataManagementStagingTable = value; break;
2763
+ case "ispublic": e.IsPublic = ParseNoYes(value); break;
2764
+ case "datamanagementenabled": e.DataManagementEnabled = ParseNoYes(value); break;
2765
+ case "allowrowversionchangetracking": e.AllowRowVersionChangeTracking = ParseNoYes(value); break;
2766
+ case "allowretention": e.AllowRetention = ParseNoYes(value); break;
2767
+ case "entitycategory":
2768
+ if (!Enum.TryParse<Microsoft.Dynamics.AX.Metadata.Core.MetaModel.EntityCategory>(value, true, out var ec))
2769
+ throw new ArgumentException(
2770
+ $"'{value}' is not a valid EntityCategory. Valid values: " +
2771
+ string.Join(", ", Enum.GetNames(typeof(Microsoft.Dynamics.AX.Metadata.Core.MetaModel.EntityCategory))) + ".");
2772
+ e.EntityCategory = ec;
2773
+ break;
2774
+ default:
2775
+ Console.Error.WriteLine($"[WriteService] Unknown AxDataEntityView property: {prop}");
2776
+ return false;
2777
+ }
2778
+ return true;
2779
+ }
2780
+
2728
2781
  private bool SetAxViewProperty(AxView v, string prop, string value)
2729
2782
  {
2730
2783
  switch (prop.ToLowerInvariant())
@@ -358,7 +358,7 @@ export declare function bridgeDiscoverFormPatterns(bridge: BridgeClient | undefi
358
358
  export declare function tryBridgeSecurityArtifact(bridge: BridgeClient | undefined, name: string, artifactType: 'privilege' | 'duty' | 'role', includeChain: boolean): Promise<ToolResult | null>;
359
359
  export declare function tryBridgeMenuItem(bridge: BridgeClient | undefined, name: string, itemType?: string): Promise<ToolResult | null>;
360
360
  export declare function tryBridgeTableExtensions(bridge: BridgeClient | undefined, baseTableName: string): Promise<ToolResult | null>;
361
- export declare function tryBridgeCompletion(bridge: BridgeClient | undefined, symbolName: string, prefix?: string): Promise<ToolResult | null>;
361
+ export declare function tryBridgeCompletion(bridge: BridgeClient | undefined, symbolName: string, prefix?: string, ancestors?: string[]): Promise<ToolResult | null>;
362
362
  export declare function tryBridgeCocExtensions(bridge: BridgeClient | undefined, baseClassName: string, methodName?: string): Promise<ToolResult | null>;
363
363
  export declare function tryBridgeEventHandlers(bridge: BridgeClient | undefined, targetName: string, eventName?: string, handlerType?: string): Promise<ToolResult | null>;
364
364
  export declare function tryBridgeApiUsageCallers(bridge: BridgeClient | undefined, apiName: string, limit?: number): Promise<ToolResult | null>;
@@ -1077,7 +1077,7 @@ const BRIDGE_MODIFY_OPS = new Set([
1077
1077
  */
1078
1078
  const BRIDGE_MODIFY_TYPES = new Set([
1079
1079
  'class', 'table', 'enum', 'edt',
1080
- 'form', 'query', 'view',
1080
+ 'form', 'query', 'view', 'data-entity',
1081
1081
  'class-extension', 'table-extension', 'form-extension', 'enum-extension',
1082
1082
  'menu-item-action', 'menu-item-display', 'menu-item-output',
1083
1083
  'menu',
@@ -1911,13 +1911,38 @@ function formatTableExtensions(r) {
1911
1911
  return out;
1912
1912
  }
1913
1913
  // CODE COMPLETION (Phase 6)
1914
- export async function tryBridgeCompletion(bridge, symbolName, prefix) {
1914
+ export async function tryBridgeCompletion(bridge, symbolName, prefix, ancestors) {
1915
1915
  if (!bridge?.isReady || !bridge.metadataAvailable)
1916
1916
  return null;
1917
1917
  try {
1918
1918
  const result = await bridge.getCompletionMembers(symbolName);
1919
1919
  if (!result || !result.members || result.members.length === 0)
1920
1920
  return null;
1921
+ // IMetadataProvider returns DECLARED members only, so a subclass lists
1922
+ // nothing it inherits and the reader concludes the member does not exist.
1923
+ // Merge the base classes in, nearest first; a name already present wins,
1924
+ // since that is the subclass's own override.
1925
+ if (ancestors?.length) {
1926
+ const seen = new Set(result.members.map(m => m.name.toLowerCase()));
1927
+ for (const ancestor of ancestors) {
1928
+ let inherited = null;
1929
+ try {
1930
+ inherited = await bridge.getCompletionMembers(ancestor);
1931
+ }
1932
+ catch (e) {
1933
+ // One unreadable link must not discard the members already merged.
1934
+ console.error(`[BridgeAdapter] getCompletionMembers(${ancestor}) failed: ${e}`);
1935
+ continue;
1936
+ }
1937
+ for (const m of inherited?.members ?? []) {
1938
+ const key = m.name.toLowerCase();
1939
+ if (seen.has(key))
1940
+ continue;
1941
+ seen.add(key);
1942
+ result.members.push({ ...m, inheritedFrom: inherited?.symbolName || ancestor });
1943
+ }
1944
+ }
1945
+ }
1921
1946
  return { content: [{ type: 'text', text: formatCompletion(result, prefix) }] };
1922
1947
  }
1923
1948
  catch (e) {
@@ -1943,9 +1968,15 @@ function formatCompletion(r, prefix) {
1943
1968
  const methodMembers = members.filter(m => m.kind === 'method');
1944
1969
  const fieldMembers = members.filter(m => m.kind === 'field');
1945
1970
  if (methodMembers.length > 0) {
1971
+ const inheritedCount = methodMembers.filter(m => m.inheritedFrom).length;
1946
1972
  out += `## Methods (${methodMembers.length})\n`;
1947
1973
  for (const m of methodMembers) {
1948
- out += m.signature ? `- \`${m.signature}\`\n` : `- ${m.name}\n`;
1974
+ const body = m.signature ? `\`${m.signature}\`` : m.name;
1975
+ out += m.inheritedFrom ? `- ${body} _(inherited from ${m.inheritedFrom})_\n` : `- ${body}\n`;
1976
+ }
1977
+ if (inheritedCount > 0) {
1978
+ out += `\n> ${inheritedCount} of these are inherited — callable on ${r.symbolName}, but ` +
1979
+ `declared on a base class. To read or wrap one, target the class named beside it.\n`;
1949
1980
  }
1950
1981
  }
1951
1982
  if (fieldMembers.length > 0) {
@@ -19,7 +19,7 @@ export * from './bridgeTypes.js';
19
19
  export interface BridgeClientOptions {
20
20
  /** Path to the D365MetadataBridge.exe (auto-detected if omitted) */
21
21
  bridgeExePath?: string;
22
- /** K:\AosService\PackagesLocalDirectory */
22
+ /** e.g. K:\AosService\PackagesLocalDirectory — the volume varies by VM image */
23
23
  packagesPath: string;
24
24
  /**
25
25
  * Optional secondary packages path.
@@ -17,6 +17,7 @@ import { EventEmitter } from 'events';
17
17
  import * as path from 'path';
18
18
  import * as fs from 'fs';
19
19
  import { fileURLToPath } from 'url';
20
+ import { packagesRoots } from '../utils/packagesRoot.js';
20
21
  export * from './bridgeTypes.js';
21
22
  const BRIDGE_EXE_NAME = 'D365MetadataBridge.exe';
22
23
  /** Parse a positive-integer env var with a fallback (ignores invalid/non-positive values). */
@@ -751,11 +752,8 @@ function detectPackagesPath() {
751
752
  const candidates = [
752
753
  process.env.D365FO_PACKAGE_PATH ?? '',
753
754
  process.env.PACKAGES_PATH ?? '',
754
- // Well-known fallback locations (traditional D365FO VM layouts)
755
- 'C:\\AosService\\PackagesLocalDirectory',
756
- 'C:\\AOSService\\PackagesLocalDirectory',
757
- 'J:\\AosService\\PackagesLocalDirectory',
758
- 'K:\\AosService\\PackagesLocalDirectory',
755
+ // Whatever AosService volumes this machine actually has (C:, J:, K:, )
756
+ ...packagesRoots(),
759
757
  ].filter(Boolean);
760
758
  for (const p of candidates) {
761
759
  // Traditional: bin is directly under packagesPath
@@ -92,6 +92,14 @@ export interface BridgeMethodInfo {
92
92
  returnType?: string;
93
93
  source?: string;
94
94
  isStatic?: boolean;
95
+ /**
96
+ * ⚠️ Never sent by the bridge. The C# MethodInfoModel behind readClass carries
97
+ * only name/source/isStatic, so this is always undefined on bridge-sourced
98
+ * methods — it is populated exclusively by the XML parser path
99
+ * (xmlParser.parseClassFile). Reading it off a readClass result silently
100
+ * yields nothing; for a bridge-sourced modifier parse the declaration line out
101
+ * of getCompletionMembers().members[].signature instead.
102
+ */
95
103
  visibility?: string;
96
104
  }
97
105
  export interface BridgeEnumInfo {
@@ -499,6 +507,13 @@ export interface BridgeCompletionMember {
499
507
  name: string;
500
508
  signature?: string;
501
509
  kind: string;
510
+ /**
511
+ * Set by the caller (not the bridge) when the member was picked up from a
512
+ * base class rather than declared on the requested one. IMetadataProvider
513
+ * returns declared members only, so inherited members are merged in on the
514
+ * TypeScript side and tagged here.
515
+ */
516
+ inheritedFrom?: string;
502
517
  }
503
518
  export interface BridgeCompletionResult {
504
519
  symbolName: string;
@@ -16,6 +16,7 @@ import { checkRelease } from '../npmRegistry.js';
16
16
  import { conflictingLegacyValues, readPath, readSetting } from '../settingsStore.js';
17
17
  import { instanceTarget, rootTarget } from '../target.js';
18
18
  import { isXppConfigStale, listXppConfigs, xppConfigDir } from '../xppConfig.js';
19
+ import { describePackagesRootScan, packagesRoots } from '../../utils/packagesRoot.js';
19
20
  const REQUIRED_NODE_MAJOR = 24;
20
21
  /**
21
22
  * How to reach the wizard from here. `npm run setup` only exists in a checkout
@@ -74,6 +75,40 @@ function checkConfig(target, label) {
74
75
  fix: SETUP_COMMAND,
75
76
  };
76
77
  }
78
+ /**
79
+ * The configured packages root against what the machine actually has.
80
+ *
81
+ * A packagePath pointing at a drive this image does not use is the failure
82
+ * behind "setup can't find the namespaces" (#769) — the index then builds from
83
+ * nothing and every lookup comes back empty, with no error saying why. Naming
84
+ * the detected volume turns that into a one-line fix.
85
+ */
86
+ function checkPackagesRoot(store, label) {
87
+ if (!isWindows)
88
+ return [];
89
+ const configured = String(readSetting(store, settingByPath('environment.packagePath')) ?? '').trim();
90
+ const detected = packagesRoots();
91
+ if (!configured) {
92
+ // UDE resolves its roots from the XPP config, so silence here is normal.
93
+ if (detected.length === 0)
94
+ return [];
95
+ return [{
96
+ severity: 'info',
97
+ message: `${label}: packages root not configured — the server will use ${detected[0]}`,
98
+ }];
99
+ }
100
+ if (fs.existsSync(configured)) {
101
+ return [{ severity: 'ok', message: `${label}: packages root OK (${configured})` }];
102
+ }
103
+ return [{
104
+ severity: 'fail',
105
+ message: `${label}: packages root does not exist (${configured})` +
106
+ (detected.length > 0 ? `\n found instead: ${detected.join(', ')}` : `\n ${describePackagesRootScan()}`),
107
+ fix: detected.length > 0
108
+ ? `set environment.packagePath to ${detected[0]} (${SETUP_COMMAND})`
109
+ : `point environment.packagePath at this machine's PackagesLocalDirectory (${SETUP_COMMAND})`,
110
+ }];
111
+ }
77
112
  /** A legacy .env that disagrees with the config is a trap: the config wins. */
78
113
  function legacyEnvChecks(target, label) {
79
114
  if (!target.envFile || !fs.existsSync(target.store.configPath))
@@ -205,6 +240,8 @@ export async function doctorCommand() {
205
240
  emit(checkConfig(root, 'Root'));
206
241
  for (const r of legacyEnvChecks(root, 'Root'))
207
242
  emit(r);
243
+ for (const r of checkPackagesRoot(root.store, 'Root'))
244
+ emit(r);
208
245
  // Database (root)
209
246
  emit(checkDb(root.store, paths.defaultDb, 'Root'));
210
247
  // C# bridge: the only write path; Windows-only.
@@ -246,6 +283,8 @@ export async function doctorCommand() {
246
283
  emit(r);
247
284
  for (const r of legacyEnvChecks(target, `Instance '${inst.name}'`))
248
285
  emit(r);
286
+ for (const r of checkPackagesRoot(target.store, `Instance '${inst.name}'`))
287
+ emit(r);
249
288
  emit(checkDb(target.store, resolve(inst.dir, 'data', 'xpp-metadata.db'), `Instance '${inst.name}'`));
250
289
  if (isWindows && isXppConfigStale(target.store)) {
251
290
  emit({
@@ -8,7 +8,7 @@ import * as fs from 'node:fs';
8
8
  import { resolve } from 'node:path';
9
9
  import { settingByPath, settingsInSection } from '../../config/settings.js';
10
10
  import { pinBridgeExe } from '../bridgePath.js';
11
- import { finalizeStagedCopilotFiles, maybePrepareCopilotInstructions } from '../copilotFiles.js';
11
+ import { maybePrepareCopilotInstructions } from '../copilotFiles.js';
12
12
  import { createInstance, getInstance, listInstances, normalizeInstanceLayout, suggestPort } from '../instances.js';
13
13
  import { mcpJsonNote, placementNote, stdioServer } from '../mcpJson.js';
14
14
  import { selectXppConfig } from './config.js';
@@ -17,6 +17,7 @@ import { openInstanceStore, readPath, readSetting, saveStore, writeSetting } fro
17
17
  import { instanceTarget } from '../target.js';
18
18
  import { askConfirm, askSelect, askText, p } from '../ui.js';
19
19
  import { listXppConfigs, xppConfigDir } from '../xppConfig.js';
20
+ import { findPackagesRoot } from '../../utils/packagesRoot.js';
20
21
  import { rebuildIndex } from './indexCmd.js';
21
22
  const dbPathSetting = settingByPath('index.dbPath');
22
23
  const xppConfigNameSetting = settingByPath('environment.xppConfigName');
@@ -93,7 +94,10 @@ export async function instanceAddCommand(name, portArg) {
93
94
  await selectXppConfig(store);
94
95
  }
95
96
  else {
96
- await askSetting(store, settingByPath('environment.packagePath'), { required: true });
97
+ await askSetting(store, settingByPath('environment.packagePath'), {
98
+ required: true,
99
+ initial: findPackagesRoot() ?? undefined,
100
+ });
97
101
  await askSetting(store, settingByPath('environment.customModels'), { required: true });
98
102
  }
99
103
  p.log.step('Workspace and naming');
@@ -108,14 +112,13 @@ export async function instanceAddCommand(name, portArg) {
108
112
  // Copilot needs the instructions file just as much as it needs .mcp.json,
109
113
  // and an instance is somebody's only setup run — asking here is the only
110
114
  // chance scenario F gets.
111
- const copilotPlan = await maybePrepareCopilotInstructions(String(readSetting(store, settingByPath('workspace.solutionsPath')) ?? ''));
115
+ await maybePrepareCopilotInstructions(String(readSetting(store, settingByPath('workspace.solutionsPath')) ?? ''));
112
116
  // Both ways to reach this instance: the IDE spawning it over stdio with its
113
117
  // own config, or an HTTP client on the port it was given.
114
118
  mcpJsonNote({
115
119
  [`d365fo-${instanceName}`]: stdioServer(store),
116
120
  [`d365fo-${instanceName}-http`]: { url: `http://localhost:${port}/mcp/` },
117
121
  }, `.mcp.json — keep the stdio entry OR the http one, not both`);
118
- finalizeStagedCopilotFiles(copilotPlan);
119
122
  placementNote();
120
123
  p.note(`Config: ${store.configPath}\n\n` +
121
124
  `1. Build the index: d365fo-mcp instance rebuild ${instanceName}\n` +
@@ -15,11 +15,12 @@ import { DOTNET_MISSING, dataRoot, installMode, isWindows, paths, repoRoot, setD
15
15
  import { settingByPath, settingsInSection } from '../../config/settings.js';
16
16
  import { commandExists, runExe, runShell } from '../exec.js';
17
17
  import { pinBridgeExe } from '../bridgePath.js';
18
- import { finalizeStagedCopilotFiles, maybePrepareCopilotInstructions } from '../copilotFiles.js';
18
+ import { maybePrepareCopilotInstructions } from '../copilotFiles.js';
19
19
  import { mcpJsonNote, placementNote, stdioServer } from '../mcpJson.js';
20
20
  import { checkRelease } from '../npmRegistry.js';
21
21
  import { askAdvanced, askSecrets, askSetting, askSettings } from '../settingsPrompt.js';
22
22
  import { migrateLegacyEnv, openStore, readSetting, saveStore, writeSetting } from '../settingsStore.js';
23
+ import { findPackagesRoot } from '../../utils/packagesRoot.js';
23
24
  import { rootTarget } from '../target.js';
24
25
  import { askConfirm, askSelect, askText, p, requireFullInstall } from '../ui.js';
25
26
  import { listXppConfigs } from '../xppConfig.js';
@@ -188,7 +189,13 @@ async function configureEnvironment(store, scenario) {
188
189
  }
189
190
  return 'ude';
190
191
  }
191
- await askSetting(store, setting('environment.packagePath'), { required: true });
192
+ // Offer the AosService volume this machine actually has. Which drive that is
193
+ // differs per VM image (K:, C:, J:, …), and a wrong guess is what turns the
194
+ // rest of setup into "no namespaces found" (#769).
195
+ const detected = findPackagesRoot();
196
+ if (detected)
197
+ p.log.success(`Found PackagesLocalDirectory at ${detected}`);
198
+ await askSetting(store, setting('environment.packagePath'), { required: true, initial: detected ?? undefined });
192
199
  await askSetting(store, setting('environment.customModels'), { required: true });
193
200
  return 'traditional';
194
201
  }
@@ -276,7 +283,7 @@ export async function setupCommand() {
276
283
  const url = await askText({ message: 'Azure server URL', placeholder: 'https://your-server.azurewebsites.net/mcp/', required: true });
277
284
  await configureEnvironment(store, scenario);
278
285
  await configureWorkspace(store, scenario);
279
- const copilotPlan = await maybePrepareCopilotInstructions(solutionsPath(store));
286
+ await maybePrepareCopilotInstructions(solutionsPath(store));
280
287
  await configureNaming(store);
281
288
  await askSecrets(store, ['behavior']);
282
289
  await askAdvanced(store, ['environment', 'workspace', 'naming', 'bridge', 'behavior', 'server']);
@@ -286,7 +293,6 @@ export async function setupCommand() {
286
293
  'd365fo-azure': { url },
287
294
  'd365fo-local': stdioServer(store),
288
295
  });
289
- finalizeStagedCopilotFiles(copilotPlan);
290
296
  placementNote();
291
297
  p.outro('Hybrid setup complete — no local index needed (Azure serves the search).');
292
298
  return;
@@ -295,7 +301,7 @@ export async function setupCommand() {
295
301
  writeSetting(store, setting('server.mode'), 'full');
296
302
  await configureEnvironment(store, scenario);
297
303
  await configureWorkspace(store, scenario);
298
- const copilotPlan = await maybePrepareCopilotInstructions(solutionsPath(store));
304
+ await maybePrepareCopilotInstructions(solutionsPath(store));
299
305
  await configureNaming(store);
300
306
  await configureIndex(store);
301
307
  let port = Number(readSetting(store, setting('server.port')) ?? 8080);
@@ -313,7 +319,6 @@ export async function setupCommand() {
313
319
  }
314
320
  if (scenario === 'local-http') {
315
321
  mcpJsonNote({ 'd365fo-mcp-tools': { url: `http://localhost:${port}/mcp/` } });
316
- finalizeStagedCopilotFiles(copilotPlan);
317
322
  placementNote();
318
323
  p.outro('Done. Start the server with: d365fo-mcp start');
319
324
  return;
@@ -321,7 +326,6 @@ export async function setupCommand() {
321
326
  // D / E — the IDE spawns dist/index.js itself and is pointed at the config
322
327
  // file; every other setting comes from there.
323
328
  mcpJsonNote({ 'd365fo-mcp-tools': stdioServer(store) });
324
- finalizeStagedCopilotFiles(copilotPlan);
325
329
  placementNote();
326
330
  p.outro('Done. VS spawns the server automatically — no manual start needed.');
327
331
  }
@@ -1,15 +1,9 @@
1
- export type CopilotFilePlan = {
2
- stagingDir?: string;
3
- };
4
1
  /**
5
2
  * Offer to place copilot-instructions.md, falling back to a staging folder.
6
3
  *
7
4
  * `solutionsPath` is what the user gave for workspace.solutionsPath; when it
8
- * is empty there is nowhere to copy to, so the files are staged instead and
9
- * the returned plan tells {@link finalizeStagedCopilotFiles} to complete it
10
- * once the .mcp.json exists.
5
+ * is empty there is nowhere to copy to, so the file is staged instead and the
6
+ * README says where it has to end up.
11
7
  */
12
- export declare function maybePrepareCopilotInstructions(solutionsPath: string): Promise<CopilotFilePlan>;
13
- /** Add the generated .mcp.json to the staging folder — call after mcpJsonNote(). */
14
- export declare function finalizeStagedCopilotFiles(plan: CopilotFilePlan): void;
8
+ export declare function maybePrepareCopilotInstructions(solutionsPath: string): Promise<void>;
15
9
  //# sourceMappingURL=copilotFiles.d.ts.map
@@ -6,8 +6,12 @@
6
6
  * built-in file tools and edits X++ behind the metadata provider's back. Both
7
7
  * files therefore have to be placed for a working install, so the wizards
8
8
  * offer to put this one down as well — either straight into the solutions
9
- * folder, or, when that is unknown, into a staging folder next to the
10
- * generated .mcp.json with a README naming the two destinations.
9
+ * folder, or, when that is unknown, into a staging folder with a README
10
+ * naming the two destinations.
11
+ *
12
+ * The README points at the one .mcp.json `mcpJsonNote()` writes in the data
13
+ * root rather than copying it: the staging folder sits in that same data
14
+ * root, so a second copy is only one more file to keep in sync.
11
15
  *
12
16
  * Shared by `setup` (scenarios B–E) and `instance add` (scenario F), which
13
17
  * both end on the same two-file placement step.
@@ -28,9 +32,7 @@ function writeCopilotSetupReadme(targetDir, opts) {
28
32
  '1. Copy .mcp.json',
29
33
  ' - Recommended (all solutions): %USERPROFILE%\\.mcp.json',
30
34
  ' - Per solution: place .mcp.json next to your .sln file',
31
- opts.includeLocalMcpCopy
32
- ? ' - Local copy prepared here: .mcp.json'
33
- : ` - Local copy path: ${paths.mcpSuggestion}`,
35
+ ` - Generated copy: ${paths.mcpSuggestion}`,
34
36
  '',
35
37
  '2. Copy .github/copilot-instructions.md',
36
38
  opts.copilotAlreadyPlaced
@@ -46,16 +48,15 @@ function writeCopilotSetupReadme(targetDir, opts) {
46
48
  * Offer to place copilot-instructions.md, falling back to a staging folder.
47
49
  *
48
50
  * `solutionsPath` is what the user gave for workspace.solutionsPath; when it
49
- * is empty there is nowhere to copy to, so the files are staged instead and
50
- * the returned plan tells {@link finalizeStagedCopilotFiles} to complete it
51
- * once the .mcp.json exists.
51
+ * is empty there is nowhere to copy to, so the file is staged instead and the
52
+ * README says where it has to end up.
52
53
  */
53
54
  export async function maybePrepareCopilotInstructions(solutionsPath) {
54
55
  const source = copilotSource();
55
56
  if (!fs.existsSync(source)) {
56
57
  p.log.warn('Cannot find copilot-instructions.md in this installation; skipping copy helper.\n' +
57
58
  ` Looked for: ${source}`);
58
- return {};
59
+ return;
59
60
  }
60
61
  const wantsDirectCopy = await askConfirm('Create/copy .github/copilot-instructions.md into the solutions folder now?', true);
61
62
  const target = solutionsPath.trim();
@@ -63,16 +64,16 @@ export async function maybePrepareCopilotInstructions(solutionsPath) {
63
64
  const targetDir = resolve(target, '.github');
64
65
  fs.mkdirSync(targetDir, { recursive: true });
65
66
  fs.copyFileSync(source, resolve(targetDir, 'copilot-instructions.md'));
66
- writeCopilotSetupReadme(target, { includeLocalMcpCopy: false, copilotAlreadyPlaced: true });
67
+ writeCopilotSetupReadme(target, { copilotAlreadyPlaced: true });
67
68
  p.log.success(`Prepared: ${resolve(targetDir, 'copilot-instructions.md')}`);
68
69
  p.log.success(`Prepared: ${resolve(target, 'README.md')}`);
69
- return {};
70
+ return;
70
71
  }
71
72
  const stageDir = resolve(dataRoot(), 'copilot-setup');
72
73
  const stageGitHubDir = resolve(stageDir, '.github');
73
74
  fs.mkdirSync(stageGitHubDir, { recursive: true });
74
75
  fs.copyFileSync(source, resolve(stageGitHubDir, 'copilot-instructions.md'));
75
- writeCopilotSetupReadme(stageDir, { includeLocalMcpCopy: false, copilotAlreadyPlaced: false });
76
+ writeCopilotSetupReadme(stageDir, { copilotAlreadyPlaced: false });
76
77
  if (wantsDirectCopy && !target) {
77
78
  p.log.warn('Solutions folder is empty, so files were prepared in the local staging folder instead.');
78
79
  }
@@ -80,17 +81,5 @@ export async function maybePrepareCopilotInstructions(solutionsPath) {
80
81
  p.log.info('Copy was skipped; files were prepared in a local staging folder for later use.');
81
82
  }
82
83
  p.log.info(`Staging folder: ${stageDir}`);
83
- return { stagingDir: stageDir };
84
- }
85
- /** Add the generated .mcp.json to the staging folder — call after mcpJsonNote(). */
86
- export function finalizeStagedCopilotFiles(plan) {
87
- if (!plan.stagingDir)
88
- return;
89
- const localMcp = paths.mcpSuggestion;
90
- if (fs.existsSync(localMcp)) {
91
- fs.copyFileSync(localMcp, resolve(plan.stagingDir, '.mcp.json'));
92
- writeCopilotSetupReadme(plan.stagingDir, { includeLocalMcpCopy: true, copilotAlreadyPlaced: false });
93
- p.log.success(`Prepared: ${resolve(plan.stagingDir, '.mcp.json')}`);
94
- }
95
84
  }
96
85
  //# sourceMappingURL=copilotFiles.js.map
@@ -107,9 +107,12 @@ export async function askSetting(store, setting, opts) {
107
107
  return list;
108
108
  }
109
109
  default: {
110
+ // A stored answer always wins; the caller's suggestion only fills an
111
+ // empty field, so re-running setup never overwrites a deliberate value
112
+ // with something merely detected.
110
113
  const raw = await askText({
111
114
  message: message(setting, required ? '' : c.dim('(Enter to skip)')),
112
- initialValue: initialText(store, setting),
115
+ initialValue: initialText(store, setting) || opts?.initial || '',
113
116
  placeholder: setting.placeholder,
114
117
  required,
115
118
  });
@@ -81,7 +81,9 @@ export const SETTINGS = [
81
81
  type: 'path',
82
82
  label: 'Packages root (PackagesLocalDirectory)',
83
83
  description: 'AOT packages folder (PackagesLocalDirectory) used as the read-only source for indexing. ' +
84
- 'Machine-wide on a traditional VM; UDE resolves it from the XPP config instead.',
84
+ 'Machine-wide on a traditional VM; UDE resolves it from the XPP config instead. ' +
85
+ 'Left empty, the server scans the machine\'s drives for AosService\\PackagesLocalDirectory — ' +
86
+ 'which volume that is depends on the VM image (K:, C:, J:, …).',
85
87
  placeholder: 'C:\\AOSService\\PackagesLocalDirectory',
86
88
  },
87
89
  {