bcdocker 1.0.0 → 1.0.2
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/LICENSE +1 -1
- package/README.md +75 -104
- package/dist/cli-main.d.ts +2 -0
- package/dist/cli-main.js +12 -0
- package/dist/cli.d.ts +4 -1
- package/dist/cli.js +198 -189
- package/dist/executor.d.ts +6 -0
- package/dist/executor.js +20 -5
- package/dist/server-handlers.d.ts +145 -0
- package/dist/server-handlers.js +147 -0
- package/dist/server.d.ts +3 -1
- package/dist/server.js +29 -148
- package/package.json +11 -6
- package/ps/BCDocker.psm1 +1 -0
- package/ps/Container.ps1 +48 -11
- package/ps/Helpers.ps1 +20 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { runPowerShell, runRawPowerShell, psEscape } from "./executor.js";
|
|
2
|
+
function mcpText(text) {
|
|
3
|
+
return { content: [{ type: "text", text }] };
|
|
4
|
+
}
|
|
5
|
+
export async function handleListContainers() {
|
|
6
|
+
const result = await runRawPowerShell(`
|
|
7
|
+
Import-Module BcContainerHelper -DisableNameChecking -ErrorAction Stop
|
|
8
|
+
$containers = Get-BcContainers
|
|
9
|
+
if ($containers.Count -eq 0) {
|
|
10
|
+
Write-Output "No BC containers found."
|
|
11
|
+
} else {
|
|
12
|
+
foreach ($name in $containers) {
|
|
13
|
+
$status = docker inspect $name --format '{{.State.Status}}' 2>$null
|
|
14
|
+
Write-Output "$name [$status]"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
`);
|
|
18
|
+
return mcpText(result.stdout || "No containers found.");
|
|
19
|
+
}
|
|
20
|
+
export async function handleContainerInfo({ containerName }) {
|
|
21
|
+
const cn = psEscape(containerName);
|
|
22
|
+
const result = await runPowerShell(`
|
|
23
|
+
Import-Module BcContainerHelper -DisableNameChecking -ErrorAction Stop
|
|
24
|
+
$bcVersion = Get-BcContainerNavVersion -containerOrImageName '${cn}'
|
|
25
|
+
$webUrl = Get-BCDockerWebClientUrl -ContainerName '${cn}' -UseHttps:$false
|
|
26
|
+
$status = docker inspect '${cn}' --format '{{.State.Status}}' 2>$null
|
|
27
|
+
Write-Output "Container : ${cn}"
|
|
28
|
+
Write-Output "Status : $status"
|
|
29
|
+
Write-Output "BC Version : $bcVersion"
|
|
30
|
+
Write-Output "Web Client : $webUrl"
|
|
31
|
+
Write-Output "OData/API : http://${cn}:7048/BC/api"
|
|
32
|
+
Write-Output "Dev Service: http://${cn}:7049/BC"
|
|
33
|
+
`);
|
|
34
|
+
return mcpText(result.stdout);
|
|
35
|
+
}
|
|
36
|
+
export async function handleCreateContainer({ containerName, version, country, userName, password, memoryLimit, isolation, testToolkit, bypassCDN, licenseFile, }) {
|
|
37
|
+
const includeToolkit = testToolkit !== "none" ? "$true" : "$false";
|
|
38
|
+
const libOnly = testToolkit === "libraries" ? "$true" : "$false";
|
|
39
|
+
const cdn = bypassCDN ? "-BypassCDN" : "";
|
|
40
|
+
const lic = licenseFile ? `-LicenseFile '${psEscape(licenseFile)}'` : "";
|
|
41
|
+
const result = await runPowerShell(`
|
|
42
|
+
New-BCDContainer \`
|
|
43
|
+
-ContainerName '${psEscape(containerName)}' \`
|
|
44
|
+
-Version '${psEscape(version)}' \`
|
|
45
|
+
-Country '${psEscape(country)}' \`
|
|
46
|
+
-UserName '${psEscape(userName)}' \`
|
|
47
|
+
-Password '${psEscape(password)}' \`
|
|
48
|
+
-MemoryLimit '${psEscape(memoryLimit)}' \`
|
|
49
|
+
-Isolation '${psEscape(isolation)}' \`
|
|
50
|
+
-IncludeTestToolkit ${includeToolkit} \`
|
|
51
|
+
-TestLibrariesOnly ${libOnly} \`
|
|
52
|
+
${cdn} ${lic}
|
|
53
|
+
`, 1_800_000);
|
|
54
|
+
return mcpText(result.stdout || "Container creation complete.");
|
|
55
|
+
}
|
|
56
|
+
export async function handleRemoveContainer({ containerName }) {
|
|
57
|
+
const result = await runPowerShell(`Remove-BCDContainer -ContainerName '${psEscape(containerName)}'`);
|
|
58
|
+
return mcpText(result.stdout || `Container '${psEscape(containerName)}' removed.`);
|
|
59
|
+
}
|
|
60
|
+
export async function handleStartContainer({ containerName }) {
|
|
61
|
+
const result = await runPowerShell(`Start-BCDContainer -ContainerName '${psEscape(containerName)}'`);
|
|
62
|
+
return mcpText(result.stdout || `Container '${psEscape(containerName)}' started.`);
|
|
63
|
+
}
|
|
64
|
+
export async function handleStopContainer({ containerName }) {
|
|
65
|
+
const result = await runPowerShell(`Stop-BCDContainer -ContainerName '${psEscape(containerName)}'`);
|
|
66
|
+
return mcpText(result.stdout || `Container '${psEscape(containerName)}' stopped.`);
|
|
67
|
+
}
|
|
68
|
+
export async function handleRestartContainer({ containerName }) {
|
|
69
|
+
const result = await runPowerShell(`Restart-BCDContainer -ContainerName '${psEscape(containerName)}'`);
|
|
70
|
+
return mcpText(result.stdout || `Container '${psEscape(containerName)}' restarted.`);
|
|
71
|
+
}
|
|
72
|
+
export async function handleOpenWebclient({ containerName }) {
|
|
73
|
+
const result = await runPowerShell(`
|
|
74
|
+
$url = Get-BCDockerWebClientUrl -ContainerName '${psEscape(containerName)}' -UseHttps:$false
|
|
75
|
+
Write-Output $url
|
|
76
|
+
`);
|
|
77
|
+
return mcpText(result.stdout || "Could not resolve URL.");
|
|
78
|
+
}
|
|
79
|
+
export async function handleListApps({ containerName, publisher, }) {
|
|
80
|
+
const cn = psEscape(containerName);
|
|
81
|
+
const filter = publisher ? `| Where-Object { $_.Publisher -eq '${psEscape(publisher)}' }` : "";
|
|
82
|
+
const result = await runRawPowerShell(`
|
|
83
|
+
Import-Module BcContainerHelper -DisableNameChecking -ErrorAction Stop
|
|
84
|
+
$apps = Get-BcContainerAppInfo -containerName '${cn}' -tenant default -tenantSpecificProperties ${filter}
|
|
85
|
+
$apps | Select-Object Name, Publisher, Version, IsInstalled, Scope |
|
|
86
|
+
Sort-Object Publisher, Name |
|
|
87
|
+
Format-Table -AutoSize | Out-String -Width 200
|
|
88
|
+
`);
|
|
89
|
+
return mcpText(result.stdout || "No apps found.");
|
|
90
|
+
}
|
|
91
|
+
export async function handleInstallApp({ containerName, appFile, userName, password, }) {
|
|
92
|
+
const result = await runPowerShell(`
|
|
93
|
+
$cred = Get-BCCredential -UserName '${psEscape(userName)}' -Password '${psEscape(password)}'
|
|
94
|
+
Install-BCDApp -ContainerName '${psEscape(containerName)}' -AppFile '${psEscape(appFile)}' -Credential $cred
|
|
95
|
+
`);
|
|
96
|
+
return mcpText(result.stdout || "App installed.");
|
|
97
|
+
}
|
|
98
|
+
export async function handleUninstallApp({ containerName, appName, appPublisher, }) {
|
|
99
|
+
const cn = psEscape(containerName);
|
|
100
|
+
const an = psEscape(appName);
|
|
101
|
+
const ap = psEscape(appPublisher);
|
|
102
|
+
const result = await runRawPowerShell(`
|
|
103
|
+
Import-Module BcContainerHelper -DisableNameChecking -ErrorAction Stop
|
|
104
|
+
$allApps = Get-BcContainerAppInfo -containerName '${cn}' -tenant default -tenantSpecificProperties
|
|
105
|
+
$sorted = Get-BcContainerAppInfo -containerName '${cn}' -tenant default -tenantSpecificProperties -sort DependenciesLast
|
|
106
|
+
$target = $allApps | Where-Object { $_.Name -eq '${an}' -and $_.Publisher -eq '${ap}' }
|
|
107
|
+
if (-not $target) { Write-Output "App '${an}' by '${ap}' not found."; exit }
|
|
108
|
+
foreach ($app in $sorted) {
|
|
109
|
+
if ($app.Name -eq '${an}' -and $app.Publisher -eq '${ap}' -and $app.IsInstalled) {
|
|
110
|
+
UnInstall-BcContainerApp -name $app.Name -containerName '${cn}' -publisher $app.Publisher -version $app.Version -force
|
|
111
|
+
Write-Output "Uninstalled: $($app.Name) v$($app.Version)"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
`);
|
|
115
|
+
return mcpText(result.stdout || "Uninstall complete.");
|
|
116
|
+
}
|
|
117
|
+
export async function handlePublishProject({ containerName, projectFolder, userName, password, }) {
|
|
118
|
+
const result = await runPowerShell(`
|
|
119
|
+
$cred = Get-BCCredential -UserName '${psEscape(userName)}' -Password '${psEscape(password)}'
|
|
120
|
+
Publish-BCDProject -ContainerName '${psEscape(containerName)}' -ProjectFolder '${psEscape(projectFolder)}' -Credential $cred
|
|
121
|
+
`, 300_000);
|
|
122
|
+
return mcpText(result.stdout || "Project compiled and published.");
|
|
123
|
+
}
|
|
124
|
+
export async function handleImportTestToolkit({ containerName, librariesOnly, userName, password, }) {
|
|
125
|
+
const libPs = librariesOnly ? "$true" : "$false";
|
|
126
|
+
const result = await runPowerShell(`
|
|
127
|
+
$cred = Get-BCCredential -UserName '${psEscape(userName)}' -Password '${psEscape(password)}'
|
|
128
|
+
Import-BCDTestToolkit -ContainerName '${psEscape(containerName)}' -Credential $cred -LibrariesOnly ${libPs}
|
|
129
|
+
`, 300_000);
|
|
130
|
+
return mcpText(result.stdout || "Test toolkit imported.");
|
|
131
|
+
}
|
|
132
|
+
export async function handleImportLicense({ containerName, licenseFile, }) {
|
|
133
|
+
const result = await runPowerShell(`Import-BCDLicense -ContainerName '${psEscape(containerName)}' -LicenseFile '${psEscape(licenseFile)}'`);
|
|
134
|
+
return mcpText(result.stdout || "License imported.");
|
|
135
|
+
}
|
|
136
|
+
export async function handleRunTests({ containerName, testCodeunitId, testFunctionName, appProjectFolder, userName, password, }) {
|
|
137
|
+
const params = [`-ContainerName '${psEscape(containerName)}'`];
|
|
138
|
+
params.push(`-Credential (Get-BCCredential -UserName '${psEscape(userName)}' -Password '${psEscape(password)}')`);
|
|
139
|
+
if (testCodeunitId)
|
|
140
|
+
params.push(`-TestCodeunitId ${testCodeunitId}`);
|
|
141
|
+
if (testFunctionName)
|
|
142
|
+
params.push(`-TestFunctionName '${psEscape(testFunctionName)}'`);
|
|
143
|
+
if (appProjectFolder)
|
|
144
|
+
params.push(`-AppProjectFolder '${psEscape(appProjectFolder)}'`);
|
|
145
|
+
const result = await runPowerShell(`Invoke-BCDTests ${params.join(" ")}`, 600_000);
|
|
146
|
+
return mcpText(result.stdout || "Test run complete.");
|
|
147
|
+
}
|
package/dist/server.d.ts
CHANGED
package/dist/server.js
CHANGED
|
@@ -1,42 +1,16 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
3
5
|
import { z } from "zod";
|
|
4
|
-
import
|
|
5
|
-
const server = new McpServer({
|
|
6
|
+
import * as handlers from "./server-handlers.js";
|
|
7
|
+
export const server = new McpServer({
|
|
6
8
|
name: "bcd",
|
|
7
|
-
version: "1.0.
|
|
9
|
+
version: "1.0.2",
|
|
8
10
|
});
|
|
9
11
|
// ── Container Tools ──────────────────────────────────────
|
|
10
|
-
server.tool("list-containers", "List all Business Central Docker containers with their running status", {},
|
|
11
|
-
|
|
12
|
-
Import-Module BcContainerHelper -DisableNameChecking -ErrorAction Stop
|
|
13
|
-
$containers = Get-BcContainers
|
|
14
|
-
if ($containers.Count -eq 0) {
|
|
15
|
-
Write-Output "No BC containers found."
|
|
16
|
-
} else {
|
|
17
|
-
foreach ($name in $containers) {
|
|
18
|
-
$status = docker inspect $name --format '{{.State.Status}}' 2>$null
|
|
19
|
-
Write-Output "$name [$status]"
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
`);
|
|
23
|
-
return { content: [{ type: "text", text: result.stdout || "No containers found." }] };
|
|
24
|
-
});
|
|
25
|
-
server.tool("container-info", "Get detailed info about a BC container: version, status, and service endpoints", { containerName: z.string().describe("Name of the BC container") }, async ({ containerName }) => {
|
|
26
|
-
const result = await runRawPowerShell(`
|
|
27
|
-
Import-Module BcContainerHelper -DisableNameChecking -ErrorAction Stop
|
|
28
|
-
$bcVersion = Get-BcContainerNavVersion -containerNameOrId '${containerName}'
|
|
29
|
-
$webUrl = Get-BcContainerUrl -containerName '${containerName}' -useHttps:$false
|
|
30
|
-
$status = docker inspect '${containerName}' --format '{{.State.Status}}' 2>$null
|
|
31
|
-
Write-Output "Container : ${containerName}"
|
|
32
|
-
Write-Output "Status : $status"
|
|
33
|
-
Write-Output "BC Version : $bcVersion"
|
|
34
|
-
Write-Output "Web Client : $webUrl"
|
|
35
|
-
Write-Output "OData/API : http://${containerName}:7048/BC/api"
|
|
36
|
-
Write-Output "Dev Service: http://${containerName}:7049/BC"
|
|
37
|
-
`);
|
|
38
|
-
return { content: [{ type: "text", text: result.stdout }] };
|
|
39
|
-
});
|
|
12
|
+
server.tool("list-containers", "List all Business Central Docker containers with their running status", {}, handlers.handleListContainers);
|
|
13
|
+
server.tool("container-info", "Get detailed info about a BC container: version, status, and service endpoints", { containerName: z.string().describe("Name of the BC container") }, handlers.handleContainerInfo);
|
|
40
14
|
server.tool("create-container", "Create a new Business Central Docker container with test toolkit. Long-running (5-30 min).", {
|
|
41
15
|
containerName: z.string().default("bcsandbox").describe("Docker container name"),
|
|
42
16
|
version: z.string().default("sandbox").describe("BC version: 'sandbox', 'onprem', or specific like '26.0'"),
|
|
@@ -48,131 +22,45 @@ server.tool("create-container", "Create a new Business Central Docker container
|
|
|
48
22
|
testToolkit: z.enum(["none", "libraries", "full"]).default("libraries").describe("Test toolkit: none, libraries (faster), or full (all MS tests)"),
|
|
49
23
|
bypassCDN: z.boolean().default(false).describe("Skip Azure CDN, download from blob storage directly"),
|
|
50
24
|
licenseFile: z.string().optional().describe("Path or URL to .bclicense / .flf license file"),
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
New-BCDContainer \`
|
|
58
|
-
-ContainerName '${containerName}' \`
|
|
59
|
-
-Version '${version}' \`
|
|
60
|
-
-Country '${country}' \`
|
|
61
|
-
-UserName '${userName}' \`
|
|
62
|
-
-Password '${password}' \`
|
|
63
|
-
-MemoryLimit '${memoryLimit}' \`
|
|
64
|
-
-Isolation '${isolation}' \`
|
|
65
|
-
-IncludeTestToolkit ${includeToolkit} \`
|
|
66
|
-
-TestLibrariesOnly ${libOnly} \`
|
|
67
|
-
${cdn} ${lic}
|
|
68
|
-
`, 1_800_000);
|
|
69
|
-
return { content: [{ type: "text", text: result.stdout || "Container creation complete." }] };
|
|
70
|
-
});
|
|
71
|
-
server.tool("remove-container", "Remove a Business Central Docker container", { containerName: z.string().describe("Name of the container to remove") }, async ({ containerName }) => {
|
|
72
|
-
const result = await runPowerShell(`Remove-BCDContainer -ContainerName '${containerName}'`);
|
|
73
|
-
return { content: [{ type: "text", text: result.stdout || `Container '${containerName}' removed.` }] };
|
|
74
|
-
});
|
|
75
|
-
server.tool("start-container", "Start a stopped BC Docker container", { containerName: z.string().describe("Name of the container to start") }, async ({ containerName }) => {
|
|
76
|
-
const result = await runPowerShell(`Start-BCDContainer -ContainerName '${containerName}'`);
|
|
77
|
-
return { content: [{ type: "text", text: result.stdout || `Container '${containerName}' started.` }] };
|
|
78
|
-
});
|
|
79
|
-
server.tool("stop-container", "Stop a running BC Docker container", { containerName: z.string().describe("Name of the container to stop") }, async ({ containerName }) => {
|
|
80
|
-
const result = await runPowerShell(`Stop-BCDContainer -ContainerName '${containerName}'`);
|
|
81
|
-
return { content: [{ type: "text", text: result.stdout || `Container '${containerName}' stopped.` }] };
|
|
82
|
-
});
|
|
83
|
-
server.tool("restart-container", "Restart a BC Docker container", { containerName: z.string().describe("Name of the container to restart") }, async ({ containerName }) => {
|
|
84
|
-
const result = await runPowerShell(`Restart-BCDContainer -ContainerName '${containerName}'`);
|
|
85
|
-
return { content: [{ type: "text", text: result.stdout || `Container '${containerName}' restarted.` }] };
|
|
86
|
-
});
|
|
87
|
-
server.tool("open-webclient", "Open the BC Web Client URL for a container. Returns the URL.", { containerName: z.string().describe("Name of the BC container") }, async ({ containerName }) => {
|
|
88
|
-
const result = await runRawPowerShell(`
|
|
89
|
-
Import-Module BcContainerHelper -DisableNameChecking -ErrorAction Stop
|
|
90
|
-
$url = Get-BcContainerUrl -containerName '${containerName}' -useHttps:$false
|
|
91
|
-
Write-Output $url
|
|
92
|
-
`);
|
|
93
|
-
return { content: [{ type: "text", text: result.stdout || "Could not resolve URL." }] };
|
|
94
|
-
});
|
|
25
|
+
}, handlers.handleCreateContainer);
|
|
26
|
+
server.tool("remove-container", "Remove a Business Central Docker container", { containerName: z.string().describe("Name of the container to remove") }, handlers.handleRemoveContainer);
|
|
27
|
+
server.tool("start-container", "Start a stopped BC Docker container", { containerName: z.string().describe("Name of the container to start") }, handlers.handleStartContainer);
|
|
28
|
+
server.tool("stop-container", "Stop a running BC Docker container", { containerName: z.string().describe("Name of the container to stop") }, handlers.handleStopContainer);
|
|
29
|
+
server.tool("restart-container", "Restart a BC Docker container", { containerName: z.string().describe("Name of the container to restart") }, handlers.handleRestartContainer);
|
|
30
|
+
server.tool("open-webclient", "Open the BC Web Client URL for a container. Returns the URL.", { containerName: z.string().describe("Name of the BC container") }, handlers.handleOpenWebclient);
|
|
95
31
|
// ── App Tools ────────────────────────────────────────────
|
|
96
32
|
server.tool("list-apps", "List all apps installed in a BC container, optionally filtered by publisher", {
|
|
97
33
|
containerName: z.string().describe("Target container name"),
|
|
98
34
|
publisher: z.string().optional().describe("Filter by publisher (e.g. 'Microsoft')"),
|
|
99
|
-
},
|
|
100
|
-
const filter = publisher
|
|
101
|
-
? `| Where-Object { $_.Publisher -eq '${publisher}' }`
|
|
102
|
-
: "";
|
|
103
|
-
const result = await runRawPowerShell(`
|
|
104
|
-
Import-Module BcContainerHelper -DisableNameChecking -ErrorAction Stop
|
|
105
|
-
$apps = Get-BcContainerAppInfo -containerName '${containerName}' -tenant default -tenantSpecificProperties ${filter}
|
|
106
|
-
$apps | Select-Object Name, Publisher, Version, IsInstalled, Scope |
|
|
107
|
-
Sort-Object Publisher, Name |
|
|
108
|
-
Format-Table -AutoSize | Out-String -Width 200
|
|
109
|
-
`);
|
|
110
|
-
return { content: [{ type: "text", text: result.stdout || "No apps found." }] };
|
|
111
|
-
});
|
|
35
|
+
}, handlers.handleListApps);
|
|
112
36
|
server.tool("install-app", "Publish and install a .app file into a BC container", {
|
|
113
37
|
containerName: z.string().describe("Target container name"),
|
|
114
38
|
appFile: z.string().describe("Full path to the .app file"),
|
|
115
39
|
userName: z.string().default("admin").describe("Admin username"),
|
|
116
40
|
password: z.string().default("P@ssw0rd!").describe("Admin password"),
|
|
117
|
-
},
|
|
118
|
-
const result = await runPowerShell(`
|
|
119
|
-
$cred = Get-BCCredential -UserName '${userName}' -Password '${password}'
|
|
120
|
-
Install-BCDApp -ContainerName '${containerName}' -AppFile '${appFile}' -Credential $cred
|
|
121
|
-
`);
|
|
122
|
-
return { content: [{ type: "text", text: result.stdout || "App installed." }] };
|
|
123
|
-
});
|
|
41
|
+
}, handlers.handleInstallApp);
|
|
124
42
|
server.tool("uninstall-app", "Uninstall an app from a BC container by name. Automatically handles dependency order.", {
|
|
125
43
|
containerName: z.string().describe("Target container name"),
|
|
126
44
|
appName: z.string().describe("Name of the app to uninstall"),
|
|
127
45
|
appPublisher: z.string().describe("Publisher of the app"),
|
|
128
|
-
},
|
|
129
|
-
const result = await runRawPowerShell(`
|
|
130
|
-
Import-Module BcContainerHelper -DisableNameChecking -ErrorAction Stop
|
|
131
|
-
$allApps = Get-BcContainerAppInfo -containerName '${containerName}' -tenant default -tenantSpecificProperties
|
|
132
|
-
$sorted = Get-BcContainerAppInfo -containerName '${containerName}' -tenant default -tenantSpecificProperties -sort DependenciesLast
|
|
133
|
-
$target = $allApps | Where-Object { $_.Name -eq '${appName}' -and $_.Publisher -eq '${appPublisher}' }
|
|
134
|
-
if (-not $target) { Write-Output "App '${appName}' by '${appPublisher}' not found."; exit }
|
|
135
|
-
foreach ($app in $sorted) {
|
|
136
|
-
if ($app.Name -eq '${appName}' -and $app.Publisher -eq '${appPublisher}' -and $app.IsInstalled) {
|
|
137
|
-
UnInstall-BcContainerApp -name $app.Name -containerName '${containerName}' -publisher $app.Publisher -version $app.Version -force
|
|
138
|
-
Write-Output "Uninstalled: $($app.Name) v$($app.Version)"
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
`);
|
|
142
|
-
return { content: [{ type: "text", text: result.stdout || "Uninstall complete." }] };
|
|
143
|
-
});
|
|
46
|
+
}, handlers.handleUninstallApp);
|
|
144
47
|
server.tool("publish-project", "Compile and publish an AL project folder into a BC container", {
|
|
145
48
|
containerName: z.string().describe("Target container name"),
|
|
146
49
|
projectFolder: z.string().describe("Full path to the AL project folder"),
|
|
147
50
|
userName: z.string().default("admin").describe("Admin username"),
|
|
148
51
|
password: z.string().default("P@ssw0rd!").describe("Admin password"),
|
|
149
|
-
},
|
|
150
|
-
const result = await runPowerShell(`
|
|
151
|
-
$cred = Get-BCCredential -UserName '${userName}' -Password '${password}'
|
|
152
|
-
Publish-BCDProject -ContainerName '${containerName}' -ProjectFolder '${projectFolder}' -Credential $cred
|
|
153
|
-
`, 300_000);
|
|
154
|
-
return { content: [{ type: "text", text: result.stdout || "Project compiled and published." }] };
|
|
155
|
-
});
|
|
52
|
+
}, handlers.handlePublishProject);
|
|
156
53
|
// ── Test & License Tools ─────────────────────────────────
|
|
157
54
|
server.tool("import-test-toolkit", "Import the BC Test Toolkit (libraries only or full framework) into a container", {
|
|
158
55
|
containerName: z.string().describe("Target container name"),
|
|
159
56
|
librariesOnly: z.boolean().default(true).describe("true = just helper libs (faster), false = full MS test codeunits"),
|
|
160
57
|
userName: z.string().default("admin").describe("Admin username"),
|
|
161
58
|
password: z.string().default("P@ssw0rd!").describe("Admin password"),
|
|
162
|
-
},
|
|
163
|
-
const result = await runPowerShell(`
|
|
164
|
-
$cred = Get-BCCredential -UserName '${userName}' -Password '${password}'
|
|
165
|
-
Import-BCDTestToolkit -ContainerName '${containerName}' -Credential $cred -LibrariesOnly $${librariesOnly}
|
|
166
|
-
`, 300_000);
|
|
167
|
-
return { content: [{ type: "text", text: result.stdout || "Test toolkit imported." }] };
|
|
168
|
-
});
|
|
59
|
+
}, handlers.handleImportTestToolkit);
|
|
169
60
|
server.tool("import-license", "Import a license file into a BC container", {
|
|
170
61
|
containerName: z.string().describe("Target container name"),
|
|
171
62
|
licenseFile: z.string().describe("Full path or URL to .bclicense / .flf file"),
|
|
172
|
-
},
|
|
173
|
-
const result = await runPowerShell(`Import-BCDLicense -ContainerName '${containerName}' -LicenseFile '${licenseFile}'`);
|
|
174
|
-
return { content: [{ type: "text", text: result.stdout || "License imported." }] };
|
|
175
|
-
});
|
|
63
|
+
}, handlers.handleImportLicense);
|
|
176
64
|
server.tool("run-tests", "Run AL tests in a BC container. Can run all tests, a specific codeunit, or compile-and-run from a project folder.", {
|
|
177
65
|
containerName: z.string().default("bcsandbox").describe("Target container name"),
|
|
178
66
|
testCodeunitId: z.number().optional().describe("Specific test codeunit ID (omit for all)"),
|
|
@@ -180,24 +68,17 @@ server.tool("run-tests", "Run AL tests in a BC container. Can run all tests, a s
|
|
|
180
68
|
appProjectFolder: z.string().optional().describe("AL test project folder to compile/publish/run"),
|
|
181
69
|
userName: z.string().default("admin").describe("Admin username"),
|
|
182
70
|
password: z.string().default("P@ssw0rd!").describe("Admin password"),
|
|
183
|
-
},
|
|
184
|
-
const params = [`-ContainerName '${containerName}'`];
|
|
185
|
-
params.push(`-Credential (Get-BCCredential -UserName '${userName}' -Password '${password}')`);
|
|
186
|
-
if (testCodeunitId)
|
|
187
|
-
params.push(`-TestCodeunitId ${testCodeunitId}`);
|
|
188
|
-
if (testFunctionName)
|
|
189
|
-
params.push(`-TestFunctionName '${testFunctionName}'`);
|
|
190
|
-
if (appProjectFolder)
|
|
191
|
-
params.push(`-AppProjectFolder '${appProjectFolder}'`);
|
|
192
|
-
const result = await runPowerShell(`Invoke-BCDTests ${params.join(" ")}`, 600_000);
|
|
193
|
-
return { content: [{ type: "text", text: result.stdout || "Test run complete." }] };
|
|
194
|
-
});
|
|
71
|
+
}, handlers.handleRunTests);
|
|
195
72
|
// ── Start ────────────────────────────────────────────────
|
|
196
|
-
async function
|
|
73
|
+
export async function startMcpServer() {
|
|
197
74
|
const transport = new StdioServerTransport();
|
|
198
75
|
await server.connect(transport);
|
|
199
76
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
77
|
+
const isMainModule = process.argv[1] &&
|
|
78
|
+
resolve(fileURLToPath(import.meta.url)) === resolve(process.argv[1]);
|
|
79
|
+
if (isMainModule) {
|
|
80
|
+
startMcpServer().catch((err) => {
|
|
81
|
+
console.error("MCP server failed:", err);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
});
|
|
84
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bcdocker",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "MCP server and CLI
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Node.js MCP server and CLI that drive the BCDocker Toolkit PowerShell module — for AI assistants and scripts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"bcd": "
|
|
7
|
+
"bcd": "dist/cli-main.js"
|
|
8
8
|
},
|
|
9
9
|
"main": "./dist/server.js",
|
|
10
10
|
"files": [
|
|
11
|
-
"dist",
|
|
11
|
+
"dist/*.js",
|
|
12
|
+
"dist/*.d.ts",
|
|
12
13
|
"ps",
|
|
13
14
|
"README.md",
|
|
14
15
|
"LICENSE"
|
|
@@ -17,6 +18,8 @@
|
|
|
17
18
|
"build": "tsc",
|
|
18
19
|
"start": "node dist/server.js",
|
|
19
20
|
"cli": "node dist/cli.js",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:coverage": "vitest run --coverage",
|
|
20
23
|
"prepublishOnly": "npm run build"
|
|
21
24
|
},
|
|
22
25
|
"keywords": [
|
|
@@ -31,7 +34,7 @@
|
|
|
31
34
|
"devops",
|
|
32
35
|
"cli"
|
|
33
36
|
],
|
|
34
|
-
"author": "
|
|
37
|
+
"author": "olederkach",
|
|
35
38
|
"license": "MIT",
|
|
36
39
|
"repository": {
|
|
37
40
|
"type": "git",
|
|
@@ -54,6 +57,8 @@
|
|
|
54
57
|
},
|
|
55
58
|
"devDependencies": {
|
|
56
59
|
"@types/node": "^22.0.0",
|
|
57
|
-
"
|
|
60
|
+
"@vitest/coverage-v8": "^3.0.0",
|
|
61
|
+
"typescript": "^5.7.0",
|
|
62
|
+
"vitest": "^3.0.0"
|
|
58
63
|
}
|
|
59
64
|
}
|
package/ps/BCDocker.psm1
CHANGED
package/ps/Container.ps1
CHANGED
|
@@ -97,13 +97,29 @@ function New-BCDContainer {
|
|
|
97
97
|
Write-BCSuccess "Resolved: $artifactUrl"
|
|
98
98
|
|
|
99
99
|
if ($BypassCDN) {
|
|
100
|
-
$
|
|
101
|
-
|
|
100
|
+
$cdnReplacements = @{
|
|
101
|
+
'bcartifacts.azureedge.net' = 'bcartifacts.blob.core.windows.net'
|
|
102
|
+
'bcartifacts-exdbf9fwegejdqak.b02.azurefd.net' = 'bcartifacts.blob.core.windows.net'
|
|
103
|
+
'bcinsider.azureedge.net' = 'bcinsider.blob.core.windows.net'
|
|
104
|
+
'bcinsider-fvh2ekdjecfjd6gk.b02.azurefd.net' = 'bcinsider.blob.core.windows.net'
|
|
105
|
+
'bcpublicpreview.azureedge.net' = 'bcpublicpreview.blob.core.windows.net'
|
|
106
|
+
'bcpublicpreview-f2ajahg0e2cudpgh.b02.azurefd.net' = 'bcpublicpreview.blob.core.windows.net'
|
|
107
|
+
'businesscentralapps.azureedge.net' = 'businesscentralapps.blob.core.windows.net'
|
|
108
|
+
'businesscentralapps-hkdrdkaeangzfydv.b02.azurefd.net'= 'businesscentralapps.blob.core.windows.net'
|
|
109
|
+
}
|
|
110
|
+
foreach ($cdn in $cdnReplacements.Keys) {
|
|
111
|
+
if ($artifactUrl -match [regex]::Escape($cdn)) {
|
|
112
|
+
$artifactUrl = $artifactUrl.Replace($cdn, $cdnReplacements[$cdn])
|
|
113
|
+
Write-BCInfo "CDN bypass: $cdn -> $($cdnReplacements[$cdn])"
|
|
114
|
+
break
|
|
115
|
+
}
|
|
116
|
+
}
|
|
102
117
|
}
|
|
103
118
|
|
|
104
119
|
# Step 5 — Connectivity test
|
|
105
120
|
Write-BCStep "5/8" "Testing connectivity..."
|
|
106
|
-
|
|
121
|
+
$artifactHost = ([uri]$artifactUrl).Host
|
|
122
|
+
foreach ($endpoint in @($artifactHost, "bcartifacts.blob.core.windows.net")) {
|
|
107
123
|
try {
|
|
108
124
|
$tcp = Test-NetConnection -ComputerName $endpoint -Port 443 `
|
|
109
125
|
-WarningAction SilentlyContinue -ErrorAction SilentlyContinue
|
|
@@ -212,8 +228,8 @@ function New-BCDContainer {
|
|
|
212
228
|
|
|
213
229
|
Write-BCBanner "Container Created Successfully" "Green"
|
|
214
230
|
|
|
215
|
-
$bcVersion = Get-BcContainerNavVersion -
|
|
216
|
-
$webclientUrl = Get-
|
|
231
|
+
$bcVersion = Get-BcContainerNavVersion -containerOrImageName $ContainerName
|
|
232
|
+
$webclientUrl = Get-BCDockerWebClientUrl -ContainerName $ContainerName -UseHttps:$false
|
|
217
233
|
$testMode = if (-not $IncludeTestToolkit) { "None" }
|
|
218
234
|
elseif ($TestLibrariesOnly) { "Libraries only" }
|
|
219
235
|
else { "Full test framework apps" }
|
|
@@ -278,7 +294,14 @@ function Start-BCDContainer {
|
|
|
278
294
|
}
|
|
279
295
|
|
|
280
296
|
Write-BCStep "START" "Starting '$ContainerName'..."
|
|
281
|
-
|
|
297
|
+
$prevEAP = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
|
|
298
|
+
$null = docker start $ContainerName 2>&1
|
|
299
|
+
$ErrorActionPreference = $prevEAP
|
|
300
|
+
if ($LASTEXITCODE -ne 0) {
|
|
301
|
+
$msg = "Failed to start '$ContainerName'. Container may not exist."
|
|
302
|
+
Write-BCError $msg
|
|
303
|
+
return
|
|
304
|
+
}
|
|
282
305
|
Write-BCSuccess "Container '$ContainerName' started."
|
|
283
306
|
}
|
|
284
307
|
|
|
@@ -296,7 +319,14 @@ function Stop-BCDContainer {
|
|
|
296
319
|
}
|
|
297
320
|
|
|
298
321
|
Write-BCStep "STOP" "Stopping '$ContainerName'..."
|
|
299
|
-
|
|
322
|
+
$prevEAP = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
|
|
323
|
+
$null = docker stop $ContainerName 2>&1
|
|
324
|
+
$ErrorActionPreference = $prevEAP
|
|
325
|
+
if ($LASTEXITCODE -ne 0) {
|
|
326
|
+
$msg = "Failed to stop '$ContainerName'. Container may not exist or is not running."
|
|
327
|
+
Write-BCError $msg
|
|
328
|
+
return
|
|
329
|
+
}
|
|
300
330
|
Write-BCSuccess "Container '$ContainerName' stopped."
|
|
301
331
|
}
|
|
302
332
|
|
|
@@ -314,7 +344,14 @@ function Restart-BCDContainer {
|
|
|
314
344
|
}
|
|
315
345
|
|
|
316
346
|
Write-BCStep "RESTART" "Restarting '$ContainerName'..."
|
|
317
|
-
|
|
347
|
+
$prevEAP = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
|
|
348
|
+
$null = docker restart $ContainerName 2>&1
|
|
349
|
+
$ErrorActionPreference = $prevEAP
|
|
350
|
+
if ($LASTEXITCODE -ne 0) {
|
|
351
|
+
$msg = "Failed to restart '$ContainerName'. Container may not exist."
|
|
352
|
+
Write-BCError $msg
|
|
353
|
+
return
|
|
354
|
+
}
|
|
318
355
|
Write-BCSuccess "Container '$ContainerName' restarted."
|
|
319
356
|
}
|
|
320
357
|
|
|
@@ -362,8 +399,8 @@ function Get-BCDContainerInfo {
|
|
|
362
399
|
if (-not $ContainerName) { return }
|
|
363
400
|
}
|
|
364
401
|
|
|
365
|
-
$bcVersion = Get-BcContainerNavVersion -
|
|
366
|
-
$webUrl = Get-
|
|
402
|
+
$bcVersion = Get-BcContainerNavVersion -containerOrImageName $ContainerName
|
|
403
|
+
$webUrl = Get-BCDockerWebClientUrl -ContainerName $ContainerName -UseHttps:$false
|
|
367
404
|
$inspect = docker inspect $ContainerName --format '{{.State.Status}}' 2>$null
|
|
368
405
|
$statusColor = if ($inspect -eq "running") { "Green" } else { "Red" }
|
|
369
406
|
|
|
@@ -391,7 +428,7 @@ function Open-BCDWebClient {
|
|
|
391
428
|
if (-not $ContainerName) { return }
|
|
392
429
|
}
|
|
393
430
|
|
|
394
|
-
$url = Get-
|
|
431
|
+
$url = Get-BCDockerWebClientUrl -ContainerName $ContainerName -UseHttps:$false
|
|
395
432
|
Write-BCInfo "Opening $url"
|
|
396
433
|
Start-Process $url
|
|
397
434
|
}
|
package/ps/Helpers.ps1
CHANGED
|
@@ -68,6 +68,26 @@ function Assert-BcContainerHelper {
|
|
|
68
68
|
return $true
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function Get-BCDockerWebClientUrl {
|
|
72
|
+
<#
|
|
73
|
+
.SYNOPSIS
|
|
74
|
+
Resolves Web Client URL for a BC container (BcContainerHelper has no Get-BcContainerUrl in recent versions).
|
|
75
|
+
#>
|
|
76
|
+
param(
|
|
77
|
+
[Parameter(Mandatory)][string]$ContainerName,
|
|
78
|
+
[switch]$UseHttps
|
|
79
|
+
)
|
|
80
|
+
try {
|
|
81
|
+
$cfg = Get-BcContainerServerConfiguration -ContainerName $ContainerName -ErrorAction SilentlyContinue
|
|
82
|
+
if ($null -ne $cfg -and $cfg.PublicWebBaseUrl -and -not [string]::IsNullOrWhiteSpace([string]$cfg.PublicWebBaseUrl)) {
|
|
83
|
+
return ([string]$cfg.PublicWebBaseUrl).TrimEnd('/')
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch { }
|
|
87
|
+
$scheme = if ($UseHttps) { 'https' } else { 'http' }
|
|
88
|
+
return "$($scheme)://$ContainerName/BC"
|
|
89
|
+
}
|
|
90
|
+
|
|
71
91
|
function Select-BCContainer {
|
|
72
92
|
[CmdletBinding()]
|
|
73
93
|
param(
|