bcdocker 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oleksandr (Ciellos)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,165 @@
1
+ # bcd — BC Docker MCP Server & CLI
2
+
3
+ An MCP (Model Context Protocol) server and CLI for managing Business Central Docker containers. Lets AI assistants in Cursor, VS Code, or Claude Desktop directly manage your BC dev environments.
4
+
5
+ ## Prerequisites
6
+
7
+ - **Node.js 18+**
8
+ - **Windows PowerShell 5.1** (BC management cmdlets require it)
9
+ - **Docker Desktop** in Windows containers mode
10
+ - **BCDocker PowerShell module** (the `PartnerScript/` sibling folder)
11
+
12
+ ## Setup
13
+
14
+ ```bash
15
+ cd bcd
16
+ npm install
17
+ npm run build
18
+ ```
19
+
20
+ ## Usage: MCP Server
21
+
22
+ ### Cursor
23
+
24
+ Add to your Cursor MCP settings (`.cursor/mcp.json` or workspace settings):
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "bcd": {
30
+ "command": "node",
31
+ "args": ["C:/myspace/work/Mine/DockerUtils/bcd/dist/server.js"]
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ ### Claude Desktop
38
+
39
+ Add to `claude_desktop_config.json`:
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "bcd": {
45
+ "command": "node",
46
+ "args": ["C:/myspace/work/Mine/DockerUtils/bcd/dist/server.js"]
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ ### VS Code (Copilot)
53
+
54
+ Add to `.vscode/mcp.json`:
55
+
56
+ ```json
57
+ {
58
+ "servers": {
59
+ "bcd": {
60
+ "type": "stdio",
61
+ "command": "node",
62
+ "args": ["C:/myspace/work/Mine/DockerUtils/bcd/dist/server.js"]
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ After configuring, your AI assistant can:
69
+
70
+ - "List my BC containers"
71
+ - "Create a new BC sandbox container with version 26.0"
72
+ - "Show me the apps in bcsandbox"
73
+ - "Run tests in my container"
74
+ - "Compile and publish my AL project to bcsandbox"
75
+ - "Stop the bcsandbox container"
76
+
77
+ ## Usage: CLI
78
+
79
+ ```bash
80
+ # List containers
81
+ node dist/cli.js list
82
+
83
+ # Container details
84
+ node dist/cli.js info bcsandbox
85
+
86
+ # Create container
87
+ node dist/cli.js create --name bcsandbox --version 26.0 --country w1 --bypass-cdn
88
+
89
+ # Start / Stop / Restart
90
+ node dist/cli.js start bcsandbox
91
+ node dist/cli.js stop bcsandbox
92
+ node dist/cli.js restart bcsandbox
93
+
94
+ # Remove container
95
+ node dist/cli.js remove bcsandbox
96
+
97
+ # Open web client
98
+ node dist/cli.js open bcsandbox
99
+
100
+ # List apps
101
+ node dist/cli.js apps bcsandbox
102
+ node dist/cli.js apps bcsandbox --publisher Microsoft
103
+
104
+ # Install .app file
105
+ node dist/cli.js install bcsandbox "C:\apps\MyApp.app"
106
+
107
+ # Compile and publish AL project
108
+ node dist/cli.js publish bcsandbox "C:\Projects\MyApp"
109
+
110
+ # Run tests
111
+ node dist/cli.js test bcsandbox
112
+ node dist/cli.js test bcsandbox --codeunit 50100
113
+ node dist/cli.js test bcsandbox --app "C:\Projects\MyApp.Test"
114
+
115
+ # Import test toolkit
116
+ node dist/cli.js toolkit bcsandbox
117
+ node dist/cli.js toolkit bcsandbox --full
118
+
119
+ # Import license
120
+ node dist/cli.js license bcsandbox "C:\license.bclicense"
121
+ ```
122
+
123
+ ### Global install (optional)
124
+
125
+ ```bash
126
+ npm link
127
+ # Now use 'bcd' directly:
128
+ bcd list
129
+ bcd create --name mybc --version 26.0
130
+ bcd apps mybc
131
+ ```
132
+
133
+ ## MCP Tools Reference
134
+
135
+ | Tool | Description |
136
+ |---|---|
137
+ | `list-containers` | List all BC containers with status |
138
+ | `container-info` | Get version, status, and endpoints |
139
+ | `create-container` | Create a new BC container (5-30 min) |
140
+ | `remove-container` | Remove a container |
141
+ | `start-container` | Start a stopped container |
142
+ | `stop-container` | Stop a running container |
143
+ | `restart-container` | Restart a container |
144
+ | `list-apps` | List apps in a container |
145
+ | `install-app` | Install a .app file |
146
+ | `publish-project` | Compile and publish an AL project |
147
+ | `import-test-toolkit` | Import test toolkit |
148
+ | `import-license` | Import a license file |
149
+ | `run-tests` | Run AL tests |
150
+
151
+ ## Architecture
152
+
153
+ ```
154
+ bcd/
155
+ ├── src/
156
+ │ ├── server.ts # MCP server — exposes tools over stdio
157
+ │ ├── cli.ts # CLI — same operations via command line
158
+ │ └── executor.ts # PowerShell 5.1 execution layer
159
+ ├── dist/ # Compiled JS (after npm run build)
160
+ ├── package.json
161
+ ├── tsconfig.json
162
+ └── README.md
163
+ ```
164
+
165
+ The MCP server and CLI both call through `executor.ts`, which spawns `powershell.exe` (5.1, not pwsh 7.x) and imports the `BCDocker.psm1` module from the sibling `PartnerScript/` folder. This ensures all the BcContainerHelper cmdlets work correctly, including on BC 28+.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { runPowerShell, runRawPowerShell } from "./executor.js";
4
+ const program = new Command();
5
+ program
6
+ .name("bcd")
7
+ .description("CLI for Business Central Docker container management")
8
+ .version("1.0.0");
9
+ // ── Containers ───────────────────────────────────────────
10
+ program
11
+ .command("list")
12
+ .alias("ls")
13
+ .description("List all BC containers with status")
14
+ .action(async () => {
15
+ const { stdout } = await runRawPowerShell(`
16
+ Import-Module BcContainerHelper -DisableNameChecking -ErrorAction Stop
17
+ $containers = Get-BcContainers
18
+ if ($containers.Count -eq 0) { Write-Output "No BC containers found." }
19
+ else {
20
+ foreach ($name in $containers) {
21
+ $status = docker inspect $name --format '{{.State.Status}}' 2>$null
22
+ Write-Output "$($name.PadRight(25)) $status"
23
+ }
24
+ }
25
+ `);
26
+ console.log(stdout);
27
+ });
28
+ program
29
+ .command("info <container>")
30
+ .description("Show container details: version, status, endpoints")
31
+ .action(async (container) => {
32
+ const { stdout } = await runRawPowerShell(`
33
+ Import-Module BcContainerHelper -DisableNameChecking -ErrorAction Stop
34
+ $v = Get-BcContainerNavVersion -containerNameOrId '${container}'
35
+ $u = Get-BcContainerUrl -containerName '${container}' -useHttps:$false
36
+ $s = docker inspect '${container}' --format '{{.State.Status}}' 2>$null
37
+ Write-Output "Container : ${container}"
38
+ Write-Output "Status : $s"
39
+ Write-Output "BC Version : $v"
40
+ Write-Output "Web Client : $u"
41
+ Write-Output "OData/API : http://${container}:7048/BC/api"
42
+ Write-Output "Dev Service: http://${container}:7049/BC"
43
+ `);
44
+ console.log(stdout);
45
+ });
46
+ program
47
+ .command("create")
48
+ .description("Create a new BC container")
49
+ .option("-n, --name <name>", "Container name", "bcsandbox")
50
+ .option("-v, --version <version>", "BC version (sandbox, onprem, 26.0)", "sandbox")
51
+ .option("-c, --country <code>", "Country code (us, w1, gb, nl)", "us")
52
+ .option("-u, --user <username>", "Admin username", "admin")
53
+ .option("-p, --password <password>", "Admin password", "P@ssw0rd!")
54
+ .option("-m, --memory <limit>", "Memory limit", "8G")
55
+ .option("-i, --isolation <mode>", "Isolation: hyperv or process", "hyperv")
56
+ .option("-t, --toolkit <mode>", "Test toolkit: none, libraries, full", "libraries")
57
+ .option("--bypass-cdn", "Skip Azure CDN, use blob storage")
58
+ .option("-l, --license <file>", "Path or URL to .bclicense / .flf license file")
59
+ .action(async (opts) => {
60
+ const includeToolkit = opts.toolkit !== "none" ? "$true" : "$false";
61
+ const libOnly = opts.toolkit === "libraries" ? "$true" : "$false";
62
+ const cdn = opts.bypassCdn ? "-BypassCDN" : "";
63
+ const lic = opts.license ? `-LicenseFile '${opts.license}'` : "";
64
+ console.log(`Creating container '${opts.name}'...`);
65
+ console.log(` Version: ${opts.version}, Country: ${opts.country}, Toolkit: ${opts.toolkit}`);
66
+ console.log(` This may take 5-30 minutes.\n`);
67
+ const { stdout, stderr } = await runPowerShell(`
68
+ New-BCDContainer \`
69
+ -ContainerName '${opts.name}' \`
70
+ -Version '${opts.version}' \`
71
+ -Country '${opts.country}' \`
72
+ -UserName '${opts.user}' \`
73
+ -Password '${opts.password}' \`
74
+ -MemoryLimit '${opts.memory}' \`
75
+ -Isolation '${opts.isolation}' \`
76
+ -IncludeTestToolkit ${includeToolkit} \`
77
+ -TestLibrariesOnly ${libOnly} \`
78
+ ${cdn} ${lic}
79
+ `, 1_800_000);
80
+ if (stdout)
81
+ console.log(stdout);
82
+ if (stderr)
83
+ console.error(stderr);
84
+ });
85
+ program
86
+ .command("remove <container>")
87
+ .alias("rm")
88
+ .description("Remove a BC container")
89
+ .action(async (container) => {
90
+ const { stdout } = await runPowerShell(`Remove-BCDContainer -ContainerName '${container}'`);
91
+ console.log(stdout || `Container '${container}' removed.`);
92
+ });
93
+ program
94
+ .command("start <container>")
95
+ .description("Start a stopped container")
96
+ .action(async (container) => {
97
+ const { stdout } = await runPowerShell(`Start-BCDContainer -ContainerName '${container}'`);
98
+ console.log(stdout || `Container '${container}' started.`);
99
+ });
100
+ program
101
+ .command("stop <container>")
102
+ .description("Stop a running container")
103
+ .action(async (container) => {
104
+ const { stdout } = await runPowerShell(`Stop-BCDContainer -ContainerName '${container}'`);
105
+ console.log(stdout || `Container '${container}' stopped.`);
106
+ });
107
+ program
108
+ .command("restart <container>")
109
+ .description("Restart a container")
110
+ .action(async (container) => {
111
+ const { stdout } = await runPowerShell(`Restart-BCDContainer -ContainerName '${container}'`);
112
+ console.log(stdout || `Container '${container}' restarted.`);
113
+ });
114
+ program
115
+ .command("open <container>")
116
+ .description("Open the BC Web Client in your browser")
117
+ .action(async (container) => {
118
+ const { stdout } = await runPowerShell(`Open-BCDWebClient -ContainerName '${container}'`);
119
+ console.log(stdout || "Opening web client...");
120
+ });
121
+ // ── Apps ─────────────────────────────────────────────────
122
+ program
123
+ .command("apps <container>")
124
+ .description("List apps in a container")
125
+ .option("--publisher <name>", "Filter by publisher")
126
+ .action(async (container, opts) => {
127
+ const filter = opts.publisher
128
+ ? `| Where-Object { $_.Publisher -eq '${opts.publisher}' }`
129
+ : "";
130
+ const { stdout } = await runRawPowerShell(`
131
+ Import-Module BcContainerHelper -DisableNameChecking -ErrorAction Stop
132
+ Get-BcContainerAppInfo -containerName '${container}' -tenant default -tenantSpecificProperties ${filter} |
133
+ Select-Object Name, Publisher, Version, IsInstalled, Scope |
134
+ Sort-Object Publisher, Name |
135
+ Format-Table -AutoSize | Out-String -Width 200
136
+ `);
137
+ console.log(stdout || "No apps found.");
138
+ });
139
+ program
140
+ .command("install <container> <appFile>")
141
+ .description("Install a .app file into a container")
142
+ .option("-u, --user <username>", "Admin username", "admin")
143
+ .option("-p, --password <password>", "Admin password", "P@ssw0rd!")
144
+ .action(async (container, appFile, opts) => {
145
+ const { stdout } = await runPowerShell(`
146
+ $cred = Get-BCCredential -UserName '${opts.user}' -Password '${opts.password}'
147
+ Install-BCDApp -ContainerName '${container}' -AppFile '${appFile}' -Credential $cred
148
+ `);
149
+ console.log(stdout || "App installed.");
150
+ });
151
+ program
152
+ .command("uninstall <container> <appName> <publisher>")
153
+ .description("Uninstall an app by name and publisher")
154
+ .action(async (container, appName, publisher) => {
155
+ const { stdout } = await runRawPowerShell(`
156
+ Import-Module BcContainerHelper -DisableNameChecking -ErrorAction Stop
157
+ $sorted = Get-BcContainerAppInfo -containerName '${container}' -tenant default -tenantSpecificProperties -sort DependenciesLast
158
+ $target = $sorted | Where-Object { $_.Name -eq '${appName}' -and $_.Publisher -eq '${publisher}' -and $_.IsInstalled }
159
+ if (-not $target) { Write-Output "App '${appName}' by '${publisher}' not found or not installed."; exit }
160
+ foreach ($app in $target) {
161
+ UnInstall-BcContainerApp -name $app.Name -containerName '${container}' -publisher $app.Publisher -version $app.Version -force
162
+ Write-Output "Uninstalled: $($app.Name) v$($app.Version)"
163
+ }
164
+ `);
165
+ console.log(stdout);
166
+ });
167
+ program
168
+ .command("publish <container> <projectFolder>")
169
+ .description("Compile and publish an AL project into a container")
170
+ .option("-u, --user <username>", "Admin username", "admin")
171
+ .option("-p, --password <password>", "Admin password", "P@ssw0rd!")
172
+ .action(async (container, folder, opts) => {
173
+ const { stdout } = await runPowerShell(`
174
+ $cred = Get-BCCredential -UserName '${opts.user}' -Password '${opts.password}'
175
+ Publish-BCDProject -ContainerName '${container}' -ProjectFolder '${folder}' -Credential $cred
176
+ `, 300_000);
177
+ console.log(stdout || "Project compiled and published.");
178
+ });
179
+ // ── Tests & License ──────────────────────────────────────
180
+ program
181
+ .command("test [container]")
182
+ .description("Run AL tests in a container")
183
+ .option("-c, --codeunit <id>", "Test codeunit ID")
184
+ .option("-f, --function <name>", "Test function name")
185
+ .option("-a, --app <folder>", "AL test project folder to compile/publish/run")
186
+ .option("-u, --user <username>", "Admin username", "admin")
187
+ .option("-p, --password <password>", "Admin password", "P@ssw0rd!")
188
+ .action(async (container = "bcsandbox", opts) => {
189
+ const params = [`-ContainerName '${container}'`];
190
+ params.push(`-Credential (Get-BCCredential -UserName '${opts.user}' -Password '${opts.password}')`);
191
+ if (opts.codeunit)
192
+ params.push(`-TestCodeunitId ${opts.codeunit}`);
193
+ if (opts.function)
194
+ params.push(`-TestFunctionName '${opts.function}'`);
195
+ if (opts.app)
196
+ params.push(`-AppProjectFolder '${opts.app}'`);
197
+ const { stdout } = await runPowerShell(`Invoke-BCDTests ${params.join(" ")}`, 600_000);
198
+ console.log(stdout || "Test run complete.");
199
+ });
200
+ program
201
+ .command("toolkit <container>")
202
+ .description("Import BC Test Toolkit into a container")
203
+ .option("--full", "Import full test framework (default: libraries only)")
204
+ .option("-u, --user <username>", "Admin username", "admin")
205
+ .option("-p, --password <password>", "Admin password", "P@ssw0rd!")
206
+ .action(async (container, opts) => {
207
+ const libOnly = opts.full ? "$false" : "$true";
208
+ const { stdout } = await runPowerShell(`
209
+ $cred = Get-BCCredential -UserName '${opts.user}' -Password '${opts.password}'
210
+ Import-BCDTestToolkit -ContainerName '${container}' -Credential $cred -LibrariesOnly ${libOnly}
211
+ `, 300_000);
212
+ console.log(stdout || "Test toolkit imported.");
213
+ });
214
+ program
215
+ .command("license <container> <file>")
216
+ .description("Import a license file into a container")
217
+ .action(async (container, file) => {
218
+ const { stdout } = await runPowerShell(`Import-BCDLicense -ContainerName '${container}' -LicenseFile '${file}'`);
219
+ console.log(stdout || "License imported.");
220
+ });
221
+ program.parseAsync(process.argv).catch((err) => {
222
+ console.error("Error:", err.message);
223
+ process.exit(1);
224
+ });
@@ -0,0 +1,7 @@
1
+ export interface ExecResult {
2
+ stdout: string;
3
+ stderr: string;
4
+ exitCode: number;
5
+ }
6
+ export declare function runPowerShell(script: string, timeoutMs?: number): Promise<ExecResult>;
7
+ export declare function runRawPowerShell(script: string, timeoutMs?: number): Promise<ExecResult>;
@@ -0,0 +1,72 @@
1
+ import { execFile } from "node:child_process";
2
+ import { resolve, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { existsSync } from "node:fs";
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ function resolveModulePath() {
7
+ // 1. Environment variable override
8
+ if (process.env.BCD_MODULE_PATH)
9
+ return process.env.BCD_MODULE_PATH;
10
+ // 2. Bundled inside npm package (dist/../ps/BCDocker.psm1)
11
+ const bundled = resolve(__dirname, "../ps/BCDocker.psm1");
12
+ if (existsSync(bundled))
13
+ return bundled;
14
+ // 3. Development layout (dist/../../PartnerScript/BCDocker.psm1)
15
+ const dev = resolve(__dirname, "../../PartnerScript/BCDocker.psm1");
16
+ if (existsSync(dev))
17
+ return dev;
18
+ throw new Error("BCDocker.psm1 not found. Set BCD_MODULE_PATH or ensure the ps/ folder exists.");
19
+ }
20
+ const MODULE_PATH = resolveModulePath();
21
+ // BC management cmdlets require Windows PowerShell 5.1, not pwsh 7.x
22
+ const PS_EXE = "powershell.exe";
23
+ export function runPowerShell(script, timeoutMs = 600_000) {
24
+ const wrappedScript = `
25
+ $ErrorActionPreference = 'Stop'
26
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
27
+ Import-Module '${MODULE_PATH.replace(/\\/g, "\\\\")}' -DisableNameChecking -Force
28
+ ${script}
29
+ `;
30
+ return new Promise((resolve, reject) => {
31
+ const child = execFile(PS_EXE, [
32
+ "-NoProfile",
33
+ "-NonInteractive",
34
+ "-ExecutionPolicy",
35
+ "Bypass",
36
+ "-Command",
37
+ wrappedScript,
38
+ ], { timeout: timeoutMs, maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
39
+ if (error && !stdout) {
40
+ reject(new Error(stderr || error.message));
41
+ return;
42
+ }
43
+ resolve({
44
+ stdout: stdout.trim(),
45
+ stderr: stderr.trim(),
46
+ exitCode: error ? 1 : 0,
47
+ });
48
+ });
49
+ });
50
+ }
51
+ export function runRawPowerShell(script, timeoutMs = 600_000) {
52
+ return new Promise((resolve, reject) => {
53
+ execFile(PS_EXE, [
54
+ "-NoProfile",
55
+ "-NonInteractive",
56
+ "-ExecutionPolicy",
57
+ "Bypass",
58
+ "-Command",
59
+ script,
60
+ ], { timeout: timeoutMs, maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
61
+ if (error && !stdout) {
62
+ reject(new Error(stderr || error.message));
63
+ return;
64
+ }
65
+ resolve({
66
+ stdout: stdout.trim(),
67
+ stderr: stderr.trim(),
68
+ exitCode: error ? 1 : 0,
69
+ });
70
+ });
71
+ });
72
+ }
@@ -0,0 +1 @@
1
+ export {};