d365fo-mcp 1.10.0 → 1.11.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 (70) hide show
  1. package/dist/bridge/bridgeAdapter.js +13 -1
  2. package/dist/cli/commands/doctor.d.ts +52 -1
  3. package/dist/cli/commands/doctor.js +156 -11
  4. package/dist/cli/settingsStore.d.ts +7 -0
  5. package/dist/cli/settingsStore.js +20 -0
  6. package/dist/config/settings.d.ts +18 -9
  7. package/dist/config/settings.js +25 -11
  8. package/dist/index.js +20 -2
  9. package/dist/knowledge/tableDataMethods.d.ts +47 -0
  10. package/dist/knowledge/tableDataMethods.js +159 -0
  11. package/dist/metadata/labelParser.d.ts +2 -0
  12. package/dist/metadata/labelParser.js +26 -9
  13. package/dist/metadata/symbolIndex.d.ts +124 -3
  14. package/dist/metadata/symbolIndex.js +385 -57
  15. package/dist/metadata/types.d.ts +21 -3
  16. package/dist/metadata/xmlParser.d.ts +13 -0
  17. package/dist/metadata/xmlParser.js +32 -7
  18. package/dist/metadata/xppDeclaration.d.ts +18 -0
  19. package/dist/metadata/xppDeclaration.js +14 -0
  20. package/dist/middleware/apiKeyAuth.d.ts +37 -0
  21. package/dist/middleware/apiKeyAuth.js +85 -2
  22. package/dist/scripts/build-database.js +449 -78
  23. package/dist/scripts/build-fts.js +450 -76
  24. package/dist/scripts/extract-metadata.js +42 -12
  25. package/dist/server/toolSchemas/labels.js +2 -1
  26. package/dist/tools/analysis/labelSearchHistory.d.ts +27 -1
  27. package/dist/tools/analysis/labelSearchHistory.js +58 -1
  28. package/dist/tools/analysis/prefixDiagnostics.js +1 -1
  29. package/dist/tools/analysis/searchLabels.js +8 -2
  30. package/dist/tools/analysis/validateCode.js +13 -5
  31. package/dist/tools/analysis/validateFormPattern.d.ts +13 -0
  32. package/dist/tools/analysis/validateFormPattern.js +49 -16
  33. package/dist/tools/analysis/validateObjectNaming.js +24 -12
  34. package/dist/tools/analysis/validateXpp.d.ts +2 -0
  35. package/dist/tools/analysis/validateXpp.js +178 -0
  36. package/dist/tools/knowledge/methodSignature.js +25 -0
  37. package/dist/tools/knowledge/xppKnowledge.d.ts +2 -0
  38. package/dist/tools/knowledge/xppKnowledge.js +172 -7
  39. package/dist/tools/labels.d.ts +6 -3
  40. package/dist/tools/labels.js +30 -17
  41. package/dist/tools/prepare/prepareChange.js +16 -4
  42. package/dist/tools/prepare/prepareCreate.js +15 -1
  43. package/dist/tools/readers/classInfo.js +10 -0
  44. package/dist/tools/readers/getLabelInfo.js +24 -6
  45. package/dist/tools/readers/getWorkspaceInfo.js +9 -4
  46. package/dist/tools/sdlc/runBpCheck.js +35 -6
  47. package/dist/tools/sdlc/undoLastModification.js +26 -8
  48. package/dist/tools/sdlc/updateSymbolIndex.d.ts +0 -13
  49. package/dist/tools/sdlc/updateSymbolIndex.js +87 -2
  50. package/dist/tools/smart/generateSmartForm.js +13 -2
  51. package/dist/tools/specs/opSpecs.js +1 -1
  52. package/dist/tools/write/createD365File.js +10 -2
  53. package/dist/tools/write/createLabel.js +11 -3
  54. package/dist/tools/write/modifyD365File.d.ts +16 -0
  55. package/dist/tools/write/modifyD365File.js +89 -9
  56. package/dist/tools/write/resolveReferences.js +7 -1
  57. package/dist/utils/buildMarker.d.ts +17 -2
  58. package/dist/utils/buildMarker.js +27 -12
  59. package/dist/utils/concurrency.d.ts +20 -0
  60. package/dist/utils/concurrency.js +41 -0
  61. package/dist/utils/effectivePrefix.js +1 -1
  62. package/dist/utils/labelReference.d.ts +33 -0
  63. package/dist/utils/labelReference.js +53 -0
  64. package/dist/utils/modelClassifier.js +10 -3
  65. package/dist/utils/modelToken.d.ts +30 -0
  66. package/dist/utils/modelToken.js +32 -0
  67. package/dist/utils/objectNaming.js +46 -10
  68. package/dist/utils/packagesRoot.d.ts +31 -0
  69. package/dist/utils/packagesRoot.js +33 -0
  70. package/package.json +1 -1
@@ -25,7 +25,7 @@ import * as debouncedRefresh from './debouncedRefresh.js';
25
25
  import { debugLog } from '../utils/logger.js';
26
26
  import { xppMethodSourceForXml } from '../utils/xppFormat.js';
27
27
  import { ensureXppDocComment, ensureBlankLineBeforeClosingBrace } from '../utils/xppDocGen.js';
28
- import { parseXppDeclaration } from '../metadata/xppDeclaration.js';
28
+ import { parseXppDeclaration, parseXppClassHeader } from '../metadata/xppDeclaration.js';
29
29
  import { rankCustomFirst, isExactNameMatch } from '../utils/exactMatchRanking.js';
30
30
  import { pageFields, fieldsHeading, fieldsFooter, createControlBudget, chargeControl, chargeSkippedSubtree, controlsFooter, } from '../utils/payloadBudget.js';
31
31
  // TABLE
@@ -176,11 +176,23 @@ function formatClass(cls, compact, methodOffset) {
176
176
  if (cls.isAbstract)
177
177
  modifiers.push('abstract');
178
178
  const modStr = modifiers.length > 0 ? ` (${modifiers.join(', ')})` : '';
179
+ // The bridge sends no access modifier of its own, but it does send the
180
+ // declaration verbatim — and that is where the modifier lives, since AxClass
181
+ // XML has no element for it either. Parsed rather than regexed so a modifier
182
+ // named in an attribute or a doc comment cannot be mistaken for the real one.
183
+ const visibility = cls.declaration
184
+ ? parseXppClassHeader(cls.declaration)?.visibility
185
+ : undefined;
179
186
  let out = `# Class: ${cls.name}${modStr}\n\n`;
180
187
  if (cls.extends)
181
188
  out += `**Extends:** ${cls.extends}\n`;
182
189
  if (cls.model)
183
190
  out += `**Model:** ${cls.model}\n`;
191
+ // Printed on every path, so a reader comparing two classes is not left to infer
192
+ // package scope from a line's absence. Silent only when there was no
193
+ // declaration to read at all (#902).
194
+ if (cls.declaration)
195
+ out += `**Access:** ${visibility ?? 'public'}\n`;
184
196
  out += `**Abstract:** ${cls.isAbstract ? 'Yes' : 'No'}\n`;
185
197
  out += `**Final:** ${cls.isFinal ? 'Yes' : 'No'}\n`;
186
198
  out += `_Source: C# bridge (IMetadataProvider)_\n\n`;
@@ -4,6 +4,51 @@ interface CheckResult {
4
4
  message: string;
5
5
  fix?: string;
6
6
  }
7
+ /** Classic AOSService VM, or Unified Developer Experience. */
8
+ export type EnvKind = 'traditional' | 'ude';
9
+ /**
10
+ * What a path setting IS, so that "this folder is missing" can name the right
11
+ * cause and the right cure for it.
12
+ *
13
+ * Without this the three settings shared one message, and it fitted only one of
14
+ * them: `packagePath` on UDE. It told a traditional VM that its packages root
15
+ * is "normally auto-detected from the active XPP config" (it is not — the drive
16
+ * scan finds it), and it offered `<drive>:\AosService\PackagesLocalDirectory`,
17
+ * a traditional-VM artifact, as the value to give the UDE ModelStoreFolder and
18
+ * FrameworkDirectory.
19
+ *
20
+ * `autoSource` is the load-bearing one: a stale pin is only a *stale pin* where
21
+ * something would otherwise resolve the value live. Where nothing would, the
22
+ * pin is the configuration, and telling the user to delete it is telling them
23
+ * to break their install — `customPackagesPath` on a traditional VM is exactly
24
+ * that case, the documented fix for junction layouts (docs/MCP_CONFIG.md).
25
+ */
26
+ interface PathFacts {
27
+ humanName: string;
28
+ /** How the value resolves when nothing pins it, or null when nothing does. */
29
+ autoSource(kind: EnvKind): string | null;
30
+ /** Values doctor can actually propose — empty when it has no way to know one. */
31
+ candidates(kind: EnvKind): string[];
32
+ /** What the setting ought to point at, in words. */
33
+ target(kind: EnvKind): string;
34
+ /** Extra context line when there is no candidate to name, or null. */
35
+ note(kind: EnvKind): string | null;
36
+ }
37
+ export declare const PATH_FACTS: Record<string, PathFacts>;
38
+ /**
39
+ * "Path setting points at a folder that isn't there" — with the cause named.
40
+ *
41
+ * A value pinned by the legacy .env instead of the JSON config is the likely bug
42
+ * whenever something else would have resolved it live: the .env copy goes stale
43
+ * the moment a platform update moves the folder, and it keeps outranking the
44
+ * now-correct detection at every startup (see configManager's envContext, which
45
+ * reads these env vars before consulting the XPP config). The server then dies
46
+ * with "C# bridge unavailable (ude)" and nothing points at the .env.
47
+ *
48
+ * Pure — every input is passed in — so the messages can be tested without a
49
+ * Windows box, an XPP config or a real .env.
50
+ */
51
+ export declare function missingPathFix(setting: import('../../config/settings.js').Setting, facts: PathFacts, label: string, configured: string, kind: EnvKind, pinnedByEnv: boolean): Omit<CheckResult, 'severity'>;
7
52
  /**
8
53
  * The configured prefix against the one the model's own objects use.
9
54
  *
@@ -11,8 +56,14 @@ interface CheckResult {
11
56
  * for every model — but the server resolves the model's own naming ABOVE the
12
57
  * configuration, so a user reading only their config has the wrong answer. State
13
58
  * both, and how to pin the configured one.
59
+ *
60
+ * `pinned` is naming.prefixSource=config. This check used to call the inference
61
+ * directly and so never saw it — modelPrefixInference reads it in
62
+ * getInferredModelPrefix, one level above inferPrefixFromObjectNames — which
63
+ * meant a user who had already pinned the prefix was still told their model's
64
+ * naming wins (it does not) and offered the fix they had already applied (#893).
14
65
  */
15
- export declare function checkPrefixResolution(configuredPrefix: string, modelName: string | null, modelObjectNames: string[], label: string): CheckResult[];
66
+ export declare function checkPrefixResolution(configuredPrefix: string, modelName: string | null, modelObjectNames: string[], label: string, pinned?: boolean): CheckResult[];
16
67
  export declare function doctorCommand(): Promise<void>;
17
68
  export {};
18
69
  //# sourceMappingURL=doctor.d.ts.map
@@ -13,7 +13,7 @@ import { bridgeBuildCommand, dataRoot, installMode, isWindows, paths, repoRoot }
13
13
  import { commandExists } from '../exec.js';
14
14
  import { isLegacyInstanceLayout, listInstances } from '../instances.js';
15
15
  import { checkRelease } from '../npmRegistry.js';
16
- import { conflictingLegacyValues, readPath, readSetting } from '../settingsStore.js';
16
+ import { conflictingLegacyValues, readPath, readSetting, settingSource } from '../settingsStore.js';
17
17
  import { instanceTarget, rootTarget } from '../target.js';
18
18
  import { isXppConfigStale, listXppConfigs, xppConfigDir } from '../xppConfig.js';
19
19
  import { describePackagesRootScan, packagesRoots } from '../../utils/packagesRoot.js';
@@ -89,8 +89,8 @@ function checkConfig(target, label) {
89
89
  function checkPackagesRoot(store, label) {
90
90
  if (!isWindows)
91
91
  return [];
92
- const configured = String(readSetting(store, settingByPath('environment.packagePath')) ?? '').trim();
93
92
  const detected = packagesRoots();
93
+ const configured = String(readSetting(store, settingByPath('environment.packagePath')) ?? '').trim();
94
94
  if (!configured) {
95
95
  // UDE resolves its roots from the XPP config, so silence here is normal.
96
96
  if (detected.length === 0)
@@ -100,18 +100,124 @@ function checkPackagesRoot(store, label) {
100
100
  message: `${label}: packages root not configured — the server will use ${detected[0]}`,
101
101
  }];
102
102
  }
103
+ return checkPathSetting(store, label, 'environment.packagePath');
104
+ }
105
+ /**
106
+ * One configured path setting against what the machine actually has.
107
+ *
108
+ * Unset is silent: for the two UDE roots that is the normal and recommended
109
+ * state (they resolve live from the active XPP config, which is how the server
110
+ * notices a platform update), and packagePath has its own not-configured
111
+ * branch above.
112
+ */
113
+ function checkPathSetting(store, label, settingPath) {
114
+ if (!isWindows)
115
+ return [];
116
+ const setting = settingByPath(settingPath);
117
+ const facts = PATH_FACTS[settingPath];
118
+ const configured = String(readSetting(store, setting) ?? '').trim();
119
+ if (!configured)
120
+ return [];
103
121
  if (fs.existsSync(configured)) {
104
- return [{ severity: 'ok', message: `${label}: packages root OK (${configured})` }];
122
+ return [{ severity: 'ok', message: `${label}: ${facts.humanName} OK (${configured})` }];
105
123
  }
106
124
  return [{
107
125
  severity: 'fail',
108
- message: `${label}: packages root does not exist (${configured})` +
109
- (detected.length > 0 ? `\n found instead: ${detected.join(', ')}` : `\n ${describePackagesRootScan()}`),
110
- fix: detected.length > 0
111
- ? `set environment.packagePath to ${detected[0]} (${SETUP_COMMAND})`
112
- : `point environment.packagePath at this machine's PackagesLocalDirectory (${SETUP_COMMAND})`,
126
+ ...missingPathFix(setting, facts, label, configured, environmentKind(store), settingSource(store, setting) === 'env'),
113
127
  }];
114
128
  }
129
+ /**
130
+ * Which of the two this install is. The configured value wins; with nothing
131
+ * configured this falls back to the same XPP-config detection the server itself
132
+ * uses (see environment.type in config/settings.ts), so doctor's diagnosis
133
+ * matches the runtime's behaviour rather than a second guess at it.
134
+ */
135
+ function environmentKind(store) {
136
+ const configured = String(readSetting(store, settingByPath('environment.type')) ?? '').trim().toLowerCase();
137
+ if (configured === 'ude' || configured === 'traditional')
138
+ return configured;
139
+ return listXppConfigs().length > 0 ? 'ude' : 'traditional';
140
+ }
141
+ export const PATH_FACTS = {
142
+ 'environment.packagePath': {
143
+ humanName: 'packages root',
144
+ autoSource: kind => kind === 'ude'
145
+ ? 'the active XPP config'
146
+ : (packagesRoots().length > 0 ? 'the drive scan for AosService\\PackagesLocalDirectory' : null),
147
+ // On UDE the XPP config is the authority; a stray empty C:\AosService stub
148
+ // is exactly the wrong thing to propose there.
149
+ candidates: kind => kind === 'ude' ? [] : packagesRoots(),
150
+ target: () => "this machine's PackagesLocalDirectory",
151
+ note: kind => kind === 'ude' ? null : describePackagesRootScan(),
152
+ },
153
+ 'environment.customPackagesPath': {
154
+ humanName: 'custom X++ root',
155
+ // Traditional: not auto-detected at all. It is a deliberate pin naming the
156
+ // repo that holds custom metadata outside PackagesLocalDirectory.
157
+ autoSource: kind => kind === 'ude' ? 'the active XPP config (ModelStoreFolder)' : null,
158
+ candidates: () => [],
159
+ target: kind => kind === 'ude'
160
+ ? 'the ModelStoreFolder of the active XPP config'
161
+ : 'the folder your custom model metadata actually lives in',
162
+ note: () => null,
163
+ },
164
+ 'environment.microsoftPackagesPath': {
165
+ humanName: 'Microsoft X++ root',
166
+ autoSource: kind => kind === 'ude' ? 'the active XPP config (FrameworkDirectory)' : null,
167
+ candidates: () => [],
168
+ target: () => 'the read-only Microsoft packages folder (FrameworkDirectory)',
169
+ note: kind => kind === 'ude' ? null : 'this setting applies to UDE installs only',
170
+ },
171
+ };
172
+ /**
173
+ * "Path setting points at a folder that isn't there" — with the cause named.
174
+ *
175
+ * A value pinned by the legacy .env instead of the JSON config is the likely bug
176
+ * whenever something else would have resolved it live: the .env copy goes stale
177
+ * the moment a platform update moves the folder, and it keeps outranking the
178
+ * now-correct detection at every startup (see configManager's envContext, which
179
+ * reads these env vars before consulting the XPP config). The server then dies
180
+ * with "C# bridge unavailable (ude)" and nothing points at the .env.
181
+ *
182
+ * Pure — every input is passed in — so the messages can be tested without a
183
+ * Windows box, an XPP config or a real .env.
184
+ */
185
+ export function missingPathFix(setting, facts, label, configured, kind, pinnedByEnv) {
186
+ const auto = facts.autoSource(kind);
187
+ const candidates = facts.candidates(kind);
188
+ const note = candidates.length > 0 ? `found instead: ${candidates.join(', ')}` : facts.note(kind);
189
+ const message = `${label}: ${facts.humanName} does not exist (${configured})` +
190
+ (pinnedByEnv ? `\n pinned by legacy .env (${setting.env}), not the JSON config` : '') +
191
+ (note ? `\n ${note}` : '');
192
+ // Deleting the key IS the whole fix here: the value it hides is already right.
193
+ if (pinnedByEnv && auto) {
194
+ return {
195
+ message,
196
+ fix: `remove ${setting.env} from .env — ${facts.humanName} is resolved from ${auto}, and a hardcoded ` +
197
+ `copy silently overrides that once a platform update moves or deletes the folder`,
198
+ };
199
+ }
200
+ const dropEnvFirst = pinnedByEnv ? `remove ${setting.env} from .env, then set` : `set`;
201
+ if (candidates.length > 0) {
202
+ return { message, fix: `${dropEnvFirst} ${setting.path} to ${candidates[0]} (${SETUP_COMMAND})` };
203
+ }
204
+ if (auto) {
205
+ // Naming `target` again here would only repeat `auto` in other words — on
206
+ // UDE the folder to point at IS the one the XPP config would have supplied.
207
+ return {
208
+ message,
209
+ fix: `clear ${setting.path} so it resolves from ${auto}, or repoint it if this install genuinely ` +
210
+ `keeps ${facts.humanName} elsewhere (${SETUP_COMMAND})`,
211
+ };
212
+ }
213
+ // Nothing would resolve this value on its own, so the pin is the configuration
214
+ // — it needs correcting, not deleting.
215
+ return {
216
+ message,
217
+ fix: `point ${setting.path} at ${facts.target(kind)} (${SETUP_COMMAND})` +
218
+ (pinnedByEnv ? `, and note that ${setting.env} in .env currently outranks the JSON config` : ''),
219
+ };
220
+ }
115
221
  /**
116
222
  * Which detection source resolves the workspace — and which model it lands on.
117
223
  *
@@ -160,10 +266,36 @@ async function checkWorkspaceDetection(store, label) {
160
266
  * for every model — but the server resolves the model's own naming ABOVE the
161
267
  * configuration, so a user reading only their config has the wrong answer. State
162
268
  * both, and how to pin the configured one.
269
+ *
270
+ * `pinned` is naming.prefixSource=config. This check used to call the inference
271
+ * directly and so never saw it — modelPrefixInference reads it in
272
+ * getInferredModelPrefix, one level above inferPrefixFromObjectNames — which
273
+ * meant a user who had already pinned the prefix was still told their model's
274
+ * naming wins (it does not) and offered the fix they had already applied (#893).
163
275
  */
164
- export function checkPrefixResolution(configuredPrefix, modelName, modelObjectNames, label) {
276
+ export function checkPrefixResolution(configuredPrefix, modelName, modelObjectNames, label, pinned = false) {
165
277
  const inferred = modelName ? inferPrefixFromObjectNames(modelObjectNames, modelName) : null;
166
278
  const bare = (s) => s.replace(/_+$/, '').toLowerCase();
279
+ if (pinned) {
280
+ // Inference is off, so naming.prefix is the whole answer — and an empty one
281
+ // is worse here than anywhere else: pinning it leaves nothing to fall back
282
+ // to but the model name.
283
+ if (!configuredPrefix) {
284
+ return [{
285
+ severity: 'warn',
286
+ message: `${label}: naming.prefixSource=config pins the configured prefix, but naming.prefix is empty ` +
287
+ `— new objects will be prefixed with the model name`,
288
+ fix: `set naming.prefix to your ISV prefix (${SETUP_COMMAND})`,
289
+ }];
290
+ }
291
+ const ignored = inferred?.regular && bare(inferred.regular) !== bare(configuredPrefix)
292
+ ? ` — model "${modelName}"'s objects use "${inferred.regular}", ignored while the prefix is pinned`
293
+ : '';
294
+ return [{
295
+ severity: 'ok',
296
+ message: `${label}: prefix "${configuredPrefix}" (naming.prefix, pinned by naming.prefixSource=config)${ignored}`,
297
+ }];
298
+ }
167
299
  if (!inferred?.regular) {
168
300
  if (!configuredPrefix) {
169
301
  return [{
@@ -185,7 +317,8 @@ export function checkPrefixResolution(configuredPrefix, modelName, modelObjectNa
185
317
  message: `${label}: prefix conflict — model "${modelName}"'s objects use "${inferred.regular}" ` +
186
318
  `(${inferred.coverage}/${inferred.sampleSize}), naming.prefix says "${configuredPrefix}". ` +
187
319
  `The model's own naming wins, so new objects are named "${inferred.regular}…".`,
188
- fix: 'EXTENSION_PREFIX_SOURCE=config pins the configured value instead',
320
+ fix: `set naming.prefixSource=config to pin the configured value instead (${SETUP_COMMAND}, or ` +
321
+ `EXTENSION_PREFIX_SOURCE=config in the environment)`,
189
322
  }];
190
323
  }
191
324
  /**
@@ -348,6 +481,10 @@ export async function doctorCommand() {
348
481
  emit(r);
349
482
  for (const r of checkPackagesRoot(root.store, 'Root'))
350
483
  emit(r);
484
+ for (const r of checkPathSetting(root.store, 'Root', 'environment.customPackagesPath'))
485
+ emit(r);
486
+ for (const r of checkPathSetting(root.store, 'Root', 'environment.microsoftPackagesPath'))
487
+ emit(r);
351
488
  // Database (root)
352
489
  emit(checkDb(root.store, paths.defaultDb, 'Root'));
353
490
  // Which source resolves the workspace, and the prefix that follows from it —
@@ -359,7 +496,11 @@ export async function doctorCommand() {
359
496
  const names = detection.modelName
360
497
  ? await modelObjectNames(readPath(root.store, settingByPath('index.dbPath'), paths.defaultDb), detection.modelName)
361
498
  : [];
362
- for (const r of checkPrefixResolution(configuredPrefix, detection.modelName, names, 'Naming'))
499
+ // Same precedence the server applies: the real environment outranks the
500
+ // config file (loadEnv), and the CLI never projects one onto the other.
501
+ const prefixSource = (process.env.EXTENSION_PREFIX_SOURCE
502
+ ?? String(readSetting(root.store, settingByPath('naming.prefixSource')) ?? '')).trim().toLowerCase();
503
+ for (const r of checkPrefixResolution(configuredPrefix, detection.modelName, names, 'Naming', prefixSource === 'config'))
363
504
  emit(r);
364
505
  // C# bridge: the only write path; Windows-only.
365
506
  if (isWindows) {
@@ -402,6 +543,10 @@ export async function doctorCommand() {
402
543
  emit(r);
403
544
  for (const r of checkPackagesRoot(target.store, `Instance '${inst.name}'`))
404
545
  emit(r);
546
+ for (const r of checkPathSetting(target.store, `Instance '${inst.name}'`, 'environment.customPackagesPath'))
547
+ emit(r);
548
+ for (const r of checkPathSetting(target.store, `Instance '${inst.name}'`, 'environment.microsoftPackagesPath'))
549
+ emit(r);
405
550
  emit(checkDb(target.store, resolve(inst.dir, 'data', 'xpp-metadata.db'), `Instance '${inst.name}'`));
406
551
  if (isWindows && isXppConfigStale(target.store)) {
407
552
  emit({
@@ -19,6 +19,13 @@ export interface SettingsStore {
19
19
  export declare function openStore(baseDir: string, legacyEnvFile: string | null, fallbackConfigPath?: string): SettingsStore;
20
20
  /** Store rooted at an instance folder: instances/<name>/d365fo-mcp.json. */
21
21
  export declare function openInstanceStore(instanceDir: string): SettingsStore;
22
+ /**
23
+ * Where a setting's effective value actually comes from — the JSON config,
24
+ * the legacy .env fallback, or nowhere. Mirrors readSetting's own precedence
25
+ * so callers can tell "explicitly configured" apart from "inherited from a
26
+ * .env that may have gone stale" without re-deriving the value themselves.
27
+ */
28
+ export declare function settingSource(store: SettingsStore, setting: Setting): 'config' | 'env' | 'none';
22
29
  /** Effective value of a setting, or undefined when nothing configures it. */
23
30
  export declare function readSetting(store: SettingsStore, setting: Setting): unknown;
24
31
  /** Effective value, falling back to the documented default. */
@@ -35,6 +35,26 @@ export function openInstanceStore(instanceDir) {
35
35
  // layout (not the config/ repo layout) so listInstances() can find it.
36
36
  return openStore(instanceDir, join(instanceDir, '.env'), join(instanceDir, 'd365fo-mcp.json'));
37
37
  }
38
+ /**
39
+ * Where a setting's effective value actually comes from — the JSON config,
40
+ * the legacy .env fallback, or nowhere. Mirrors readSetting's own precedence
41
+ * so callers can tell "explicitly configured" apart from "inherited from a
42
+ * .env that may have gone stale" without re-deriving the value themselves.
43
+ */
44
+ export function settingSource(store, setting) {
45
+ const fromJson = getAtPath(setting.tier === 'secret' ? store.secrets : store.config, setting.path);
46
+ if (fromJson !== undefined && fromJson !== null && fromJson !== '')
47
+ return 'config';
48
+ if (store.legacyEnvFile) {
49
+ // stripInlineComment, exactly as readSetting applies it: `KEY= # note` has
50
+ // no value, and reporting 'env' for one would have this function disagree
51
+ // with the value the caller reads a line later.
52
+ const raw = readEnvValue(store.legacyEnvFile, setting.env);
53
+ if (raw !== null && stripInlineComment(raw) !== '')
54
+ return 'env';
55
+ }
56
+ return 'none';
57
+ }
38
58
  /** Effective value of a setting, or undefined when nothing configures it. */
39
59
  export function readSetting(store, setting) {
40
60
  const fromJson = getAtPath(setting.tier === 'secret' ? store.secrets : store.config, setting.path);
@@ -11,8 +11,9 @@
11
11
  * Everything else — the wizard, the config loader, the doctor and the docs
12
12
  * table — is generated from this list, so a new setting only has to be added
13
13
  * here. Purely operational variables the user should never have to set
14
- * (NODE_ENV, WEBSITES_PORT, CI/TERM detection, ENV_FILE, MCP_STDIO_MODE) are
15
- * deliberately absent: they are runtime/platform inputs, not configuration.
14
+ * (NODE_ENV, WEBSITES_PORT, CI/TERM detection, ENV_FILE, MCP_STDIO_MODE,
15
+ * ALLOW_UNAUTHENTICATED) are deliberately absent: they are runtime/platform
16
+ * inputs, not configuration.
16
17
  */
17
18
  export type SettingType = 'string' | 'path' | 'boolean' | 'int' | 'list' | 'enum';
18
19
  /**
@@ -30,13 +31,21 @@ export interface Setting {
30
31
  /**
31
32
  * Dotted path inside the JSON config, e.g. "environment.packagePath".
32
33
  *
33
- * Absent for `env-only` settings: consent-style switches whose whole point is
34
- * that they are re-read from the environment (or the .env file) between tool
35
- * calls, so writing them into the wizard-managed JSON would misrepresent when
36
- * a change takes effect. They are still registered here — without an entry
37
- * the docs generator silently drops them, which is how three cross-model
38
- * variables disappeared from docs/CONFIGURATION.md the last time it was
39
- * regenerated.
34
+ * Absent for `env-only` settings: values whose reader is somewhere the
35
+ * wizard-managed JSON cannot honestly describe — the cross-model consent
36
+ * switches, re-read from the .env before every guard decision so a grant
37
+ * applies without a restart (loadEnv.reloadWritePolicy), and the lock
38
+ * heartbeat, read at acquire time in a process the wizard never configures.
39
+ * A JSON key would misrepresent when a change takes effect.
40
+ *
41
+ * "The runtime reads it straight from process.env" is NOT that reason — every
42
+ * setting here does, via toEnvRecord. EXTENSION_PREFIX_SOURCE sat in this
43
+ * group on that basis alone until #893, which cost a multi-instance install a
44
+ * whole second configuration file for one static naming preference.
45
+ *
46
+ * env-only settings are still registered — without an entry the docs
47
+ * generator silently drops them, which is how three cross-model variables
48
+ * disappeared from docs/CONFIGURATION.md the last time it was regenerated.
40
49
  */
41
50
  path?: string;
42
51
  /** Environment variable the server/scripts read at runtime. */
@@ -11,8 +11,9 @@
11
11
  * Everything else — the wizard, the config loader, the doctor and the docs
12
12
  * table — is generated from this list, so a new setting only has to be added
13
13
  * here. Purely operational variables the user should never have to set
14
- * (NODE_ENV, WEBSITES_PORT, CI/TERM detection, ENV_FILE, MCP_STDIO_MODE) are
15
- * deliberately absent: they are runtime/platform inputs, not configuration.
14
+ * (NODE_ENV, WEBSITES_PORT, CI/TERM detection, ENV_FILE, MCP_STDIO_MODE,
15
+ * ALLOW_UNAUTHENTICATED) are deliberately absent: they are runtime/platform
16
+ * inputs, not configuration.
16
17
  */
17
18
  export const SECTIONS = [
18
19
  {
@@ -214,13 +215,21 @@ export const SETTINGS = [
214
215
  required: true,
215
216
  },
216
217
  {
218
+ path: 'naming.prefixSource',
217
219
  env: 'EXTENSION_PREFIX_SOURCE',
218
220
  section: 'naming',
219
- tier: 'env-only',
220
- type: 'string',
221
- label: 'Pin the configured prefix',
222
- description: 'Set to `config` to make `EXTENSION_PREFIX` authoritative again instead of learning each model\'s prefix from ' +
223
- 'its own objects.',
221
+ tier: 'advanced',
222
+ type: 'enum',
223
+ label: 'Where the prefix comes from',
224
+ description: 'Whether the effective prefix is learned from the active model\'s own objects or pinned to the configured ' +
225
+ '`naming.prefix`. Pin it when one model carries several feature prefixes that share a stem — inference learns ' +
226
+ 'the shared stem, while the objects you write need the full one. See ' +
227
+ '[Where the prefix comes from](CUSTOM_EXTENSIONS.md#where-the-prefix-comes-from).',
228
+ default: 'model',
229
+ choices: [
230
+ { value: 'model', hint: 'the model\'s own objects decide, falling back to naming.prefix' },
231
+ { value: 'config', hint: 'always naming.prefix, inference off (pre-1.8.2 behaviour)' },
232
+ ],
224
233
  },
225
234
  {
226
235
  path: 'naming.suffix',
@@ -411,8 +420,10 @@ export const SETTINGS = [
411
420
  tier: 'advanced',
412
421
  type: 'string',
413
422
  label: 'HTTP bind address',
414
- description: 'Interface the HTTP transport binds to. The default accepts connections from anywhere, which is what a ' +
415
- 'container or App Service needs; set 127.0.0.1 to make a local server unreachable from the network.',
423
+ description: 'Interface the HTTP transport binds to. Left unset it follows the API key: 0.0.0.0 once a key (or ' +
424
+ 'ALLOW_UNAUTHENTICATED) is configured, which is what a container or App Service needs, and 127.0.0.1 when ' +
425
+ 'neither is, so an unauthenticated server stays off the network. Setting it to a public interface without a ' +
426
+ 'key is refused at startup.',
416
427
  default: '0.0.0.0',
417
428
  },
418
429
  {
@@ -705,8 +716,11 @@ export const SETTINGS = [
705
716
  tier: 'secret',
706
717
  type: 'string',
707
718
  label: 'API key required from HTTP clients',
708
- description: 'When set, every HTTP request must present this key. Leave empty for a localhost-only server; set it whenever ' +
709
- 'the port is reachable from another machine.',
719
+ description: 'Every HTTP request must present this key as X-Api-Key (or Authorization: Bearer). Required for any server ' +
720
+ 'reachable from the network — without it the listener serves your indexed X++ source to anyone who can reach ' +
721
+ 'the port, so with no key set the server binds 127.0.0.1 instead, and refuses to start if HOST asks for a ' +
722
+ 'public interface anyway. May be left empty only for a localhost-only development server. Generate with ' +
723
+ '`openssl rand -hex 32`.',
710
724
  },
711
725
  {
712
726
  path: 'behavior.groundingSecret',
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ import { initializeDatabase } from './database/download.js';
21
21
  import { initializeConfig, getConfigManager } from './utils/configManager.js';
22
22
  import { SERVER_MODE, LOCAL_TOOLS, TOOL_PROFILE, EXTRA_TOOLS, isToolEnabled } from './server/serverMode.js';
23
23
  import { TOOL_ANNOTATIONS } from './server/toolAnnotations.js';
24
- import { apiKeyAuth } from './middleware/apiKeyAuth.js';
24
+ import { apiKeyAuth, authStartupError, resolveBindHost } from './middleware/apiKeyAuth.js';
25
25
  import { VERSION } from './version.js';
26
26
  import { setInitializeParams } from './utils/stdioSessionInfo.js';
27
27
  import { setModelObjectNameSource } from './utils/modelPrefixInference.js';
@@ -645,7 +645,18 @@ async function main() {
645
645
  // Branded banner first — connection details are known immediately, before
646
646
  // the (potentially long) database load. Symbol counts are intentionally NOT
647
647
  // shown here; they appear once during the load (`✓ Loaded … symbols`).
648
- const host = process.env.HOST || '0.0.0.0';
648
+ // Fail closed before anything binds: a network-reachable listener with no
649
+ // API_KEY would serve the whole read surface to anonymous callers.
650
+ const authError = authStartupError();
651
+ if (authError) {
652
+ console.error('');
653
+ console.error(authError);
654
+ console.error('');
655
+ process.exit(1);
656
+ }
657
+ // Not `process.env.HOST || '0.0.0.0'` — with no key configured the default
658
+ // is loopback, so forgetting the key costs reachability, not secrecy.
659
+ const host = resolveBindHost();
649
660
  const W = 50;
650
661
  console.log('');
651
662
  for (const line of box([
@@ -656,6 +667,13 @@ async function main() {
656
667
  }
657
668
  console.log('');
658
669
  console.log(kv('Mode', `HTTP ${c.dim(glyph.dot)} ${SERVER_MODE}`));
670
+ // Three states, and the guard above has already ruled out the fourth
671
+ // (no key on a public interface), so "none" here always means loopback.
672
+ console.log(kv('Auth', process.env.API_KEY?.trim()
673
+ ? `API key ${c.dim(glyph.dot)} X-Api-Key`
674
+ : process.env.ALLOW_UNAUTHENTICATED === 'true'
675
+ ? c.yellow(`delegated upstream ${c.dim(glyph.dot)} nothing is checked here`)
676
+ : `none ${c.dim(glyph.dot)} ${c.dim('loopback only, not reachable from the network')}`));
659
677
  console.log(kv('Endpoint', c.cyan(`http://${host}:${PORT}/mcp`)));
660
678
  console.log(kv('Health', c.cyan(`http://localhost:${PORT}/health`)));
661
679
  console.log(kv('Runtime', `Node ${process.version} ${c.dim(glyph.dot)} pid ${process.pid}`));
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The data methods every table inherits from `xRecord` / `Common`.
3
+ *
4
+ * Those are kernel types with no AOT metadata, and the symbol index stores
5
+ * declared members only, so a table's `validateWrite` has no row anywhere.
6
+ * prepare(mode="change") and get_method both reported that as "not found",
7
+ * which reads as "the method does not exist" for the most common CoC target
8
+ * there is and leaves the caller to invent the wrapper unaided.
9
+ *
10
+ * The contract below is the part a green build cannot teach — above all that
11
+ * the pre-image is `this.orig()`, already in memory, so re-reading the row by
12
+ * its own RecId is a database round trip per write AND a different value: the
13
+ * current stored state rather than what this buffer was fetched with.
14
+ *
15
+ * A FALLBACK only: consulted when neither index, bridge nor XML declares the
16
+ * method, so a table that overrides `insert()` still reports its own signature.
17
+ */
18
+ export interface TableDataMethod {
19
+ /** Canonical AOT spelling. */
20
+ name: string;
21
+ /** The declaration a CoC wrapper has to match exactly. */
22
+ signature: string;
23
+ /** Kernel type that declares it. */
24
+ declaredOn: 'xRecord' | 'Common';
25
+ /** What wrapping it is for, in one line. */
26
+ purpose: string;
27
+ /** Non-negotiables a green build will not teach. */
28
+ contract: string[];
29
+ }
30
+ /** Keyed by lower-cased method name. */
31
+ export declare const TABLE_DATA_METHODS: Record<string, TableDataMethod>;
32
+ /** The inherited data method by that name, or undefined. Case-insensitive, as X++ is. */
33
+ export declare function lookupTableDataMethod(methodName: string): TableDataMethod | undefined;
34
+ /**
35
+ * True for the object types this fallback speaks for.
36
+ *
37
+ * Tables only, deliberately. Views, maps and data entities descend from `Common`
38
+ * too, but they do not all wrap through `tableStr` and not every one of these
39
+ * methods fires on them — a fallback that guessed there would be inventing a
40
+ * signature, which is the failure it exists to prevent.
41
+ */
42
+ export declare function hasTableDataMethods(objectType: string | undefined): boolean;
43
+ /** The `### Method signature` body when only this fallback knows the method. */
44
+ export declare function renderTableDataMethodSignature(method: TableDataMethod, objectName: string): string;
45
+ /** The `### CoC eligibility` body, plus the contract that is the reason this exists. */
46
+ export declare function renderTableDataMethodEligibility(method: TableDataMethod, objectName: string): string;
47
+ //# sourceMappingURL=tableDataMethods.d.ts.map