d365fo-mcp 1.12.0 → 1.13.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 +36 -1
- package/dist/server/serverMode.d.ts +2 -2
- package/dist/server/serverMode.js +2 -2
- package/dist/server/toolAnnotations.js +1 -1
- package/dist/server/toolSchemas/d365foFile.js +11 -7
- package/dist/tools/d365foFile.d.ts +3 -2
- package/dist/tools/d365foFile.js +10 -4
- package/dist/tools/knowledge/bpMonikerHelp.js +7 -1
- package/dist/tools/specs/d365foFileOpSpecs.js +169 -1
- package/dist/tools/specs/opSpecs.js +39 -0
- package/dist/tools/write/deleteD365File.d.ts +49 -0
- package/dist/tools/write/deleteD365File.js +357 -0
- package/dist/tools/write/modifyD365File.js +765 -14
- package/dist/tools/xml/securityPrivilegeXml.d.ts +54 -0
- package/dist/tools/xml/securityPrivilegeXml.js +86 -0
- package/dist/utils/crossModelWriteGuard.d.ts +8 -16
- package/dist/utils/crossModelWriteGuard.js +18 -2
- package/dist/utils/formControlRemoval.d.ts +70 -0
- package/dist/utils/formControlRemoval.js +216 -0
- package/dist/utils/formExtensionControlXml.d.ts +12 -0
- package/dist/utils/formExtensionControlXml.js +14 -128
- package/dist/utils/ignoreDiagnosticListXml.d.ts +186 -0
- package/dist/utils/ignoreDiagnosticListXml.js +267 -0
- package/dist/utils/objectFileLookup.d.ts +19 -0
- package/dist/utils/objectFileLookup.js +85 -29
- package/dist/utils/xmlNodeTree.d.ts +54 -0
- package/dist/utils/xmlNodeTree.js +144 -0
- package/dist/workspace/projectFile.js +57 -14
- package/dist/workspace/projectMembership.d.ts +17 -3
- package/dist/workspace/projectMembership.js +22 -7
- package/package.json +1 -1
|
@@ -1140,8 +1140,20 @@ const BRIDGE_MODIFY_OPS = new Set([
|
|
|
1140
1140
|
'modify-property',
|
|
1141
1141
|
'add-enum-value', 'modify-enum-value', 'remove-enum-value',
|
|
1142
1142
|
'add-control', 'add-data-source',
|
|
1143
|
+
// Removal of a control, and of a privilege's entry point, have no C# op either
|
|
1144
|
+
// (MetadataWriteService exposes no RemoveControl, and security objects have no
|
|
1145
|
+
// bridge write path at all) — both are served by a direct-XML writer and still
|
|
1146
|
+
// pass through this gate.
|
|
1147
|
+
'remove-control', 'remove-entry-point',
|
|
1148
|
+
// AxIgnoreDiagnosticList is not an AOT object at all — MetadataWriteService has
|
|
1149
|
+
// no concept of it — so this is XML-only for the same structural reason as the
|
|
1150
|
+
// two above.
|
|
1151
|
+
'remove-diagnostic-suppression', 'add-diagnostic-suppression',
|
|
1143
1152
|
'add-display-method', 'add-table-method',
|
|
1144
1153
|
'add-field-modification', 'add-menu-item-to-menu',
|
|
1154
|
+
// No C# op exists for query ranges on entities — served entirely by a
|
|
1155
|
+
// direct-XML writer (data-entity is already in BRIDGE_MODIFY_TYPES).
|
|
1156
|
+
'add-query-range', 'remove-query-range',
|
|
1145
1157
|
]);
|
|
1146
1158
|
/**
|
|
1147
1159
|
* Supported object types for bridge-based modification.
|
|
@@ -1156,6 +1168,25 @@ const BRIDGE_MODIFY_TYPES = new Set([
|
|
|
1156
1168
|
'menu-item-action', 'menu-item-display', 'menu-item-output',
|
|
1157
1169
|
'menu',
|
|
1158
1170
|
]);
|
|
1171
|
+
/**
|
|
1172
|
+
* Operation → the object types it may target that BRIDGE_MODIFY_TYPES does not
|
|
1173
|
+
* cover, because a direct-XML writer serves the pair and the bridge never will.
|
|
1174
|
+
*
|
|
1175
|
+
* This gate is what EVERY modify operation clears before dispatch, whether or not
|
|
1176
|
+
* a C# op backs it, so an XML-only writer for an XML-only type has nowhere else to
|
|
1177
|
+
* be admitted. The pairing is per-operation rather than per-type on purpose:
|
|
1178
|
+
* dropping 'security-privilege' into BRIDGE_MODIFY_TYPES would also claim
|
|
1179
|
+
* add-method, replace-code and modify-property work on a privilege, which they do
|
|
1180
|
+
* not — the bridge has no write path for security objects at all (the same reason
|
|
1181
|
+
* they are absent from BRIDGE_CREATE_TYPES: a generic
|
|
1182
|
+
* Dictionary<string,string> cannot carry <EntryPoints>). The caller would then get
|
|
1183
|
+
* a bridge resolution failure instead of "not supported for this object type".
|
|
1184
|
+
*/
|
|
1185
|
+
const XML_ONLY_MODIFY_PAIRS = {
|
|
1186
|
+
'remove-entry-point': new Set(['security-privilege']),
|
|
1187
|
+
'remove-diagnostic-suppression': new Set(['ignore-diagnostic-list']),
|
|
1188
|
+
'add-diagnostic-suppression': new Set(['ignore-diagnostic-list']),
|
|
1189
|
+
};
|
|
1159
1190
|
/**
|
|
1160
1191
|
* Names the properties the bridge could not write, for appending to a success message.
|
|
1161
1192
|
*
|
|
@@ -1180,7 +1211,11 @@ export function canBridgeCreate(objectType) {
|
|
|
1180
1211
|
* Checks if bridge can handle this modify operation.
|
|
1181
1212
|
*/
|
|
1182
1213
|
export function canBridgeModify(objectType, operation) {
|
|
1183
|
-
|
|
1214
|
+
const type = objectType.toLowerCase();
|
|
1215
|
+
const op = operation.toLowerCase();
|
|
1216
|
+
if (!BRIDGE_MODIFY_OPS.has(op))
|
|
1217
|
+
return false;
|
|
1218
|
+
return BRIDGE_MODIFY_TYPES.has(type) || (XML_ONLY_MODIFY_PAIRS[op]?.has(type) ?? false);
|
|
1184
1219
|
}
|
|
1185
1220
|
/**
|
|
1186
1221
|
* Give a create's X++ the same doc comments and indentation the XML fallback gives it.
|
|
@@ -36,8 +36,8 @@ export declare const LOCAL_TOOLS: Set<string>;
|
|
|
36
36
|
* SQLite-backed types (Azure read-only).
|
|
37
37
|
* - labels: read actions (search/info) work on Azure; write actions
|
|
38
38
|
* (create/rename) need K:\ and error clearly when unreachable.
|
|
39
|
-
* - d365fo_file: generate works on Azure; create/modify need K:\ and
|
|
40
|
-
* clearly when unreachable.
|
|
39
|
+
* - d365fo_file: generate works on Azure; create/modify/delete need K:\ and
|
|
40
|
+
* error clearly when unreachable.
|
|
41
41
|
*/
|
|
42
42
|
export declare const ALWAYS_TOOLS: Set<string>;
|
|
43
43
|
/**
|
|
@@ -46,8 +46,8 @@ export const LOCAL_TOOLS = new Set([
|
|
|
46
46
|
* SQLite-backed types (Azure read-only).
|
|
47
47
|
* - labels: read actions (search/info) work on Azure; write actions
|
|
48
48
|
* (create/rename) need K:\ and error clearly when unreachable.
|
|
49
|
-
* - d365fo_file: generate works on Azure; create/modify need K:\ and
|
|
50
|
-
* clearly when unreachable.
|
|
49
|
+
* - d365fo_file: generate works on Azure; create/modify/delete need K:\ and
|
|
50
|
+
* error clearly when unreachable.
|
|
51
51
|
*/
|
|
52
52
|
export const ALWAYS_TOOLS = new Set([
|
|
53
53
|
'get_object_info',
|
|
@@ -56,7 +56,7 @@ export const TOOL_ANNOTATIONS = {
|
|
|
56
56
|
// File & label writes. Marked destructive/write so clients prompt for
|
|
57
57
|
// confirmation even though some actions (generate, search/info) are read-only —
|
|
58
58
|
// annotations are hints, not gates.
|
|
59
|
-
d365fo_file: write('D365FO file (create/modify/generate)', { destructive: true }),
|
|
59
|
+
d365fo_file: write('D365FO file (create/modify/delete/generate)', { destructive: true }),
|
|
60
60
|
labels: write('Label operations', { destructive: true }),
|
|
61
61
|
undo_last_modification: write('Undo last modification', { destructive: true }),
|
|
62
62
|
generate_object: write('Generate code (pattern/scaffold)'),
|
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export const d365foFileTool = {
|
|
16
16
|
name: 'd365fo_file',
|
|
17
|
-
description: `Create, modify, or generate a D365FO AOT object. Choose an \`action\`:
|
|
17
|
+
description: `Create, modify, delete, or generate a D365FO AOT object. Choose an \`action\`:
|
|
18
18
|
• create → write a NEW object file into PackagesLocalDirectory (UTF-8 BOM, auto-added to .rnrproj). THE WRITE STEP — incomplete until isError=false; ⚠️/❌ = failure. Extensions: objectName="Base.PrefixExtension".
|
|
19
19
|
• modify → edit an EXISTING object. APPLIES IMMEDIATELY, no dry-run — confirm with the user first; revert with undo_last_modification. Needs \`operation\`.
|
|
20
|
-
•
|
|
20
|
+
• delete → remove an object's XML from disk AND un-register it from every .rnrproj of the model that lists it. IRREVERSIBLE — confirm with the user first. Reports ❌ when the object is not found, never a silent no-op.
|
|
21
|
+
• generate → XML as TEXT only, no write (Azure/Linux fallback). Try create first. create/modify/delete need Windows.
|
|
21
22
|
📖 Parameters are NOT inlined here: get_knowledge(kind="op-spec", topic="<operation>"|"<objectType>") returns the contract for the one you picked — pass its values nested in \`params\` (modify) / \`properties\` (create), along with any packageName/packagePath/solutionPath/workspacePath override.
|
|
22
23
|
Model + prefix auto-applied. Classes: member vars inside the class { }, methods after the closing }.`,
|
|
23
24
|
inputSchema: {
|
|
@@ -25,8 +26,8 @@ Model + prefix auto-applied. Classes: member vars inside the class { }, methods
|
|
|
25
26
|
properties: {
|
|
26
27
|
action: {
|
|
27
28
|
type: 'string',
|
|
28
|
-
enum: ['create', 'modify', 'generate'],
|
|
29
|
-
description: 'One of the
|
|
29
|
+
enum: ['create', 'modify', 'delete', 'generate'],
|
|
30
|
+
description: 'One of the four modes described above.',
|
|
30
31
|
},
|
|
31
32
|
objectType: {
|
|
32
33
|
type: 'string',
|
|
@@ -38,12 +39,13 @@ Model + prefix auto-applied. Classes: member vars inside the class { }, methods
|
|
|
38
39
|
'menu-item-display', 'menu-item-action', 'menu-item-output', 'menu',
|
|
39
40
|
'security-privilege', 'security-duty', 'security-role',
|
|
40
41
|
'security-duty-extension', 'security-role-extension',
|
|
42
|
+
'ignore-diagnostic-list',
|
|
41
43
|
'business-event', 'tile', 'kpi', 'map',
|
|
42
44
|
'service', 'service-group',
|
|
43
45
|
'macro', 'configuration-key', 'security-policy', 'aggregate-measurement', 'license-code',
|
|
44
46
|
],
|
|
45
47
|
description: 'Each security/menu-item type is its own AOT folder — NEVER use security-privilege for duty or role. ' +
|
|
46
|
-
'[modify]/[generate] cover the core families + their *-extension variants.'
|
|
48
|
+
'[modify]/[generate] cover the core families + their *-extension variants; [delete] takes the same enum as [create].'
|
|
47
49
|
},
|
|
48
50
|
objectName: {
|
|
49
51
|
type: 'string',
|
|
@@ -85,10 +87,12 @@ Model + prefix auto-applied. Classes: member vars inside the class { }, methods
|
|
|
85
87
|
'add-delete-action', 'remove-delete-action',
|
|
86
88
|
'add-field-group', 'remove-field-group', 'add-field-to-field-group',
|
|
87
89
|
'add-field-modification',
|
|
88
|
-
'add-data-source', 'add-control',
|
|
90
|
+
'add-data-source', 'add-control', 'remove-control',
|
|
91
|
+
'remove-entry-point', 'remove-diagnostic-suppression', 'add-diagnostic-suppression',
|
|
89
92
|
'add-enum-value', 'modify-enum-value', 'remove-enum-value',
|
|
90
93
|
'add-menu-item-to-menu',
|
|
91
94
|
'modify-property',
|
|
95
|
+
'add-query-range', 'remove-query-range',
|
|
92
96
|
],
|
|
93
97
|
description: '[modify] REQUIRED unless using operations[]. add-method also UPDATES in place; replace-code is the surgical oldCode→newCode path. ' +
|
|
94
98
|
'Parameters: get_knowledge(kind="op-spec", topic="<operation>").'
|
|
@@ -109,7 +113,7 @@ Model + prefix auto-applied. Classes: member vars inside the class { }, methods
|
|
|
109
113
|
'topic="<operation>"). A missing/wrong one returns that COMPLETE spec — follow it, do not guess.',
|
|
110
114
|
},
|
|
111
115
|
createBackup: { type: 'boolean', description: '[modify] Back up before modifying.', default: false },
|
|
112
|
-
filePath: { type: 'string', description: '[modify] Absolute XML path — bypasses symbol-DB lookup. Use for objects just created.' },
|
|
116
|
+
filePath: { type: 'string', description: '[modify|delete] Absolute XML path — bypasses symbol-DB lookup. Use for objects just created.' },
|
|
113
117
|
},
|
|
114
118
|
required: ['action'],
|
|
115
119
|
},
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* d365fo_file Tool — unified file/metadata-operation entry point.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* One tool discriminated by `action`:
|
|
5
5
|
* • generate → produce AOT XML as TEXT only (Azure/Linux fallback, no write)
|
|
6
6
|
* • create → write a NEW AOT object file into PackagesLocalDirectory (write)
|
|
7
7
|
* • modify → edit an EXISTING object via IMetadataProvider (write)
|
|
8
|
+
* • delete → remove an object's XML and its .rnrproj registration (write)
|
|
8
9
|
*
|
|
9
10
|
* Like `labels`, this mixes a read-capable action (generate works on Azure
|
|
10
11
|
* read-only) with write actions that need local Windows-VM filesystem access;
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
*/
|
|
15
16
|
import type { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
16
17
|
import type { XppServerContext } from '../types/context.js';
|
|
17
|
-
export declare const D365_FILE_ACTIONS: readonly ['generate', 'create', 'modify'];
|
|
18
|
+
export declare const D365_FILE_ACTIONS: readonly ['generate', 'create', 'modify', 'delete'];
|
|
18
19
|
export type D365FileAction = (typeof D365_FILE_ACTIONS)[number];
|
|
19
20
|
export declare function d365foFileTool(request: CallToolRequest, context: XppServerContext): Promise<any>;
|
|
20
21
|
//# sourceMappingURL=d365foFile.d.ts.map
|
package/dist/tools/d365foFile.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* d365fo_file Tool — unified file/metadata-operation entry point.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* One tool discriminated by `action`:
|
|
5
5
|
* • generate → produce AOT XML as TEXT only (Azure/Linux fallback, no write)
|
|
6
6
|
* • create → write a NEW AOT object file into PackagesLocalDirectory (write)
|
|
7
7
|
* • modify → edit an EXISTING object via IMetadataProvider (write)
|
|
8
|
+
* • delete → remove an object's XML and its .rnrproj registration (write)
|
|
8
9
|
*
|
|
9
10
|
* Like `labels`, this mixes a read-capable action (generate works on Azure
|
|
10
11
|
* read-only) with write actions that need local Windows-VM filesystem access;
|
|
@@ -15,13 +16,15 @@
|
|
|
15
16
|
import { z } from 'zod';
|
|
16
17
|
import { handleGenerateD365Xml } from './xml/generateD365Xml.js';
|
|
17
18
|
import { handleCreateD365File } from './write/createD365File.js';
|
|
19
|
+
import { handleDeleteD365File } from './write/deleteD365File.js';
|
|
18
20
|
import { modifyD365FileTool } from './write/modifyD365File.js';
|
|
19
21
|
import { resetRecentPrepares } from './prepare/prepare.js';
|
|
20
|
-
export const D365_FILE_ACTIONS = ['generate', 'create', 'modify'];
|
|
22
|
+
export const D365_FILE_ACTIONS = ['generate', 'create', 'modify', 'delete'];
|
|
21
23
|
const D365FileArgsSchema = z
|
|
22
24
|
.object({
|
|
23
25
|
action: z.enum(D365_FILE_ACTIONS).describe('generate → XML text only (no file written, Azure/Linux fallback); ' +
|
|
24
|
-
'create → write a NEW object file (Windows); modify → edit an EXISTING object (Windows)
|
|
26
|
+
'create → write a NEW object file (Windows); modify → edit an EXISTING object (Windows); ' +
|
|
27
|
+
'delete → remove an object file and its project registration (Windows).'),
|
|
25
28
|
// Operation-specific parameters may arrive nested in `params` (the published
|
|
26
29
|
// schema advertises only this object) — they are flattened before dispatch.
|
|
27
30
|
params: z.record(z.string(), z.unknown()).optional(),
|
|
@@ -163,7 +166,7 @@ export async function d365foFileTool(request, context) {
|
|
|
163
166
|
// A write changes the AOT out from under anything prepare aggregated earlier, so
|
|
164
167
|
// the remembered answers stop being answers. Cleared before the write rather than
|
|
165
168
|
// after: a handler that throws half-way has still touched disk.
|
|
166
|
-
if (action === 'create' || action === 'modify') {
|
|
169
|
+
if (action === 'create' || action === 'modify' || action === 'delete') {
|
|
167
170
|
resetRecentPrepares();
|
|
168
171
|
}
|
|
169
172
|
if (action === 'create') {
|
|
@@ -175,6 +178,9 @@ export async function d365foFileTool(request, context) {
|
|
|
175
178
|
}
|
|
176
179
|
return modifyD365FileTool(subRequest('modify_d365fo_file', rest), context);
|
|
177
180
|
}
|
|
181
|
+
if (action === 'delete') {
|
|
182
|
+
return handleDeleteD365File(subRequest('delete_d365fo_file', rest), context);
|
|
183
|
+
}
|
|
178
184
|
// generate: handler takes the request only (no context).
|
|
179
185
|
return handleGenerateD365Xml(subRequest('generate_d365fo_xml', rest));
|
|
180
186
|
}
|
|
@@ -141,7 +141,13 @@ export async function bpMonikerHelpTool(request) {
|
|
|
141
141
|
return {
|
|
142
142
|
content: [{
|
|
143
143
|
type: 'text',
|
|
144
|
-
text: `${warningText}
|
|
144
|
+
text: `${warningText}This is the <Diagnostic> block. You do not have to place it by hand — ` +
|
|
145
|
+
`d365fo_file(action="modify", objectType="ignore-diagnostic-list", objectName="{Model}_BPSuppressions", ` +
|
|
146
|
+
`operation="add-diagnostic-suppression") builds it from these same arguments and writes it into ` +
|
|
147
|
+
`<Items> for you, creating the suppression file if the model has none yet. Pass the moniker as ` +
|
|
148
|
+
`diagnosticMoniker, the path as diagnosticPath, and so on — get_knowledge(kind="op-spec", ` +
|
|
149
|
+
`topic="add-diagnostic-suppression") has the full contract.\n\n` +
|
|
150
|
+
`To place it manually instead, add it inside <Items> of ` +
|
|
145
151
|
`{Model}/{Model}/AxIgnoreDiagnosticList/{Model}_BPSuppressions.xml:\n\n${built.xml}`,
|
|
146
152
|
}],
|
|
147
153
|
};
|
|
@@ -124,7 +124,9 @@ export const D365FO_FILE_PARAM_SPECS = {
|
|
|
124
124
|
// form controls
|
|
125
125
|
controlName: {
|
|
126
126
|
type: 'string',
|
|
127
|
-
description: '
|
|
127
|
+
description: 'add-control: name of the new form control — MUST match the field name in the table extension so ' +
|
|
128
|
+
'the binding works. remove-control: <Name> of the existing control to delete, at any depth in the ' +
|
|
129
|
+
'design.',
|
|
128
130
|
},
|
|
129
131
|
parentControl: {
|
|
130
132
|
type: 'string',
|
|
@@ -234,6 +236,22 @@ export const D365FO_FILE_PARAM_SPECS = {
|
|
|
234
236
|
description: 'Optional join/link type when joinSource is set: InnerJoin | OuterJoin | ExistJoin | NotExistJoin | ' +
|
|
235
237
|
'Delayed | Active | Passive.',
|
|
236
238
|
},
|
|
239
|
+
// add-query-range
|
|
240
|
+
rangeField: {
|
|
241
|
+
type: 'string',
|
|
242
|
+
description: 'Field name to filter on (e.g. "IsActive"). Becomes <Field> in the range object.',
|
|
243
|
+
},
|
|
244
|
+
rangeName: {
|
|
245
|
+
type: 'string',
|
|
246
|
+
description: 'Name for the range object (<Name>). Defaults to rangeField when omitted. ' +
|
|
247
|
+
'Only ever matched against other ranges of the SAME data source.',
|
|
248
|
+
},
|
|
249
|
+
rangeValue: {
|
|
250
|
+
type: 'string',
|
|
251
|
+
description: 'Filter value the range applies (e.g. "1" for a NoYes field, "Sales" for an enum, ' +
|
|
252
|
+
'"1..99" for an interval). Required: a range with no value filters nothing. ' +
|
|
253
|
+
'For the empty-string filter pass the two characters "" — that is how D365FO stores it.',
|
|
254
|
+
},
|
|
237
255
|
// enum values
|
|
238
256
|
enumValueName: { type: 'string', description: 'Enum value name (e.g. "Approved").' },
|
|
239
257
|
enumValueNewName: {
|
|
@@ -252,6 +270,80 @@ export const D365FO_FILE_PARAM_SPECS = {
|
|
|
252
270
|
type: 'string',
|
|
253
271
|
description: 'ISO country/region codes, comma-separated (e.g. "CZ,SK").',
|
|
254
272
|
},
|
|
273
|
+
removeSeparator: {
|
|
274
|
+
type: 'boolean (default false)',
|
|
275
|
+
description: 'remove-control: also delete the adjacent AxFormButtonSeparatorControl — the sibling after the ' +
|
|
276
|
+
'control, else the one before it. Removing a toolbar button usually orphans its separator, which ' +
|
|
277
|
+
'then shows as a stray divider. Opt-in: a separator between two REMAINING buttons is load-bearing.',
|
|
278
|
+
},
|
|
279
|
+
// security privileges
|
|
280
|
+
entryPointName: {
|
|
281
|
+
type: 'string',
|
|
282
|
+
description: '<Name> of the AxSecurityEntryPointReference to remove — conventionally the menu item name. ' +
|
|
283
|
+
'This is the entry point ON the privilege, not the privilege itself (that is objectName).',
|
|
284
|
+
},
|
|
285
|
+
entryPointObjectName: {
|
|
286
|
+
type: 'string',
|
|
287
|
+
description: '<ObjectName> of the entry point — the menu item or service operation it grants access to. Use ' +
|
|
288
|
+
'instead of entryPointName when the entry point carries a different <Name> than its target.',
|
|
289
|
+
},
|
|
290
|
+
entryPointObjectType: {
|
|
291
|
+
type: 'string (MenuItemDisplay | MenuItemAction | MenuItemOutput | ServiceOperation | None)',
|
|
292
|
+
description: '<ObjectType> (EntryPointType) of the entry point. Only needed to disambiguate one ObjectName ' +
|
|
293
|
+
'referenced through two entry-point types — two matches are refused, never guessed.',
|
|
294
|
+
},
|
|
295
|
+
// BP-check suppressions
|
|
296
|
+
diagnosticPath: {
|
|
297
|
+
type: 'string',
|
|
298
|
+
description: 'remove-diagnostic-suppression: REQUIRED — exact <Path> of the <Diagnostic> to remove (e.g. ' +
|
|
299
|
+
'"dynamics://Form/MyForm"), copied verbatim from the suppression entry. ' +
|
|
300
|
+
'add-diagnostic-suppression: the dynamics:// path a BP-check finding was raised against — copy it ' +
|
|
301
|
+
'verbatim from the finding when you have it (the only way to address a sub-element: a control, a ' +
|
|
302
|
+
'field, a method, an enum value). Preferred over diagnosticElementType + diagnosticElementName, which ' +
|
|
303
|
+
'can only derive a path to a whole top-level object. This is the same value ' +
|
|
304
|
+
'get_knowledge(kind="bp-moniker", action="suppress") renders it from.',
|
|
305
|
+
},
|
|
306
|
+
diagnosticMoniker: {
|
|
307
|
+
type: 'string',
|
|
308
|
+
description: 'remove-diagnostic-suppression: <Moniker> of the suppression to remove. Only needed when the same ' +
|
|
309
|
+
'diagnosticPath carries more than one <Diagnostic> (two different rules ignored on the same target) ' +
|
|
310
|
+
'— two matches on path alone are refused, never guessed. ' +
|
|
311
|
+
'add-diagnostic-suppression: REQUIRED — the BP moniker being suppressed, validated against the known ' +
|
|
312
|
+
'catalog (e.g. "BPErrorPrivilegeNotCoveredByDuty").',
|
|
313
|
+
},
|
|
314
|
+
diagnosticElementType: {
|
|
315
|
+
type: 'string (AxClass | AxTable | AxForm | AxView | AxMap | AxEnum | AxQuerySimple | ' +
|
|
316
|
+
'AxDataEntityView | AxSecurityPrivilege | AxSecurityDuty | AxSecurityRole | AxTableExtension | ' +
|
|
317
|
+
'AxFormExtension | AxMenuExtension | AxMenu | AxMenuItemDisplay | AxMenuItemAction | ' +
|
|
318
|
+
'AxMenuItemOutput | AxEdtString | AxEdtInt | … | AxConfigurationKey | AxLicenseCode)',
|
|
319
|
+
description: 'add-diagnostic-suppression: top-level AOT element type of the object the finding was raised against ' +
|
|
320
|
+
'— used with diagnosticElementName to DERIVE diagnosticPath when it is not given directly. Only ' +
|
|
321
|
+
'addresses a whole object; a sub-element needs diagnosticPath verbatim from the finding instead.',
|
|
322
|
+
},
|
|
323
|
+
diagnosticElementName: {
|
|
324
|
+
type: 'string',
|
|
325
|
+
description: 'add-diagnostic-suppression: name of the object the finding was raised against, paired with ' +
|
|
326
|
+
'diagnosticElementType to derive diagnosticPath.',
|
|
327
|
+
},
|
|
328
|
+
diagnosticJustification: {
|
|
329
|
+
type: 'string',
|
|
330
|
+
description: 'add-diagnostic-suppression: why this warning is being ignored. Omitting it writes an obvious TODO ' +
|
|
331
|
+
'placeholder plus a warning — a suppression with no stated reason is what a reviewer rejects.',
|
|
332
|
+
},
|
|
333
|
+
diagnosticMessage: {
|
|
334
|
+
type: 'string',
|
|
335
|
+
description: 'add-diagnostic-suppression: the real message text from the BP-check finding, if known. Never ' +
|
|
336
|
+
'invented when omitted — <Message> is simply left off, which is normal (absent from most real entries).',
|
|
337
|
+
},
|
|
338
|
+
diagnosticSeverity: {
|
|
339
|
+
type: 'string (Error | Warning)',
|
|
340
|
+
description: 'add-diagnostic-suppression: <Severity> of the diagnostic being suppressed. Default: Warning.',
|
|
341
|
+
},
|
|
342
|
+
diagnosticItemSpecific: {
|
|
343
|
+
type: 'boolean (default false)',
|
|
344
|
+
description: 'add-diagnostic-suppression: emit the <ItemSpecific> block — rare, only for element-specific rules ' +
|
|
345
|
+
'(BPErrorUnknownLabel, BPXmlDoc*, BPErrorPrivilegeNotCoveredByDuty, …). Requires diagnosticElementName.',
|
|
346
|
+
},
|
|
255
347
|
// menus
|
|
256
348
|
menuItemToAdd: { type: 'string', description: 'Name of the menu item to add (e.g. "MyCustomForm").' },
|
|
257
349
|
menuItemToAddType: {
|
|
@@ -376,6 +468,25 @@ export const D365FO_FILE_OP_SPECS = {
|
|
|
376
468
|
optional: ['joinSource', 'linkType'],
|
|
377
469
|
note: 'form-extension only.',
|
|
378
470
|
},
|
|
471
|
+
'add-query-range': {
|
|
472
|
+
required: ['dataSourceName', 'rangeField', 'rangeValue'],
|
|
473
|
+
optional: ['rangeName'],
|
|
474
|
+
note: 'objectType="data-entity" only. Adds an <AxQuerySimpleDataSourceRange> to the <Ranges> that the ' +
|
|
475
|
+
'named data source OWNS inside <ViewMetadata>. dataSourceName is the <Name> of either the root ' +
|
|
476
|
+
'data source (<AxQuerySimpleRootDataSource>, usually the primary table) or a joined one ' +
|
|
477
|
+
'(<AxQuerySimpleEmbeddedDataSource>) — a joined data source keeps its own <Ranges>, and filtering ' +
|
|
478
|
+
'the joined table is not the same query as filtering the root. ' +
|
|
479
|
+
'rangeName defaults to rangeField when omitted. ' +
|
|
480
|
+
'rangeValue is required (e.g. "1" to restrict to active rows); pass "" (two characters) for the ' +
|
|
481
|
+
'empty-string filter. Idempotent per data source.',
|
|
482
|
+
},
|
|
483
|
+
'remove-query-range': {
|
|
484
|
+
required: ['dataSourceName', 'rangeName'],
|
|
485
|
+
optional: [],
|
|
486
|
+
note: 'objectType="data-entity" only. Removes the <AxQuerySimpleDataSourceRange> whose <Name> equals ' +
|
|
487
|
+
'rangeName from the <Ranges> the named data source OWNS — a same-named range on a joined data ' +
|
|
488
|
+
'source is left alone. Collapses <Ranges> to <Ranges /> when empty. Idempotent.',
|
|
489
|
+
},
|
|
379
490
|
'add-control': {
|
|
380
491
|
required: ['controlName', 'parentControl'],
|
|
381
492
|
optional: [
|
|
@@ -397,6 +508,63 @@ export const D365FO_FILE_OP_SPECS = {
|
|
|
397
508
|
+ 'control in the XML, under a base-form parent it is written as '
|
|
398
509
|
+ '<PositionType>AfterItem</PositionType> + <PreviousSibling>.',
|
|
399
510
|
},
|
|
511
|
+
'remove-control': {
|
|
512
|
+
required: ['controlName'],
|
|
513
|
+
optional: ['removeSeparator'],
|
|
514
|
+
note: 'objectType="form" or "form-extension". Removes the control WHEREVER it sits in the design — '
|
|
515
|
+
+ 'controls nest (ActionPane → ButtonGroup → Button), so no parentControl is needed. '
|
|
516
|
+
+ 'On a form-extension the whole <AxFormExtensionControl> envelope goes, not just its '
|
|
517
|
+
+ '<FormControl>: an envelope without its control is a <Parent> reference to nothing. '
|
|
518
|
+
+ 'A control the form SHOWS but does not DEFINE belongs to the base form and is reported as '
|
|
519
|
+
+ 'not found — a form extension cannot delete a base control, only hide it '
|
|
520
|
+
+ '(modify-property Visible=No on a control extension). Emptying a <Controls> collection '
|
|
521
|
+
+ 'collapses it to <Controls />, the spelling the serializer uses.',
|
|
522
|
+
},
|
|
523
|
+
'remove-entry-point': {
|
|
524
|
+
required: [],
|
|
525
|
+
optional: ['entryPointName', 'entryPointObjectName', 'entryPointObjectType'],
|
|
526
|
+
mutationOneOf: ['entryPointName', 'entryPointObjectName'],
|
|
527
|
+
note: 'objectType="security-privilege". Removes one <AxSecurityEntryPointReference> — the block that '
|
|
528
|
+
+ 'grants a menu item through this privilege. Identify it by entryPointName, or by '
|
|
529
|
+
+ 'entryPointObjectName (+ entryPointObjectType when the same object is referenced through two '
|
|
530
|
+
+ 'entry-point types). Two matches are REFUSED rather than resolved: removing the wrong entry '
|
|
531
|
+
+ 'point revokes access to a different object, builds clean, and only surfaces as a user losing '
|
|
532
|
+
+ 'a form. Removing the last one collapses <EntryPoints> to <EntryPoints />. '
|
|
533
|
+
+ 'A privilege left with no entry points and no data-entity permissions grants nothing — delete '
|
|
534
|
+
+ 'it with d365fo_file(action="delete") and drop its BP suppression entry.',
|
|
535
|
+
},
|
|
536
|
+
'remove-diagnostic-suppression': {
|
|
537
|
+
required: ['diagnosticPath'],
|
|
538
|
+
optional: ['diagnosticMoniker'],
|
|
539
|
+
note: 'objectType="ignore-diagnostic-list". Removes one <Diagnostic> from a {Model}_BPSuppressions.xml ' +
|
|
540
|
+
'— objectName is the file\'s own base name, "{Model}_BPSuppressions" (or pass filePath). Identify ' +
|
|
541
|
+
'the entry by diagnosticPath, the exact <Path> a BP-check finding was suppressed against; add ' +
|
|
542
|
+
'diagnosticMoniker when the same path carries more than one suppressed rule. Two matches on path ' +
|
|
543
|
+
'alone are REFUSED rather than resolved: removing the wrong one leaves a live finding silenced. ' +
|
|
544
|
+
'Removing the last entry collapses <Items> to <Items />. ' +
|
|
545
|
+
'd365fo_file(action="delete") already strips suppressions whose <Path> targets the deleted object ' +
|
|
546
|
+
'— use this operation for suppressions left stale by other means (a moniker fixed in code, a ' +
|
|
547
|
+
'renamed sub-element).',
|
|
548
|
+
},
|
|
549
|
+
'add-diagnostic-suppression': {
|
|
550
|
+
required: ['diagnosticMoniker'],
|
|
551
|
+
optional: [
|
|
552
|
+
'diagnosticPath', 'diagnosticElementType', 'diagnosticElementName',
|
|
553
|
+
'diagnosticJustification', 'diagnosticMessage', 'diagnosticSeverity', 'diagnosticItemSpecific',
|
|
554
|
+
],
|
|
555
|
+
note: 'objectType="ignore-diagnostic-list". Adds one <Diagnostic> to a {Model}_BPSuppressions.xml — ' +
|
|
556
|
+
'objectName is the file\'s own base name, "{Model}_BPSuppressions" (or pass filePath). Needs ' +
|
|
557
|
+
'diagnosticMoniker PLUS either diagnosticPath (verbatim from the finding — the only way to address ' +
|
|
558
|
+
'a control/field/method/enum value) or diagnosticElementType + diagnosticElementName (derives a path ' +
|
|
559
|
+
'to a whole top-level object only). Builds the <Diagnostic> the same way ' +
|
|
560
|
+
'get_knowledge(kind="bp-moniker", action="suppress") does, so the two cannot describe two different ' +
|
|
561
|
+
'shapes — that helper is now redundant for anyone with write access to the metadata; call this ' +
|
|
562
|
+
'directly instead of rendering text to paste by hand. Refuses a duplicate (same diagnosticPath AND ' +
|
|
563
|
+
'diagnosticMoniker already present) rather than writing a second copy. When the model has never ' +
|
|
564
|
+
'suppressed anything before, {Model}_BPSuppressions.xml does not exist yet — this creates it and its ' +
|
|
565
|
+
'AxIgnoreDiagnosticList folder, in the shape real shipped suppression lists have, and says so in the ' +
|
|
566
|
+
'reply so you can add it to the model\'s .rnrproj if Visual Studio does not pick it up.',
|
|
567
|
+
},
|
|
400
568
|
'add-enum-value': {
|
|
401
569
|
required: ['enumValueName'],
|
|
402
570
|
optional: ['enumValueLabel', 'enumValueHelpText', 'enumValueInt', 'enumValueCountryRegionCodes'],
|
|
@@ -37,6 +37,13 @@ const LABELS_TOPICS = ['labels', 'label', 'labels.create', 'labels.rename', 'cre
|
|
|
37
37
|
* spent on a dead end at the exact moment the caller was already unsure about a name.
|
|
38
38
|
*/
|
|
39
39
|
const TOPIC_REDIRECTS = {
|
|
40
|
+
// `delete` is an ACTION, not an operation, so it has no entry in
|
|
41
|
+
// D365FO_FILE_OP_SPECS to resolve against — and it is the one action whose
|
|
42
|
+
// contract a caller most wants before calling it. Without this it fell through
|
|
43
|
+
// to the catalogue of 33 modify operations, none of which is what was asked.
|
|
44
|
+
delete: 'delete',
|
|
45
|
+
'delete-object': 'delete',
|
|
46
|
+
'remove-object': 'delete',
|
|
40
47
|
naming: 'naming',
|
|
41
48
|
prefix: 'naming',
|
|
42
49
|
'object-naming': 'naming',
|
|
@@ -45,6 +52,36 @@ const TOPIC_REDIRECTS = {
|
|
|
45
52
|
suffix: 'naming',
|
|
46
53
|
};
|
|
47
54
|
const REDIRECT_ANSWERS = {
|
|
55
|
+
delete: [
|
|
56
|
+
'd365fo_file(action="delete") — remove an AOT object from the model.',
|
|
57
|
+
'',
|
|
58
|
+
'Removes the object XML from disk AND the <Content Include> entry from every .rnrproj of the',
|
|
59
|
+
'model that lists it. Confirm with the user first, and run find_references first — every',
|
|
60
|
+
'remaining reference becomes a compile error. Recovery is only partial: when the model directory',
|
|
61
|
+
'is under git, undo_last_modification(filePath=…) restores the XML but NOT the project entries.',
|
|
62
|
+
'',
|
|
63
|
+
' REQUIRED objectType (string): the same enum action="create" takes. It must match the AOT',
|
|
64
|
+
' folder the file actually sits in — a mismatch is refused, because the un-register step',
|
|
65
|
+
' builds its <Content Include> from it and would clean a different object.',
|
|
66
|
+
' REQUIRED objectName (string): base name; the model prefix is applied on a miss, so the name',
|
|
67
|
+
' passed to create resolves too. Optional when filePath is given (derived from the basename).',
|
|
68
|
+
' optional modelName (string): owning model — auto-detected when omitted.',
|
|
69
|
+
' optional filePath (string): absolute path to the .xml, bypassing lookup.',
|
|
70
|
+
' optional packagePath (string): packages root, for metadata outside PackagesLocalDirectory.',
|
|
71
|
+
' optional projectPath (string): a .rnrproj to include in the set searched for includes to remove.',
|
|
72
|
+
' optional groundingToken (string): from prepare(mode="change"). Required for *-extension',
|
|
73
|
+
' objects when GROUNDING_ENFORCE=true — the same gate create and modify apply.',
|
|
74
|
+
'',
|
|
75
|
+
'Refused, never silently skipped: an object that resolves to nothing (❌, so a wrong name is not',
|
|
76
|
+
'read as a completed delete), an objectType that disagrees with the file\'s AOT folder, a file in',
|
|
77
|
+
'a standard Microsoft model, one owned by a different custom model than the write anchor, and any',
|
|
78
|
+
'path outside the allowed metadata roots. A project that lists the object but whose entry could',
|
|
79
|
+
'not be removed is reported as ⚠️, never as "no project referenced it".',
|
|
80
|
+
'',
|
|
81
|
+
'Deleting a form control or a privilege entry point instead of a whole object:',
|
|
82
|
+
' get_knowledge(kind="op-spec", topic="remove-control")',
|
|
83
|
+
' get_knowledge(kind="op-spec", topic="remove-entry-point")',
|
|
84
|
+
].join('\n'),
|
|
48
85
|
naming: [
|
|
49
86
|
'Naming is not an op-spec — it is resolved per model, so ask the tools that know your model:',
|
|
50
87
|
'',
|
|
@@ -121,6 +158,8 @@ export function renderOpSpecIndex(unknownTopic) {
|
|
|
121
158
|
'generate_object modes:',
|
|
122
159
|
` ${topics.generateModes.join(', ')}`,
|
|
123
160
|
'',
|
|
161
|
+
'd365fo_file(action="delete") — the contract for removing an object (topic="delete").',
|
|
162
|
+
'',
|
|
124
163
|
'd365fo_file resolution overrides (any action, nested in `params`):',
|
|
125
164
|
...Object.entries(D365FO_FILE_OVERRIDE_PARAMS).map(([k, v]) => ` ${k}: ${v}`),
|
|
126
165
|
'',
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* d365fo_file(action="delete") — remove an AOT object from the model.
|
|
3
|
+
*
|
|
4
|
+
* The counterpart to `create`, and it has to undo BOTH halves of what create
|
|
5
|
+
* did: the XML file on disk, and the `<Content Include>` entry that makes the
|
|
6
|
+
* element part of a Visual Studio project. Deleting only the file leaves an
|
|
7
|
+
* include pointing at nothing — VS reports it, nothing else does, and the next
|
|
8
|
+
* developer to open the project gets a load error for an object that was
|
|
9
|
+
* intentionally removed weeks earlier.
|
|
10
|
+
*
|
|
11
|
+
* It un-registers from EVERY project of the model that lists the object, not
|
|
12
|
+
* just the active one. An element may legitimately belong to several .rnrproj of
|
|
13
|
+
* one model (see registerFileInActiveProject for the measurement behind that),
|
|
14
|
+
* so cleaning only the active project is exactly the case that leaves a dangling
|
|
15
|
+
* include behind.
|
|
16
|
+
*
|
|
17
|
+
* Guards, in order, and none of them optional:
|
|
18
|
+
* • grounding — the gate create and modify apply to *-extension objects, under
|
|
19
|
+
* GROUNDING_ENFORCE=true. Exempting the one action that cannot be undone
|
|
20
|
+
* would mean an agent barred from CREATING an extension without a prepare
|
|
21
|
+
* token is free to DELETE one;
|
|
22
|
+
* • the object must resolve to a real file — a name that matches nothing is
|
|
23
|
+
* reported as ❌, never as "done" (a silent no-op reads as a successful
|
|
24
|
+
* delete and the object is still in the build);
|
|
25
|
+
* • path containment — the target must sit under a configured
|
|
26
|
+
* <PackagesLocalDirectory>/<Package>/<Model>/Ax<Type>/<File>.xml layout, so
|
|
27
|
+
* an explicit filePath cannot traverse out of the metadata tree;
|
|
28
|
+
* • objectType against the file's own Ax<Type> folder — everything downstream
|
|
29
|
+
* trusts objectType, and axFolderForObjectType answers 'AxClass' for anything
|
|
30
|
+
* it does not recognise, so a mismatch would delete this file and un-register
|
|
31
|
+
* a different object;
|
|
32
|
+
* • model ownership — a file in a standard Microsoft model is refused
|
|
33
|
+
* outright, and one owned by a different CUSTOM model than the write anchor
|
|
34
|
+
* goes through the same cross-model refusal every write does.
|
|
35
|
+
*
|
|
36
|
+
* There is no bridge path: MetadataWriteService exposes no delete, and going
|
|
37
|
+
* through the provider would be worse anyway — the file and the project entry
|
|
38
|
+
* are what "deleted" means here, and both are on disk.
|
|
39
|
+
*/
|
|
40
|
+
import type { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
41
|
+
import type { XppServerContext } from '../../types/context.js';
|
|
42
|
+
export declare function handleDeleteD365File(request: CallToolRequest, context?: XppServerContext): Promise<{
|
|
43
|
+
content: Array<{
|
|
44
|
+
type: string;
|
|
45
|
+
text: string;
|
|
46
|
+
}>;
|
|
47
|
+
isError?: boolean;
|
|
48
|
+
}>;
|
|
49
|
+
//# sourceMappingURL=deleteD365File.d.ts.map
|