dbatools-mcp-server 0.1.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 +21 -0
- package/README.md +160 -0
- package/dist/help-indexer.d.ts +21 -0
- package/dist/help-indexer.d.ts.map +1 -0
- package/dist/help-indexer.js +52 -0
- package/dist/help-indexer.js.map +1 -0
- package/dist/powershell.d.ts +34 -0
- package/dist/powershell.d.ts.map +1 -0
- package/dist/powershell.js +137 -0
- package/dist/powershell.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +330 -0
- package/dist/server.js.map +1 -0
- package/dist/tool-registry.d.ts +34 -0
- package/dist/tool-registry.d.ts.map +1 -0
- package/dist/tool-registry.js +102 -0
- package/dist/tool-registry.js.map +1 -0
- package/dist/types.d.ts +55 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/generated/.gitkeep +3 -0
- package/package.json +66 -0
- package/scripts/refresh-help.ps1 +192 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DataPlat contributors
|
|
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,160 @@
|
|
|
1
|
+
# dbatools-mcp-server
|
|
2
|
+
|
|
3
|
+
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for the [dbatools](https://dbatools.io) PowerShell module.
|
|
4
|
+
|
|
5
|
+
Exposes dbatools commands as MCP tools so AI assistants (GitHub Copilot, Claude, etc.) can discover, explain, and execute dbatools commands directly — with all metadata sourced from dbatools' own **comment-based help**.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **`list_dbatools_commands`** — search commands by verb, noun, keyword, or risk level
|
|
12
|
+
- **`get_dbatools_command_help`** — full normalized help (synopsis, parameters, examples) from `Get-Help -Full`
|
|
13
|
+
- **`invoke_dbatools_command`** — execute any dbatools command with safe parameter validation, risk gating, and structured JSON output
|
|
14
|
+
- **`check_dbatools_environment`** — verify PowerShell + dbatools installation, index freshness, and version alignment
|
|
15
|
+
- **Version mismatch detection** — warns when installed dbatools version differs from the indexed version
|
|
16
|
+
- **Safe mode** — non-readonly commands require explicit `confirm: true` to execute
|
|
17
|
+
- **SQL Authentication support** — pass `SqlCredential: { username, password }` for SQL auth instances
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Prerequisites
|
|
22
|
+
|
|
23
|
+
- [Node.js](https://nodejs.org/) 20+
|
|
24
|
+
- [PowerShell 7+](https://github.com/PowerShell/PowerShell/releases) (`pwsh`)
|
|
25
|
+
- [dbatools](https://dbatools.io/download) PowerShell module
|
|
26
|
+
|
|
27
|
+
```powershell
|
|
28
|
+
Install-Module dbatools -Scope CurrentUser
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
```powershell
|
|
36
|
+
# 1. Clone the repo
|
|
37
|
+
git clone https://github.com/Dataplat/dbatools-mcp-server.git
|
|
38
|
+
cd dbatools-mcp-server
|
|
39
|
+
|
|
40
|
+
# 2. Install Node dependencies
|
|
41
|
+
npm install
|
|
42
|
+
|
|
43
|
+
# 3. Generate the help index from your local dbatools installation
|
|
44
|
+
npm run refresh-help
|
|
45
|
+
|
|
46
|
+
# 4. Build
|
|
47
|
+
npm run build
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then open the folder in VS Code — the `.vscode/mcp.json` file automatically registers the MCP server.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Connecting to VS Code
|
|
55
|
+
|
|
56
|
+
The included [`.vscode/mcp.json`](.vscode/mcp.json) registers the server as a local STDIO MCP server.
|
|
57
|
+
Open this folder in VS Code and the server will appear in the GitHub Copilot MCP panel.
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"servers": {
|
|
62
|
+
"dbatools": {
|
|
63
|
+
"type": "stdio",
|
|
64
|
+
"command": "node",
|
|
65
|
+
"args": ["${workspaceFolder}/dist/server.js"],
|
|
66
|
+
"env": {
|
|
67
|
+
"DBATOOLS_SAFE_MODE": "true",
|
|
68
|
+
"MAX_OUTPUT_ROWS": "100",
|
|
69
|
+
"COMMAND_TIMEOUT_SECONDS": "60"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
All settings are controlled via environment variables (set in `.vscode/mcp.json` or your shell):
|
|
81
|
+
|
|
82
|
+
| Variable | Default | Description |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `PWSH_EXE` | `pwsh` | Path to PowerShell executable |
|
|
85
|
+
| `DBATOOLS_SAFE_MODE` | `true` | When `true`, non-readonly commands require `confirm: true` |
|
|
86
|
+
| `MAX_OUTPUT_ROWS` | `100` | Maximum rows returned per command execution |
|
|
87
|
+
| `COMMAND_TIMEOUT_SECONDS` | `60` | Seconds before PowerShell process is killed |
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Refreshing the Help Index
|
|
92
|
+
|
|
93
|
+
The help index (`generated/dbatools-help.json`) is generated from your locally installed dbatools module.
|
|
94
|
+
Re-run whenever dbatools is updated:
|
|
95
|
+
|
|
96
|
+
```powershell
|
|
97
|
+
Update-Module dbatools -Scope CurrentUser
|
|
98
|
+
npm run refresh-help
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The server detects version mismatches at runtime and warns you when the index is stale.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Risk Levels
|
|
106
|
+
|
|
107
|
+
Commands are automatically classified by verb:
|
|
108
|
+
|
|
109
|
+
| Risk Level | Verbs | Behavior |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| `readonly` | Get, Test, Find, Compare, … | Always allowed |
|
|
112
|
+
| `change` | Set, New, Add, Copy, Enable, … | Requires `confirm: true` in safe mode |
|
|
113
|
+
| `destructive` | Remove, Drop, Disable, Reset, … | Requires `confirm: true` in safe mode |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## SQL Authentication
|
|
118
|
+
|
|
119
|
+
For SQL-auth-only instances (e.g. Docker), pass credentials via the `SqlCredential` parameter:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"SqlInstance": "localhost,1433",
|
|
124
|
+
"SqlCredential": { "username": "<SqlLogin>", "password": "YourPassword" }
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Project Structure
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
dbatools-mcp-server/
|
|
134
|
+
├── src/
|
|
135
|
+
│ ├── server.ts # MCP server entry point, tool definitions
|
|
136
|
+
│ ├── powershell.ts # PowerShell process runner, health checks, version detection
|
|
137
|
+
│ ├── help-indexer.ts # Help manifest loader and command search
|
|
138
|
+
│ ├── tool-registry.ts # Risk classification, safe argument builder
|
|
139
|
+
│ └── types.ts # Shared TypeScript interfaces
|
|
140
|
+
├── scripts/
|
|
141
|
+
│ └── refresh-help.ps1 # Generates generated/dbatools-help.json
|
|
142
|
+
├── generated/ # Help index (gitignored, generated locally)
|
|
143
|
+
├── .vscode/
|
|
144
|
+
│ └── mcp.json # VS Code MCP local server registration
|
|
145
|
+
└── dist/ # Compiled output (gitignored)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Contributing
|
|
151
|
+
|
|
152
|
+
Contributions are welcome! Please open an issue first for significant changes.
|
|
153
|
+
|
|
154
|
+
This project follows the same community spirit as [dbatools](https://github.com/dataplat/dbatools).
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
[MIT](LICENSE) — © 2026 DataPlat contributors
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { HelpIndex, HelpManifest, DbatoolsCommandHelp } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Load and cache the help manifest from generated/dbatools-help.json.
|
|
4
|
+
* Throws an actionable error if the file has not been generated yet.
|
|
5
|
+
*/
|
|
6
|
+
export declare function loadHelpManifest(): HelpManifest;
|
|
7
|
+
/** Shorthand to get just the commands map from the manifest. */
|
|
8
|
+
export declare function loadHelpIndex(): HelpIndex;
|
|
9
|
+
export interface SearchOptions {
|
|
10
|
+
verb?: string;
|
|
11
|
+
noun?: string;
|
|
12
|
+
keyword?: string;
|
|
13
|
+
riskLevel?: string;
|
|
14
|
+
limit?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Filter the help index with optional verb / noun / keyword / riskLevel filters.
|
|
18
|
+
* Keyword matches against name, synopsis, and description (case-insensitive).
|
|
19
|
+
*/
|
|
20
|
+
export declare function searchCommands(index: HelpIndex, opts: SearchOptions): DbatoolsCommandHelp[];
|
|
21
|
+
//# sourceMappingURL=help-indexer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"help-indexer.d.ts","sourceRoot":"","sources":["../src/help-indexer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAO/E;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,YAAY,CAa/C;AAED,gEAAgE;AAChE,wBAAgB,aAAa,IAAI,SAAS,CAEzC;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,SAAS,EAChB,IAAI,EAAE,aAAa,GAClB,mBAAmB,EAAE,CA6BvB"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const HELP_INDEX_PATH = join(__dirname, "../generated/dbatools-help.json");
|
|
6
|
+
let cachedManifest = null;
|
|
7
|
+
/**
|
|
8
|
+
* Load and cache the help manifest from generated/dbatools-help.json.
|
|
9
|
+
* Throws an actionable error if the file has not been generated yet.
|
|
10
|
+
*/
|
|
11
|
+
export function loadHelpManifest() {
|
|
12
|
+
if (cachedManifest)
|
|
13
|
+
return cachedManifest;
|
|
14
|
+
if (!existsSync(HELP_INDEX_PATH)) {
|
|
15
|
+
throw new Error(`Help index not found at: ${HELP_INDEX_PATH}\n` +
|
|
16
|
+
`Run 'npm run refresh-help' to generate it from your local dbatools installation.`);
|
|
17
|
+
}
|
|
18
|
+
const raw = readFileSync(HELP_INDEX_PATH, "utf-8");
|
|
19
|
+
cachedManifest = JSON.parse(raw);
|
|
20
|
+
return cachedManifest;
|
|
21
|
+
}
|
|
22
|
+
/** Shorthand to get just the commands map from the manifest. */
|
|
23
|
+
export function loadHelpIndex() {
|
|
24
|
+
return loadHelpManifest().commands;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Filter the help index with optional verb / noun / keyword / riskLevel filters.
|
|
28
|
+
* Keyword matches against name, synopsis, and description (case-insensitive).
|
|
29
|
+
*/
|
|
30
|
+
export function searchCommands(index, opts) {
|
|
31
|
+
let commands = Object.values(index);
|
|
32
|
+
if (opts.verb) {
|
|
33
|
+
const v = opts.verb.toLowerCase();
|
|
34
|
+
commands = commands.filter((c) => c.verb.toLowerCase() === v);
|
|
35
|
+
}
|
|
36
|
+
if (opts.noun) {
|
|
37
|
+
const n = opts.noun.toLowerCase();
|
|
38
|
+
commands = commands.filter((c) => c.noun.toLowerCase().includes(n));
|
|
39
|
+
}
|
|
40
|
+
if (opts.keyword) {
|
|
41
|
+
const kw = opts.keyword.toLowerCase();
|
|
42
|
+
commands = commands.filter((c) => c.name.toLowerCase().includes(kw) ||
|
|
43
|
+
c.synopsis.toLowerCase().includes(kw) ||
|
|
44
|
+
c.description.toLowerCase().includes(kw) ||
|
|
45
|
+
c.tags.some((t) => t.toLowerCase().includes(kw)));
|
|
46
|
+
}
|
|
47
|
+
if (opts.riskLevel) {
|
|
48
|
+
commands = commands.filter((c) => c.riskLevel === opts.riskLevel);
|
|
49
|
+
}
|
|
50
|
+
return commands.slice(0, opts.limit ?? 50);
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=help-indexer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"help-indexer.js","sourceRoot":"","sources":["../src/help-indexer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAGpC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1D,MAAM,eAAe,GAAG,IAAI,CAAC,SAAS,EAAE,iCAAiC,CAAC,CAAC;AAE3E,IAAI,cAAc,GAAwB,IAAI,CAAC;AAE/C;;;GAGG;AACH,MAAM,UAAU,gBAAgB;IAC9B,IAAI,cAAc;QAAE,OAAO,cAAc,CAAC;IAE1C,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CACb,4BAA4B,eAAe,IAAI;YAC7C,kFAAkF,CACrF,CAAC;IACJ,CAAC;IAED,MAAM,GAAG,GAAG,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;IACnD,cAAc,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAiB,CAAC;IACjD,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,aAAa;IAC3B,OAAO,gBAAgB,EAAE,CAAC,QAAQ,CAAC;AACrC,CAAC;AAUD;;;GAGG;AACH,MAAM,UAAU,cAAc,CAC5B,KAAgB,EAChB,IAAmB;IAEnB,IAAI,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAEpC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACd,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;QAClC,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC;IAChE,CAAC;IAED,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACd,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;QAClC,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IACtE,CAAC;IAED,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QACtC,QAAQ,GAAG,QAAQ,CAAC,MAAM,CACxB,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC;YACjC,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC;YACrC,CAAC,CAAC,WAAW,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC;YACxC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CACnD,CAAC;IACJ,CAAC;IAED,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;QACnB,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,IAAI,CAAC,SAAS,CAAC,CAAC;IACpE,CAAC;IAED,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;AAC7C,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { PowerShellResult, ServerConfig } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve runtime configuration from environment variables with safe defaults.
|
|
4
|
+
* All values can be overridden without code changes.
|
|
5
|
+
*/
|
|
6
|
+
export declare function getConfig(): ServerConfig;
|
|
7
|
+
/**
|
|
8
|
+
* Spawn a PowerShell process, run the given script, and collect stdout/stderr.
|
|
9
|
+
* Rejects only on process spawn failure; non-zero exit codes are resolved normally
|
|
10
|
+
* so the caller can inspect exitCode and stderr.
|
|
11
|
+
*/
|
|
12
|
+
export declare function runPowerShell(script: string, config: ServerConfig): Promise<PowerShellResult>;
|
|
13
|
+
/**
|
|
14
|
+
* Quick health-check: verifies that PowerShell and the dbatools module are
|
|
15
|
+
* available on the current machine.
|
|
16
|
+
*/
|
|
17
|
+
export declare function checkDbatools(config: ServerConfig): Promise<{
|
|
18
|
+
ok: boolean;
|
|
19
|
+
version?: string;
|
|
20
|
+
error?: string;
|
|
21
|
+
}>;
|
|
22
|
+
export interface VersionMismatchResult {
|
|
23
|
+
installedVersion: string;
|
|
24
|
+
indexedVersion: string;
|
|
25
|
+
isStale: boolean;
|
|
26
|
+
message: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Compare the installed dbatools version against the version stored in the
|
|
30
|
+
* help-index manifest. Returns a structured result with a human-readable
|
|
31
|
+
* warning when the two differ.
|
|
32
|
+
*/
|
|
33
|
+
export declare function checkVersionMismatch(config: ServerConfig, indexedVersion: string): Promise<VersionMismatchResult>;
|
|
34
|
+
//# sourceMappingURL=powershell.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"powershell.d.ts","sourceRoot":"","sources":["../src/powershell.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAsBjE;;;GAGG;AACH,wBAAgB,SAAS,IAAI,YAAY,CAOxC;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,gBAAgB,CAAC,CAgE3B;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAmB5D;AAED,MAAM,WAAW,qBAAqB;IACpC,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,YAAY,EACpB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,qBAAqB,CAAC,CAwBhC"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
/** Maximum total stdout+stderr bytes buffered per PowerShell invocation (10 MB). */
|
|
3
|
+
const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
4
|
+
/**
|
|
5
|
+
* Parse an integer env var, falling back to `defaultVal` when the value is
|
|
6
|
+
* missing, non-numeric, or outside the given bounds.
|
|
7
|
+
*/
|
|
8
|
+
function parseIntEnv(key, defaultVal, min, max) {
|
|
9
|
+
const raw = process.env[key];
|
|
10
|
+
if (!raw)
|
|
11
|
+
return defaultVal;
|
|
12
|
+
const parsed = parseInt(raw, 10);
|
|
13
|
+
if (!Number.isFinite(parsed) || parsed < min || parsed > max)
|
|
14
|
+
return defaultVal;
|
|
15
|
+
return parsed;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve runtime configuration from environment variables with safe defaults.
|
|
19
|
+
* All values can be overridden without code changes.
|
|
20
|
+
*/
|
|
21
|
+
export function getConfig() {
|
|
22
|
+
return {
|
|
23
|
+
powershellExe: process.env["PWSH_EXE"] ?? "pwsh",
|
|
24
|
+
safeMode: process.env["DBATOOLS_SAFE_MODE"] !== "false",
|
|
25
|
+
maxOutputRows: parseIntEnv("MAX_OUTPUT_ROWS", 100, 1, 10_000),
|
|
26
|
+
commandTimeout: parseIntEnv("COMMAND_TIMEOUT_SECONDS", 60, 5, 3600),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Spawn a PowerShell process, run the given script, and collect stdout/stderr.
|
|
31
|
+
* Rejects only on process spawn failure; non-zero exit codes are resolved normally
|
|
32
|
+
* so the caller can inspect exitCode and stderr.
|
|
33
|
+
*/
|
|
34
|
+
export function runPowerShell(script, config) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const pwsh = spawn(config.powershellExe, [
|
|
37
|
+
"-NonInteractive",
|
|
38
|
+
"-NoProfile",
|
|
39
|
+
"-NoLogo",
|
|
40
|
+
"-ExecutionPolicy",
|
|
41
|
+
"Bypass",
|
|
42
|
+
"-Command",
|
|
43
|
+
script,
|
|
44
|
+
], { shell: false });
|
|
45
|
+
let stdout = "";
|
|
46
|
+
let stderr = "";
|
|
47
|
+
let totalBytes = 0;
|
|
48
|
+
let settled = false;
|
|
49
|
+
function fail(err) {
|
|
50
|
+
if (settled)
|
|
51
|
+
return;
|
|
52
|
+
settled = true;
|
|
53
|
+
pwsh.kill("SIGKILL");
|
|
54
|
+
reject(err);
|
|
55
|
+
}
|
|
56
|
+
const timer = setTimeout(() => {
|
|
57
|
+
fail(new Error(`PowerShell process timed out after ${config.commandTimeout}s`));
|
|
58
|
+
}, config.commandTimeout * 1000);
|
|
59
|
+
pwsh.stdout.on("data", (chunk) => {
|
|
60
|
+
totalBytes += chunk.length;
|
|
61
|
+
if (totalBytes > MAX_BUFFER_BYTES) {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
fail(new Error(`PowerShell output exceeded the ${MAX_BUFFER_BYTES / 1024 / 1024} MB safety limit`));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
stdout += chunk.toString();
|
|
67
|
+
});
|
|
68
|
+
pwsh.stderr.on("data", (chunk) => {
|
|
69
|
+
totalBytes += chunk.length;
|
|
70
|
+
if (totalBytes > MAX_BUFFER_BYTES) {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
fail(new Error(`PowerShell output exceeded the ${MAX_BUFFER_BYTES / 1024 / 1024} MB safety limit`));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
stderr += chunk.toString();
|
|
76
|
+
});
|
|
77
|
+
pwsh.on("close", (code) => {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
if (settled)
|
|
80
|
+
return;
|
|
81
|
+
settled = true;
|
|
82
|
+
resolve({ stdout, stderr, exitCode: code ?? 1 });
|
|
83
|
+
});
|
|
84
|
+
pwsh.on("error", (err) => {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
fail(new Error(`Failed to launch '${config.powershellExe}': ${err.message}`));
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Quick health-check: verifies that PowerShell and the dbatools module are
|
|
92
|
+
* available on the current machine.
|
|
93
|
+
*/
|
|
94
|
+
export async function checkDbatools(config) {
|
|
95
|
+
try {
|
|
96
|
+
const result = await runPowerShell(`$m = Get-Module -ListAvailable -Name dbatools | ` +
|
|
97
|
+
`Sort-Object Version -Descending | Select-Object -First 1; ` +
|
|
98
|
+
`if ($m) { Write-Output $m.Version.ToString() } else { exit 1 }`, config);
|
|
99
|
+
if (result.exitCode === 0 && result.stdout.trim()) {
|
|
100
|
+
return { ok: true, version: result.stdout.trim() };
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
error: "dbatools module not found. Install it with: Install-Module dbatools -Scope CurrentUser",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
return { ok: false, error: String(e) };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Compare the installed dbatools version against the version stored in the
|
|
113
|
+
* help-index manifest. Returns a structured result with a human-readable
|
|
114
|
+
* warning when the two differ.
|
|
115
|
+
*/
|
|
116
|
+
export async function checkVersionMismatch(config, indexedVersion) {
|
|
117
|
+
const check = await checkDbatools(config);
|
|
118
|
+
if (!check.ok || !check.version) {
|
|
119
|
+
return {
|
|
120
|
+
installedVersion: "unknown",
|
|
121
|
+
indexedVersion,
|
|
122
|
+
isStale: false,
|
|
123
|
+
message: check.error ?? "Could not determine installed dbatools version.",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const installed = check.version.trim();
|
|
127
|
+
const isStale = installed !== indexedVersion;
|
|
128
|
+
const message = isStale
|
|
129
|
+
? `⚠️ dbatools version mismatch detected!\n` +
|
|
130
|
+
` Installed : ${installed}\n` +
|
|
131
|
+
` Index : ${indexedVersion}\n` +
|
|
132
|
+
` The help index is stale. Run 'npm run refresh-help' to rebuild it.\n` +
|
|
133
|
+
` Until then, command metadata may be inaccurate for new/changed commands.`
|
|
134
|
+
: `✅ dbatools version matches index (${installed}).`;
|
|
135
|
+
return { installedVersion: installed, indexedVersion, isStale, message };
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=powershell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"powershell.js","sourceRoot":"","sources":["../src/powershell.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":""}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { loadHelpIndex, loadHelpManifest, searchCommands } from "./help-indexer.js";
|
|
5
|
+
import { runPowerShell, checkDbatools, getConfig, checkVersionMismatch } from "./powershell.js";
|
|
6
|
+
import { buildPowerShellScript, } from "./tool-registry.js";
|
|
7
|
+
const config = getConfig();
|
|
8
|
+
// Version mismatch state — resolved once at startup, reused by tools
|
|
9
|
+
let versionState = null;
|
|
10
|
+
async function getVersionState() {
|
|
11
|
+
if (versionState)
|
|
12
|
+
return versionState;
|
|
13
|
+
try {
|
|
14
|
+
const manifest = loadHelpManifest();
|
|
15
|
+
versionState = await checkVersionMismatch(config, manifest.dbatoolsVersion);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
versionState = {
|
|
19
|
+
installedVersion: "unknown",
|
|
20
|
+
indexedVersion: "unknown",
|
|
21
|
+
isStale: false,
|
|
22
|
+
message: "Help index not yet generated — run 'npm run refresh-help'.",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return versionState;
|
|
26
|
+
}
|
|
27
|
+
const server = new McpServer({
|
|
28
|
+
name: "dbatools-mcp-server",
|
|
29
|
+
version: "0.1.0",
|
|
30
|
+
});
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Tool: list_dbatools_commands
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
server.tool("list_dbatools_commands", "Search and list dbatools commands. Filter by verb, noun, keyword, or risk level.", {
|
|
35
|
+
verb: z
|
|
36
|
+
.string()
|
|
37
|
+
.max(50)
|
|
38
|
+
.optional()
|
|
39
|
+
.describe("PowerShell verb (e.g. Get, Set, New, Remove, Test)"),
|
|
40
|
+
noun: z
|
|
41
|
+
.string()
|
|
42
|
+
.max(100)
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Noun fragment to match (e.g. Database, Login, AgentJob)"),
|
|
45
|
+
keyword: z
|
|
46
|
+
.string()
|
|
47
|
+
.max(200)
|
|
48
|
+
.optional()
|
|
49
|
+
.describe("Keyword to search in name, synopsis, and description"),
|
|
50
|
+
riskLevel: z
|
|
51
|
+
.enum(["readonly", "change", "destructive"])
|
|
52
|
+
.optional()
|
|
53
|
+
.describe("Filter by risk tier"),
|
|
54
|
+
limit: z
|
|
55
|
+
.number()
|
|
56
|
+
.int()
|
|
57
|
+
.min(1)
|
|
58
|
+
.max(200)
|
|
59
|
+
.default(50)
|
|
60
|
+
.describe("Maximum number of results (default 50, max 200)"),
|
|
61
|
+
}, async ({ verb, noun, keyword, riskLevel, limit }) => {
|
|
62
|
+
const vs = await getVersionState();
|
|
63
|
+
const stalePrefix = vs.isStale ? vs.message + "\n\n" : "";
|
|
64
|
+
let index;
|
|
65
|
+
try {
|
|
66
|
+
index = loadHelpIndex();
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
return { content: [{ type: "text", text: `Help index unavailable: ${String(e)}` }], isError: true };
|
|
70
|
+
}
|
|
71
|
+
const results = searchCommands(index, { verb, noun, keyword, riskLevel, limit });
|
|
72
|
+
if (results.length === 0) {
|
|
73
|
+
return {
|
|
74
|
+
content: [
|
|
75
|
+
{ type: "text", text: stalePrefix + "No commands found matching the given filters." },
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const header = `Found ${results.length} command(s):\n`;
|
|
80
|
+
const rows = results
|
|
81
|
+
.map((c) => `${c.name.padEnd(50)} [${c.riskLevel.padEnd(11)}] ${(c.synopsis ?? "").substring(0, 80)}`)
|
|
82
|
+
.join("\n");
|
|
83
|
+
return { content: [{ type: "text", text: stalePrefix + header + rows }] };
|
|
84
|
+
});
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Tool: get_dbatools_command_help
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
server.tool("get_dbatools_command_help", "Get the full normalized help for a specific dbatools command, including parameters and examples sourced from comment-based help.", {
|
|
89
|
+
commandName: z
|
|
90
|
+
.string()
|
|
91
|
+
.max(100)
|
|
92
|
+
.describe("Exact command name, e.g. Get-DbaDatabase"),
|
|
93
|
+
}, async ({ commandName }) => {
|
|
94
|
+
const vs = await getVersionState();
|
|
95
|
+
const stalePrefix = vs.isStale ? vs.message + "\n\n" : "";
|
|
96
|
+
let index;
|
|
97
|
+
try {
|
|
98
|
+
index = loadHelpIndex();
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
return { content: [{ type: "text", text: `Help index unavailable: ${String(e)}` }], isError: true };
|
|
102
|
+
}
|
|
103
|
+
const help = index[commandName];
|
|
104
|
+
if (!help) {
|
|
105
|
+
return {
|
|
106
|
+
content: [
|
|
107
|
+
{
|
|
108
|
+
type: "text",
|
|
109
|
+
text: stalePrefix +
|
|
110
|
+
`Command '${commandName}' not found in the help index.\n` +
|
|
111
|
+
`Use list_dbatools_commands to discover available commands.`,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
isError: true,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const paramLines = help.parameters.length === 0
|
|
118
|
+
? " (none documented)"
|
|
119
|
+
: help.parameters
|
|
120
|
+
.map((p) => {
|
|
121
|
+
const req = p.required ? " [REQUIRED]" : "";
|
|
122
|
+
const aliases = p.aliases.length > 0
|
|
123
|
+
? ` aliases: ${p.aliases.join(", ")}`
|
|
124
|
+
: "";
|
|
125
|
+
const pipeline = p.pipelineInput ? " pipeline input: yes" : "";
|
|
126
|
+
return (` -${p.name} <${p.type}>${req}` +
|
|
127
|
+
`${aliases}${pipeline}\n ${p.description || "(no description)"}`);
|
|
128
|
+
})
|
|
129
|
+
.join("\n\n");
|
|
130
|
+
const exampleLines = help.examples.length === 0
|
|
131
|
+
? " (none documented)"
|
|
132
|
+
: help.examples
|
|
133
|
+
.slice(0, 5)
|
|
134
|
+
.map((ex, i) => `--- Example ${i + 1}${ex.title ? ": " + ex.title : ""} ---\n` +
|
|
135
|
+
`${ex.code}\n` +
|
|
136
|
+
(ex.remarks ? `Remarks: ${ex.remarks}` : ""))
|
|
137
|
+
.join("\n\n");
|
|
138
|
+
const text = [
|
|
139
|
+
`NAME: ${help.name}`,
|
|
140
|
+
`RISK LEVEL: ${help.riskLevel}`,
|
|
141
|
+
`VERB / NOUN: ${help.verb} / ${help.noun}`,
|
|
142
|
+
``,
|
|
143
|
+
`SYNOPSIS`,
|
|
144
|
+
`--------`,
|
|
145
|
+
help.synopsis || "(none)",
|
|
146
|
+
``,
|
|
147
|
+
`DESCRIPTION`,
|
|
148
|
+
`-----------`,
|
|
149
|
+
help.description || "(none)",
|
|
150
|
+
``,
|
|
151
|
+
`PARAMETERS`,
|
|
152
|
+
`----------`,
|
|
153
|
+
paramLines,
|
|
154
|
+
``,
|
|
155
|
+
`EXAMPLES`,
|
|
156
|
+
`--------`,
|
|
157
|
+
exampleLines,
|
|
158
|
+
help.relatedLinks.length > 0
|
|
159
|
+
? `\nRELATED LINKS\n-------------\n${help.relatedLinks.join("\n")}`
|
|
160
|
+
: "",
|
|
161
|
+
]
|
|
162
|
+
.join("\n")
|
|
163
|
+
.trim();
|
|
164
|
+
return { content: [{ type: "text", text: stalePrefix + text }] };
|
|
165
|
+
});
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Tool: invoke_dbatools_command
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
server.tool("invoke_dbatools_command", "Execute a dbatools command via PowerShell and return structured JSON output. Non-readonly commands require confirm:true when safe mode is enabled.", {
|
|
170
|
+
commandName: z
|
|
171
|
+
.string()
|
|
172
|
+
.max(100)
|
|
173
|
+
.describe("Exact dbatools command name to execute, e.g. Get-DbaDatabase"),
|
|
174
|
+
parameters: z
|
|
175
|
+
.record(z.unknown())
|
|
176
|
+
.default({})
|
|
177
|
+
.describe("Key-value map of parameters. Strings, numbers, and booleans map directly to PowerShell parameters. " +
|
|
178
|
+
"For SQL authentication pass SqlCredential as an object: { \"username\": \"sa\", \"password\": \"secret\" }. " +
|
|
179
|
+
"Example: { \"SqlInstance\": \"localhost,2022\", \"SqlCredential\": { \"username\": \"sa\", \"password\": \"P@ssw0rd\" } }"),
|
|
180
|
+
confirm: z
|
|
181
|
+
.boolean()
|
|
182
|
+
.default(false)
|
|
183
|
+
.describe("Set to true to allow execution of change/destructive commands (required when safeMode is on)"),
|
|
184
|
+
}, async ({ commandName, parameters, confirm }) => {
|
|
185
|
+
let index;
|
|
186
|
+
try {
|
|
187
|
+
index = loadHelpIndex();
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
return { content: [{ type: "text", text: `Help index unavailable: ${String(e)}` }], isError: true };
|
|
191
|
+
}
|
|
192
|
+
const help = index[commandName];
|
|
193
|
+
if (!help) {
|
|
194
|
+
return {
|
|
195
|
+
content: [
|
|
196
|
+
{
|
|
197
|
+
type: "text",
|
|
198
|
+
text: `Unknown command: '${commandName}'.\n` +
|
|
199
|
+
`Use list_dbatools_commands to discover available commands.`,
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
isError: true,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
// Safety gate: block non-readonly commands unless confirmed
|
|
206
|
+
if (config.safeMode && help.riskLevel !== "readonly" && !confirm) {
|
|
207
|
+
return {
|
|
208
|
+
content: [
|
|
209
|
+
{
|
|
210
|
+
type: "text",
|
|
211
|
+
text: `Command '${commandName}' has risk level '${help.riskLevel}'.\n` +
|
|
212
|
+
`Set confirm: true to allow execution, or use a readonly command instead.`,
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
isError: true,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
let script;
|
|
219
|
+
try {
|
|
220
|
+
script = buildPowerShellScript(commandName, parameters, config.maxOutputRows);
|
|
221
|
+
}
|
|
222
|
+
catch (e) {
|
|
223
|
+
return {
|
|
224
|
+
content: [{ type: "text", text: `Parameter validation error: ${String(e)}` }],
|
|
225
|
+
isError: true,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
let result;
|
|
229
|
+
try {
|
|
230
|
+
result = await runPowerShell(script, config);
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
return {
|
|
234
|
+
content: [{ type: "text", text: `PowerShell execution failed: ${String(e)}` }],
|
|
235
|
+
isError: true,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (result.exitCode !== 0) {
|
|
239
|
+
return {
|
|
240
|
+
content: [
|
|
241
|
+
{
|
|
242
|
+
type: "text",
|
|
243
|
+
text: `Command failed (exit ${result.exitCode}):\n` +
|
|
244
|
+
(result.stderr || result.stdout || "(no output)"),
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
isError: true,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
// Try to parse structured output
|
|
251
|
+
let parsed = undefined;
|
|
252
|
+
const trimmed = result.stdout.trim();
|
|
253
|
+
if (trimmed) {
|
|
254
|
+
try {
|
|
255
|
+
parsed = JSON.parse(trimmed);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// Not JSON — return raw text
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const recordCount = Array.isArray(parsed)
|
|
262
|
+
? parsed.length
|
|
263
|
+
: parsed != null
|
|
264
|
+
? 1
|
|
265
|
+
: 0;
|
|
266
|
+
const summary = `Executed '${commandName}' — ${recordCount} record(s) returned.`;
|
|
267
|
+
const body = parsed
|
|
268
|
+
? JSON.stringify(parsed, null, 2)
|
|
269
|
+
: trimmed || "(no output)";
|
|
270
|
+
const stderrSection = result.stderr.trim() ? `\n\nSTDERR:\n${result.stderr.trim()}` : "";
|
|
271
|
+
return {
|
|
272
|
+
content: [
|
|
273
|
+
{ type: "text", text: `${summary}\n\n${body}${stderrSection}` },
|
|
274
|
+
],
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
// Tool: check_dbatools_environment
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
server.tool("check_dbatools_environment", "Verify that PowerShell and the dbatools module are installed and report the help-index status.", {}, async () => {
|
|
281
|
+
const check = await checkDbatools(config);
|
|
282
|
+
if (!check.ok) {
|
|
283
|
+
return {
|
|
284
|
+
content: [
|
|
285
|
+
{
|
|
286
|
+
type: "text",
|
|
287
|
+
text: `dbatools not available: ${check.error}`,
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
isError: true,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
let indexInfo = "Help index: not generated yet (run 'npm run refresh-help')";
|
|
294
|
+
let vsMismatch = "";
|
|
295
|
+
try {
|
|
296
|
+
const manifest = loadHelpManifest();
|
|
297
|
+
indexInfo =
|
|
298
|
+
`Help index: ${manifest.commandCount} commands indexed\n` +
|
|
299
|
+
` dbatools version in index: ${manifest.dbatoolsVersion}\n` +
|
|
300
|
+
` Generated at: ${manifest.generatedAt}`;
|
|
301
|
+
const vs = await checkVersionMismatch(config, manifest.dbatoolsVersion);
|
|
302
|
+
// Invalidate cached state so next tool call re-evaluates
|
|
303
|
+
versionState = vs;
|
|
304
|
+
vsMismatch = vs.message;
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
// Index missing — that's fine, non-fatal
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
content: [
|
|
311
|
+
{
|
|
312
|
+
type: "text",
|
|
313
|
+
text: [
|
|
314
|
+
`dbatools ${check.version} is installed and ready.`,
|
|
315
|
+
indexInfo,
|
|
316
|
+
vsMismatch,
|
|
317
|
+
`Safe mode: ${config.safeMode ? "ON (non-readonly commands require confirm:true)" : "OFF"}`,
|
|
318
|
+
`Max output rows: ${config.maxOutputRows}`,
|
|
319
|
+
`Timeout: ${config.commandTimeout}s`,
|
|
320
|
+
].filter(Boolean).join("\n"),
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
};
|
|
324
|
+
});
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// Start
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
const transport = new StdioServerTransport();
|
|
329
|
+
await server.connect(transport);
|
|
330
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACpF,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,SAAS,EAAE,oBAAoB,EAA8B,MAAM,iBAAiB,CAAC;AAC5H,OAAO,EAEL,qBAAqB,GAEtB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;AAE3B,qEAAqE;AACrE,IAAI,YAAY,GAAiC,IAAI,CAAC;AAEtD,KAAK,UAAU,eAAe;IAC5B,IAAI,YAAY;QAAE,OAAO,YAAY,CAAC;IACtC,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,gBAAgB,EAAE,CAAC;QACpC,YAAY,GAAG,MAAM,oBAAoB,CAAC,MAAM,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;IAC9E,CAAC;IAAC,MAAM,CAAC;QACP,YAAY,GAAG;YACb,gBAAgB,EAAE,SAAS;YAC3B,cAAc,EAAE,SAAS;YACzB,OAAO,EAAE,KAAK;YACd,OAAO,EAAE,4DAA4D;SACtE,CAAC;IACJ,CAAC;IACD,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,qBAAqB;IAC3B,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH,8EAA8E;AAC9E,+BAA+B;AAC/B,8EAA8E;AAC9E,MAAM,CAAC,IAAI,CACT,wBAAwB,EACxB,kFAAkF,EAClF;IACE,IAAI,EAAE,CAAC;SACJ,MAAM,EAAE;SACR,GAAG,CAAC,EAAE,CAAC;SACP,QAAQ,EAAE;SACV,QAAQ,CAAC,oDAAoD,CAAC;IACjE,IAAI,EAAE,CAAC;SACJ,MAAM,EAAE;SACR,GAAG,CAAC,GAAG,CAAC;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,yDAAyD,CAAC;IACtE,OAAO,EAAE,CAAC;SACP,MAAM,EAAE;SACR,GAAG,CAAC,GAAG,CAAC;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,sDAAsD,CAAC;IACnE,SAAS,EAAE,CAAC;SACT,IAAI,CAAC,CAAC,UAAU,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAC;SAC3C,QAAQ,EAAE;SACV,QAAQ,CAAC,qBAAqB,CAAC;IAClC,KAAK,EAAE,CAAC;SACL,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,GAAG,CAAC;SACR,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CAAC,iDAAiD,CAAC;CAC/D,EACD,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,EAAE;IAClD,MAAM,EAAE,GAAG,MAAM,eAAe,EAAE,CAAC;IACnC,MAAM,WAAW,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IAE1D,IAAI,KAAK,CAAC;IACV,IAAI,CAAC;QACH,KAAK,GAAG,aAAa,EAAE,CAAC;IAC1B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,2BAA2B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC/G,CAAC;IACD,MAAM,OAAO,GAAG,cAAc,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;IAEjF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO;YACL,OAAO,EAAE;gBACP,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,+CAA+C,EAAE;aACtF;SACF,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,SAAS,OAAO,CAAC,MAAM,gBAAgB,CAAC;IACvD,MAAM,IAAI,GAAG,OAAO;SACjB,GAAG,CACF,CAAC,CAAC,EAAE,EAAE,CACJ,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAC7F;SACA,IAAI,CAAC,IAAI,CAAC,CAAC;IAEd,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,EAAE,CAAC,EAAE,CAAC;AAC5E,CAAC,CACF,CAAC;AAEF,8EAA8E;AAC9E,kCAAkC;AAClC,8EAA8E;AAC9E,MAAM,CAAC,IAAI,CACT,2BAA2B,EAC3B,kIAAkI,EAClI;IACE,WAAW,EAAE,CAAC;SACX,MAAM,EAAE;SACR,GAAG,CAAC,GAAG,CAAC;SACR,QAAQ,CAAC,0CAA0C,CAAC;CACxD,EACD,KAAK,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE;IACxB,MAAM,EAAE,GAAG,MAAM,eAAe,EAAE,CAAC;IACnC,MAAM,WAAW,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IAE1D,IAAI,KAAK,CAAC;IACV,IAAI,CAAC;QACH,KAAK,GAAG,aAAa,EAAE,CAAC;IAC1B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,2BAA2B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC/G,CAAC;IACD,MAAM,IAAI,GAAG,KAAK,CAAC,WAAW,CAAC,CAAC;IAEhC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAM;oBACZ,IAAI,EACF,WAAW;wBACX,YAAY,WAAW,kCAAkC;wBACzD,4DAA4D;iBAC/D;aACF;YACD,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,GACd,IAAI,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC;QAC1B,CAAC,CAAC,qBAAqB;QACvB,CAAC,CAAC,IAAI,CAAC,UAAU;aACZ,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YACT,MAAM,GAAG,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC;YAC5C,MAAM,OAAO,GACX,CAAC,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC;gBAClB,CAAC,CAAC,cAAc,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;gBACtC,CAAC,CAAC,EAAE,CAAC;YACT,MAAM,QAAQ,GAAG,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,EAAE,CAAC;YAChE,OAAO,CACL,MAAM,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,IAAI,GAAG,EAAE;gBAChC,GAAG,OAAO,GAAG,QAAQ,SAAS,CAAC,CAAC,WAAW,IAAI,kBAAkB,EAAE,CACpE,CAAC;QACJ,CAAC,CAAC;aACD,IAAI,CAAC,MAAM,CAAC,CAAC;IAEtB,MAAM,YAAY,GAChB,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;QACxB,CAAC,CAAC,qBAAqB;QACvB,CAAC,CAAC,IAAI,CAAC,QAAQ;aACV,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;aACX,GAAG,CACF,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,CACR,eAAe,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ;YAC9D,GAAG,EAAE,CAAC,IAAI,IAAI;YACd,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAC/C;aACA,IAAI,CAAC,MAAM,CAAC,CAAC;IAEtB,MAAM,IAAI,GAAG;QACX,gBAAgB,IAAI,CAAC,IAAI,EAAE;QAC3B,gBAAgB,IAAI,CAAC,SAAS,EAAE;QAChC,gBAAgB,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,IAAI,EAAE;QAC1C,EAAE;QACF,UAAU;QACV,UAAU;QACV,IAAI,CAAC,QAAQ,IAAI,QAAQ;QACzB,EAAE;QACF,aAAa;QACb,aAAa;QACb,IAAI,CAAC,WAAW,IAAI,QAAQ;QAC5B,EAAE;QACF,YAAY;QACZ,YAAY;QACZ,UAAU;QACV,EAAE;QACF,UAAU;QACV,UAAU;QACV,YAAY;QACZ,IAAI,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC;YAC1B,CAAC,CAAC,mCAAmC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;YACnE,CAAC,CAAC,EAAE;KACP;SACE,IAAI,CAAC,IAAI,CAAC;SACV,IAAI,EAAE,CAAC;IAEV,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,EAAE,CAAC,EAAE,CAAC;AACnE,CAAC,CACF,CAAC;AAEF,8EAA8E;AAC9E,gCAAgC;AAChC,8EAA8E;AAC9E,MAAM,CAAC,IAAI,CACT,yBAAyB,EACzB,oJAAoJ,EACpJ;IACE,WAAW,EAAE,CAAC;SACX,MAAM,EAAE;SACR,GAAG,CAAC,GAAG,CAAC;SACR,QAAQ,CAAC,8DAA8D,CAAC;IAC3E,UAAU,EAAE,CAAC;SACV,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;SACnB,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CACP,qGAAqG;QACrG,8GAA8G;QAC9G,2HAA2H,CAC5H;IACH,OAAO,EAAE,CAAC;SACP,OAAO,EAAE;SACT,OAAO,CAAC,KAAK,CAAC;SACd,QAAQ,CACP,8FAA8F,CAC/F;CACJ,EACD,KAAK,EAAE,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,EAAE;IAC7C,IAAI,KAAK,CAAC;IACV,IAAI,CAAC;QACH,KAAK,GAAG,aAAa,EAAE,CAAC;IAC1B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,2BAA2B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC/G,CAAC;IACD,MAAM,IAAI,GAAG,KAAK,CAAC,WAAW,CAAC,CAAC;IAEhC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAM;oBACZ,IAAI,EACF,qBAAqB,WAAW,MAAM;wBACtC,4DAA4D;iBAC/D;aACF;YACD,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,4DAA4D;IAC5D,IAAI,MAAM,CAAC,QAAQ,IAAI,IAAI,CAAC,SAAS,KAAK,UAAU,IAAI,CAAC,OAAO,EAAE,CAAC;QACjE,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAM;oBACZ,IAAI,EACF,YAAY,WAAW,qBAAqB,IAAI,CAAC,SAAS,MAAM;wBAChE,0EAA0E;iBAC7E;aACF;YACD,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,IAAI,MAAc,CAAC;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,qBAAqB,CAC5B,WAAW,EACX,UAAqC,EACrC,MAAM,CAAC,aAAa,CACrB,CAAC;IACJ,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,+BAA+B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;YAC7E,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/C,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,gCAAgC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;YAC9E,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAM;oBACZ,IAAI,EACF,wBAAwB,MAAM,CAAC,QAAQ,MAAM;wBAC7C,CAAC,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,IAAI,aAAa,CAAC;iBACpD;aACF;YACD,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,iCAAiC;IACjC,IAAI,MAAM,GAAY,SAAS,CAAC;IAChC,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IACrC,IAAI,OAAO,EAAE,CAAC;QACZ,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC/B,CAAC;QAAC,MAAM,CAAC;YACP,6BAA6B;QAC/B,CAAC;IACH,CAAC;IAED,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QACvC,CAAC,CAAC,MAAM,CAAC,MAAM;QACf,CAAC,CAAC,MAAM,IAAI,IAAI;YACd,CAAC,CAAC,CAAC;YACH,CAAC,CAAC,CAAC,CAAC;IAER,MAAM,OAAO,GAAG,aAAa,WAAW,OAAO,WAAW,sBAAsB,CAAC;IAEjF,MAAM,IAAI,GAAG,MAAM;QACjB,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;QACjC,CAAC,CAAC,OAAO,IAAI,aAAa,CAAC;IAE7B,MAAM,aAAa,GACjB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,gBAAgB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAErE,OAAO;QACL,OAAO,EAAE;YACP,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,OAAO,IAAI,GAAG,aAAa,EAAE,EAAE;SAChE;KACF,CAAC;AACJ,CAAC,CACF,CAAC;AAEF,8EAA8E;AAC9E,mCAAmC;AACnC,8EAA8E;AAC9E,MAAM,CAAC,IAAI,CACT,4BAA4B,EAC5B,gGAAgG,EAChG,EAAE,EACF,KAAK,IAAI,EAAE;IACT,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC;IAE1C,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;QACd,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE,2BAA2B,KAAK,CAAC,KAAK,EAAE;iBAC/C;aACF;YACD,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,IAAI,SAAS,GAAG,4DAA4D,CAAC;IAC7E,IAAI,UAAU,GAAG,EAAE,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,gBAAgB,EAAE,CAAC;QACpC,SAAS;YACP,eAAe,QAAQ,CAAC,YAAY,qBAAqB;gBACzD,gCAAgC,QAAQ,CAAC,eAAe,IAAI;gBAC5D,gCAAgC,QAAQ,CAAC,WAAW,EAAE,CAAC;QACzD,MAAM,EAAE,GAAG,MAAM,oBAAoB,CAAC,MAAM,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;QACxE,yDAAyD;QACzD,YAAY,GAAG,EAAE,CAAC;QAClB,UAAU,GAAG,EAAE,CAAC,OAAO,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,yCAAyC;IAC3C,CAAC;IAED,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAM;gBACZ,IAAI,EAAE;oBACJ,YAAY,KAAK,CAAC,OAAO,0BAA0B;oBACnD,SAAS;oBACT,UAAU;oBACV,oBAAoB,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,iDAAiD,CAAC,CAAC,CAAC,KAAK,EAAE;oBACjG,oBAAoB,MAAM,CAAC,aAAa,EAAE;oBAC1C,oBAAoB,MAAM,CAAC,cAAc,GAAG;iBAC7C,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;aAC7B;SACF;KACF,CAAC;AACJ,CAAC,CACF,CAAC;AAEF,8EAA8E;AAC9E,QAAQ;AACR,8EAA8E;AAC9E,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;AAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { DbatoolsCommandHelp } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Classify a dbatools command into a risk tier based on its verb.
|
|
4
|
+
* readonly — safe to run without confirmation
|
|
5
|
+
* change — modifies state but is reversible
|
|
6
|
+
* destructive — data loss risk; blocked by safe mode until confirm:true
|
|
7
|
+
*/
|
|
8
|
+
export declare function classifyCommand(name: string): "readonly" | "change" | "destructive";
|
|
9
|
+
/**
|
|
10
|
+
* Credential descriptor accepted in the `parameters` map under the key `SqlCredential`.
|
|
11
|
+
* Pass as: { "SqlCredential": { "username": "sa", "password": "secret" } }
|
|
12
|
+
*/
|
|
13
|
+
export interface SqlCredentialDescriptor {
|
|
14
|
+
username: string;
|
|
15
|
+
password: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Build a self-contained PowerShell script that imports dbatools, invokes the
|
|
19
|
+
* requested command with the supplied parameters, and emits JSON via
|
|
20
|
+
* ConvertTo-Json so the MCP server gets structured output.
|
|
21
|
+
*
|
|
22
|
+
* String values are single-quoted and internal single-quotes are escaped.
|
|
23
|
+
* Switch parameters are passed as bare flags when value is true.
|
|
24
|
+
*
|
|
25
|
+
* Special parameter: SqlCredential — accepts { username, password } and is
|
|
26
|
+
* converted into a PSCredential object so SQL authentication works.
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildPowerShellScript(commandName: string, args: Record<string, unknown>, maxRows: number): string;
|
|
29
|
+
/**
|
|
30
|
+
* Format a DbatoolsCommandHelp record into a concise MCP tool description
|
|
31
|
+
* (≤ 1024 chars, which is the MCP spec recommendation).
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildToolDescription(cmd: DbatoolsCommandHelp): string;
|
|
34
|
+
//# sourceMappingURL=tool-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-registry.d.ts","sourceRoot":"","sources":["../src/tool-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAWtD;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,GACX,UAAU,GAAG,QAAQ,GAAG,aAAa,CAKvC;AAED;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAWD;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,MAAM,GACd,MAAM,CAuDR;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,mBAAmB,GAAG,MAAM,CAQrE"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const READONLY_VERBS = new Set([
|
|
2
|
+
"Get", "Test", "Find", "Measure", "Select", "Show",
|
|
3
|
+
"Watch", "Compare", "Search", "Resolve",
|
|
4
|
+
]);
|
|
5
|
+
const DESTRUCTIVE_VERBS = new Set([
|
|
6
|
+
"Remove", "Drop", "Delete", "Uninstall", "Revoke", "Disable", "Reset",
|
|
7
|
+
]);
|
|
8
|
+
/**
|
|
9
|
+
* Classify a dbatools command into a risk tier based on its verb.
|
|
10
|
+
* readonly — safe to run without confirmation
|
|
11
|
+
* change — modifies state but is reversible
|
|
12
|
+
* destructive — data loss risk; blocked by safe mode until confirm:true
|
|
13
|
+
*/
|
|
14
|
+
export function classifyCommand(name) {
|
|
15
|
+
const verb = name.split("-")[0] ?? "";
|
|
16
|
+
if (READONLY_VERBS.has(verb))
|
|
17
|
+
return "readonly";
|
|
18
|
+
if (DESTRUCTIVE_VERBS.has(verb))
|
|
19
|
+
return "destructive";
|
|
20
|
+
return "change";
|
|
21
|
+
}
|
|
22
|
+
function isSqlCredentialDescriptor(v) {
|
|
23
|
+
return (typeof v === "object" &&
|
|
24
|
+
v !== null &&
|
|
25
|
+
typeof v["username"] === "string" &&
|
|
26
|
+
typeof v["password"] === "string");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build a self-contained PowerShell script that imports dbatools, invokes the
|
|
30
|
+
* requested command with the supplied parameters, and emits JSON via
|
|
31
|
+
* ConvertTo-Json so the MCP server gets structured output.
|
|
32
|
+
*
|
|
33
|
+
* String values are single-quoted and internal single-quotes are escaped.
|
|
34
|
+
* Switch parameters are passed as bare flags when value is true.
|
|
35
|
+
*
|
|
36
|
+
* Special parameter: SqlCredential — accepts { username, password } and is
|
|
37
|
+
* converted into a PSCredential object so SQL authentication works.
|
|
38
|
+
*/
|
|
39
|
+
export function buildPowerShellScript(commandName, args, maxRows) {
|
|
40
|
+
// Validate command name against an allowlist pattern (letters, digits, hyphens only)
|
|
41
|
+
if (!/^[A-Za-z]+-[A-Za-z][A-Za-z0-9]*$/.test(commandName)) {
|
|
42
|
+
throw new Error(`Invalid command name: ${commandName}`);
|
|
43
|
+
}
|
|
44
|
+
const preambleLines = [];
|
|
45
|
+
const splatEntries = [];
|
|
46
|
+
for (const [key, value] of Object.entries(args)) {
|
|
47
|
+
// Validate parameter key (letters, digits only)
|
|
48
|
+
if (!/^[A-Za-z][A-Za-z0-9]*$/.test(key)) {
|
|
49
|
+
throw new Error(`Invalid parameter name: ${key}`);
|
|
50
|
+
}
|
|
51
|
+
if (value === null || value === undefined)
|
|
52
|
+
continue;
|
|
53
|
+
// Special case: SqlCredential object → build a PSCredential in the script
|
|
54
|
+
if (key === "SqlCredential" && isSqlCredentialDescriptor(value)) {
|
|
55
|
+
const escapedUser = value.username.replace(/'/g, "''");
|
|
56
|
+
const escapedPass = value.password.replace(/'/g, "''");
|
|
57
|
+
preambleLines.push(`$__securePass = ConvertTo-SecureString '${escapedPass}' -AsPlainText -Force`, `$__cred = New-Object System.Management.Automation.PSCredential('${escapedUser}', $__securePass)`);
|
|
58
|
+
splatEntries.push(` SqlCredential = $__cred`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (typeof value === "boolean") {
|
|
62
|
+
if (value)
|
|
63
|
+
splatEntries.push(` ${key} = $true`);
|
|
64
|
+
}
|
|
65
|
+
else if (typeof value === "number") {
|
|
66
|
+
if (!Number.isFinite(value))
|
|
67
|
+
throw new Error(`Non-finite number for -${key}`);
|
|
68
|
+
splatEntries.push(` ${key} = ${value}`);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Sanitize string: escape embedded single-quotes
|
|
72
|
+
const escaped = String(value).replace(/'/g, "''");
|
|
73
|
+
splatEntries.push(` ${key} = '${escaped}'`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const splatBlock = splatEntries.length > 0
|
|
77
|
+
? [`$params = @{`, ...splatEntries, `}`].join("\n")
|
|
78
|
+
: `$params = @{}`;
|
|
79
|
+
return [
|
|
80
|
+
`Set-StrictMode -Off`,
|
|
81
|
+
`$ErrorActionPreference = 'Stop'`,
|
|
82
|
+
`Import-Module dbatools -ErrorAction Stop`,
|
|
83
|
+
...preambleLines,
|
|
84
|
+
splatBlock,
|
|
85
|
+
`$result = ${commandName} @params | Select-Object -First ${maxRows}`,
|
|
86
|
+
`if ($null -eq $result) { Write-Output '[]'; exit 0 }`,
|
|
87
|
+
`$result | ConvertTo-Json -Depth 5 -Compress`,
|
|
88
|
+
].join("\n");
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Format a DbatoolsCommandHelp record into a concise MCP tool description
|
|
92
|
+
* (≤ 1024 chars, which is the MCP spec recommendation).
|
|
93
|
+
*/
|
|
94
|
+
export function buildToolDescription(cmd) {
|
|
95
|
+
const base = cmd.synopsis || cmd.description || cmd.name;
|
|
96
|
+
const risk = cmd.riskLevel === "readonly"
|
|
97
|
+
? ""
|
|
98
|
+
: ` [${cmd.riskLevel.toUpperCase()} — requires confirm:true]`;
|
|
99
|
+
const full = `${base}${risk}`;
|
|
100
|
+
return full.length > 1024 ? full.substring(0, 1021) + "..." : full;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=tool-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-registry.js","sourceRoot":"","sources":["../src/tool-registry.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Represents one parameter from Get-Help -Full output */
|
|
2
|
+
export interface DbatoolsParameter {
|
|
3
|
+
name: string;
|
|
4
|
+
type: string;
|
|
5
|
+
required: boolean;
|
|
6
|
+
aliases: string[];
|
|
7
|
+
pipelineInput: boolean;
|
|
8
|
+
description: string;
|
|
9
|
+
defaultValue?: string;
|
|
10
|
+
}
|
|
11
|
+
/** One example block from Get-Help -Full output */
|
|
12
|
+
export interface DbatoolsExample {
|
|
13
|
+
title: string;
|
|
14
|
+
code: string;
|
|
15
|
+
remarks: string;
|
|
16
|
+
}
|
|
17
|
+
/** Full normalized help payload for one dbatools command */
|
|
18
|
+
export interface DbatoolsCommandHelp {
|
|
19
|
+
name: string;
|
|
20
|
+
verb: string;
|
|
21
|
+
noun: string;
|
|
22
|
+
synopsis: string;
|
|
23
|
+
description: string;
|
|
24
|
+
parameters: DbatoolsParameter[];
|
|
25
|
+
examples: DbatoolsExample[];
|
|
26
|
+
relatedLinks: string[];
|
|
27
|
+
tags: string[];
|
|
28
|
+
riskLevel: "readonly" | "change" | "destructive";
|
|
29
|
+
}
|
|
30
|
+
/** The full dbatools-help.json manifest written by refresh-help.ps1 */
|
|
31
|
+
export interface HelpManifest {
|
|
32
|
+
generatedAt: string;
|
|
33
|
+
dbatoolsVersion: string;
|
|
34
|
+
commandCount: number;
|
|
35
|
+
commands: HelpIndex;
|
|
36
|
+
}
|
|
37
|
+
/** Keyed by command name, e.g. "Get-DbaDatabase" */
|
|
38
|
+
export type HelpIndex = Record<string, DbatoolsCommandHelp>;
|
|
39
|
+
/** Result returned from running a PowerShell script */
|
|
40
|
+
export interface PowerShellResult {
|
|
41
|
+
stdout: string;
|
|
42
|
+
stderr: string;
|
|
43
|
+
exitCode: number;
|
|
44
|
+
}
|
|
45
|
+
/** Runtime configuration resolved from environment variables */
|
|
46
|
+
export interface ServerConfig {
|
|
47
|
+
powershellExe: string;
|
|
48
|
+
/** When true, non-readonly commands require confirm:true */
|
|
49
|
+
safeMode: boolean;
|
|
50
|
+
/** Maximum rows piped through Select-Object before ConvertTo-Json */
|
|
51
|
+
maxOutputRows: number;
|
|
52
|
+
/** Seconds before the PowerShell child process is killed */
|
|
53
|
+
commandTimeout: number;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,0DAA0D;AAC1D,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,aAAa,EAAE,OAAO,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,mDAAmD;AACnD,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,4DAA4D;AAC5D,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,iBAAiB,EAAE,CAAC;IAChC,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,EAAE,UAAU,GAAG,QAAQ,GAAG,aAAa,CAAC;CAClD;AAED,uEAAuE;AACvE,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,oDAAoD;AACpD,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;AAE5D,uDAAuD;AACvD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,gEAAgE;AAChE,MAAM,WAAW,YAAY;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,EAAE,OAAO,CAAC;IAClB,qEAAqE;IACrE,aAAa,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,cAAc,EAAE,MAAM,CAAC;CACxB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dbatools-mcp-server",
|
|
3
|
+
"mcpName": "io.github.dataplat/dbatools-mcp-server",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "MCP server for the dbatools PowerShell module — exposes dbatools commands as MCP tools driven by comment-based help",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"dbatools",
|
|
8
|
+
"mcp",
|
|
9
|
+
"sql server",
|
|
10
|
+
"powershell",
|
|
11
|
+
"database"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/dataplat/dbatools-mcp-server.git"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"generated/.gitkeep",
|
|
22
|
+
"scripts/refresh-help.ps1"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"dbatools-mcp-server": "./dist/server.js"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc",
|
|
29
|
+
"dev": "tsx src/server.ts",
|
|
30
|
+
"start": "node dist/server.js",
|
|
31
|
+
"refresh-help": "pwsh -NonInteractive -File scripts/refresh-help.ps1",
|
|
32
|
+
"test": "node --experimental-vm-modules node_modules/.bin/jest --passWithNoTests"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@modelcontextprotocol/sdk": "^1.10.0",
|
|
36
|
+
"zod": "^3.24.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/jest": "^29.5.0",
|
|
40
|
+
"@types/node": "^22.0.0",
|
|
41
|
+
"jest": "^29.7.0",
|
|
42
|
+
"ts-jest": "^29.2.0",
|
|
43
|
+
"tsx": "^4.19.0",
|
|
44
|
+
"typescript": "^5.7.0"
|
|
45
|
+
},
|
|
46
|
+
"jest": {
|
|
47
|
+
"preset": "ts-jest/presets/default-esm",
|
|
48
|
+
"extensionsToTreatAsEsm": [
|
|
49
|
+
".ts"
|
|
50
|
+
],
|
|
51
|
+
"moduleNameMapper": {
|
|
52
|
+
"^(\\.{1,2}/.*)\\.js$": "$1"
|
|
53
|
+
},
|
|
54
|
+
"transform": {
|
|
55
|
+
"^.+\\.tsx?$": [
|
|
56
|
+
"ts-jest",
|
|
57
|
+
{
|
|
58
|
+
"useESM": true
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=20.0.0"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Generates generated/dbatools-help.json by extracting comment-based help from
|
|
4
|
+
every command in the locally installed dbatools module.
|
|
5
|
+
|
|
6
|
+
.DESCRIPTION
|
|
7
|
+
Run this script whenever dbatools is installed or updated.
|
|
8
|
+
Output is consumed by the MCP server at startup and cached for the session.
|
|
9
|
+
|
|
10
|
+
Usage: npm run refresh-help
|
|
11
|
+
pwsh -File scripts/refresh-help.ps1
|
|
12
|
+
|
|
13
|
+
.PARAMETER OutputPath
|
|
14
|
+
Override the output file path (default: <repo-root>/generated/dbatools-help.json)
|
|
15
|
+
|
|
16
|
+
.PARAMETER MaxCommands
|
|
17
|
+
Limit to N commands for quick development iterations (0 = all commands)
|
|
18
|
+
#>
|
|
19
|
+
[CmdletBinding()]
|
|
20
|
+
param(
|
|
21
|
+
[string]$OutputPath = "",
|
|
22
|
+
[int]$MaxCommands = 0
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
$ErrorActionPreference = 'Stop'
|
|
26
|
+
$InformationPreference = 'Continue'
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Resolve paths
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
$ScriptDir = Split-Path -Parent $PSCommandPath
|
|
32
|
+
$RepoRoot = Split-Path -Parent $ScriptDir
|
|
33
|
+
$OutputDir = Join-Path $RepoRoot 'generated'
|
|
34
|
+
$DefaultOut = Join-Path $OutputDir 'dbatools-help.json'
|
|
35
|
+
|
|
36
|
+
if ($OutputPath -eq "") { $OutputPath = $DefaultOut }
|
|
37
|
+
|
|
38
|
+
if (-not (Test-Path $OutputDir)) {
|
|
39
|
+
New-Item -ItemType Directory -Path $OutputDir | Out-Null
|
|
40
|
+
Write-Information "Created directory: $OutputDir"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Verify dbatools
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
$module = Get-Module -ListAvailable -Name dbatools |
|
|
47
|
+
Sort-Object Version -Descending |
|
|
48
|
+
Select-Object -First 1
|
|
49
|
+
|
|
50
|
+
if (-not $module) {
|
|
51
|
+
Write-Error "dbatools is not installed.`nRun: Install-Module dbatools -Scope CurrentUser"
|
|
52
|
+
exit 1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
Write-Information "Found dbatools $($module.Version) at $($module.ModuleBase)"
|
|
56
|
+
Write-Information "Importing module..."
|
|
57
|
+
Import-Module dbatools -ErrorAction Stop
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# Enumerate commands
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
$commands = Get-Command -Module dbatools | Sort-Object Name
|
|
63
|
+
|
|
64
|
+
if ($MaxCommands -gt 0) {
|
|
65
|
+
$commands = $commands | Select-Object -First $MaxCommands
|
|
66
|
+
Write-Warning "MaxCommands=$MaxCommands — indexing a subset only."
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
$total = $commands.Count
|
|
70
|
+
Write-Information "Indexing $total commands..."
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
# Risk classification helpers
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
$ReadonlyVerbs = @('Get','Test','Find','Measure','Select','Show','Watch','Compare','Search','Resolve')
|
|
76
|
+
$DestructiveVerbs = @('Remove','Drop','Delete','Uninstall','Revoke','Disable','Reset')
|
|
77
|
+
|
|
78
|
+
function Get-RiskLevel([string]$verb) {
|
|
79
|
+
if ($ReadonlyVerbs -contains $verb) { return 'readonly' }
|
|
80
|
+
if ($DestructiveVerbs -contains $verb) { return 'destructive' }
|
|
81
|
+
return 'change'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# Help extraction loop
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
$index = [System.Collections.Specialized.OrderedDictionary]::new()
|
|
88
|
+
$failures = 0
|
|
89
|
+
$counter = 0
|
|
90
|
+
|
|
91
|
+
foreach ($cmd in $commands) {
|
|
92
|
+
$counter++
|
|
93
|
+
if ($counter % 100 -eq 0) {
|
|
94
|
+
Write-Information " $counter / $total ($([Math]::Round($counter/$total*100))%)"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
$help = Get-Help $cmd.Name -Full -ErrorAction SilentlyContinue
|
|
99
|
+
|
|
100
|
+
# --- Parameters ---------------------------------------------------
|
|
101
|
+
$params = [System.Collections.Generic.List[hashtable]]::new()
|
|
102
|
+
if ($help.parameters -and $help.parameters.parameter) {
|
|
103
|
+
foreach ($p in $help.parameters.parameter) {
|
|
104
|
+
$desc = if ($p.description) {
|
|
105
|
+
($p.description | ForEach-Object { $_.Text }) -join ' '
|
|
106
|
+
} else { '' }
|
|
107
|
+
|
|
108
|
+
$aliasArr = if ($p.aliases -and $p.aliases -ne 'None') {
|
|
109
|
+
@($p.aliases -split ',\s*' | Where-Object { $_ -ne '' })
|
|
110
|
+
} else { @() }
|
|
111
|
+
|
|
112
|
+
$params.Add([ordered]@{
|
|
113
|
+
name = [string]$p.name
|
|
114
|
+
type = if ($p.type -and $p.type.name) { [string]$p.type.name } else { 'Object' }
|
|
115
|
+
required = ($p.required -eq 'true')
|
|
116
|
+
aliases = $aliasArr
|
|
117
|
+
pipelineInput = ($p.pipelineInput -and $p.pipelineInput -ne 'false')
|
|
118
|
+
description = $desc.Trim()
|
|
119
|
+
defaultValue = if ($p.defaultValue) { [string]$p.defaultValue } else { $null }
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# --- Examples -----------------------------------------------------
|
|
125
|
+
$examples = [System.Collections.Generic.List[hashtable]]::new()
|
|
126
|
+
if ($help.examples -and $help.examples.example) {
|
|
127
|
+
foreach ($ex in $help.examples.example) {
|
|
128
|
+
$remarks = if ($ex.remarks) {
|
|
129
|
+
($ex.remarks | ForEach-Object { $_.Text }) -join ' '
|
|
130
|
+
} else { '' }
|
|
131
|
+
|
|
132
|
+
$examples.Add([ordered]@{
|
|
133
|
+
title = ([string]($ex.title ?? '')).TrimStart('-').Trim()
|
|
134
|
+
code = ([string]($ex.code ?? '')).Trim()
|
|
135
|
+
remarks = $remarks.Trim()
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# --- Related links ------------------------------------------------
|
|
141
|
+
$links = @()
|
|
142
|
+
if ($help.relatedLinks -and $help.relatedLinks.navigationLink) {
|
|
143
|
+
$links = @(
|
|
144
|
+
$help.relatedLinks.navigationLink |
|
|
145
|
+
ForEach-Object { if ($_.uri) { $_.uri } elseif ($_.linkText) { $_.linkText } } |
|
|
146
|
+
Where-Object { $_ -and $_ -ne '' }
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# --- Synopsis / Description ---------------------------------------
|
|
151
|
+
$synopsis = if ($help.Synopsis) { $help.Synopsis.Trim() } else { '' }
|
|
152
|
+
|
|
153
|
+
$description = if ($help.description) {
|
|
154
|
+
($help.description | ForEach-Object { $_.Text }) -join ' '
|
|
155
|
+
} else { '' }
|
|
156
|
+
|
|
157
|
+
$index[$cmd.Name] = [ordered]@{
|
|
158
|
+
name = $cmd.Name
|
|
159
|
+
verb = $cmd.Verb
|
|
160
|
+
noun = $cmd.Noun
|
|
161
|
+
synopsis = $synopsis
|
|
162
|
+
description = $description.Trim()
|
|
163
|
+
parameters = $params.ToArray()
|
|
164
|
+
examples = $examples.ToArray()
|
|
165
|
+
relatedLinks = $links
|
|
166
|
+
tags = @()
|
|
167
|
+
riskLevel = Get-RiskLevel $cmd.Verb
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
$failures++
|
|
172
|
+
Write-Warning "[$counter/$total] Failed to get help for $($cmd.Name): $_"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
# Write manifest
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
$manifest = [ordered]@{
|
|
180
|
+
generatedAt = (Get-Date -Format 'o')
|
|
181
|
+
dbatoolsVersion = $module.Version.ToString()
|
|
182
|
+
commandCount = $index.Count
|
|
183
|
+
commands = $index
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
$manifest | ConvertTo-Json -Depth 12 | Set-Content -Path $OutputPath -Encoding UTF8
|
|
187
|
+
|
|
188
|
+
Write-Information ""
|
|
189
|
+
Write-Information "Done! Indexed $($index.Count) commands -> $OutputPath"
|
|
190
|
+
if ($failures -gt 0) {
|
|
191
|
+
Write-Warning "$failures command(s) failed to index (see warnings above)."
|
|
192
|
+
}
|