d365fo-mcp 1.5.1 → 1.6.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 (54) hide show
  1. package/.github/copilot-instructions.md +86 -0
  2. package/README.md +1 -1
  3. package/bridge/D365MetadataBridge/D365MetadataBridge.csproj +33 -3
  4. package/bridge/D365MetadataBridge/Program.cs +1 -1
  5. package/dist/bridge/bridgeAdapter.d.ts +1 -1
  6. package/dist/bridge/bridgeAdapter.js +33 -2
  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 +11 -1
  12. package/dist/cli/commands/setup.js +15 -67
  13. package/dist/cli/copilotFiles.d.ts +9 -0
  14. package/dist/cli/copilotFiles.js +85 -0
  15. package/dist/cli/settingsPrompt.js +4 -1
  16. package/dist/config/settings.js +3 -1
  17. package/dist/scripts/build-database.js +66 -4
  18. package/dist/scripts/build-fts.js +66 -4
  19. package/dist/scripts/extract-metadata.js +124 -62
  20. package/dist/server/toolSchemas/d365foFile.js +1 -1
  21. package/dist/server/toolSchemas/verifyD365foProject.js +1 -1
  22. package/dist/tools/analyzeExtensionPoints.js +141 -5
  23. package/dist/tools/buildProject.d.ts +48 -0
  24. package/dist/tools/buildProject.js +101 -20
  25. package/dist/tools/completion.js +11 -2
  26. package/dist/tools/createD365File.d.ts +15 -7
  27. package/dist/tools/createD365File.js +26 -21
  28. package/dist/tools/createLabel.js +5 -4
  29. package/dist/tools/dataEntityViewExtensionXml.d.ts +74 -0
  30. package/dist/tools/dataEntityViewExtensionXml.js +142 -0
  31. package/dist/tools/dbSync.js +2 -1
  32. package/dist/tools/generateD365Xml.js +21 -10
  33. package/dist/tools/generateSmart.js +1 -1
  34. package/dist/tools/generateSmartForm.js +2 -1
  35. package/dist/tools/generateSmartReport.d.ts +2 -1
  36. package/dist/tools/generateSmartReport.js +119 -117
  37. package/dist/tools/generateSmartTable.js +5 -4
  38. package/dist/tools/getMethodSource.js +49 -1
  39. package/dist/tools/menuItemExtensionXml.d.ts +39 -0
  40. package/dist/tools/menuItemExtensionXml.js +91 -0
  41. package/dist/tools/methodSignature.js +71 -25
  42. package/dist/tools/prepareChange.js +70 -34
  43. package/dist/tools/renameLabel.js +5 -4
  44. package/dist/tools/runBpCheck.js +5 -16
  45. package/dist/tools/sysTestRunner.js +2 -1
  46. package/dist/tools/verifyD365Project.js +6 -5
  47. package/dist/tools/xppKnowledge.js +107 -3
  48. package/dist/utils/configManager.d.ts +7 -0
  49. package/dist/utils/configManager.js +17 -25
  50. package/dist/utils/inheritanceChain.d.ts +44 -0
  51. package/dist/utils/inheritanceChain.js +99 -0
  52. package/dist/utils/packagesRoot.d.ts +55 -0
  53. package/dist/utils/packagesRoot.js +134 -0
  54. package/package.json +2 -1
@@ -0,0 +1,86 @@
1
+ # D365 Finance & Operations X++ Development
2
+
3
+ <!-- Thin pointer — full rules are delivered via the MCP `xpp_system_instructions` prompt.
4
+ This file provides only the minimum static context needed when the MCP server
5
+ is not yet connected or the prompt hasn't been loaded.
6
+ Serves Claude Code too: copy it to a parent of your solution folders as
7
+ CLAUDE.md (docs/SETUP.md § Claude Code CLI, Step 3). -->
8
+
9
+ ## Tool Priority
10
+
11
+ This workspace contains a D365FO MCP server. **Always use the specialized MCP tools** for D365FO objects (`.xml`, `.xpp`, `.rnrproj`, `.label.txt`). Built-in file/search tools are fine for `.cs`, `.json`, `.yml`, `.md`, `.config` files.
12
+
13
+ ## Mandatory First Check
14
+
15
+ Call `get_workspace_info()` before doing anything with D365FO objects.
16
+
17
+ | Response | Action |
18
+ |----------|--------|
19
+ | Call fails | STOP. MCP server not connected. Ask user to start it. |
20
+ | `⛔ CONFIGURATION PROBLEM` | STOP. Relay message. Wait for user. |
21
+ | `✅ Configuration looks valid` | Note model name. Proceed. |
22
+
23
+ ## Terminal Prohibition
24
+
25
+ PowerShell / any terminal command **WILL HANG** in VS 2022 / VS 2026 MCP integration. Never use `run_in_terminal` or generate scripts as a fallback when an MCP tool fails — STOP and report the error verbatim.
26
+
27
+ ## Core Tool Mapping
28
+
29
+ | Action | Tool |
30
+ |--------|------|
31
+ | Plan an extension before changing code | `prepare(mode="change", goal, objectName, methodName?)` — returns signature, existing CoC wrappers, strategy + `groundingToken` |
32
+ | Plan a new object before creating it | `prepare(mode="create", goal, objectName, objectType)` — returns collision check, naming, EDT/label hints + `groundingToken` |
33
+ | Create a D365FO object | `d365fo_file(action="create")` (never `create_file`) |
34
+ | Edit an existing object | `d365fo_file(action="modify")` (applies immediately — confirm in chat first) |
35
+ | Revert the last write | `undo_last_modification` |
36
+ | Search objects | `search` — multiple via `search(queries[])`, custom-only via `search(scope="extensions")` |
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: `batch_get_info(objects[])` |
38
+ | Method signature for CoC | `get_method(include="signature")` (already returned by `prepare(mode="change")`) |
39
+ | Validate X++ before write | `validate_code(mode="syntax", code)` — offline BP check, <50 ms |
40
+ | X++ rules & patterns | `get_knowledge(kind="knowledge", topic)` — select grammar, CoC, BP rules, SysOperation, workflow, … |
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
+ | Validate form XML against its pattern | `object_patterns(domain="form", action="validate", xml \| formName \| filePath)` — structural errors block form writes (FORM_PATTERN_ENFORCE) |
43
+ | Resolve label / EDT / class refs | `validate_code(mode="references", code)` |
44
+ | Build / BP / Sync | `build_d365fo_project` / `run_bp_check` / `trigger_db_sync` |
45
+ | Error diagnosis | `get_knowledge(kind="error", errorText)` |
46
+
47
+ ## Key Rules
48
+
49
+ ### Workspace & model targeting
50
+
51
+ 1. **The target model comes from `.mcp.json`** — never infer it from search results or object names. The symbol database contains objects from all models (Microsoft + ISV + custom); the model on a search/`get_*_info` result is the source model, not where new files belong.
52
+
53
+ ### Writes & file editing
54
+
55
+ 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
+ 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.
57
+
58
+ ### Build automation
59
+
60
+ 4. Never run `build_d365fo_project()` automatically — only on explicit user request ("build", "compile", "check errors").
61
+
62
+ ### X++ correctness (BP-clean code)
63
+
64
+ 5. Never copy default parameter values into CoC wrapper signatures.
65
+ 6. Never use `today()` — use `DateTimeUtil::getToday(DateTimeUtil::getUserPreferredTimeZone())`.
66
+ 7. Never use hardcoded strings in `Info()` / `warning()` / `error()` — use `@Model:Label` references.
67
+ 8. Call `labels(action="search")` before `labels(action="create")` — reuse existing labels.
68
+
69
+ ### Extension naming
70
+
71
+ 9. Extension naming follows `EXTENSION_NAMING_STYLE` (see `get_workspace_info`):
72
+ - `prefix` (default) → class `{Target}{Prefix}_Extension`, element `{Target}.{Prefix}Extension`
73
+ - `model-name` → class `{Target}_{ModelName}_Extension`, element `{Target}.{ModelName}`
74
+
75
+ Pass the BASE object name to `d365fo_file(action="create")` and let the tool inject the token — don't hand-build the infix.
76
+
77
+ ### Reuse & diff safety
78
+
79
+ 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.
80
+ 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
+ 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.
82
+
83
+ ## Full Instructions
84
+
85
+ The complete X++ rules, query grammar, CoC authoring rules, and workflow details are delivered via the MCP prompt `xpp_system_instructions`. If that prompt is not loaded, request it or consult [src/prompts/systemInstructions.ts](../src/prompts/systemInstructions.ts) directly.
86
+
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-85.9%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.
@@ -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>;
@@ -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,6 +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 { maybePrepareCopilotInstructions } from '../copilotFiles.js';
11
12
  import { createInstance, getInstance, listInstances, normalizeInstanceLayout, suggestPort } from '../instances.js';
12
13
  import { mcpJsonNote, placementNote, stdioServer } from '../mcpJson.js';
13
14
  import { selectXppConfig } from './config.js';
@@ -16,6 +17,7 @@ import { openInstanceStore, readPath, readSetting, saveStore, writeSetting } fro
16
17
  import { instanceTarget } from '../target.js';
17
18
  import { askConfirm, askSelect, askText, p } from '../ui.js';
18
19
  import { listXppConfigs, xppConfigDir } from '../xppConfig.js';
20
+ import { findPackagesRoot } from '../../utils/packagesRoot.js';
19
21
  import { rebuildIndex } from './indexCmd.js';
20
22
  const dbPathSetting = settingByPath('index.dbPath');
21
23
  const xppConfigNameSetting = settingByPath('environment.xppConfigName');
@@ -92,17 +94,25 @@ export async function instanceAddCommand(name, portArg) {
92
94
  await selectXppConfig(store);
93
95
  }
94
96
  else {
95
- await askSetting(store, settingByPath('environment.packagePath'), { required: true });
97
+ await askSetting(store, settingByPath('environment.packagePath'), {
98
+ required: true,
99
+ initial: findPackagesRoot() ?? undefined,
100
+ });
96
101
  await askSetting(store, settingByPath('environment.customModels'), { required: true });
97
102
  }
98
103
  p.log.step('Workspace and naming');
99
104
  await askSetting(store, settingByPath('workspace.modelName'));
100
105
  await askSetting(store, settingByPath('workspace.path'));
106
+ await askSetting(store, settingByPath('workspace.solutionsPath'));
101
107
  await askSettings(store, settingsInSection('naming', 'basic'));
102
108
  p.log.step('Metadata index');
103
109
  await askSettings(store, settingsInSection('index', 'basic'));
104
110
  await askAdvanced(store, ['environment', 'workspace', 'index', 'server', 'bridge', 'behavior']);
105
111
  saveStore(store);
112
+ // Copilot needs the instructions file just as much as it needs .mcp.json,
113
+ // and an instance is somebody's only setup run — asking here is the only
114
+ // chance scenario F gets.
115
+ await maybePrepareCopilotInstructions(String(readSetting(store, settingByPath('workspace.solutionsPath')) ?? ''));
106
116
  // Both ways to reach this instance: the IDE spawning it over stdio with its
107
117
  // own config, or an HTTP client on the port it was given.
108
118
  mcpJsonNote({
@@ -15,10 +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 { maybePrepareCopilotInstructions } from '../copilotFiles.js';
18
19
  import { mcpJsonNote, placementNote, stdioServer } from '../mcpJson.js';
19
20
  import { checkRelease } from '../npmRegistry.js';
20
21
  import { askAdvanced, askSecrets, askSetting, askSettings } from '../settingsPrompt.js';
21
22
  import { migrateLegacyEnv, openStore, readSetting, saveStore, writeSetting } from '../settingsStore.js';
23
+ import { findPackagesRoot } from '../../utils/packagesRoot.js';
22
24
  import { rootTarget } from '../target.js';
23
25
  import { askConfirm, askSelect, askText, p, requireFullInstall } from '../ui.js';
24
26
  import { listXppConfigs } from '../xppConfig.js';
@@ -187,7 +189,13 @@ async function configureEnvironment(store, scenario) {
187
189
  }
188
190
  return 'ude';
189
191
  }
190
- 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 });
191
199
  await askSetting(store, setting('environment.customModels'), { required: true });
192
200
  return 'traditional';
193
201
  }
@@ -198,6 +206,10 @@ async function configureWorkspace(store, scenario) {
198
206
  await askSetting(store, setting('workspace.path'), { required: scenario === 'hybrid' || scenario === 'ude' });
199
207
  await askSetting(store, setting('workspace.solutionsPath'));
200
208
  }
209
+ /** Where the user keeps the .sln folders — the copy target for the client files. */
210
+ function solutionsPath(store) {
211
+ return String(readSetting(store, setting('workspace.solutionsPath')) ?? '');
212
+ }
201
213
  async function configureNaming(store) {
202
214
  p.log.step('Naming');
203
215
  await askSettings(store, settingsInSection('naming', 'basic'));
@@ -216,67 +228,6 @@ async function maybeBuildIndex() {
216
228
  }
217
229
  return rebuildIndex(rootTarget());
218
230
  }
219
- function writeCopilotSetupReadme(stageDir, includeLocalMcpCopy) {
220
- const readmePath = resolve(stageDir, 'README.md');
221
- const lines = [
222
- '# VS setup quick guide',
223
- '',
224
- 'This folder is generated by `d365fo-mcp setup` to simplify Visual Studio onboarding.',
225
- '',
226
- '1. Copy .mcp.json',
227
- ' - Recommended (all solutions): %USERPROFILE%\\.mcp.json',
228
- ' - Per solution: place .mcp.json next to your .sln file',
229
- includeLocalMcpCopy
230
- ? ' - Local copy prepared here: .mcp.json'
231
- : ` - Local copy path: ${paths.mcpSuggestion}`,
232
- '',
233
- '2. Copy .github/copilot-instructions.md',
234
- ' - Destination: a parent folder of your solution directories',
235
- ' - Why: provides mandatory D365FO tool-routing and safety rules for Copilot',
236
- '',
237
- '3. Restart Visual Studio after copying files.',
238
- ];
239
- fs.writeFileSync(readmePath, lines.join('\n') + '\n', 'utf8');
240
- }
241
- async function maybePrepareCopilotInstructions(store) {
242
- const source = resolve(repoRoot, '.github', 'copilot-instructions.md');
243
- if (!fs.existsSync(source)) {
244
- p.log.warn('Cannot find .github\\copilot-instructions.md in the package; skipping copy helper.');
245
- return {};
246
- }
247
- const wantsDirectCopy = await askConfirm('Create/copy .github/copilot-instructions.md into the solutions folder now?', true);
248
- const solutionsPath = String(readSetting(store, setting('workspace.solutionsPath')) ?? '').trim();
249
- if (wantsDirectCopy && solutionsPath) {
250
- const targetDir = resolve(solutionsPath, '.github');
251
- fs.mkdirSync(targetDir, { recursive: true });
252
- fs.copyFileSync(source, resolve(targetDir, 'copilot-instructions.md'));
253
- p.log.success(`Prepared: ${resolve(targetDir, 'copilot-instructions.md')}`);
254
- return {};
255
- }
256
- const stageDir = resolve(dataRoot(), 'copilot-setup');
257
- const stageGitHubDir = resolve(stageDir, '.github');
258
- fs.mkdirSync(stageGitHubDir, { recursive: true });
259
- fs.copyFileSync(source, resolve(stageGitHubDir, 'copilot-instructions.md'));
260
- writeCopilotSetupReadme(stageDir, false);
261
- if (wantsDirectCopy && !solutionsPath) {
262
- p.log.warn('Solutions folder is empty, so files were prepared in the local staging folder instead.');
263
- }
264
- else {
265
- p.log.info('Copy was skipped; files were prepared in a local staging folder for later use.');
266
- }
267
- p.log.info(`Staging folder: ${stageDir}`);
268
- return { stagingDir: stageDir };
269
- }
270
- function finalizeStagedCopilotFiles(plan) {
271
- if (!plan.stagingDir)
272
- return;
273
- const localMcp = paths.mcpSuggestion;
274
- if (fs.existsSync(localMcp)) {
275
- fs.copyFileSync(localMcp, resolve(plan.stagingDir, '.mcp.json'));
276
- writeCopilotSetupReadme(plan.stagingDir, true);
277
- p.log.success(`Prepared: ${resolve(plan.stagingDir, '.mcp.json')}`);
278
- }
279
- }
280
231
  export function savedNote(store) {
281
232
  const rel = relative(dataRoot(), store.configPath) || store.configPath;
282
233
  const lines = [`Settings written to ${rel}`];
@@ -332,7 +283,7 @@ export async function setupCommand() {
332
283
  const url = await askText({ message: 'Azure server URL', placeholder: 'https://your-server.azurewebsites.net/mcp/', required: true });
333
284
  await configureEnvironment(store, scenario);
334
285
  await configureWorkspace(store, scenario);
335
- const copilotPlan = await maybePrepareCopilotInstructions(store);
286
+ await maybePrepareCopilotInstructions(solutionsPath(store));
336
287
  await configureNaming(store);
337
288
  await askSecrets(store, ['behavior']);
338
289
  await askAdvanced(store, ['environment', 'workspace', 'naming', 'bridge', 'behavior', 'server']);
@@ -342,7 +293,6 @@ export async function setupCommand() {
342
293
  'd365fo-azure': { url },
343
294
  'd365fo-local': stdioServer(store),
344
295
  });
345
- finalizeStagedCopilotFiles(copilotPlan);
346
296
  placementNote();
347
297
  p.outro('Hybrid setup complete — no local index needed (Azure serves the search).');
348
298
  return;
@@ -351,7 +301,7 @@ export async function setupCommand() {
351
301
  writeSetting(store, setting('server.mode'), 'full');
352
302
  await configureEnvironment(store, scenario);
353
303
  await configureWorkspace(store, scenario);
354
- const copilotPlan = await maybePrepareCopilotInstructions(store);
304
+ await maybePrepareCopilotInstructions(solutionsPath(store));
355
305
  await configureNaming(store);
356
306
  await configureIndex(store);
357
307
  let port = Number(readSetting(store, setting('server.port')) ?? 8080);
@@ -369,7 +319,6 @@ export async function setupCommand() {
369
319
  }
370
320
  if (scenario === 'local-http') {
371
321
  mcpJsonNote({ 'd365fo-mcp-tools': { url: `http://localhost:${port}/mcp/` } });
372
- finalizeStagedCopilotFiles(copilotPlan);
373
322
  placementNote();
374
323
  p.outro('Done. Start the server with: d365fo-mcp start');
375
324
  return;
@@ -377,7 +326,6 @@ export async function setupCommand() {
377
326
  // D / E — the IDE spawns dist/index.js itself and is pointed at the config
378
327
  // file; every other setting comes from there.
379
328
  mcpJsonNote({ 'd365fo-mcp-tools': stdioServer(store) });
380
- finalizeStagedCopilotFiles(copilotPlan);
381
329
  placementNote();
382
330
  p.outro('Done. VS spawns the server automatically — no manual start needed.');
383
331
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Offer to place copilot-instructions.md, falling back to a staging folder.
3
+ *
4
+ * `solutionsPath` is what the user gave for workspace.solutionsPath; when it
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.
7
+ */
8
+ export declare function maybePrepareCopilotInstructions(solutionsPath: string): Promise<void>;
9
+ //# sourceMappingURL=copilotFiles.d.ts.map