d365fo-mcp 1.4.0 → 1.5.1

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/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-64.9%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-72.7%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*
@@ -74,6 +74,8 @@ Structural violations (wrong order, missing container, disallowed control) **blo
74
74
 
75
75
  ## Quick Start
76
76
 
77
+ > **From D365FO platform update 10.0.49 (PU74), Visual Studio 2026 is the supported IDE for X++ development** — Microsoft no longer supports VS 2022. Earlier platform versions still use VS 2022 ≥ 17.14. [Details](https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/fin-ops/get-started/whats-new-platform-updates-10-0-49)
78
+
77
79
  **Installing on your own D365FO VM** — the usual case. One line in PowerShell installs Node.js if it is missing, installs the server from npm, and runs the setup wizard, which asks where the index should live and builds the C# bridge for you:
78
80
 
79
81
  ```powershell
@@ -105,7 +107,7 @@ One shared instance for the whole team — the metadata index lives in Blob Stor
105
107
 
106
108
  [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fdynamics365ninja%2Fd365fo-mcp-server%2Frefs%2Fheads%2Fmain%2Finfrastructure%2Fazuredeploy.json)
107
109
 
108
- Deployment guide: [docs/SETUP_AZURE.md](docs/SETUP_AZURE.md) · CI/CD automation: [docs/PIPELINES.md](docs/PIPELINES.md)
110
+ Deployment guide: [docs/SETUP_AZURE.md](docs/SETUP_AZURE.md) includes CI/CD pipeline automation
109
111
 
110
112
  ---
111
113
 
@@ -114,11 +116,10 @@ Deployment guide: [docs/SETUP_AZURE.md](docs/SETUP_AZURE.md) · CI/CD automation
114
116
  | Getting started | Reference | Operations |
115
117
  |-----------------|-----------|------------|
116
118
  | [Quick Start](docs/QUICK_START.md) — connect or install | [All 26 tools](docs/MCP_TOOLS.md) | [Azure deployment](docs/SETUP_AZURE.md) |
117
- | [Setup scenarios A–F](docs/SETUP.md) | [`.mcp.json` reference](docs/MCP_CONFIG.md) | [DevOps pipelines](docs/PIPELINES.md) |
118
- | [Claude Code setup](docs/CLAUDE_CODE_SETUP.md) | [Architecture](docs/ARCHITECTURE.md) | [Testing](docs/TESTING.md) |
119
- | [Usage examples](docs/USAGE_EXAMPLES.md) — real tool chains | [C# Bridge](docs/BRIDGE.md) | [Custom / ISV models](docs/CUSTOM_EXTENSIONS.md) |
120
- | | [Workspace detection](docs/WORKSPACE_DETECTION.md) | [SQLite vs Bridge](docs/SQLITE_DEPENDENCY.md) |
121
- | | [Backlog](docs/BACKLOG.md) — deferred work & ideas | [Coverage](eval/COVERAGE.md) — what the badge counts |
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
+ | [Claude Code setup](docs/SETUP.md#claude-code-cli) | [Configuration](docs/CONFIGURATION.md) | [Testing](docs/TESTING.md) |
121
+ | [Usage examples](docs/USAGE_EXAMPLES.md) — real tool chains | [Architecture](docs/ARCHITECTURE.md) | [Custom / ISV models](docs/CUSTOM_EXTENSIONS.md) |
122
+ | | | [Coverage](eval/COVERAGE.md) what the badge counts |
122
123
 
123
124
  ## License
124
125
 
@@ -0,0 +1,18 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!--
3
+ D365FO dev VMs frequently ship a machine-wide NuGet.config that only lists
4
+ offline/VS package sources (e.g. "C:\Program Files\dotnet\library-packs" and
5
+ "Microsoft Visual Studio Offline Packages") — nuget.org isn't in it, so
6
+ restoring System.Text.Json / Microsoft.NETFramework.ReferenceAssemblies.net48
7
+ fails with NU1101 even though the machine has internet access.
8
+
9
+ This file has no <clear/>, so NuGet MERGES it with whatever the machine/user
10
+ config already defines — it only adds nuget.org, it never removes the
11
+ offline sources the D365FO tooling relies on. See docs/SETUP.md
12
+ "Restrictive NuGet feed".
13
+ -->
14
+ <configuration>
15
+ <packageSources>
16
+ <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
17
+ </packageSources>
18
+ </configuration>
@@ -104,7 +104,7 @@ function serverEntry(url, apiKey, alwaysLoad) {
104
104
  url,
105
105
  ...(apiKey ? { headers: { 'X-Api-Key': apiKey } } : {}),
106
106
  // Without it Claude Code defers the tools and may answer X++ questions from
107
- // built-in search instead (docs/CLAUDE_CODE_SETUP.md).
107
+ // built-in search instead (docs/SETUP.md § Claude Code CLI).
108
108
  ...(alwaysLoad ? { alwaysLoad: true } : {}),
109
109
  };
110
110
  }
@@ -153,7 +153,7 @@ function openRootStore() {
153
153
  }
154
154
  /** D365FO environment: type, then the paths/models that type needs. */
155
155
  async function configureEnvironment(store, scenario) {
156
- p.log.step('D365FO environment — where the X++ packages live');
156
+ p.log.step('D365FO environment');
157
157
  let envType;
158
158
  if (scenario === 'ude') {
159
159
  envType = 'ude';
@@ -193,29 +193,90 @@ async function configureEnvironment(store, scenario) {
193
193
  }
194
194
  /** Model/paths the write tools target. Auto-detection covers what is left empty. */
195
195
  async function configureWorkspace(store, scenario) {
196
- p.log.step('Workspace — which model the server writes to (leave empty to auto-detect from the IDE)');
196
+ p.log.step('Workspace target');
197
197
  await askSetting(store, setting('workspace.modelName'));
198
198
  await askSetting(store, setting('workspace.path'), { required: scenario === 'hybrid' || scenario === 'ude' });
199
199
  await askSetting(store, setting('workspace.solutionsPath'));
200
200
  }
201
201
  async function configureNaming(store) {
202
- p.log.step('Naming convention — applied to every generated object');
202
+ p.log.step('Naming');
203
203
  await askSettings(store, settingsInSection('naming', 'basic'));
204
204
  }
205
205
  async function configureIndex(store) {
206
- p.log.step('Metadata index — what gets extracted and how long the build takes');
206
+ p.log.step('Metadata index');
207
207
  await askSetting(store, setting('index.extractMode'));
208
208
  const includeLabels = await askSetting(store, setting('index.includeLabels'));
209
209
  if (includeLabels)
210
210
  await askSetting(store, setting('index.labelLanguages'));
211
211
  }
212
212
  async function maybeBuildIndex() {
213
- if (!await askConfirm('Build the metadata index now? (custom models: minutes; full index: 12 h)')) {
213
+ if (!await askConfirm('Build the metadata index now? (custom: minutes, full: 1-2 h)')) {
214
214
  p.log.warn('Skipped — run `d365fo-mcp index` before first use.');
215
215
  return true;
216
216
  }
217
217
  return rebuildIndex(rootTarget());
218
218
  }
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
+ }
219
280
  export function savedNote(store) {
220
281
  const rel = relative(dataRoot(), store.configPath) || store.configPath;
221
282
  const lines = [`Settings written to ${rel}`];
@@ -237,12 +298,12 @@ export async function setupCommand() {
237
298
  p.log.warn(`This is d365fo-mcp ${release.current}; ${release.latest} is published.\n` +
238
299
  ` Updating first avoids rebuilding the index twice: ${installMode === 'npm' ? 'npm install -g d365fo-mcp@latest' : 'd365fo-mcp update'}`);
239
300
  }
240
- const scenario = await askSelect('How will this developer machine use the MCP server? (docs/SETUP.md)', [
241
- { value: 'local-stdio', label: 'E — Local stdio ★', hint: 'single developer on a D365FO VM; VS launches the server' },
242
- { value: 'hybrid', label: 'B — Hybrid ★', hint: 'Azure serves the shared index; local companion handles writes' },
243
- { value: 'local-http', label: 'C — Local HTTP', hint: 'several clients on this machine share one server on a port' },
244
- { value: 'ude', label: 'D — UDE', hint: 'Unified Developer Experience / Power Platform Tools' },
245
- { value: 'multi', label: 'F — Multi-instance', hint: 'several D365FO clients on one machine, one instance each' },
301
+ const scenario = await askSelect('How will this machine use MCP? (docs/SETUP.md)', [
302
+ { value: 'local-stdio', label: 'E — Local stdio ★', hint: 'Single developer VM. Visual Studio launches MCP.' },
303
+ { value: 'hybrid', label: 'B — Hybrid ★', hint: 'Azure serves search. Local companion handles writes.' },
304
+ { value: 'local-http', label: 'C — Local HTTP', hint: 'Multiple local clients share one MCP HTTP port.' },
305
+ { value: 'ude', label: 'D — UDE', hint: 'Unified Developer Experience setup.' },
306
+ { value: 'multi', label: 'F — Multi-instance', hint: 'One machine, multiple isolated D365FO instances.' },
246
307
  ]);
247
308
  // Before anything is written: every path below hangs off the data directory.
248
309
  await configureDataRoot();
@@ -271,6 +332,7 @@ export async function setupCommand() {
271
332
  const url = await askText({ message: 'Azure server URL', placeholder: 'https://your-server.azurewebsites.net/mcp/', required: true });
272
333
  await configureEnvironment(store, scenario);
273
334
  await configureWorkspace(store, scenario);
335
+ const copilotPlan = await maybePrepareCopilotInstructions(store);
274
336
  await configureNaming(store);
275
337
  await askSecrets(store, ['behavior']);
276
338
  await askAdvanced(store, ['environment', 'workspace', 'naming', 'bridge', 'behavior', 'server']);
@@ -280,6 +342,7 @@ export async function setupCommand() {
280
342
  'd365fo-azure': { url },
281
343
  'd365fo-local': stdioServer(store),
282
344
  });
345
+ finalizeStagedCopilotFiles(copilotPlan);
283
346
  placementNote();
284
347
  p.outro('Hybrid setup complete — no local index needed (Azure serves the search).');
285
348
  return;
@@ -288,6 +351,7 @@ export async function setupCommand() {
288
351
  writeSetting(store, setting('server.mode'), 'full');
289
352
  await configureEnvironment(store, scenario);
290
353
  await configureWorkspace(store, scenario);
354
+ const copilotPlan = await maybePrepareCopilotInstructions(store);
291
355
  await configureNaming(store);
292
356
  await configureIndex(store);
293
357
  let port = Number(readSetting(store, setting('server.port')) ?? 8080);
@@ -305,6 +369,7 @@ export async function setupCommand() {
305
369
  }
306
370
  if (scenario === 'local-http') {
307
371
  mcpJsonNote({ 'd365fo-mcp-tools': { url: `http://localhost:${port}/mcp/` } });
372
+ finalizeStagedCopilotFiles(copilotPlan);
308
373
  placementNote();
309
374
  p.outro('Done. Start the server with: d365fo-mcp start');
310
375
  return;
@@ -312,6 +377,7 @@ export async function setupCommand() {
312
377
  // D / E — the IDE spawns dist/index.js itself and is pointed at the config
313
378
  // file; every other setting comes from there.
314
379
  mcpJsonNote({ 'd365fo-mcp-tools': stdioServer(store) });
380
+ finalizeStagedCopilotFiles(copilotPlan);
315
381
  placementNote();
316
382
  p.outro('Done. VS spawns the server automatically — no manual start needed.');
317
383
  }
@@ -60,7 +60,7 @@ export declare const paths: {
60
60
  readonly instancesDir: string;
61
61
  readonly defaultDb: string;
62
62
  readonly defaultLabelsDb: string;
63
- /** Where the wizard drops the ready-to-paste .mcp.json block. */
63
+ /** Where setup writes the ready-to-copy .mcp.json file. */
64
64
  readonly mcpSuggestion: string;
65
65
  /**
66
66
  * Where `dotnet build` puts the bridge.
@@ -120,8 +120,8 @@ export const paths = {
120
120
  get instancesDir() { return resolve(currentDataRoot, 'instances'); },
121
121
  get defaultDb() { return resolve(currentDataRoot, 'data', 'xpp-metadata.db'); },
122
122
  get defaultLabelsDb() { return resolve(currentDataRoot, 'data', 'xpp-metadata-labels.db'); },
123
- /** Where the wizard drops the ready-to-paste .mcp.json block. */
124
- get mcpSuggestion() { return resolve(currentDataRoot, 'mcp-config-suggestion.json'); },
123
+ /** Where setup writes the ready-to-copy .mcp.json file. */
124
+ get mcpSuggestion() { return resolve(currentDataRoot, '.mcp.json'); },
125
125
  /**
126
126
  * Where `dotnet build` puts the bridge.
127
127
  *
@@ -28,20 +28,20 @@ export function stdioServer(store, extraEnv) {
28
28
  export function mcpJsonNote(servers, title = '.mcp.json') {
29
29
  const json = JSON.stringify({ servers }, null, 2);
30
30
  p.note(json, title);
31
- // Also write the raw JSON to a file so it can be copied without terminal box
32
- // characters. It goes to the data directory, not the package: an npm install
33
- // replaces the package on every update, and may not even be writable.
31
+ // Also write the JSON to a file so it can be copied as-is into VS.
32
+ // It goes to the data directory, not the package: npm updates replace the
33
+ // package directory and may not be writable.
34
34
  const outPath = paths.mcpSuggestion;
35
35
  fs.mkdirSync(dirname(outPath), { recursive: true });
36
36
  fs.writeFileSync(outPath, json + '\n', 'utf8');
37
- p.log.info(`Raw JSON written to: ${outPath}`);
37
+ p.log.info(`.mcp.json prepared at: ${outPath}`);
38
38
  }
39
39
  export function placementNote() {
40
- p.note('Place the block above in:\n' +
40
+ p.note('Copy the generated .mcp.json to one of these locations:\n' +
41
41
  ' %USERPROFILE%\\.mcp.json — all solutions (recommended)\n' +
42
42
  ' next to the .sln — that solution only\n\n' +
43
- 'Also copy .github\\copilot-instructions.md into a parent of your\n' +
44
- 'solution folders (mandatory for Copilot — see docs/SETUP.md).\n' +
45
- 'Restart Visual Studio after editing .mcp.json.', 'Where it goes');
43
+ 'Make sure .github\\copilot-instructions.md exists in a parent folder\n' +
44
+ 'of your solutions (mandatory for Copilot — see docs/SETUP.md).\n' +
45
+ 'Restart Visual Studio after copying .mcp.json.', 'Where it goes');
46
46
  }
47
47
  //# sourceMappingURL=mcpJson.js.map
@@ -14,7 +14,19 @@ import { askConfirm, askSelect, askText, ensure, p } from './ui.js';
14
14
  /** "Label\n description" — the uniform question layout. */
15
15
  function message(setting, suffix) {
16
16
  const head = suffix ? `${setting.label} ${suffix}` : setting.label;
17
- return `${head}\n${c.dim(wrapText(setting.description, 76, ' '))}`;
17
+ if (setting.path === 'environment.type')
18
+ return head;
19
+ return `${head}\n${c.dim(compactDescription(setting.description))}`;
20
+ }
21
+ function compactDescription(text) {
22
+ const normalized = text.replace(/\s+/g, ' ').trim();
23
+ const primary = normalized.split(/(?<=[.!?])\s+/)[0] ?? normalized;
24
+ const wrapped = wrapText(primary, 76, ' ').split('\n');
25
+ if (wrapped.length <= 2)
26
+ return wrapped.join('\n');
27
+ const lines = wrapped.slice(0, 2);
28
+ lines[1] = `${lines[1].replace(/\.*\s*$/, '')}...`;
29
+ return lines.join('\n');
18
30
  }
19
31
  function wrapText(text, width, indent) {
20
32
  const words = text.split(/\s+/);
@@ -80,7 +80,7 @@ export const SETTINGS = [
80
80
  tier: 'basic',
81
81
  type: 'path',
82
82
  label: 'Packages root (PackagesLocalDirectory)',
83
- description: 'Root folder of all D365FO packages — the read-only reference the index is built from. ' +
83
+ description: 'AOT packages folder (PackagesLocalDirectory) used as the read-only source for indexing. ' +
84
84
  'Machine-wide on a traditional VM; UDE resolves it from the XPP config instead.',
85
85
  placeholder: 'C:\\AOSService\\PackagesLocalDirectory',
86
86
  },
@@ -48,8 +48,8 @@ export interface XppClassExtensionRecord extends XppExtensionMembers {
48
48
  */
49
49
  export declare function buildClassExtensionRecord(classInfo: XppClassInfo, model: string): XppClassExtensionRecord | null;
50
50
  export declare class XppMetadataParser {
51
- private parser;
52
51
  private enhancedParser;
52
+ private get parser();
53
53
  constructor();
54
54
  /**
55
55
  * Parse an X++ class file (AxClass XML)
@@ -57,14 +57,15 @@ export function buildClassExtensionRecord(classInfo, model) {
57
57
  };
58
58
  }
59
59
  export class XppMetadataParser {
60
- parser;
61
60
  enhancedParser;
62
- constructor() {
63
- this.parser = new Parser({
61
+ get parser() {
62
+ return new Parser({
64
63
  explicitArray: false,
65
64
  mergeAttrs: true,
66
65
  trim: true,
67
66
  });
67
+ }
68
+ constructor() {
68
69
  this.enhancedParser = new EnhancedXppParser();
69
70
  }
70
71
  /**
@@ -31,7 +31,7 @@ var SETTINGS = [
31
31
  tier: "basic",
32
32
  type: "path",
33
33
  label: "Packages root (PackagesLocalDirectory)",
34
- description: "Root folder of all D365FO packages \u2014 the read-only reference the index is built from. Machine-wide on a traditional VM; UDE resolves it from the XPP config instead.",
34
+ description: "AOT packages folder (PackagesLocalDirectory) used as the read-only source for indexing. Machine-wide on a traditional VM; UDE resolves it from the XPP config instead.",
35
35
  placeholder: "C:\\AOSService\\PackagesLocalDirectory"
36
36
  },
37
37
  {
@@ -31,7 +31,7 @@ var SETTINGS = [
31
31
  tier: "basic",
32
32
  type: "path",
33
33
  label: "Packages root (PackagesLocalDirectory)",
34
- description: "Root folder of all D365FO packages \u2014 the read-only reference the index is built from. Machine-wide on a traditional VM; UDE resolves it from the XPP config instead.",
34
+ description: "AOT packages folder (PackagesLocalDirectory) used as the read-only source for indexing. Machine-wide on a traditional VM; UDE resolves it from the XPP config instead.",
35
35
  placeholder: "C:\\AOSService\\PackagesLocalDirectory"
36
36
  },
37
37
  {