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.
- package/dist/bridge/bridgeAdapter.js +13 -1
- package/dist/cli/commands/doctor.d.ts +52 -1
- package/dist/cli/commands/doctor.js +156 -11
- package/dist/cli/settingsStore.d.ts +7 -0
- package/dist/cli/settingsStore.js +20 -0
- package/dist/config/settings.d.ts +18 -9
- package/dist/config/settings.js +25 -11
- package/dist/index.js +20 -2
- package/dist/knowledge/tableDataMethods.d.ts +47 -0
- package/dist/knowledge/tableDataMethods.js +159 -0
- package/dist/metadata/labelParser.d.ts +2 -0
- package/dist/metadata/labelParser.js +26 -9
- package/dist/metadata/symbolIndex.d.ts +124 -3
- package/dist/metadata/symbolIndex.js +385 -57
- package/dist/metadata/types.d.ts +21 -3
- package/dist/metadata/xmlParser.d.ts +13 -0
- package/dist/metadata/xmlParser.js +32 -7
- package/dist/metadata/xppDeclaration.d.ts +18 -0
- package/dist/metadata/xppDeclaration.js +14 -0
- package/dist/middleware/apiKeyAuth.d.ts +37 -0
- package/dist/middleware/apiKeyAuth.js +85 -2
- package/dist/scripts/build-database.js +449 -78
- package/dist/scripts/build-fts.js +450 -76
- package/dist/scripts/extract-metadata.js +42 -12
- package/dist/server/toolSchemas/labels.js +2 -1
- package/dist/tools/analysis/labelSearchHistory.d.ts +27 -1
- package/dist/tools/analysis/labelSearchHistory.js +58 -1
- package/dist/tools/analysis/prefixDiagnostics.js +1 -1
- package/dist/tools/analysis/searchLabels.js +8 -2
- package/dist/tools/analysis/validateCode.js +13 -5
- package/dist/tools/analysis/validateFormPattern.d.ts +13 -0
- package/dist/tools/analysis/validateFormPattern.js +49 -16
- package/dist/tools/analysis/validateObjectNaming.js +24 -12
- package/dist/tools/analysis/validateXpp.d.ts +2 -0
- package/dist/tools/analysis/validateXpp.js +178 -0
- package/dist/tools/knowledge/methodSignature.js +25 -0
- package/dist/tools/knowledge/xppKnowledge.d.ts +2 -0
- package/dist/tools/knowledge/xppKnowledge.js +172 -7
- package/dist/tools/labels.d.ts +6 -3
- package/dist/tools/labels.js +30 -17
- package/dist/tools/prepare/prepareChange.js +16 -4
- package/dist/tools/prepare/prepareCreate.js +15 -1
- package/dist/tools/readers/classInfo.js +10 -0
- package/dist/tools/readers/getLabelInfo.js +24 -6
- package/dist/tools/readers/getWorkspaceInfo.js +9 -4
- package/dist/tools/sdlc/runBpCheck.js +35 -6
- package/dist/tools/sdlc/undoLastModification.js +26 -8
- package/dist/tools/sdlc/updateSymbolIndex.d.ts +0 -13
- package/dist/tools/sdlc/updateSymbolIndex.js +87 -2
- package/dist/tools/smart/generateSmartForm.js +13 -2
- package/dist/tools/specs/opSpecs.js +1 -1
- package/dist/tools/write/createD365File.js +10 -2
- package/dist/tools/write/createLabel.js +11 -3
- package/dist/tools/write/modifyD365File.d.ts +16 -0
- package/dist/tools/write/modifyD365File.js +89 -9
- package/dist/tools/write/resolveReferences.js +7 -1
- package/dist/utils/buildMarker.d.ts +17 -2
- package/dist/utils/buildMarker.js +27 -12
- package/dist/utils/concurrency.d.ts +20 -0
- package/dist/utils/concurrency.js +41 -0
- package/dist/utils/effectivePrefix.js +1 -1
- package/dist/utils/labelReference.d.ts +33 -0
- package/dist/utils/labelReference.js +53 -0
- package/dist/utils/modelClassifier.js +10 -3
- package/dist/utils/modelToken.d.ts +30 -0
- package/dist/utils/modelToken.js +32 -0
- package/dist/utils/objectNaming.js +46 -10
- package/dist/utils/packagesRoot.d.ts +31 -0
- package/dist/utils/packagesRoot.js +33 -0
- 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}:
|
|
122
|
+
return [{ severity: 'ok', message: `${label}: ${facts.humanName} OK (${configured})` }];
|
|
105
123
|
}
|
|
106
124
|
return [{
|
|
107
125
|
severity: 'fail',
|
|
108
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
15
|
-
* deliberately absent: they are runtime/platform
|
|
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:
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* a
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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. */
|
package/dist/config/settings.js
CHANGED
|
@@ -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
|
|
15
|
-
* deliberately absent: they are runtime/platform
|
|
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: '
|
|
220
|
-
type: '
|
|
221
|
-
label: '
|
|
222
|
-
description: '
|
|
223
|
-
'
|
|
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.
|
|
415
|
-
'container or App Service needs
|
|
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: '
|
|
709
|
-
'the
|
|
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
|
-
|
|
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
|