d365fo-mcp 1.8.5 → 1.9.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/.github/copilot-instructions.md +1 -1
- package/dist/cli/copilotFiles.js +9 -7
- package/dist/prompts/systemInstructions.d.ts +1 -1
- package/dist/prompts/systemInstructions.js +2 -2
- package/dist/server/toolSchemas/getWorkspaceInfo.js +1 -1
- package/dist/tools/codeGen.js +23 -21
- package/dist/tools/createD365File.js +6 -9
- package/dist/tools/extensionStrategyAdvisor.js +26 -26
- package/dist/tools/prefixDiagnostics.d.ts +8 -1
- package/dist/tools/prefixDiagnostics.js +25 -9
- package/dist/tools/search.js +3 -1
- package/dist/tools/toolHandler.js +113 -61
- package/dist/tools/validateFormPattern.d.ts +1 -1
- package/dist/tools/validateFormPattern.js +1 -1
- package/dist/utils/indexStaleness.d.ts +7 -0
- package/dist/utils/indexStaleness.js +28 -5
- package/dist/workspace/contextSnapshot.d.ts +5 -3
- package/dist/workspace/contextSnapshot.js +22 -0
- package/package.json +1 -1
|
@@ -18,7 +18,7 @@ Call `get_workspace_info()` before doing anything with D365FO objects.
|
|
|
18
18
|
|----------|--------|
|
|
19
19
|
| Call fails | STOP. MCP server not connected. Ask user to start it. |
|
|
20
20
|
| `⛔ CONFIGURATION PROBLEM` | STOP. Relay message. Wait for user. |
|
|
21
|
-
|
|
|
21
|
+
| No `⛔` in the response | Note the `Model` / `Prefix` lines. Proceed. |
|
|
22
22
|
|
|
23
23
|
## Terminal Prohibition
|
|
24
24
|
|
package/dist/cli/copilotFiles.js
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
* folder, or, when that is unknown, into a staging folder with a README
|
|
10
10
|
* naming the two destinations.
|
|
11
11
|
*
|
|
12
|
+
* The README belongs to the staging folder only. The solutions folder is the
|
|
13
|
+
* user's own projects directory, so the direct copy leaves nothing there but
|
|
14
|
+
* `.github\copilot-instructions.md` and says the rest on the console.
|
|
15
|
+
*
|
|
12
16
|
* The README points at the one .mcp.json `mcpJsonNote()` writes in the data
|
|
13
17
|
* root rather than copying it: the staging folder sits in that same data
|
|
14
18
|
* root, so a second copy is only one more file to keep in sync.
|
|
@@ -22,7 +26,7 @@ import { dataRoot, paths, repoRoot } from './context.js';
|
|
|
22
26
|
import { askConfirm, p } from './ui.js';
|
|
23
27
|
/** The copy shipped with this installation — package root in both install modes. */
|
|
24
28
|
const copilotSource = () => resolve(repoRoot, '.github', 'copilot-instructions.md');
|
|
25
|
-
function writeCopilotSetupReadme(targetDir
|
|
29
|
+
function writeCopilotSetupReadme(targetDir) {
|
|
26
30
|
const readmePath = resolve(targetDir, 'README.md');
|
|
27
31
|
const lines = [
|
|
28
32
|
'# VS setup quick guide',
|
|
@@ -35,9 +39,7 @@ function writeCopilotSetupReadme(targetDir, opts) {
|
|
|
35
39
|
` - Generated copy: ${paths.mcpSuggestion}`,
|
|
36
40
|
'',
|
|
37
41
|
'2. Copy .github/copilot-instructions.md',
|
|
38
|
-
|
|
39
|
-
? ' - Already placed here: .github\\copilot-instructions.md'
|
|
40
|
-
: ' - Destination: a parent folder of your solution directories',
|
|
42
|
+
' - Destination: a parent folder of your solution directories',
|
|
41
43
|
' - Why: provides mandatory D365FO tool-routing and safety rules for Copilot',
|
|
42
44
|
'',
|
|
43
45
|
'3. Restart Visual Studio after copying files.',
|
|
@@ -64,16 +66,16 @@ export async function maybePrepareCopilotInstructions(solutionsPath) {
|
|
|
64
66
|
const targetDir = resolve(target, '.github');
|
|
65
67
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
66
68
|
fs.copyFileSync(source, resolve(targetDir, 'copilot-instructions.md'));
|
|
67
|
-
writeCopilotSetupReadme(target, { copilotAlreadyPlaced: true });
|
|
68
69
|
p.log.success(`Prepared: ${resolve(targetDir, 'copilot-instructions.md')}`);
|
|
69
|
-
p.log.
|
|
70
|
+
p.log.info('Still to do: copy .mcp.json to %USERPROFILE%\\.mcp.json (or next to your .sln), then restart Visual Studio.\n' +
|
|
71
|
+
` Generated copy: ${paths.mcpSuggestion}`);
|
|
70
72
|
return;
|
|
71
73
|
}
|
|
72
74
|
const stageDir = resolve(dataRoot(), 'copilot-setup');
|
|
73
75
|
const stageGitHubDir = resolve(stageDir, '.github');
|
|
74
76
|
fs.mkdirSync(stageGitHubDir, { recursive: true });
|
|
75
77
|
fs.copyFileSync(source, resolve(stageGitHubDir, 'copilot-instructions.md'));
|
|
76
|
-
writeCopilotSetupReadme(stageDir
|
|
78
|
+
writeCopilotSetupReadme(stageDir);
|
|
77
79
|
if (wantsDirectCopy && !target) {
|
|
78
80
|
p.log.warn('Solutions folder is empty, so files were prepared in the local staging folder instead.');
|
|
79
81
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Kept deliberately under 200 lines: the prompt holds only the tool decision
|
|
10
10
|
* tree and hard prohibitions. Everything that is a rule about CODE lives in
|
|
11
|
-
* the queryable knowledge base —
|
|
11
|
+
* the queryable knowledge base — get_knowledge (see the ID table below).
|
|
12
12
|
*/
|
|
13
13
|
/**
|
|
14
14
|
* Get the system instructions prompt definition
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Kept deliberately under 200 lines: the prompt holds only the tool decision
|
|
10
10
|
* tree and hard prohibitions. Everything that is a rule about CODE lives in
|
|
11
|
-
* the queryable knowledge base —
|
|
11
|
+
* the queryable knowledge base — get_knowledge (see the ID table below).
|
|
12
12
|
*/
|
|
13
13
|
/**
|
|
14
14
|
* Get the system instructions prompt definition
|
|
@@ -117,7 +117,7 @@ You are an AI assistant with access to D365FO MCP tools, assisting with Dynamics
|
|
|
117
117
|
- No literal strings in \`Info()\`/\`error()\`/labels — use \`@Model:LabelId\` (reuse via \`labels(action="search")\` first)
|
|
118
118
|
- Every public/protected member needs a meaningful \`/// <summary>\` (not "MyClass class.")
|
|
119
119
|
|
|
120
|
-
**For full rules and examples call \`
|
|
120
|
+
**For full rules and examples call \`get_knowledge(kind="knowledge", topic=<id>)\` BEFORE generating code:**
|
|
121
121
|
|
|
122
122
|
| Knowledge ID | Covers |
|
|
123
123
|
|---|---|
|
|
@@ -20,7 +20,7 @@ export const getWorkspaceInfoTool = {
|
|
|
20
20
|
diagnostics: {
|
|
21
21
|
type: 'boolean',
|
|
22
22
|
default: false,
|
|
23
|
-
description: 'Include verbose
|
|
23
|
+
description: 'Include verbose sections (config sources, suffix, project paths, index scan, stdio handshake). Use when debugging config or connectivity.',
|
|
24
24
|
},
|
|
25
25
|
},
|
|
26
26
|
required: [],
|
package/dist/tools/codeGen.js
CHANGED
|
@@ -84,7 +84,7 @@ internal final class ${name}
|
|
|
84
84
|
// D365FO Data Entity: ${name}Entity
|
|
85
85
|
// ══════════════════════════════════════════════════════════════════
|
|
86
86
|
// Data entities are AxDataEntityView XML objects (NOT X++ classes).
|
|
87
|
-
// Use
|
|
87
|
+
// Use d365fo_file(action="create", objectType="view") or the VS designer.
|
|
88
88
|
//
|
|
89
89
|
// Key properties to set in XML:
|
|
90
90
|
// PublicEntityName: "${name}" (OData singular name)
|
|
@@ -107,8 +107,8 @@ internal final class ${name}
|
|
|
107
107
|
//
|
|
108
108
|
// Workflow:
|
|
109
109
|
// 1. get_object_info(objectType="data-entity", name="similar entity") → study structure
|
|
110
|
-
// 2.
|
|
111
|
-
// 3.
|
|
110
|
+
// 2. generate_object(mode="scaffold", objectType="data-entity", ...) → preview XML
|
|
111
|
+
// 3. d365fo_file(action="create", objectType="view", ...) → create file
|
|
112
112
|
// 4. After deployment: refresh entity list in Data Management workspace
|
|
113
113
|
`,
|
|
114
114
|
'batch-job': (name) => `
|
|
@@ -646,7 +646,7 @@ function menuItemXmlTemplate(name, itemType, targetObject) {
|
|
|
646
646
|
function ssrsReportFullTemplate(name) {
|
|
647
647
|
return `// ══════════════════════════════════════════════════════════════════
|
|
648
648
|
// SSRS Report: ${name}
|
|
649
|
-
// 5 objects required (use
|
|
649
|
+
// 5 objects required (use d365fo_file(action="create") for each):
|
|
650
650
|
// 1. ${name}TmpTable — TempDB table (objectType="table", tableType="TempDB")
|
|
651
651
|
// 2. ${name}Contract — DataContract class (below)
|
|
652
652
|
// 3. ${name}DP — Data Provider class (below)
|
|
@@ -1053,14 +1053,14 @@ public class ${name}Controller extends MenuFunction
|
|
|
1053
1053
|
function dataEntityStagingTemplate(name) {
|
|
1054
1054
|
return `// ══════════════════════════════════════════════════════════════════════════
|
|
1055
1055
|
// Data Entity with Staging Table: ${name}
|
|
1056
|
-
// 3 objects required (use
|
|
1056
|
+
// 3 objects required (use d365fo_file(action="create") for each):
|
|
1057
1057
|
// 1. ${name}StagingTable — TempDB staging table
|
|
1058
1058
|
// 2. ${name}Entity — Data entity (AxDataEntityView)
|
|
1059
1059
|
// 3. ${name}EntityService — Optional: AIF service class
|
|
1060
1060
|
// ══════════════════════════════════════════════════════════════════════════
|
|
1061
1061
|
|
|
1062
1062
|
// ── Object 1: Staging table ${name}Staging ─────────────────────────────────
|
|
1063
|
-
//
|
|
1063
|
+
// d365fo_file(action="create", objectType="table", objectName="${name}Staging", xmlContent=...)
|
|
1064
1064
|
// Set: TableType=TempDB (NOT RegularTable), TableGroup=Main
|
|
1065
1065
|
// Fields mirror the entity's public fields exactly (same names, same EDTs)
|
|
1066
1066
|
|
|
@@ -1661,7 +1661,7 @@ public class ${name}Service
|
|
|
1661
1661
|
}
|
|
1662
1662
|
}
|
|
1663
1663
|
|
|
1664
|
-
// ── 3. AOT objects (create via
|
|
1664
|
+
// ── 3. AOT objects (create via d365fo_file(action="create")) ────────────
|
|
1665
1665
|
// Verify the result afterwards with get_object_info(objectType="service", name="${name}Service").
|
|
1666
1666
|
// a) AxService XML (real schema: ServiceOperations / AxServiceOperation / Method):
|
|
1667
1667
|
// <AxService xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
|
|
@@ -1822,7 +1822,11 @@ export async function codeGenTool(request) {
|
|
|
1822
1822
|
// Extension pattern — args.name is the BASE element; prefix becomes the infix
|
|
1823
1823
|
// Grounding enforcement: extension patterns require proof that the AI looked at the
|
|
1824
1824
|
// real codebase (via prepare_change) before generating extension code.
|
|
1825
|
-
const groundingError = enforceGrounding(args.groundingToken,
|
|
1825
|
+
const groundingError = enforceGrounding(args.groundingToken,
|
|
1826
|
+
// The MCP-visible name, not the internal one: this string is echoed back
|
|
1827
|
+
// as the call the agent must repeat with a token, and `generate_code`
|
|
1828
|
+
// has not been a registered tool since the mega-tool consolidation.
|
|
1829
|
+
`generate_object(mode="pattern", pattern="${args.pattern}", name="${args.name}")`, args.name);
|
|
1826
1830
|
if (groundingError)
|
|
1827
1831
|
return groundingError;
|
|
1828
1832
|
// ── form-datasource-extension / form-control-extension (3-param templates) ──
|
|
@@ -1924,20 +1928,18 @@ export async function codeGenTool(request) {
|
|
|
1924
1928
|
text: `Generated ${args.pattern} template for "${displayName}":\n\n` +
|
|
1925
1929
|
`\`\`\`xpp${code}\n\`\`\`\n\n` +
|
|
1926
1930
|
`---\n\n` +
|
|
1927
|
-
|
|
1931
|
+
namingNote +
|
|
1932
|
+
// Only the CoC signature rule earns a line here: guessing static vs
|
|
1933
|
+
// instance produces code that fails to compile. The rest was a menu
|
|
1934
|
+
// of optional follow-ups — and it named tools that do not exist
|
|
1935
|
+
// (`find_coc_extensions`) with argument forms analyze_code rejects
|
|
1936
|
+
// (positional, not `className`/`methodName`), so following it cost a
|
|
1937
|
+
// failed call before the agent could get anything useful.
|
|
1928
1938
|
(args.pattern === 'class-extension'
|
|
1929
|
-
?
|
|
1930
|
-
`
|
|
1931
|
-
`
|
|
1932
|
-
|
|
1933
|
-
`4. ✅ Use \`analyze_code(mode="api-usage", "<ClassName>")\` - See how to use D365FO APIs correctly\n\n` +
|
|
1934
|
-
`⚠️ Never guess static vs instance — always use get_method(include="signature") first.`
|
|
1935
|
-
: `💡 **Next Steps for Better Code Quality:**\n\n` +
|
|
1936
|
-
`1. ✅ Use \`analyze_code(mode="patterns", "<scenario>")\` - Learn what D365FO classes are commonly used together\n` +
|
|
1937
|
-
`2. ✅ Use \`analyze_code(mode="implementations", "${displayName}", "<methodName>")\` - Get real implementation examples\n` +
|
|
1938
|
-
`3. ✅ Use \`analyze_code(mode="completeness", "${displayName}")\` - Check for missing common methods\n` +
|
|
1939
|
-
`4. ✅ Use \`analyze_code(mode="api-usage", "<ClassName>")\` - See how to use D365FO APIs correctly\n\n` +
|
|
1940
|
-
`These tools provide patterns from the actual codebase, not generic templates.`),
|
|
1939
|
+
? `\n\n⚠️ Before writing any CoC method call \`get_method(include="signature", className="${displayName}", methodName="<methodName>")\` — ` +
|
|
1940
|
+
`never guess static vs instance, the return type or the parameter list. ` +
|
|
1941
|
+
`Existing wrappers: \`extension_info(mode="coc", target="${displayName}")\`.`
|
|
1942
|
+
: ``),
|
|
1941
1943
|
},
|
|
1942
1944
|
],
|
|
1943
1945
|
};
|
|
@@ -4712,16 +4712,13 @@ export async function handleCreateD365File(request, context) {
|
|
|
4712
4712
|
`\nUntil resolved, add the file manually in Visual Studio: right-click project → Add Existing Item → ${normalizedFullPath}\n`;
|
|
4713
4713
|
}
|
|
4714
4714
|
}
|
|
4715
|
-
//
|
|
4715
|
+
// Only the step the AGENT can take. "Reload the project in VS / refresh the
|
|
4716
|
+
// AOT" is a human's UI chore, repeated on every object of a feature; when it
|
|
4717
|
+
// matters (addToProject failed, no projectPath) `projectMessage` above
|
|
4718
|
+
// already says so, in that specific case.
|
|
4716
4719
|
const nextSteps = args.addToProject
|
|
4717
|
-
? `Next
|
|
4718
|
-
|
|
4719
|
-
`2. Build the project to synchronize the object\n` +
|
|
4720
|
-
`3. Refresh AOT in Visual Studio to see the new object\n`
|
|
4721
|
-
: `Next steps:\n` +
|
|
4722
|
-
`1. Add the file to your Visual Studio project (.rnrproj)\n` +
|
|
4723
|
-
`2. Build the project to synchronize the object\n` +
|
|
4724
|
-
`3. Refresh AOT in Visual Studio to see the new object\n`;
|
|
4720
|
+
? `Next: build_d365fo_project to synchronize the object.\n`
|
|
4721
|
+
: `Next: add the file to your .rnrproj, then build_d365fo_project to synchronize the object.\n`;
|
|
4725
4722
|
// Record the freshly-created file for non-git undo (see the bridge paths above).
|
|
4726
4723
|
if (!fileExisted) {
|
|
4727
4724
|
recordCreatedArtifact({
|
|
@@ -46,9 +46,9 @@ const STRATEGY_RULES = [
|
|
|
46
46
|
{ mechanism: 'Form data source validateWrite()', when: 'Validation is UI-specific and should NOT apply to service/entity imports' },
|
|
47
47
|
],
|
|
48
48
|
nextSteps: [
|
|
49
|
-
'
|
|
50
|
-
'
|
|
51
|
-
'
|
|
49
|
+
'extension_info(mode="points", target="TableName") — available events and existing extensions',
|
|
50
|
+
'extension_info(mode="events", target="TableName") — see if someone already handles the same event',
|
|
51
|
+
'generate_object(mode="pattern", pattern="event-handler", name="TableName") — handler skeleton',
|
|
52
52
|
],
|
|
53
53
|
antiPatterns: [
|
|
54
54
|
{ wrong: 'Business Event', why: 'Business Events are for outbound notifications, not validation logic' },
|
|
@@ -73,8 +73,8 @@ const STRATEGY_RULES = [
|
|
|
73
73
|
{ mechanism: 'Form data source initValue()', when: 'Default depends on form context (e.g. header record of a lines form)' },
|
|
74
74
|
],
|
|
75
75
|
nextSteps: [
|
|
76
|
-
'
|
|
77
|
-
'get_method(include="signature", "TableName", "initValue"
|
|
76
|
+
'extension_info(mode="points", target="TableName") — check initValue availability',
|
|
77
|
+
'get_method(include="signature", className="TableName", methodName="initValue") — exact signature for the CoC wrapper',
|
|
78
78
|
],
|
|
79
79
|
antiPatterns: [
|
|
80
80
|
{ wrong: 'Overriding insert()', why: 'insert() is for persistence — defaults belong in initValue()' },
|
|
@@ -110,9 +110,9 @@ const STRATEGY_RULES = [
|
|
|
110
110
|
{ mechanism: 'CoC on table.modifiedFieldValue()', when: 'You also need the previous value to decide what to do' },
|
|
111
111
|
],
|
|
112
112
|
nextSteps: [
|
|
113
|
-
'
|
|
114
|
-
'get_method(include="signature", "TableName", "modifiedField"
|
|
115
|
-
'
|
|
113
|
+
'extension_info(mode="points", target="TableName") — confirm modifiedField is CoC-eligible and see existing extensions',
|
|
114
|
+
'get_method(include="signature", className="TableName", methodName="modifiedField") — exact signature for the CoC wrapper',
|
|
115
|
+
'extension_info(mode="coc", target="TableName", method="modifiedField") — check for existing wrappers',
|
|
116
116
|
],
|
|
117
117
|
antiPatterns: [
|
|
118
118
|
{ wrong: 'CoC on initValue()', why: 'initValue fires only at record creation — it will NOT run when the user later changes the field' },
|
|
@@ -141,9 +141,9 @@ const STRATEGY_RULES = [
|
|
|
141
141
|
{ mechanism: 'Replaceable method', when: 'Method is marked [Replaceable] — you can fully replace it (rare in standard app)' },
|
|
142
142
|
],
|
|
143
143
|
nextSteps: [
|
|
144
|
-
'
|
|
145
|
-
'get_method(include="signature", "ClassName", "methodName"
|
|
146
|
-
'
|
|
144
|
+
'extension_info(mode="points", target="ClassName") — see CoC-eligible methods and delegates',
|
|
145
|
+
'get_method(include="signature", className="ClassName", methodName="methodName") — exact signature for the CoC wrapper',
|
|
146
|
+
'extension_info(mode="coc", target="ClassName", method="methodName") — check for existing CoC wrappers',
|
|
147
147
|
],
|
|
148
148
|
antiPatterns: [
|
|
149
149
|
{ wrong: 'Copy-paste the entire class', why: 'Over-layering defeats the purpose of extensions and blocks upgrades' },
|
|
@@ -172,8 +172,8 @@ const STRATEGY_RULES = [
|
|
|
172
172
|
{ mechanism: 'Dual-write', when: 'Real-time bidirectional sync with Dataverse is needed' },
|
|
173
173
|
],
|
|
174
174
|
nextSteps: [
|
|
175
|
-
'get_knowledge(kind="knowledge", "business events") — learn the pattern',
|
|
176
|
-
'
|
|
175
|
+
'get_knowledge(kind="knowledge", topic="business events") — learn the pattern',
|
|
176
|
+
'generate_object(mode="pattern", pattern="business-event", name="MyEvent") — generate skeleton',
|
|
177
177
|
],
|
|
178
178
|
antiPatterns: [
|
|
179
179
|
{ wrong: 'CoC calling HttpClient', why: 'Synchronous HTTP in a transaction blocks the user and risks timeout/rollback' },
|
|
@@ -203,9 +203,9 @@ const STRATEGY_RULES = [
|
|
|
203
203
|
{ mechanism: 'Composite entity', when: 'Header + lines structure needs to be imported as a document' },
|
|
204
204
|
],
|
|
205
205
|
nextSteps: [
|
|
206
|
-
'get_knowledge(kind="knowledge", "data-management-framework") — learn DMF patterns',
|
|
207
|
-
'search("MyTable", "data-entity") — check if an entity already exists',
|
|
208
|
-
'
|
|
206
|
+
'get_knowledge(kind="knowledge", topic="data-management-framework") — learn DMF patterns',
|
|
207
|
+
'search(query="MyTable", type="data-entity") — check if an entity already exists',
|
|
208
|
+
'generate_object(mode="pattern", pattern="data-entity", name="MyEntity") — generate entity skeleton',
|
|
209
209
|
],
|
|
210
210
|
antiPatterns: [
|
|
211
211
|
{ wrong: 'Direct table insert via custom endpoint', why: 'Bypasses validation, number sequences, and event handlers' },
|
|
@@ -234,7 +234,7 @@ const STRATEGY_RULES = [
|
|
|
234
234
|
],
|
|
235
235
|
nextSteps: [
|
|
236
236
|
'get_object_info(objectType="form", name="FormName", options={searchControl:"General"}) — find exact control names and hierarchy',
|
|
237
|
-
'
|
|
237
|
+
'extension_info(mode="points", target="FormName", objectType="form") — check form extension points',
|
|
238
238
|
'd365fo_file(action="create", objectType="form-extension") — create the extension',
|
|
239
239
|
],
|
|
240
240
|
antiPatterns: [
|
|
@@ -265,8 +265,8 @@ const STRATEGY_RULES = [
|
|
|
265
265
|
],
|
|
266
266
|
nextSteps: [
|
|
267
267
|
'get_object_info(objectType="report", name="ReportName") — inspect existing report structure',
|
|
268
|
-
'get_knowledge(kind="knowledge", "ssrs-reports") — patterns for SSRS',
|
|
269
|
-
'
|
|
268
|
+
'get_knowledge(kind="knowledge", topic="ssrs-reports") — patterns for SSRS',
|
|
269
|
+
'generate_object(mode="scaffold", objectType="report", name="MyReport") — generate full SSRS stack',
|
|
270
270
|
],
|
|
271
271
|
antiPatterns: [
|
|
272
272
|
{ wrong: 'Business Event for document delivery', why: 'Business Events send notifications, not formatted documents' },
|
|
@@ -290,8 +290,8 @@ const STRATEGY_RULES = [
|
|
|
290
290
|
{ mechanism: 'Custom counter table', when: 'Simple auto-increment without legal entity scope or configurable format (rare — prefer the framework)' },
|
|
291
291
|
],
|
|
292
292
|
nextSteps: [
|
|
293
|
-
'get_knowledge(kind="knowledge", "number-sequences") — full pattern reference',
|
|
294
|
-
'
|
|
293
|
+
'get_knowledge(kind="knowledge", topic="number-sequences") — full pattern reference',
|
|
294
|
+
'generate_object(mode="pattern", pattern="number-seq-handler", name="MyModule") — generate skeleton',
|
|
295
295
|
],
|
|
296
296
|
antiPatterns: [
|
|
297
297
|
{ wrong: 'Identity column / RecId as business number', why: 'RecId is internal — users need formatted, gapless (or configurable) business numbers' },
|
|
@@ -318,8 +318,8 @@ const STRATEGY_RULES = [
|
|
|
318
318
|
{ mechanism: 'Table permission framework override', when: 'Granting DML access without a menu item entry point (rare)' },
|
|
319
319
|
],
|
|
320
320
|
nextSteps: [
|
|
321
|
-
'get_knowledge(kind="knowledge", "security-privileges-duties") — security pattern reference',
|
|
322
|
-
'security_info(mode="coverage", "ObjectName") — check existing security chain',
|
|
321
|
+
'get_knowledge(kind="knowledge", topic="security-privileges-duties") — security pattern reference',
|
|
322
|
+
'security_info(mode="coverage", objectName="ObjectName") — check existing security chain',
|
|
323
323
|
'd365fo_file(action="create", objectType="security-privilege") — create privilege',
|
|
324
324
|
],
|
|
325
325
|
antiPatterns: [
|
|
@@ -346,9 +346,9 @@ const STRATEGY_RULES = [
|
|
|
346
346
|
{ mechanism: 'Business Event + external processor', when: 'Processing should happen outside D365FO (e.g. Azure Function)' },
|
|
347
347
|
],
|
|
348
348
|
nextSteps: [
|
|
349
|
-
'get_knowledge(kind="knowledge", "sysoperation") — SysOperation patterns',
|
|
350
|
-
'
|
|
351
|
-
'
|
|
349
|
+
'get_knowledge(kind="knowledge", topic="sysoperation") — SysOperation patterns',
|
|
350
|
+
'generate_object(mode="pattern", pattern="sysoperation", name="MyProcess") — generate SysOperation skeleton',
|
|
351
|
+
'generate_object(mode="pattern", pattern="batch-job", name="MyBatch") — generate RunBaseBatch skeleton',
|
|
352
352
|
],
|
|
353
353
|
antiPatterns: [
|
|
354
354
|
{ wrong: 'Thread.Sleep / while-polling in batch', why: 'Use batch recurrence and alerts — polling wastes AOS resources' },
|
|
@@ -24,8 +24,15 @@
|
|
|
24
24
|
*/
|
|
25
25
|
export declare function modelWritesLandIn(anchorModel: string | null, activeModel: string | null): string | null;
|
|
26
26
|
export interface PrefixDiagnostics {
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Compact default: one `Prefix : …` line, plus a note only when something is
|
|
29
|
+
* actually off (a switch is in effect, or the resolved prefix contradicts the
|
|
30
|
+
* configuration). Every call of get_workspace_info pays for these tokens, so
|
|
31
|
+
* the confirmations that only restate the value stay in `verboseLines`.
|
|
32
|
+
*/
|
|
28
33
|
lines: string[];
|
|
34
|
+
/** Full "## Prefix Configuration" section — diagnostics=true only. */
|
|
35
|
+
verboseLines: string[];
|
|
29
36
|
/** The prefix a write would apply — for the Extension Naming samples. */
|
|
30
37
|
effectivePrefix: string;
|
|
31
38
|
}
|
|
@@ -67,23 +67,39 @@ export function buildPrefixDiagnostics(writeModel, readModel) {
|
|
|
67
67
|
const disagrees = !!learned?.regular && !!extensionPrefixEnv &&
|
|
68
68
|
bare(learned.regular) !== bare(extensionPrefixEnv);
|
|
69
69
|
const switched = !!writeModel && !!readModel && !sameModel(writeModel, readModel);
|
|
70
|
-
const
|
|
70
|
+
const switchNote = `ℹ️ This is the prefix for WRITES, which are anchored to "${writeModel}". "${readModel}" is ` +
|
|
71
|
+
`merely the active project and its own prefix may differ — see the project-switch note below.`;
|
|
72
|
+
const disagreeNote = `⚠️ The model's own objects use "${learned?.regular}", which overrides EXTENSION_PREFIX="${extensionPrefixEnv}" — new objects will be named "${effectivePrefix}…". If that is wrong, the model's existing objects are the thing to check; set EXTENSION_PREFIX_SOURCE=config to pin the configured value instead.`;
|
|
73
|
+
const notConfiguredNote = `⚠️ EXTENSION_PREFIX is not set in the server environment. The model name "${writeModel}" will be used as prefix. Add EXTENSION_PREFIX=MY (or your ISV prefix) to the .env file and restart the server.`;
|
|
74
|
+
// Compact: the value, its origin, and a warning only when there is one. The
|
|
75
|
+
// warnings keep the fix but drop the explanation of it — the resolved prefix
|
|
76
|
+
// is one line above, so what is wrong is already visible.
|
|
77
|
+
const lines = [`Prefix : ${effectivePrefix || '(none)'} (${source})`];
|
|
78
|
+
if (switched)
|
|
79
|
+
lines.push(switchNote);
|
|
80
|
+
if (disagrees) {
|
|
81
|
+
lines.push(`⚠️ The model's own objects use "${learned?.regular}", overriding EXTENSION_PREFIX="${extensionPrefixEnv}". ` +
|
|
82
|
+
`To pin the configured value instead: EXTENSION_PREFIX_SOURCE=config.`);
|
|
83
|
+
}
|
|
84
|
+
else if (!learned?.regular && !extensionPrefixEnv) {
|
|
85
|
+
lines.push(`⚠️ EXTENSION_PREFIX is not set — the model name is being used as the prefix. ` +
|
|
86
|
+
`Add EXTENSION_PREFIX=MY (your ISV prefix) to .env and restart the server.`);
|
|
87
|
+
}
|
|
88
|
+
const verboseLines = [
|
|
71
89
|
`## Prefix Configuration`,
|
|
72
90
|
``,
|
|
73
91
|
`EXTENSION_PREFIX: ${extensionPrefixEnv ?? '(not set — falling back to model name)'}`,
|
|
74
92
|
`Effective prefix: ${effectivePrefix || '(none)'} (source: ${source})`,
|
|
75
93
|
];
|
|
76
|
-
if (switched)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
lines.push(disagrees
|
|
81
|
-
? `⚠️ The model's own objects use "${learned.regular}", which overrides EXTENSION_PREFIX="${extensionPrefixEnv}" — new objects will be named "${effectivePrefix}…". If that is wrong, the model's existing objects are the thing to check; set EXTENSION_PREFIX_SOURCE=config to pin the configured value instead.`
|
|
94
|
+
if (switched)
|
|
95
|
+
verboseLines.push(switchNote);
|
|
96
|
+
verboseLines.push(disagrees
|
|
97
|
+
? disagreeNote
|
|
82
98
|
: learned?.regular
|
|
83
99
|
? `✅ Prefix "${effectivePrefix}" comes from the objects model "${writeModel}" already contains.`
|
|
84
100
|
: extensionPrefixEnv
|
|
85
101
|
? `✅ EXTENSION_PREFIX is set — all new objects will use prefix "${effectivePrefix}".`
|
|
86
|
-
:
|
|
87
|
-
return { lines, effectivePrefix };
|
|
102
|
+
: notConfiguredNote, ``);
|
|
103
|
+
return { lines, verboseLines, effectivePrefix };
|
|
88
104
|
}
|
|
89
105
|
//# sourceMappingURL=prefixDiagnostics.js.map
|
package/dist/tools/search.js
CHANGED
|
@@ -237,7 +237,9 @@ async function performHybridSearch(args, context) {
|
|
|
237
237
|
output += `\n• ${tip.tip}${toolHint}`;
|
|
238
238
|
});
|
|
239
239
|
}
|
|
240
|
-
|
|
240
|
+
// No trailing "this search is workspace-aware" note: the 🔹/📦 marker on every
|
|
241
|
+
// row already says which side a hit came from, and search is the most-called
|
|
242
|
+
// tool — a fixed footer here is paid for on every call for nothing actionable.
|
|
241
243
|
return {
|
|
242
244
|
content: [
|
|
243
245
|
{
|
|
@@ -32,9 +32,11 @@ import { prepareTool } from './prepare.js';
|
|
|
32
32
|
import { recordToolStart, startMetricsLogging, recordCallSequence } from '../utils/toolMetrics.js';
|
|
33
33
|
import { DEDUP_EXCLUDED_TOOLS, DEDUP_TTL_MS, dedupKey, getDedupedResult, storeDedupResult, appendNote, getInFlight, registerInFlight, clearInFlight, } from '../utils/callDedup.js';
|
|
34
34
|
import { checkIndexStaleness } from '../utils/indexStaleness.js';
|
|
35
|
-
import { buildContextSnapshot, renderContextSnapshotSection } from '../workspace/contextSnapshot.js';
|
|
35
|
+
import { buildContextSnapshot, renderContextSnapshotSection, renderContextSnapshotCompact, } from '../workspace/contextSnapshot.js';
|
|
36
36
|
import * as nodePath from 'path';
|
|
37
37
|
import { buildProgressMessage } from '../utils/toolProgressMessage.js';
|
|
38
|
+
/** Models named inline by the compact project list before it summarises the rest. */
|
|
39
|
+
const PROJECT_NAMES_SHOWN = 12;
|
|
38
40
|
/**
|
|
39
41
|
* Extract workspace path from GitHub Copilot _meta.
|
|
40
42
|
* HTTP requests must not overwrite the shared runtimeContext (AsyncLocalStorage
|
|
@@ -86,6 +88,10 @@ const TOOL_CAP_SIZES = {
|
|
|
86
88
|
build_d365fo_project: 'uncapped', // compiler errors can appear late in long logs
|
|
87
89
|
security_info: 8000,
|
|
88
90
|
extension_info: 6000,
|
|
91
|
+
// Default output is ~1 KB. The higher cap exists for diagnostics=true, whose
|
|
92
|
+
// whole point is the full dump — truncating that at 5000 hid the stdio
|
|
93
|
+
// handshake section behind the project table.
|
|
94
|
+
get_workspace_info: 20000,
|
|
89
95
|
default: 5000,
|
|
90
96
|
};
|
|
91
97
|
function getCapForTool(toolName) {
|
|
@@ -304,7 +310,7 @@ export function registerToolHandler(server, context) {
|
|
|
304
310
|
// from — after a project switch those are different models (see
|
|
305
311
|
// buildPrefixDiagnostics and ConfigManager.getWriteAnchorModel).
|
|
306
312
|
const writeModel = modelWritesLandIn(configManager.getWriteAnchorModel() ?? modelName, modelName);
|
|
307
|
-
const { lines: prefixLines, effectivePrefix } = buildPrefixDiagnostics(writeModel, modelName);
|
|
313
|
+
const { lines: prefixLines, verboseLines: prefixVerboseLines, effectivePrefix, } = buildPrefixDiagnostics(writeModel, modelName);
|
|
308
314
|
const objectSuffixEnv = process.env.EXTENSION_SUFFIX?.trim() || null;
|
|
309
315
|
const effectiveSuffix = getObjectSuffix();
|
|
310
316
|
const PLACEHOLDER_NAMES = new Set([
|
|
@@ -327,42 +333,40 @@ export function registerToolHandler(server, context) {
|
|
|
327
333
|
const effectiveWriteSource = customPackagesPath ? customPackagesSource : packageSource;
|
|
328
334
|
// MS framework path shown in diagnostics; omitted for single-root traditional setups.
|
|
329
335
|
const msFrameworkPath = frameworkDirectory ?? (!customPackagesPath ? null : packagePath);
|
|
330
|
-
//
|
|
336
|
+
// get_workspace_info is called at the start of every session, so every
|
|
337
|
+
// line here is paid for on a cold context. The default output carries
|
|
338
|
+
// only what changes what the agent does next — identity, where writes
|
|
339
|
+
// land, the naming it must apply, and anything that is actually wrong.
|
|
340
|
+
// Sources, confirmations and the per-project path table are diagnostics.
|
|
331
341
|
const diagnostics = args.diagnostics === true;
|
|
332
|
-
const lines =
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
`
|
|
352
|
-
|
|
353
|
-
`anchored to "${toolSwitch.anchorModel}" — the model the open workspace targets — and ` +
|
|
354
|
-
`a create/modify into "${toolSwitch.forcedModel}" will be refused.`, `Tell the user the model they asked about is owned by "${toolSwitch.forcedModel}" and let ` +
|
|
355
|
-
`THEM decide: extend it from "${toolSwitch.anchorModel}", or allow the write by adding ` +
|
|
356
|
-
`D365FO_CROSS_MODEL_WRITE_MODELS=${toolSwitch.forcedModel} to the server's .env — that ` +
|
|
357
|
-
`applies to the next attempt, no restart. Do not decide this on your own.`, ``);
|
|
358
|
-
}
|
|
342
|
+
const lines = diagnostics
|
|
343
|
+
? [
|
|
344
|
+
`## D365FO Workspace Configuration`,
|
|
345
|
+
``,
|
|
346
|
+
`Model name : ${modelName ?? '(not configured)'} (source: ${modelSource})`,
|
|
347
|
+
`Custom write path: ${effectiveWritePath ?? '(not configured)'} (custom metadata, source: ${effectiveWriteSource})`,
|
|
348
|
+
`Framework dir : ${msFrameworkPath ?? '(not applicable — single-root setup)'} (Microsoft metadata, read-only)`,
|
|
349
|
+
`Project path : ${projectPath ?? '(not detected)'} (source: ${projectSource})`,
|
|
350
|
+
`Env type : ${envType}`,
|
|
351
|
+
``,
|
|
352
|
+
...prefixVerboseLines,
|
|
353
|
+
]
|
|
354
|
+
: [
|
|
355
|
+
`## D365FO Workspace`,
|
|
356
|
+
``,
|
|
357
|
+
`Model : ${modelName ?? '(not configured)'} (${modelSource})`,
|
|
358
|
+
...prefixLines,
|
|
359
|
+
`Write path : ${effectiveWritePath ?? '(not configured)'}`,
|
|
360
|
+
`Project : ${projectPath ?? '(not detected)'}`,
|
|
361
|
+
`Env : ${envType}`,
|
|
362
|
+
];
|
|
359
363
|
if (diagnostics) {
|
|
360
364
|
lines.push(`## Suffix Configuration`, ``, `EXTENSION_SUFFIX: ${objectSuffixEnv ?? '(not set)'}`, `Effective suffix: ${effectiveSuffix || '(none)'}`, effectiveSuffix
|
|
361
365
|
? `✅ EXTENSION_SUFFIX is set — new objects will have suffix "${effectiveSuffix}" appended (e.g. MyTable${effectiveSuffix}).`
|
|
362
366
|
: `ℹ️ EXTENSION_SUFFIX is not set. No suffix will be applied. This is normal — most projects use prefixes only.`, ``);
|
|
363
367
|
}
|
|
364
368
|
else if (effectiveSuffix) {
|
|
365
|
-
lines.push(`Suffix
|
|
369
|
+
lines.push(`Suffix : "${effectiveSuffix}" appended to new objects (EXTENSION_SUFFIX)`);
|
|
366
370
|
}
|
|
367
371
|
// Extension naming: prefix is the infix unless EXTENSION_NAMING_STYLE="model-name"
|
|
368
372
|
// (embeds the model name instead, VS default). Tool always normalises the token —
|
|
@@ -379,40 +383,48 @@ export function registerToolHandler(server, context) {
|
|
|
379
383
|
const sampleElemExt = extNamingStyle === 'model-name' && writeModel
|
|
380
384
|
? `CustTable.${writeModel}`
|
|
381
385
|
: `CustTable.${extInfix}Extension`;
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const rawDetectedModel = await configManager.getRawAutoDetectedModelName();
|
|
387
|
-
const detectedHint = rawDetectedModel
|
|
388
|
-
? `> ✅ Auto-detected from .rnrproj: **${rawDetectedModel}**\n` +
|
|
389
|
-
`> Update your .mcp.json: set \`modelName\` to \`"${rawDetectedModel}"\``
|
|
390
|
-
: `> ⚠️ No .rnrproj was found — make sure the MCP server is running in the right directory.`;
|
|
391
|
-
lines.push(`⛔ CONFIGURATION PROBLEM — model name "${modelName}" is a placeholder, not a real D365FO model.`, ``, `**YOU MUST STOP** and tell the user:`, `> The configured model name "${modelName}" is a placeholder.`, detectedHint, `>`, `> Please check that:`, `> 1. The MCP server is running in the correct workspace directory`, `> 2. The .mcp.json / mcp.json file has the correct modelName`, `> 3. The projectPath points to a valid .rnrproj file`, `>`, `> Do you want to fix the configuration first, or continue with built-in tools (limited functionality)?`);
|
|
392
|
-
}
|
|
393
|
-
else if (isStandardMsModel) {
|
|
394
|
-
const allProj = configManager.getAllDetectedProjects();
|
|
395
|
-
const customCandidates = allProj.filter(p => isCustomModel(p.modelName));
|
|
396
|
-
const hint = customCandidates.length > 0
|
|
397
|
-
? `Available custom models: ${customCandidates.map(p => p.modelName).join(', ')}\n` +
|
|
398
|
-
`Switch with: get_workspace_info(projectName="<model>")`
|
|
399
|
-
: `No custom models found under D365FO_SOLUTIONS_PATH. Check your project configuration.`;
|
|
400
|
-
lines.push(`⛔ CONFIGURATION PROBLEM — model name "${modelName}" is a Microsoft standard/demo model, not a custom model.`, ``, `**YOU MUST STOP** and tell the user:`, `> The auto-detected model "${modelName}" is a Microsoft standard model.`, `> This usually happens when a new VS project was created and the default model`, `> in the project wizard ("FleetManagement") was not changed to the correct custom model.`, `>`, `> How to fix:`, `> 1. In Visual Studio, open the .rnrproj file and change <Model>FleetManagement</Model>`, `> to the correct model name (e.g. <Model>ContosoCore</Model>).`, `> 2. OR explicitly switch to a known project:`, `> ${hint}`, `> 3. OR add the correct modelName to .mcp.json.`);
|
|
386
|
+
if (diagnostics) {
|
|
387
|
+
lines.push(`## Extension Naming`, ``, `EXTENSION_NAMING_STYLE: ${process.env.EXTENSION_NAMING_STYLE?.trim() || '(not set → "prefix")'}`, extNamingStyle === 'model-name'
|
|
388
|
+
? `✅ model-name style — extension token is the MODEL NAME (Visual Studio default).`
|
|
389
|
+
: `ℹ️ prefix style (default) — extension token is the EXTENSION_PREFIX infix.`, ` • Extension class → ${sampleClassExt}`, ` • Element extension → ${sampleElemExt}`, ` ⚠️ Pass the BASE object name (e.g. "CustTable") to d365fo_file(action="create") and let the tool apply the token — any infix you embed will be normalised to the above.`, ``);
|
|
401
390
|
}
|
|
402
391
|
else {
|
|
403
|
-
|
|
392
|
+
// The samples ARE the instruction: the style name and the token rule
|
|
393
|
+
// add nothing once the two produced names are in front of the agent.
|
|
394
|
+
lines.push(`Extensions : ${sampleClassExt} · ${sampleElemExt} ` +
|
|
395
|
+
`(pass the BASE name to d365fo_file(create) — the tool applies the token)`);
|
|
404
396
|
}
|
|
405
397
|
const allProjects = configManager.getAllDetectedProjects();
|
|
406
398
|
if (allProjects.length > 1) {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const
|
|
412
|
-
|
|
399
|
+
if (diagnostics) {
|
|
400
|
+
lines.push(``);
|
|
401
|
+
lines.push(`## Available Projects`);
|
|
402
|
+
lines.push(``);
|
|
403
|
+
for (const p of allProjects) {
|
|
404
|
+
const active = p.projectPath === projectPath ? '▶ ' : ' ';
|
|
405
|
+
lines.push(`${active}${p.modelName.padEnd(40)} ${p.projectPath}`);
|
|
406
|
+
}
|
|
407
|
+
lines.push(``);
|
|
408
|
+
lines.push(`To switch project: call get_workspace_info with projectName = "<ModelName>"`);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
// Names only: a switch is by projectName, so the paths are dead
|
|
412
|
+
// weight — and one solution can hold dozens of them. Duplicated
|
|
413
|
+
// model names (several projects, one model) collapse to one entry.
|
|
414
|
+
const seen = new Set();
|
|
415
|
+
const names = [];
|
|
416
|
+
for (const p of allProjects) {
|
|
417
|
+
const key = p.modelName.toLowerCase();
|
|
418
|
+
if (seen.has(key))
|
|
419
|
+
continue;
|
|
420
|
+
seen.add(key);
|
|
421
|
+
names.push(p.projectPath === projectPath ? `▶${p.modelName}` : p.modelName);
|
|
422
|
+
}
|
|
423
|
+
const shown = names.slice(0, PROJECT_NAMES_SHOWN);
|
|
424
|
+
const rest = names.length - shown.length;
|
|
425
|
+
lines.push(`Projects : ${shown.join(', ')}${rest > 0 ? `, +${rest} more` : ''} ` +
|
|
426
|
+
`(switch: get_workspace_info(projectName="<ModelName>"))`);
|
|
413
427
|
}
|
|
414
|
-
lines.push(``);
|
|
415
|
-
lines.push(`To switch project: call get_workspace_info with projectName = "<ModelName>"`);
|
|
416
428
|
}
|
|
417
429
|
// Index freshness — compare workspace mtimes vs last_indexed_at.
|
|
418
430
|
try {
|
|
@@ -421,7 +433,7 @@ export function registerToolHandler(server, context) {
|
|
|
421
433
|
? nodePath.join(effectiveWritePath, modelName)
|
|
422
434
|
: null;
|
|
423
435
|
const staleness = checkIndexStaleness(lastIndexedAt, modelMetadataDir);
|
|
424
|
-
lines.push('', ...staleness.lines);
|
|
436
|
+
lines.push(...(diagnostics ? ['', ...staleness.lines] : staleness.compactLines));
|
|
425
437
|
}
|
|
426
438
|
catch {
|
|
427
439
|
// Freshness reporting is best-effort — never break get_workspace_info
|
|
@@ -471,11 +483,51 @@ export function registerToolHandler(server, context) {
|
|
|
471
483
|
// Context Snapshot — recently edited objects + uncommitted X++ changes. Best-effort.
|
|
472
484
|
try {
|
|
473
485
|
const snapshot = await buildContextSnapshot(context);
|
|
474
|
-
lines.push(
|
|
486
|
+
lines.push(...(diagnostics
|
|
487
|
+
? ['', ...renderContextSnapshotSection(snapshot)]
|
|
488
|
+
: renderContextSnapshotCompact(snapshot)));
|
|
475
489
|
}
|
|
476
490
|
catch {
|
|
477
491
|
// Snapshot is additive — omit silently on failure.
|
|
478
492
|
}
|
|
493
|
+
// Everything below is a broken/unusual state. It sits last so the facts
|
|
494
|
+
// above stay one uninterrupted block in the normal case, and so the
|
|
495
|
+
// instruction the agent must act on is the last thing it reads.
|
|
496
|
+
// A switch moves which project is ACTIVE — nothing else. Reads never
|
|
497
|
+
// needed it (they span every model regardless) and writes stay anchored
|
|
498
|
+
// to the model the workspace resolved on its own, so switching cannot be
|
|
499
|
+
// used to reach an object the cross-model guard refused. Say both here,
|
|
500
|
+
// before anything is written, so the switch is not mistaken for access.
|
|
501
|
+
const toolSwitch = configManager.getToolProjectSwitch();
|
|
502
|
+
if (toolSwitch) {
|
|
503
|
+
lines.push(``, `## ⚠️ Project switched — writes are NOT switched`, ``, `"${toolSwitch.forcedModel}" is now the ACTIVE project. This did not change what you ` +
|
|
504
|
+
`can read: get_object_info, search and find_references span every model, switched or ` +
|
|
505
|
+
`not, so a switch is never needed to look at another model's code. Writes stay ` +
|
|
506
|
+
`anchored to "${toolSwitch.anchorModel}" — the model the open workspace targets — and ` +
|
|
507
|
+
`a create/modify into "${toolSwitch.forcedModel}" will be refused.`, `Tell the user the model they asked about is owned by "${toolSwitch.forcedModel}" and let ` +
|
|
508
|
+
`THEM decide: extend it from "${toolSwitch.anchorModel}", or allow the write by adding ` +
|
|
509
|
+
`D365FO_CROSS_MODEL_WRITE_MODELS=${toolSwitch.forcedModel} to the server's .env — that ` +
|
|
510
|
+
`applies to the next attempt, no restart. Do not decide this on your own.`);
|
|
511
|
+
}
|
|
512
|
+
if (isPlaceholder) {
|
|
513
|
+
const rawDetectedModel = await configManager.getRawAutoDetectedModelName();
|
|
514
|
+
const detectedHint = rawDetectedModel
|
|
515
|
+
? `> ✅ Auto-detected from .rnrproj: **${rawDetectedModel}**\n` +
|
|
516
|
+
`> Update your .mcp.json: set \`modelName\` to \`"${rawDetectedModel}"\``
|
|
517
|
+
: `> ⚠️ No .rnrproj was found — make sure the MCP server is running in the right directory.`;
|
|
518
|
+
lines.push(``, `⛔ CONFIGURATION PROBLEM — model name "${modelName}" is a placeholder, not a real D365FO model.`, ``, `**YOU MUST STOP** and tell the user:`, `> The configured model name "${modelName}" is a placeholder.`, detectedHint, `>`, `> Please check that:`, `> 1. The MCP server is running in the correct workspace directory`, `> 2. The .mcp.json / mcp.json file has the correct modelName`, `> 3. The projectPath points to a valid .rnrproj file`, `>`, `> Do you want to fix the configuration first, or continue with built-in tools (limited functionality)?`);
|
|
519
|
+
}
|
|
520
|
+
else if (isStandardMsModel) {
|
|
521
|
+
const customCandidates = allProjects.filter(p => isCustomModel(p.modelName));
|
|
522
|
+
const hint = customCandidates.length > 0
|
|
523
|
+
? `Available custom models: ${customCandidates.map(p => p.modelName).join(', ')}\n` +
|
|
524
|
+
`Switch with: get_workspace_info(projectName="<model>")`
|
|
525
|
+
: `No custom models found under D365FO_SOLUTIONS_PATH. Check your project configuration.`;
|
|
526
|
+
lines.push(``, `⛔ CONFIGURATION PROBLEM — model name "${modelName}" is a Microsoft standard/demo model, not a custom model.`, ``, `**YOU MUST STOP** and tell the user:`, `> The auto-detected model "${modelName}" is a Microsoft standard model.`, `> This usually happens when a new VS project was created and the default model`, `> in the project wizard ("FleetManagement") was not changed to the correct custom model.`, `>`, `> How to fix:`, `> 1. In Visual Studio, open the .rnrproj file and change <Model>FleetManagement</Model>`, `> to the correct model name (e.g. <Model>ContosoCore</Model>).`, `> 2. OR explicitly switch to a known project:`, `> ${hint}`, `> 3. OR add the correct modelName to .mcp.json.`);
|
|
527
|
+
}
|
|
528
|
+
else if (diagnostics) {
|
|
529
|
+
lines.push(``, `✅ Configuration looks valid. Proceed with D365FO operations using model "${modelName}".`);
|
|
530
|
+
}
|
|
479
531
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
480
532
|
}
|
|
481
533
|
default:
|
|
@@ -26,7 +26,7 @@ export interface AddControlPatternVerdict {
|
|
|
26
26
|
allowedTypes: string[] | 'any';
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* Pre-flight for
|
|
29
|
+
* Pre-flight for d365fo_file(action="modify", operation="add-control"): when the target parent
|
|
30
30
|
* container declares a sub-pattern, check the new control's type against the
|
|
31
31
|
* children that sub-pattern allows. Returns null when the parent cannot be
|
|
32
32
|
* found, declares no pattern, or the pattern is unknown — those cases never
|
|
@@ -115,7 +115,7 @@ export function isFormPatternEnforceEnabled() {
|
|
|
115
115
|
return v !== 'false' && v !== '0' && v !== 'off';
|
|
116
116
|
}
|
|
117
117
|
/**
|
|
118
|
-
* Pre-flight for
|
|
118
|
+
* Pre-flight for d365fo_file(action="modify", operation="add-control"): when the target parent
|
|
119
119
|
* container declares a sub-pattern, check the new control's type against the
|
|
120
120
|
* children that sub-pattern allows. Returns null when the parent cannot be
|
|
121
121
|
* found, declares no pattern, or the pattern is unknown — those cases never
|
|
@@ -20,7 +20,14 @@ export interface MtimeScanResult {
|
|
|
20
20
|
export declare function findNewestMetadataMtime(rootDir: string): MtimeScanResult | null;
|
|
21
21
|
export interface StalenessReport {
|
|
22
22
|
status: 'fresh' | 'stale' | 'unknown';
|
|
23
|
+
/** Full "## Index Freshness" section — diagnostics=true only. */
|
|
23
24
|
lines: string[];
|
|
25
|
+
/**
|
|
26
|
+
* Compact default: one `Index : …` line, and the fix only when the index is
|
|
27
|
+
* actually stale. The scan detail (newest file, files scanned) is diagnostics
|
|
28
|
+
* material — it changes nothing about what the agent should do next.
|
|
29
|
+
*/
|
|
30
|
+
compactLines: string[];
|
|
24
31
|
}
|
|
25
32
|
/**
|
|
26
33
|
* Compare workspace mtimes against the index timestamp and render a report
|
|
@@ -78,27 +78,50 @@ export function checkIndexStaleness(lastIndexedAt, modelMetadataDir) {
|
|
|
78
78
|
const lines = ['## Index Freshness', ''];
|
|
79
79
|
if (!lastIndexedAt) {
|
|
80
80
|
lines.push('ℹ️ Index has no freshness timestamp yet (built before this feature or never built).', ' It will be recorded on the next build-database run or update_symbol_index call.');
|
|
81
|
-
return {
|
|
81
|
+
return {
|
|
82
|
+
status: 'unknown',
|
|
83
|
+
lines,
|
|
84
|
+
compactLines: ['Index : no freshness timestamp yet (never indexed?)'],
|
|
85
|
+
};
|
|
82
86
|
}
|
|
83
87
|
const indexedAtMs = Date.parse(lastIndexedAt);
|
|
84
88
|
const ageHours = Math.round((Date.now() - indexedAtMs) / 3_600_000);
|
|
85
89
|
lines.push(`Last indexed : ${lastIndexedAt} (${ageHours} h ago)`);
|
|
86
90
|
if (!modelMetadataDir) {
|
|
87
91
|
lines.push('ℹ️ Model metadata folder not resolved — cannot compare workspace mtimes.');
|
|
88
|
-
return {
|
|
92
|
+
return {
|
|
93
|
+
status: 'unknown',
|
|
94
|
+
lines,
|
|
95
|
+
compactLines: [`Index : indexed ${ageHours} h ago (model folder not resolved — not compared)`],
|
|
96
|
+
};
|
|
89
97
|
}
|
|
90
98
|
const scan = findNewestMetadataMtime(modelMetadataDir);
|
|
91
99
|
if (!scan) {
|
|
92
100
|
lines.push(`ℹ️ No metadata files found under ${modelMetadataDir} — nothing to compare.`);
|
|
93
|
-
return {
|
|
101
|
+
return {
|
|
102
|
+
status: 'unknown',
|
|
103
|
+
lines,
|
|
104
|
+
compactLines: [`Index : indexed ${ageHours} h ago (no metadata files to compare)`],
|
|
105
|
+
};
|
|
94
106
|
}
|
|
95
107
|
lines.push(`Newest file : ${path.basename(scan.newestFile)} (${new Date(scan.newestMtime).toISOString()})` +
|
|
96
108
|
(scan.truncated ? ` — scanned first ${MAX_SCANNED_FILES} files` : ` — ${scan.scannedFiles} files scanned`));
|
|
97
109
|
if (scan.newestMtime > indexedAtMs + TOLERANCE_MS) {
|
|
98
110
|
lines.push('', '⚠️ **INDEX IS STALE** — the workspace contains files newer than the last index update.', ` Newest change: ${scan.newestFile}`, ' Symbol lookups may return outdated signatures/fields for recently edited objects.', ` Fix: call \`update_symbol_index(filePath="${scan.newestFile.replace(/\\/g, '\\\\')}")\` for the changed file(s),`, ' or run `npm run build-database` (EXTRACT_MODE=custom) for a full custom-model refresh.');
|
|
99
|
-
return {
|
|
111
|
+
return {
|
|
112
|
+
status: 'stale',
|
|
113
|
+
lines,
|
|
114
|
+
compactLines: [
|
|
115
|
+
`Index : ⚠️ STALE — indexed ${ageHours} h ago, workspace has newer files (lookups may be outdated)`,
|
|
116
|
+
` Fix: update_symbol_index(filePath="${scan.newestFile.replace(/\\/g, '\\\\')}")`,
|
|
117
|
+
],
|
|
118
|
+
};
|
|
100
119
|
}
|
|
101
120
|
lines.push('✅ Index is up to date with the workspace.');
|
|
102
|
-
return {
|
|
121
|
+
return {
|
|
122
|
+
status: 'fresh',
|
|
123
|
+
lines,
|
|
124
|
+
compactLines: [`Index : up to date (indexed ${ageHours} h ago)`],
|
|
125
|
+
};
|
|
103
126
|
}
|
|
104
127
|
//# sourceMappingURL=indexStaleness.js.map
|
|
@@ -60,9 +60,11 @@ export interface ContextSnapshot {
|
|
|
60
60
|
*/
|
|
61
61
|
export declare function buildContextSnapshot(context: XppServerContext): Promise<ContextSnapshot>;
|
|
62
62
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
63
|
+
* The same "live" portion as renderContextSnapshotSection, folded into at most
|
|
64
|
+
* two lines for get_workspace_info's default output. Names only enough recent
|
|
65
|
+
* objects to orient the agent — the full list, with timestamps and every
|
|
66
|
+
* uncommitted path, stays behind diagnostics=true and review_workspace_changes.
|
|
66
67
|
*/
|
|
68
|
+
export declare function renderContextSnapshotCompact(snapshot: ContextSnapshot): string[];
|
|
67
69
|
export declare function renderContextSnapshotSection(snapshot: ContextSnapshot): string[];
|
|
68
70
|
//# sourceMappingURL=contextSnapshot.d.ts.map
|
|
@@ -160,6 +160,28 @@ export async function buildContextSnapshot(context) {
|
|
|
160
160
|
* as markdown lines for embedding in get_workspace_info. Identity/prefix/index
|
|
161
161
|
* sections are already covered by that tool, so this only adds what is new.
|
|
162
162
|
*/
|
|
163
|
+
/** How many recent objects the compact rendering names before summarising. */
|
|
164
|
+
const COMPACT_RECENT_SHOWN = 3;
|
|
165
|
+
/**
|
|
166
|
+
* The same "live" portion as renderContextSnapshotSection, folded into at most
|
|
167
|
+
* two lines for get_workspace_info's default output. Names only enough recent
|
|
168
|
+
* objects to orient the agent — the full list, with timestamps and every
|
|
169
|
+
* uncommitted path, stays behind diagnostics=true and review_workspace_changes.
|
|
170
|
+
*/
|
|
171
|
+
export function renderContextSnapshotCompact(snapshot) {
|
|
172
|
+
const lines = [];
|
|
173
|
+
if (snapshot.recentObjects.length > 0) {
|
|
174
|
+
const shown = snapshot.recentObjects
|
|
175
|
+
.slice(0, COMPACT_RECENT_SHOWN)
|
|
176
|
+
.map(o => `${o.name} [${o.type}]`);
|
|
177
|
+
const rest = snapshot.recentObjects.length - shown.length;
|
|
178
|
+
lines.push(`Recent edits: ${shown.join(', ')}${rest > 0 ? ` (+${rest} more)` : ''}`);
|
|
179
|
+
}
|
|
180
|
+
if (snapshot.uncommittedFiles.length > 0) {
|
|
181
|
+
lines.push(`Uncommitted : ${snapshot.uncommittedFiles.length} X++ file(s) — review_workspace_changes`);
|
|
182
|
+
}
|
|
183
|
+
return lines;
|
|
184
|
+
}
|
|
163
185
|
export function renderContextSnapshotSection(snapshot) {
|
|
164
186
|
const lines = ['## Context Snapshot', ''];
|
|
165
187
|
if (snapshot.activeObject) {
|