d365fo-mcp 1.5.1 → 1.6.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 +86 -0
- package/README.md +1 -1
- package/bridge/D365MetadataBridge/D365MetadataBridge.csproj +33 -3
- package/bridge/D365MetadataBridge/Program.cs +1 -1
- package/dist/bridge/bridgeAdapter.d.ts +1 -1
- package/dist/bridge/bridgeAdapter.js +33 -2
- package/dist/bridge/bridgeClient.d.ts +1 -1
- package/dist/bridge/bridgeClient.js +3 -5
- package/dist/bridge/bridgeTypes.d.ts +15 -0
- package/dist/cli/commands/doctor.js +39 -0
- package/dist/cli/commands/instance.js +11 -1
- package/dist/cli/commands/setup.js +15 -67
- package/dist/cli/copilotFiles.d.ts +9 -0
- package/dist/cli/copilotFiles.js +85 -0
- package/dist/cli/settingsPrompt.js +4 -1
- package/dist/config/settings.js +3 -1
- package/dist/scripts/build-database.js +66 -4
- package/dist/scripts/build-fts.js +66 -4
- package/dist/scripts/extract-metadata.js +124 -62
- package/dist/server/toolSchemas/d365foFile.js +1 -1
- package/dist/server/toolSchemas/verifyD365foProject.js +1 -1
- package/dist/tools/analyzeExtensionPoints.js +141 -5
- package/dist/tools/buildProject.d.ts +48 -0
- package/dist/tools/buildProject.js +101 -20
- package/dist/tools/completion.js +11 -2
- package/dist/tools/createD365File.d.ts +15 -7
- package/dist/tools/createD365File.js +26 -21
- package/dist/tools/createLabel.js +5 -4
- package/dist/tools/dataEntityViewExtensionXml.d.ts +74 -0
- package/dist/tools/dataEntityViewExtensionXml.js +142 -0
- package/dist/tools/dbSync.js +2 -1
- package/dist/tools/generateD365Xml.js +21 -10
- package/dist/tools/generateSmart.js +1 -1
- package/dist/tools/generateSmartForm.js +2 -1
- package/dist/tools/generateSmartReport.d.ts +2 -1
- package/dist/tools/generateSmartReport.js +119 -117
- package/dist/tools/generateSmartTable.js +5 -4
- package/dist/tools/getMethodSource.js +49 -1
- package/dist/tools/menuItemExtensionXml.d.ts +39 -0
- package/dist/tools/menuItemExtensionXml.js +91 -0
- package/dist/tools/methodSignature.js +71 -25
- package/dist/tools/prepareChange.js +70 -34
- package/dist/tools/renameLabel.js +5 -4
- package/dist/tools/runBpCheck.js +5 -16
- package/dist/tools/sysTestRunner.js +2 -1
- package/dist/tools/verifyD365Project.js +6 -5
- package/dist/tools/xppKnowledge.js +107 -3
- package/dist/utils/configManager.d.ts +7 -0
- package/dist/utils/configManager.js +17 -25
- package/dist/utils/inheritanceChain.d.ts +44 -0
- package/dist/utils/inheritanceChain.js +99 -0
- package/dist/utils/packagesRoot.d.ts +55 -0
- package/dist/utils/packagesRoot.js +134 -0
- package/package.json +2 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# D365 Finance & Operations X++ Development
|
|
2
|
+
|
|
3
|
+
<!-- Thin pointer — full rules are delivered via the MCP `xpp_system_instructions` prompt.
|
|
4
|
+
This file provides only the minimum static context needed when the MCP server
|
|
5
|
+
is not yet connected or the prompt hasn't been loaded.
|
|
6
|
+
Serves Claude Code too: copy it to a parent of your solution folders as
|
|
7
|
+
CLAUDE.md (docs/SETUP.md § Claude Code CLI, Step 3). -->
|
|
8
|
+
|
|
9
|
+
## Tool Priority
|
|
10
|
+
|
|
11
|
+
This workspace contains a D365FO MCP server. **Always use the specialized MCP tools** for D365FO objects (`.xml`, `.xpp`, `.rnrproj`, `.label.txt`). Built-in file/search tools are fine for `.cs`, `.json`, `.yml`, `.md`, `.config` files.
|
|
12
|
+
|
|
13
|
+
## Mandatory First Check
|
|
14
|
+
|
|
15
|
+
Call `get_workspace_info()` before doing anything with D365FO objects.
|
|
16
|
+
|
|
17
|
+
| Response | Action |
|
|
18
|
+
|----------|--------|
|
|
19
|
+
| Call fails | STOP. MCP server not connected. Ask user to start it. |
|
|
20
|
+
| `⛔ CONFIGURATION PROBLEM` | STOP. Relay message. Wait for user. |
|
|
21
|
+
| `✅ Configuration looks valid` | Note model name. Proceed. |
|
|
22
|
+
|
|
23
|
+
## Terminal Prohibition
|
|
24
|
+
|
|
25
|
+
PowerShell / any terminal command **WILL HANG** in VS 2022 / VS 2026 MCP integration. Never use `run_in_terminal` or generate scripts as a fallback when an MCP tool fails — STOP and report the error verbatim.
|
|
26
|
+
|
|
27
|
+
## Core Tool Mapping
|
|
28
|
+
|
|
29
|
+
| Action | Tool |
|
|
30
|
+
|--------|------|
|
|
31
|
+
| Plan an extension before changing code | `prepare(mode="change", goal, objectName, methodName?)` — returns signature, existing CoC wrappers, strategy + `groundingToken` |
|
|
32
|
+
| Plan a new object before creating it | `prepare(mode="create", goal, objectName, objectType)` — returns collision check, naming, EDT/label hints + `groundingToken` |
|
|
33
|
+
| Create a D365FO object | `d365fo_file(action="create")` (never `create_file`) |
|
|
34
|
+
| Edit an existing object | `d365fo_file(action="modify")` (applies immediately — confirm in chat first) |
|
|
35
|
+
| Revert the last write | `undo_last_modification` |
|
|
36
|
+
| Search objects | `search` — multiple via `search(queries[])`, custom-only via `search(scope="extensions")` |
|
|
37
|
+
| Read any object's metadata | `get_object_info(objectType, name, options?)` — objectType ∈ class/table/form/query/view/enum/edt/report/data-entity/menu-item/service/map/config-key/security-policy/macro. 2+ known names: `batch_get_info(objects[])` |
|
|
38
|
+
| Method signature for CoC | `get_method(include="signature")` (already returned by `prepare(mode="change")`) |
|
|
39
|
+
| Validate X++ before write | `validate_code(mode="syntax", code)` — offline BP check, <50 ms |
|
|
40
|
+
| X++ rules & patterns | `get_knowledge(kind="knowledge", topic)` — select grammar, CoC, BP rules, SysOperation, workflow, … |
|
|
41
|
+
| Create a NEW form | `object_patterns(domain="form", action="analyze", recommend={...})` → `object_patterns(domain="form", action="spec", pattern)` → `generate_object(mode="scaffold", objectType="form", cloneFrom=referenceForm, tableMapping={...})` → `object_patterns(domain="form", action="validate", xml)` |
|
|
42
|
+
| Validate form XML against its pattern | `object_patterns(domain="form", action="validate", xml \| formName \| filePath)` — structural errors block form writes (FORM_PATTERN_ENFORCE) |
|
|
43
|
+
| Resolve label / EDT / class refs | `validate_code(mode="references", code)` |
|
|
44
|
+
| Build / BP / Sync | `build_d365fo_project` / `run_bp_check` / `trigger_db_sync` |
|
|
45
|
+
| Error diagnosis | `get_knowledge(kind="error", errorText)` |
|
|
46
|
+
|
|
47
|
+
## Key Rules
|
|
48
|
+
|
|
49
|
+
### Workspace & model targeting
|
|
50
|
+
|
|
51
|
+
1. **The target model comes from `.mcp.json`** — never infer it from search results or object names. The symbol database contains objects from all models (Microsoft + ISV + custom); the model on a search/`get_*_info` result is the source model, not where new files belong.
|
|
52
|
+
|
|
53
|
+
### Writes & file editing
|
|
54
|
+
|
|
55
|
+
2. **`d365fo_file` (action=create/modify) applies immediately** (no dry-run / preview). Describe the change in chat and wait for explicit user confirmation ("apply", "ok", "yes") before calling. Revert with `undo_last_modification` (or pass `createBackup=true` to keep a `.bak`).
|
|
56
|
+
3. **Never** use `replace_string_in_file`, `edit_file`, `apply_patch`, or any built-in file-write tool on `.xml` or `.xpp` files — **not even as a fallback** when `d365fo_file(action="modify")` fails. These bypass `IMetadataProvider` and corrupt VS 2022's in-memory model. If `d365fo_file(action="modify")` errors, STOP and report the error verbatim.
|
|
57
|
+
|
|
58
|
+
### Build automation
|
|
59
|
+
|
|
60
|
+
4. Never run `build_d365fo_project()` automatically — only on explicit user request ("build", "compile", "check errors").
|
|
61
|
+
|
|
62
|
+
### X++ correctness (BP-clean code)
|
|
63
|
+
|
|
64
|
+
5. Never copy default parameter values into CoC wrapper signatures.
|
|
65
|
+
6. Never use `today()` — use `DateTimeUtil::getToday(DateTimeUtil::getUserPreferredTimeZone())`.
|
|
66
|
+
7. Never use hardcoded strings in `Info()` / `warning()` / `error()` — use `@Model:Label` references.
|
|
67
|
+
8. Call `labels(action="search")` before `labels(action="create")` — reuse existing labels.
|
|
68
|
+
|
|
69
|
+
### Extension naming
|
|
70
|
+
|
|
71
|
+
9. Extension naming follows `EXTENSION_NAMING_STYLE` (see `get_workspace_info`):
|
|
72
|
+
- `prefix` (default) → class `{Target}{Prefix}_Extension`, element `{Target}.{Prefix}Extension`
|
|
73
|
+
- `model-name` → class `{Target}_{ModelName}_Extension`, element `{Target}.{ModelName}`
|
|
74
|
+
|
|
75
|
+
Pass the BASE object name to `d365fo_file(action="create")` and let the tool inject the token — don't hand-build the infix.
|
|
76
|
+
|
|
77
|
+
### Reuse & diff safety
|
|
78
|
+
|
|
79
|
+
10. **Reuse before creating** — `prepare(mode="change")` lists existing CoC wrappers and event handlers. If an extension or handler class in the custom model already owns the target, add the new method there. Never create a parallel feature-named class (`<Target>_<Feature>_Extension`, `<Form>_<Feature>_EH`) unless the user explicitly asks for a separate class. The suffix comes from `EXTENSION_NAMING_STYLE` / existing artifacts — never from feature, ticket, or customer names; if it cannot be derived, ask.
|
|
80
|
+
11. **The post-write diff must be additive or narrowly targeted** — verify via `review_workspace_changes` (or re-read with `get_*_info`) that no unrelated XML nodes (`<DataSources>`, `<Controls>`, methods, pattern metadata) disappeared. If they did, the edit failed: `undo_last_modification`.
|
|
81
|
+
12. **An example form named by the user is a pattern contract** — keep its pattern family and required scaffolding (datasources, ActionPane/Tab/grid/QuickFilter); missing pattern elements are a failed generation even if the XML is well-formed.
|
|
82
|
+
|
|
83
|
+
## Full Instructions
|
|
84
|
+
|
|
85
|
+
The complete X++ rules, query grammar, CoC authoring rules, and workflow details are delivered via the MCP prompt `xpp_system_instructions`. If that prompt is not loaded, request it or consult [src/prompts/systemInstructions.ts](../src/prompts/systemInstructions.ts) directly.
|
|
86
|
+
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
[](https://www.typescriptlang.org/)
|
|
11
11
|
[](docs/TESTING.md)
|
|
12
12
|
<!-- coverage-badge:start -->
|
|
13
|
-
[](eval/COVERAGE.md) [](eval/COVERAGE.md) [](eval/COVERAGE.md)
|
|
14
14
|
<!-- coverage-badge:end -->
|
|
15
15
|
|
|
16
16
|
*Grounded AI development for Dynamics 365 Finance & Operations — works with GitHub Copilot and Claude Code*
|
|
@@ -23,10 +23,18 @@
|
|
|
23
23
|
|
|
24
24
|
<!--
|
|
25
25
|
D365FO bin directory — contains all Microsoft.Dynamics.*.dll assemblies.
|
|
26
|
-
Every D365FO development desktop has this directory available
|
|
26
|
+
Every D365FO development desktop has this directory available, but WHICH
|
|
27
|
+
volume it sits on is decided by the VM image: K: on cloud-hosted
|
|
28
|
+
environments, C: on the downloadable VHD, J: on newer images (#769), and
|
|
29
|
+
nothing stops the next image from using another letter again.
|
|
30
|
+
|
|
31
|
+
So the drive is swept rather than guessed: the first
|
|
32
|
+
<drive>:\AosService\PackagesLocalDirectory\bin that exists wins, with the
|
|
33
|
+
historically standard letters tried first and the rest of the alphabet
|
|
34
|
+
after them. MSBuild has no loop at evaluation time, and the property has to
|
|
35
|
+
be resolved before Reference items are globbed — hence one line per letter.
|
|
36
|
+
A: and B: are left out on purpose (floppy letters stall the probe).
|
|
27
37
|
|
|
28
|
-
Traditional VM: K:\AosService\PackagesLocalDirectory\bin
|
|
29
|
-
Cloud-hosted: C:\AosService\PackagesLocalDirectory\bin
|
|
30
38
|
UDE (Unified Developer Experience): path from .mcp.json microsoftPackagesPath/bin
|
|
31
39
|
|
|
32
40
|
Override per-machine via:
|
|
@@ -36,6 +44,28 @@
|
|
|
36
44
|
<PropertyGroup>
|
|
37
45
|
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('K:\AosService\PackagesLocalDirectory\bin')">K:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
38
46
|
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('C:\AosService\PackagesLocalDirectory\bin')">C:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
47
|
+
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('J:\AosService\PackagesLocalDirectory\bin')">J:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
48
|
+
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('I:\AosService\PackagesLocalDirectory\bin')">I:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
49
|
+
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('D:\AosService\PackagesLocalDirectory\bin')">D:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
50
|
+
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('E:\AosService\PackagesLocalDirectory\bin')">E:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
51
|
+
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('F:\AosService\PackagesLocalDirectory\bin')">F:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
52
|
+
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('G:\AosService\PackagesLocalDirectory\bin')">G:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
53
|
+
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('H:\AosService\PackagesLocalDirectory\bin')">H:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
54
|
+
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('L:\AosService\PackagesLocalDirectory\bin')">L:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
55
|
+
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('M:\AosService\PackagesLocalDirectory\bin')">M:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
56
|
+
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('N:\AosService\PackagesLocalDirectory\bin')">N:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
57
|
+
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('O:\AosService\PackagesLocalDirectory\bin')">O:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
58
|
+
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('P:\AosService\PackagesLocalDirectory\bin')">P:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
59
|
+
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('Q:\AosService\PackagesLocalDirectory\bin')">Q:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
60
|
+
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('R:\AosService\PackagesLocalDirectory\bin')">R:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
61
|
+
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('S:\AosService\PackagesLocalDirectory\bin')">S:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
62
|
+
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('T:\AosService\PackagesLocalDirectory\bin')">T:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
63
|
+
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('U:\AosService\PackagesLocalDirectory\bin')">U:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
64
|
+
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('V:\AosService\PackagesLocalDirectory\bin')">V:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
65
|
+
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('W:\AosService\PackagesLocalDirectory\bin')">W:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
66
|
+
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('X:\AosService\PackagesLocalDirectory\bin')">X:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
67
|
+
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('Y:\AosService\PackagesLocalDirectory\bin')">Y:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
68
|
+
<D365BinPath Condition="'$(D365BinPath)' == '' And Exists('Z:\AosService\PackagesLocalDirectory\bin')">Z:\AosService\PackagesLocalDirectory\bin</D365BinPath>
|
|
39
69
|
</PropertyGroup>
|
|
40
70
|
|
|
41
71
|
<!-- NuGet packages -->
|
|
@@ -416,7 +416,7 @@ Usage:
|
|
|
416
416
|
D365MetadataBridge.exe [options]
|
|
417
417
|
|
|
418
418
|
Options:
|
|
419
|
-
--packages-path <path> Path to primary PackagesLocalDirectory (
|
|
419
|
+
--packages-path <path> Path to primary PackagesLocalDirectory (required; the server detects it per machine)
|
|
420
420
|
--reference-packages-path <path> UDE: secondary packages path (Microsoft FrameworkDirectory). Objects not found in
|
|
421
421
|
the primary path are looked up here, enabling resolution of both custom and
|
|
422
422
|
Microsoft-shipped metadata in UDE environments.
|
|
@@ -358,7 +358,7 @@ export declare function bridgeDiscoverFormPatterns(bridge: BridgeClient | undefi
|
|
|
358
358
|
export declare function tryBridgeSecurityArtifact(bridge: BridgeClient | undefined, name: string, artifactType: 'privilege' | 'duty' | 'role', includeChain: boolean): Promise<ToolResult | null>;
|
|
359
359
|
export declare function tryBridgeMenuItem(bridge: BridgeClient | undefined, name: string, itemType?: string): Promise<ToolResult | null>;
|
|
360
360
|
export declare function tryBridgeTableExtensions(bridge: BridgeClient | undefined, baseTableName: string): Promise<ToolResult | null>;
|
|
361
|
-
export declare function tryBridgeCompletion(bridge: BridgeClient | undefined, symbolName: string, prefix?: string): Promise<ToolResult | null>;
|
|
361
|
+
export declare function tryBridgeCompletion(bridge: BridgeClient | undefined, symbolName: string, prefix?: string, ancestors?: string[]): Promise<ToolResult | null>;
|
|
362
362
|
export declare function tryBridgeCocExtensions(bridge: BridgeClient | undefined, baseClassName: string, methodName?: string): Promise<ToolResult | null>;
|
|
363
363
|
export declare function tryBridgeEventHandlers(bridge: BridgeClient | undefined, targetName: string, eventName?: string, handlerType?: string): Promise<ToolResult | null>;
|
|
364
364
|
export declare function tryBridgeApiUsageCallers(bridge: BridgeClient | undefined, apiName: string, limit?: number): Promise<ToolResult | null>;
|
|
@@ -1911,13 +1911,38 @@ function formatTableExtensions(r) {
|
|
|
1911
1911
|
return out;
|
|
1912
1912
|
}
|
|
1913
1913
|
// CODE COMPLETION (Phase 6)
|
|
1914
|
-
export async function tryBridgeCompletion(bridge, symbolName, prefix) {
|
|
1914
|
+
export async function tryBridgeCompletion(bridge, symbolName, prefix, ancestors) {
|
|
1915
1915
|
if (!bridge?.isReady || !bridge.metadataAvailable)
|
|
1916
1916
|
return null;
|
|
1917
1917
|
try {
|
|
1918
1918
|
const result = await bridge.getCompletionMembers(symbolName);
|
|
1919
1919
|
if (!result || !result.members || result.members.length === 0)
|
|
1920
1920
|
return null;
|
|
1921
|
+
// IMetadataProvider returns DECLARED members only, so a subclass lists
|
|
1922
|
+
// nothing it inherits and the reader concludes the member does not exist.
|
|
1923
|
+
// Merge the base classes in, nearest first; a name already present wins,
|
|
1924
|
+
// since that is the subclass's own override.
|
|
1925
|
+
if (ancestors?.length) {
|
|
1926
|
+
const seen = new Set(result.members.map(m => m.name.toLowerCase()));
|
|
1927
|
+
for (const ancestor of ancestors) {
|
|
1928
|
+
let inherited = null;
|
|
1929
|
+
try {
|
|
1930
|
+
inherited = await bridge.getCompletionMembers(ancestor);
|
|
1931
|
+
}
|
|
1932
|
+
catch (e) {
|
|
1933
|
+
// One unreadable link must not discard the members already merged.
|
|
1934
|
+
console.error(`[BridgeAdapter] getCompletionMembers(${ancestor}) failed: ${e}`);
|
|
1935
|
+
continue;
|
|
1936
|
+
}
|
|
1937
|
+
for (const m of inherited?.members ?? []) {
|
|
1938
|
+
const key = m.name.toLowerCase();
|
|
1939
|
+
if (seen.has(key))
|
|
1940
|
+
continue;
|
|
1941
|
+
seen.add(key);
|
|
1942
|
+
result.members.push({ ...m, inheritedFrom: inherited?.symbolName || ancestor });
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1921
1946
|
return { content: [{ type: 'text', text: formatCompletion(result, prefix) }] };
|
|
1922
1947
|
}
|
|
1923
1948
|
catch (e) {
|
|
@@ -1943,9 +1968,15 @@ function formatCompletion(r, prefix) {
|
|
|
1943
1968
|
const methodMembers = members.filter(m => m.kind === 'method');
|
|
1944
1969
|
const fieldMembers = members.filter(m => m.kind === 'field');
|
|
1945
1970
|
if (methodMembers.length > 0) {
|
|
1971
|
+
const inheritedCount = methodMembers.filter(m => m.inheritedFrom).length;
|
|
1946
1972
|
out += `## Methods (${methodMembers.length})\n`;
|
|
1947
1973
|
for (const m of methodMembers) {
|
|
1948
|
-
|
|
1974
|
+
const body = m.signature ? `\`${m.signature}\`` : m.name;
|
|
1975
|
+
out += m.inheritedFrom ? `- ${body} _(inherited from ${m.inheritedFrom})_\n` : `- ${body}\n`;
|
|
1976
|
+
}
|
|
1977
|
+
if (inheritedCount > 0) {
|
|
1978
|
+
out += `\n> ${inheritedCount} of these are inherited — callable on ${r.symbolName}, but ` +
|
|
1979
|
+
`declared on a base class. To read or wrap one, target the class named beside it.\n`;
|
|
1949
1980
|
}
|
|
1950
1981
|
}
|
|
1951
1982
|
if (fieldMembers.length > 0) {
|
|
@@ -19,7 +19,7 @@ export * from './bridgeTypes.js';
|
|
|
19
19
|
export interface BridgeClientOptions {
|
|
20
20
|
/** Path to the D365MetadataBridge.exe (auto-detected if omitted) */
|
|
21
21
|
bridgeExePath?: string;
|
|
22
|
-
/** K:\AosService\PackagesLocalDirectory */
|
|
22
|
+
/** e.g. K:\AosService\PackagesLocalDirectory — the volume varies by VM image */
|
|
23
23
|
packagesPath: string;
|
|
24
24
|
/**
|
|
25
25
|
* Optional secondary packages path.
|
|
@@ -17,6 +17,7 @@ import { EventEmitter } from 'events';
|
|
|
17
17
|
import * as path from 'path';
|
|
18
18
|
import * as fs from 'fs';
|
|
19
19
|
import { fileURLToPath } from 'url';
|
|
20
|
+
import { packagesRoots } from '../utils/packagesRoot.js';
|
|
20
21
|
export * from './bridgeTypes.js';
|
|
21
22
|
const BRIDGE_EXE_NAME = 'D365MetadataBridge.exe';
|
|
22
23
|
/** Parse a positive-integer env var with a fallback (ignores invalid/non-positive values). */
|
|
@@ -751,11 +752,8 @@ function detectPackagesPath() {
|
|
|
751
752
|
const candidates = [
|
|
752
753
|
process.env.D365FO_PACKAGE_PATH ?? '',
|
|
753
754
|
process.env.PACKAGES_PATH ?? '',
|
|
754
|
-
//
|
|
755
|
-
|
|
756
|
-
'C:\\AOSService\\PackagesLocalDirectory',
|
|
757
|
-
'J:\\AosService\\PackagesLocalDirectory',
|
|
758
|
-
'K:\\AosService\\PackagesLocalDirectory',
|
|
755
|
+
// Whatever AosService volumes this machine actually has (C:, J:, K:, …)
|
|
756
|
+
...packagesRoots(),
|
|
759
757
|
].filter(Boolean);
|
|
760
758
|
for (const p of candidates) {
|
|
761
759
|
// Traditional: bin is directly under packagesPath
|
|
@@ -92,6 +92,14 @@ export interface BridgeMethodInfo {
|
|
|
92
92
|
returnType?: string;
|
|
93
93
|
source?: string;
|
|
94
94
|
isStatic?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* ⚠️ Never sent by the bridge. The C# MethodInfoModel behind readClass carries
|
|
97
|
+
* only name/source/isStatic, so this is always undefined on bridge-sourced
|
|
98
|
+
* methods — it is populated exclusively by the XML parser path
|
|
99
|
+
* (xmlParser.parseClassFile). Reading it off a readClass result silently
|
|
100
|
+
* yields nothing; for a bridge-sourced modifier parse the declaration line out
|
|
101
|
+
* of getCompletionMembers().members[].signature instead.
|
|
102
|
+
*/
|
|
95
103
|
visibility?: string;
|
|
96
104
|
}
|
|
97
105
|
export interface BridgeEnumInfo {
|
|
@@ -499,6 +507,13 @@ export interface BridgeCompletionMember {
|
|
|
499
507
|
name: string;
|
|
500
508
|
signature?: string;
|
|
501
509
|
kind: string;
|
|
510
|
+
/**
|
|
511
|
+
* Set by the caller (not the bridge) when the member was picked up from a
|
|
512
|
+
* base class rather than declared on the requested one. IMetadataProvider
|
|
513
|
+
* returns declared members only, so inherited members are merged in on the
|
|
514
|
+
* TypeScript side and tagged here.
|
|
515
|
+
*/
|
|
516
|
+
inheritedFrom?: string;
|
|
502
517
|
}
|
|
503
518
|
export interface BridgeCompletionResult {
|
|
504
519
|
symbolName: string;
|
|
@@ -16,6 +16,7 @@ import { checkRelease } from '../npmRegistry.js';
|
|
|
16
16
|
import { conflictingLegacyValues, readPath, readSetting } from '../settingsStore.js';
|
|
17
17
|
import { instanceTarget, rootTarget } from '../target.js';
|
|
18
18
|
import { isXppConfigStale, listXppConfigs, xppConfigDir } from '../xppConfig.js';
|
|
19
|
+
import { describePackagesRootScan, packagesRoots } from '../../utils/packagesRoot.js';
|
|
19
20
|
const REQUIRED_NODE_MAJOR = 24;
|
|
20
21
|
/**
|
|
21
22
|
* How to reach the wizard from here. `npm run setup` only exists in a checkout
|
|
@@ -74,6 +75,40 @@ function checkConfig(target, label) {
|
|
|
74
75
|
fix: SETUP_COMMAND,
|
|
75
76
|
};
|
|
76
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* The configured packages root against what the machine actually has.
|
|
80
|
+
*
|
|
81
|
+
* A packagePath pointing at a drive this image does not use is the failure
|
|
82
|
+
* behind "setup can't find the namespaces" (#769) — the index then builds from
|
|
83
|
+
* nothing and every lookup comes back empty, with no error saying why. Naming
|
|
84
|
+
* the detected volume turns that into a one-line fix.
|
|
85
|
+
*/
|
|
86
|
+
function checkPackagesRoot(store, label) {
|
|
87
|
+
if (!isWindows)
|
|
88
|
+
return [];
|
|
89
|
+
const configured = String(readSetting(store, settingByPath('environment.packagePath')) ?? '').trim();
|
|
90
|
+
const detected = packagesRoots();
|
|
91
|
+
if (!configured) {
|
|
92
|
+
// UDE resolves its roots from the XPP config, so silence here is normal.
|
|
93
|
+
if (detected.length === 0)
|
|
94
|
+
return [];
|
|
95
|
+
return [{
|
|
96
|
+
severity: 'info',
|
|
97
|
+
message: `${label}: packages root not configured — the server will use ${detected[0]}`,
|
|
98
|
+
}];
|
|
99
|
+
}
|
|
100
|
+
if (fs.existsSync(configured)) {
|
|
101
|
+
return [{ severity: 'ok', message: `${label}: packages root OK (${configured})` }];
|
|
102
|
+
}
|
|
103
|
+
return [{
|
|
104
|
+
severity: 'fail',
|
|
105
|
+
message: `${label}: packages root does not exist (${configured})` +
|
|
106
|
+
(detected.length > 0 ? `\n found instead: ${detected.join(', ')}` : `\n ${describePackagesRootScan()}`),
|
|
107
|
+
fix: detected.length > 0
|
|
108
|
+
? `set environment.packagePath to ${detected[0]} (${SETUP_COMMAND})`
|
|
109
|
+
: `point environment.packagePath at this machine's PackagesLocalDirectory (${SETUP_COMMAND})`,
|
|
110
|
+
}];
|
|
111
|
+
}
|
|
77
112
|
/** A legacy .env that disagrees with the config is a trap: the config wins. */
|
|
78
113
|
function legacyEnvChecks(target, label) {
|
|
79
114
|
if (!target.envFile || !fs.existsSync(target.store.configPath))
|
|
@@ -205,6 +240,8 @@ export async function doctorCommand() {
|
|
|
205
240
|
emit(checkConfig(root, 'Root'));
|
|
206
241
|
for (const r of legacyEnvChecks(root, 'Root'))
|
|
207
242
|
emit(r);
|
|
243
|
+
for (const r of checkPackagesRoot(root.store, 'Root'))
|
|
244
|
+
emit(r);
|
|
208
245
|
// Database (root)
|
|
209
246
|
emit(checkDb(root.store, paths.defaultDb, 'Root'));
|
|
210
247
|
// C# bridge: the only write path; Windows-only.
|
|
@@ -246,6 +283,8 @@ export async function doctorCommand() {
|
|
|
246
283
|
emit(r);
|
|
247
284
|
for (const r of legacyEnvChecks(target, `Instance '${inst.name}'`))
|
|
248
285
|
emit(r);
|
|
286
|
+
for (const r of checkPackagesRoot(target.store, `Instance '${inst.name}'`))
|
|
287
|
+
emit(r);
|
|
249
288
|
emit(checkDb(target.store, resolve(inst.dir, 'data', 'xpp-metadata.db'), `Instance '${inst.name}'`));
|
|
250
289
|
if (isWindows && isXppConfigStale(target.store)) {
|
|
251
290
|
emit({
|
|
@@ -8,6 +8,7 @@ import * as fs from 'node:fs';
|
|
|
8
8
|
import { resolve } from 'node:path';
|
|
9
9
|
import { settingByPath, settingsInSection } from '../../config/settings.js';
|
|
10
10
|
import { pinBridgeExe } from '../bridgePath.js';
|
|
11
|
+
import { maybePrepareCopilotInstructions } from '../copilotFiles.js';
|
|
11
12
|
import { createInstance, getInstance, listInstances, normalizeInstanceLayout, suggestPort } from '../instances.js';
|
|
12
13
|
import { mcpJsonNote, placementNote, stdioServer } from '../mcpJson.js';
|
|
13
14
|
import { selectXppConfig } from './config.js';
|
|
@@ -16,6 +17,7 @@ import { openInstanceStore, readPath, readSetting, saveStore, writeSetting } fro
|
|
|
16
17
|
import { instanceTarget } from '../target.js';
|
|
17
18
|
import { askConfirm, askSelect, askText, p } from '../ui.js';
|
|
18
19
|
import { listXppConfigs, xppConfigDir } from '../xppConfig.js';
|
|
20
|
+
import { findPackagesRoot } from '../../utils/packagesRoot.js';
|
|
19
21
|
import { rebuildIndex } from './indexCmd.js';
|
|
20
22
|
const dbPathSetting = settingByPath('index.dbPath');
|
|
21
23
|
const xppConfigNameSetting = settingByPath('environment.xppConfigName');
|
|
@@ -92,17 +94,25 @@ export async function instanceAddCommand(name, portArg) {
|
|
|
92
94
|
await selectXppConfig(store);
|
|
93
95
|
}
|
|
94
96
|
else {
|
|
95
|
-
await askSetting(store, settingByPath('environment.packagePath'), {
|
|
97
|
+
await askSetting(store, settingByPath('environment.packagePath'), {
|
|
98
|
+
required: true,
|
|
99
|
+
initial: findPackagesRoot() ?? undefined,
|
|
100
|
+
});
|
|
96
101
|
await askSetting(store, settingByPath('environment.customModels'), { required: true });
|
|
97
102
|
}
|
|
98
103
|
p.log.step('Workspace and naming');
|
|
99
104
|
await askSetting(store, settingByPath('workspace.modelName'));
|
|
100
105
|
await askSetting(store, settingByPath('workspace.path'));
|
|
106
|
+
await askSetting(store, settingByPath('workspace.solutionsPath'));
|
|
101
107
|
await askSettings(store, settingsInSection('naming', 'basic'));
|
|
102
108
|
p.log.step('Metadata index');
|
|
103
109
|
await askSettings(store, settingsInSection('index', 'basic'));
|
|
104
110
|
await askAdvanced(store, ['environment', 'workspace', 'index', 'server', 'bridge', 'behavior']);
|
|
105
111
|
saveStore(store);
|
|
112
|
+
// Copilot needs the instructions file just as much as it needs .mcp.json,
|
|
113
|
+
// and an instance is somebody's only setup run — asking here is the only
|
|
114
|
+
// chance scenario F gets.
|
|
115
|
+
await maybePrepareCopilotInstructions(String(readSetting(store, settingByPath('workspace.solutionsPath')) ?? ''));
|
|
106
116
|
// Both ways to reach this instance: the IDE spawning it over stdio with its
|
|
107
117
|
// own config, or an HTTP client on the port it was given.
|
|
108
118
|
mcpJsonNote({
|
|
@@ -15,10 +15,12 @@ import { DOTNET_MISSING, dataRoot, installMode, isWindows, paths, repoRoot, setD
|
|
|
15
15
|
import { settingByPath, settingsInSection } from '../../config/settings.js';
|
|
16
16
|
import { commandExists, runExe, runShell } from '../exec.js';
|
|
17
17
|
import { pinBridgeExe } from '../bridgePath.js';
|
|
18
|
+
import { maybePrepareCopilotInstructions } from '../copilotFiles.js';
|
|
18
19
|
import { mcpJsonNote, placementNote, stdioServer } from '../mcpJson.js';
|
|
19
20
|
import { checkRelease } from '../npmRegistry.js';
|
|
20
21
|
import { askAdvanced, askSecrets, askSetting, askSettings } from '../settingsPrompt.js';
|
|
21
22
|
import { migrateLegacyEnv, openStore, readSetting, saveStore, writeSetting } from '../settingsStore.js';
|
|
23
|
+
import { findPackagesRoot } from '../../utils/packagesRoot.js';
|
|
22
24
|
import { rootTarget } from '../target.js';
|
|
23
25
|
import { askConfirm, askSelect, askText, p, requireFullInstall } from '../ui.js';
|
|
24
26
|
import { listXppConfigs } from '../xppConfig.js';
|
|
@@ -187,7 +189,13 @@ async function configureEnvironment(store, scenario) {
|
|
|
187
189
|
}
|
|
188
190
|
return 'ude';
|
|
189
191
|
}
|
|
190
|
-
|
|
192
|
+
// Offer the AosService volume this machine actually has. Which drive that is
|
|
193
|
+
// differs per VM image (K:, C:, J:, …), and a wrong guess is what turns the
|
|
194
|
+
// rest of setup into "no namespaces found" (#769).
|
|
195
|
+
const detected = findPackagesRoot();
|
|
196
|
+
if (detected)
|
|
197
|
+
p.log.success(`Found PackagesLocalDirectory at ${detected}`);
|
|
198
|
+
await askSetting(store, setting('environment.packagePath'), { required: true, initial: detected ?? undefined });
|
|
191
199
|
await askSetting(store, setting('environment.customModels'), { required: true });
|
|
192
200
|
return 'traditional';
|
|
193
201
|
}
|
|
@@ -198,6 +206,10 @@ async function configureWorkspace(store, scenario) {
|
|
|
198
206
|
await askSetting(store, setting('workspace.path'), { required: scenario === 'hybrid' || scenario === 'ude' });
|
|
199
207
|
await askSetting(store, setting('workspace.solutionsPath'));
|
|
200
208
|
}
|
|
209
|
+
/** Where the user keeps the .sln folders — the copy target for the client files. */
|
|
210
|
+
function solutionsPath(store) {
|
|
211
|
+
return String(readSetting(store, setting('workspace.solutionsPath')) ?? '');
|
|
212
|
+
}
|
|
201
213
|
async function configureNaming(store) {
|
|
202
214
|
p.log.step('Naming');
|
|
203
215
|
await askSettings(store, settingsInSection('naming', 'basic'));
|
|
@@ -216,67 +228,6 @@ async function maybeBuildIndex() {
|
|
|
216
228
|
}
|
|
217
229
|
return rebuildIndex(rootTarget());
|
|
218
230
|
}
|
|
219
|
-
function writeCopilotSetupReadme(stageDir, includeLocalMcpCopy) {
|
|
220
|
-
const readmePath = resolve(stageDir, 'README.md');
|
|
221
|
-
const lines = [
|
|
222
|
-
'# VS setup quick guide',
|
|
223
|
-
'',
|
|
224
|
-
'This folder is generated by `d365fo-mcp setup` to simplify Visual Studio onboarding.',
|
|
225
|
-
'',
|
|
226
|
-
'1. Copy .mcp.json',
|
|
227
|
-
' - Recommended (all solutions): %USERPROFILE%\\.mcp.json',
|
|
228
|
-
' - Per solution: place .mcp.json next to your .sln file',
|
|
229
|
-
includeLocalMcpCopy
|
|
230
|
-
? ' - Local copy prepared here: .mcp.json'
|
|
231
|
-
: ` - Local copy path: ${paths.mcpSuggestion}`,
|
|
232
|
-
'',
|
|
233
|
-
'2. Copy .github/copilot-instructions.md',
|
|
234
|
-
' - Destination: a parent folder of your solution directories',
|
|
235
|
-
' - Why: provides mandatory D365FO tool-routing and safety rules for Copilot',
|
|
236
|
-
'',
|
|
237
|
-
'3. Restart Visual Studio after copying files.',
|
|
238
|
-
];
|
|
239
|
-
fs.writeFileSync(readmePath, lines.join('\n') + '\n', 'utf8');
|
|
240
|
-
}
|
|
241
|
-
async function maybePrepareCopilotInstructions(store) {
|
|
242
|
-
const source = resolve(repoRoot, '.github', 'copilot-instructions.md');
|
|
243
|
-
if (!fs.existsSync(source)) {
|
|
244
|
-
p.log.warn('Cannot find .github\\copilot-instructions.md in the package; skipping copy helper.');
|
|
245
|
-
return {};
|
|
246
|
-
}
|
|
247
|
-
const wantsDirectCopy = await askConfirm('Create/copy .github/copilot-instructions.md into the solutions folder now?', true);
|
|
248
|
-
const solutionsPath = String(readSetting(store, setting('workspace.solutionsPath')) ?? '').trim();
|
|
249
|
-
if (wantsDirectCopy && solutionsPath) {
|
|
250
|
-
const targetDir = resolve(solutionsPath, '.github');
|
|
251
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
252
|
-
fs.copyFileSync(source, resolve(targetDir, 'copilot-instructions.md'));
|
|
253
|
-
p.log.success(`Prepared: ${resolve(targetDir, 'copilot-instructions.md')}`);
|
|
254
|
-
return {};
|
|
255
|
-
}
|
|
256
|
-
const stageDir = resolve(dataRoot(), 'copilot-setup');
|
|
257
|
-
const stageGitHubDir = resolve(stageDir, '.github');
|
|
258
|
-
fs.mkdirSync(stageGitHubDir, { recursive: true });
|
|
259
|
-
fs.copyFileSync(source, resolve(stageGitHubDir, 'copilot-instructions.md'));
|
|
260
|
-
writeCopilotSetupReadme(stageDir, false);
|
|
261
|
-
if (wantsDirectCopy && !solutionsPath) {
|
|
262
|
-
p.log.warn('Solutions folder is empty, so files were prepared in the local staging folder instead.');
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
p.log.info('Copy was skipped; files were prepared in a local staging folder for later use.');
|
|
266
|
-
}
|
|
267
|
-
p.log.info(`Staging folder: ${stageDir}`);
|
|
268
|
-
return { stagingDir: stageDir };
|
|
269
|
-
}
|
|
270
|
-
function finalizeStagedCopilotFiles(plan) {
|
|
271
|
-
if (!plan.stagingDir)
|
|
272
|
-
return;
|
|
273
|
-
const localMcp = paths.mcpSuggestion;
|
|
274
|
-
if (fs.existsSync(localMcp)) {
|
|
275
|
-
fs.copyFileSync(localMcp, resolve(plan.stagingDir, '.mcp.json'));
|
|
276
|
-
writeCopilotSetupReadme(plan.stagingDir, true);
|
|
277
|
-
p.log.success(`Prepared: ${resolve(plan.stagingDir, '.mcp.json')}`);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
231
|
export function savedNote(store) {
|
|
281
232
|
const rel = relative(dataRoot(), store.configPath) || store.configPath;
|
|
282
233
|
const lines = [`Settings written to ${rel}`];
|
|
@@ -332,7 +283,7 @@ export async function setupCommand() {
|
|
|
332
283
|
const url = await askText({ message: 'Azure server URL', placeholder: 'https://your-server.azurewebsites.net/mcp/', required: true });
|
|
333
284
|
await configureEnvironment(store, scenario);
|
|
334
285
|
await configureWorkspace(store, scenario);
|
|
335
|
-
|
|
286
|
+
await maybePrepareCopilotInstructions(solutionsPath(store));
|
|
336
287
|
await configureNaming(store);
|
|
337
288
|
await askSecrets(store, ['behavior']);
|
|
338
289
|
await askAdvanced(store, ['environment', 'workspace', 'naming', 'bridge', 'behavior', 'server']);
|
|
@@ -342,7 +293,6 @@ export async function setupCommand() {
|
|
|
342
293
|
'd365fo-azure': { url },
|
|
343
294
|
'd365fo-local': stdioServer(store),
|
|
344
295
|
});
|
|
345
|
-
finalizeStagedCopilotFiles(copilotPlan);
|
|
346
296
|
placementNote();
|
|
347
297
|
p.outro('Hybrid setup complete — no local index needed (Azure serves the search).');
|
|
348
298
|
return;
|
|
@@ -351,7 +301,7 @@ export async function setupCommand() {
|
|
|
351
301
|
writeSetting(store, setting('server.mode'), 'full');
|
|
352
302
|
await configureEnvironment(store, scenario);
|
|
353
303
|
await configureWorkspace(store, scenario);
|
|
354
|
-
|
|
304
|
+
await maybePrepareCopilotInstructions(solutionsPath(store));
|
|
355
305
|
await configureNaming(store);
|
|
356
306
|
await configureIndex(store);
|
|
357
307
|
let port = Number(readSetting(store, setting('server.port')) ?? 8080);
|
|
@@ -369,7 +319,6 @@ export async function setupCommand() {
|
|
|
369
319
|
}
|
|
370
320
|
if (scenario === 'local-http') {
|
|
371
321
|
mcpJsonNote({ 'd365fo-mcp-tools': { url: `http://localhost:${port}/mcp/` } });
|
|
372
|
-
finalizeStagedCopilotFiles(copilotPlan);
|
|
373
322
|
placementNote();
|
|
374
323
|
p.outro('Done. Start the server with: d365fo-mcp start');
|
|
375
324
|
return;
|
|
@@ -377,7 +326,6 @@ export async function setupCommand() {
|
|
|
377
326
|
// D / E — the IDE spawns dist/index.js itself and is pointed at the config
|
|
378
327
|
// file; every other setting comes from there.
|
|
379
328
|
mcpJsonNote({ 'd365fo-mcp-tools': stdioServer(store) });
|
|
380
|
-
finalizeStagedCopilotFiles(copilotPlan);
|
|
381
329
|
placementNote();
|
|
382
330
|
p.outro('Done. VS spawns the server automatically — no manual start needed.');
|
|
383
331
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offer to place copilot-instructions.md, falling back to a staging folder.
|
|
3
|
+
*
|
|
4
|
+
* `solutionsPath` is what the user gave for workspace.solutionsPath; when it
|
|
5
|
+
* is empty there is nowhere to copy to, so the file is staged instead and the
|
|
6
|
+
* README says where it has to end up.
|
|
7
|
+
*/
|
|
8
|
+
export declare function maybePrepareCopilotInstructions(solutionsPath: string): Promise<void>;
|
|
9
|
+
//# sourceMappingURL=copilotFiles.d.ts.map
|