Skip to content

Commit 09a0c89

Browse files
mcp
1 parent ecf1bbc commit 09a0c89

24 files changed

Lines changed: 2254 additions & 766 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ Thumbs.db
2929
nul
3030
web.code-workspace
3131
/.claude
32+
/docs

config/_default/config.toml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,25 @@ baseName = "articles"
5353
isPlainText = true
5454
notAlternative = true
5555

56+
# Markdown mirror of every page, served at /<page>/index.md and advertised from
57+
# the HTML with <link rel="alternate" type="text/markdown">. OpenAI's and
58+
# Anthropic's crawlers do not execute JavaScript and parse HTML for text; a
59+
# clean markdown copy removes the guesswork, and it is what people get when
60+
# they paste a URL into an assistant. See layouts/commands/single.md.
61+
[outputFormats.MD]
62+
mediaType = "text/markdown"
63+
isPlainText = true
64+
isHTML = false
65+
66+
# llms.txt — an index of the site in markdown, per the llmstxt.org proposal.
67+
# No answer engine documents using this file, so it is a cheap hedge rather
68+
# than a strategy. See research/optimizing-docs-sites-for-llms-2026-07-31.md.
69+
[outputFormats.LLMS]
70+
mediaType = "text/plain"
71+
baseName = "llms"
72+
isPlainText = true
73+
notAlternative = true
74+
5675
[outputs]
57-
home = ["HTML", "RSS", "REDIRECTS", "SearchIndex", "Articles"]
76+
home = ["HTML", "RSS", "REDIRECTS", "SearchIndex", "Articles", "LLMS"]
77+
page = ["HTML", "MD"]

content/page/mcp.md

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
---
2+
title: "dbatools MCP Server"
3+
date: 2026-07-30
4+
slug: "mcp"
5+
layout: "mcp"
6+
aliases:
7+
- /mcp/
8+
- /mcp/index.html
9+
draft: false
10+
description: "A free, read-only MCP server that gives your AI assistant the real dbatools documentation - every command, parameter and published example. No signup, no API key."
11+
---
12+
13+
Ask an AI assistant how to restore a database with dbatools and you will usually get
14+
something that looks right. Then you run it and find out `-RecoveryTime` was never a
15+
parameter, or that the command it confidently recommended does not exist.
16+
17+
This server fixes that. Point your assistant at it and it searches the real dbatools
18+
documentation - 700-odd commands with their syntax, parameters and published examples,
19+
plus the articles on this site - and cites the page each answer came from, so you can
20+
check it.
21+
22+
**Endpoint:** `https://mcp.dbatools.io`
23+
24+
There is no signup, no API key and no account. Add the URL and it works.
25+
26+
It exists because of [Microsoft's Learn MCP
27+
server](#install-microsofts-learn-server-too), which does the same job for SQL Server
28+
itself and left an obvious gap where dbatools should have been. Install that one too -
29+
details below.
30+
31+
## Add it to your client
32+
33+
{{< mcp-install >}}
34+
35+
Once it is connected, ask your assistant to list its tools. You should see three names
36+
beginning with `dbatools_`.
37+
38+
## What to ask it
39+
40+
You never have to name a tool. Ask the question you actually have and your assistant
41+
will reach for the server on its own:
42+
43+
- *How do I restore a database to a point in time with dbatools?*
44+
- *What is the difference between `-SqlCredential` and `-Credential`?*
45+
- *Show me a worked example of backing up to Azure blob storage.*
46+
- *Which dbatools commands run on Linux?*
47+
- *I need to copy logins between two instances without copying passwords.*
48+
49+
It is at its best on the questions models get wrong from memory: exact parameter names,
50+
which switches are mutually exclusive, and whether a command exists at all.
51+
52+
## The three tools
53+
54+
| Tool | What it returns |
55+
|---|---|
56+
| `dbatools_docs_search` | Up to 10 commands and articles, each with an excerpt and its dbatools.io URL |
57+
| `dbatools_code_sample_search` | Up to 20 worked PowerShell examples, taken from the examples published with each command |
58+
| `dbatools_docs_fetch` | One complete command page or article as markdown, by URL or by bare command name |
59+
60+
## What it can't do
61+
62+
It reads documentation. That is the whole job.
63+
64+
It cannot connect to a SQL Server instance, cannot run a dbatools command, and holds no
65+
credentials of any kind - there is nowhere to put them and nothing that would use them.
66+
The search never leaves the process, because the documentation is compiled into the
67+
deployment artifact and searched in memory.
68+
69+
All three tools are declared read-only in the protocol, so most clients will let you
70+
auto-approve them and stop asking. That is the truthful description of the server, not a
71+
convenience setting.
72+
73+
If what you want is an MCP server that *does* run dbatools commands against your
74+
instances, that is
75+
[dbatools-mcp-server](https://github.com/dataplat/dbatools-mcp-server) - a separate
76+
project that runs locally on your own machine. You can install both at once; their tool
77+
names do not overlap.
78+
79+
## Where it falls short
80+
81+
It is a search engine over a pile of documentation, and it is worth knowing exactly how
82+
narrow that is before you rely on it.
83+
84+
**It has no notion of meaning - it has a vocabulary.** Queries and documents are both
85+
stemmed, so "migrate", "migrating" and "migration" count as one word, and a curated list
86+
of about 120 phrases maps the words DBAs type onto the words the module uses. That is why
87+
"moving a database to another server" does reach the migration commands: because somebody
88+
wrote that mapping down, not because anything understood the question. A phrasing nobody
89+
anticipated falls back to plain word matching and can still come back empty. When it
90+
does, try again in the words the documentation itself would use.
91+
92+
**It cannot tell reading from writing.** Ask which port an instance is listening on and
93+
`Set-DbaTcpPort` may well rank above `Get-DbaTcpPort`, because nothing in the ranking
94+
distinguishes a command that reports something from one that changes it. Check the verb
95+
on anything you are about to run.
96+
97+
**It only knows what is published on this site.** Command help and articles. Not the
98+
GitHub issues, not the Slack channel, not the source code, and not the book. Plenty of
99+
the best dbatools knowledge lives in those and none of it is in here.
100+
101+
**It is only as good as the docs are.** Where a command's help is thin or out of date,
102+
the answer is thin or out of date. Nothing in this server checks the documentation
103+
against the module's actual behaviour.
104+
105+
Platform support is the clearest case. Every command carries the same `Availability:
106+
Windows, Linux, macOS` in its help - `Get-DbaDiskSpace` and `Get-DbaFirewallRule`
107+
included, though both lean on WMI or remoting and do not work on Linux. The real answer
108+
is in [dbatools & SQL on Linux](https://dbatools.io/linux/), which the server does return
109+
for that question: the pure-SQL commands work, the ones that reach into Windows do not,
110+
and the article puts the split at about three quarters. Trust the article, not the field.
111+
112+
**It knows nothing about you.** Not your SQL Server version, not which dbatools version
113+
you have installed, not your instances. Every answer is the general case.
114+
115+
**It can be a day behind.** The index rebuilds daily, so a command merged this morning
116+
may not be findable until tomorrow.
117+
118+
Results are capped, too: ten documents per search, twenty code samples. That is a
119+
deliberate ceiling on how much of your context window one call can eat.
120+
121+
### What it costs your context
122+
123+
The three tool definitions and the server's instructions add up to roughly 640 tokens per
124+
session - about 0.3% of a 200K window, which is nothing. Clients that defer tool schemas
125+
until a tool is actually used bring that down to about 30 tokens for the three names.
126+
127+
The real cost is in results, not definitions. `dbatools_docs_fetch` returns a whole
128+
command page, and a page like `Backup-DbaDatabase` runs to thousands of tokens on its
129+
own. A ten-hit search with excerpts costs more than the tool definitions did for the
130+
entire session. If your context is tight, fetch deliberately rather than reflexively.
131+
132+
## What it records
133+
134+
Tool calls are logged to Application Insights: the query text, how many results came
135+
back, the top hit, how long the search took, plus your user agent and IP address. The
136+
queries that come back empty get read most closely, because they are the fastest way to
137+
find out what the documentation is missing.
138+
139+
Secret-shaped values are stripped before anything is stored - passwords and keys in a
140+
pasted connection string, long token-shaped strings, email addresses. That redaction is
141+
best effort rather than a guarantee, so treat the search box the way you would treat any
142+
other: do not paste a real connection string into it.
143+
144+
Nothing is sold or shared with anyone, and there is no account for it to be attached to.
145+
146+
## Install Microsoft's Learn server too
147+
148+
If you only add one MCP server this week, make it Microsoft's rather than this one.
149+
150+
The [Microsoft Learn MCP server](https://learn.microsoft.com/training/support/mcp-get-started)
151+
puts the whole of Microsoft Learn in front of your assistant: SQL Server itself, T-SQL,
152+
Azure SQL, PowerShell, Windows, the lot. It is free, hosted by Microsoft, and needs no
153+
sign-in. We use it constantly, and it is genuinely the reason this server exists - once
154+
you have watched an assistant stop guessing about SQL Server, the fact that it was still
155+
guessing about dbatools becomes impossible to ignore.
156+
157+
```bash
158+
claude mcp add --transport http microsoft-learn https://learn.microsoft.com/api/mcp
159+
```
160+
161+
Or in VS Code, in `.vscode/mcp.json`:
162+
163+
```json
164+
{
165+
"servers": {
166+
"microsoft-learn": {
167+
"type": "http",
168+
"url": "https://learn.microsoft.com/api/mcp"
169+
}
170+
}
171+
}
172+
```
173+
174+
Claude Code and Copilot CLI users can install their plugin instead, which brings the
175+
server plus three skills that teach the assistant when to reach for it:
176+
177+
```text
178+
/plugin marketplace add microsoftdocs/mcp
179+
/plugin install microsoft-docs@microsoft-docs-marketplace
180+
```
181+
182+
The two servers answer different halves of the same question. Learn tells you what SQL
183+
Server does and why; this one tells you which dbatools command does it and what the
184+
parameter is actually called. Run both. Their tool names do not collide, and the shape of
185+
this server - search, code samples, fetch - is copied from theirs on purpose, because it
186+
was the right design and there was no reason to invent a worse one.
187+
188+
## How it stays current
189+
190+
The index is rebuilt from three public sources every day at 09:00 UTC, and again on
191+
every deploy:
192+
193+
| Source | Provides |
194+
|---|---|
195+
| [`dbatools-index.json`](https://raw.githubusercontent.com/dataplat/dbatools/master/bin/dbatools-index.json) | Syntax, parameters and examples, straight from the module |
196+
| [`commands.json`](https://dbatools.io/commands.json) | Descriptions, categories and canonical URLs |
197+
| [`articles.json`](https://dbatools.io/articles.json) | Blog posts and pages from this site |
198+
199+
Search is lexical BM25, with stemming, stop words and the synonym map sitting in front of
200+
it. There are no embeddings and no vector database. Storing vectors would be cheap, but
201+
embedding your query at request time would mean either a 25 MB model loaded in the
202+
cold-start path or an API round trip on every single call, and a documentation search
203+
does not need to cost that. The whole corpus fits in memory instead, so a query costs
204+
nothing and returns the same results every time.

scripts/generate-command-pages.ps1

Lines changed: 103 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -238,48 +238,125 @@ function New-CommandMarkdown {
238238
$null = $markdown.Add('')
239239

240240
if ($command.Examples) {
241+
# Examples arrive as a flat block of text: a "---- EXAMPLE n ----" banner,
242+
# then prompt lines, then prose explaining what the example did.
243+
#
244+
# They are collected into whole examples first, rather than streamed
245+
# straight out, so each one can be emitted under a heading that carries
246+
# its own explanation. Microsoft's guidance for AI search is that
247+
# assistants retrieve passages rather than pages, and that headings
248+
# "act like chapter titles that define clear content slices" — an
249+
# example titled "Example 1" tells a retrieval system nothing, while
250+
# "Example 1: Restores the last full backup to a new instance" is
251+
# self-contained enough to be quoted on its own.
241252
$examples = $command.Examples.Replace("`r`n", "`n") -replace '(\r\n){2,8}', "`n"
242253
$examples = $examples.Replace("`r", '').Split("`n")
243-
$inside = 0
244-
$cleanCode = New-Object System.Collections.ArrayList
254+
255+
$parsedExamples = New-Object System.Collections.ArrayList
256+
$current = $null
245257

246258
foreach ($row in $examples) {
247259
if ($row -like '*----*') {
248-
$null = $markdown.Add('')
249-
$null = $markdown.Add('##### ' + ($row -replace '-{4,}([^-]*)-{4,}', '$1').Replace('EXAMPLE', 'Example: '))
260+
if ($null -ne $current) {
261+
$null = $parsedExamples.Add($current)
262+
}
263+
$label = ($row -replace '-{4,}([^-]*)-{4,}', '$1').Replace('EXAMPLE', '').Trim()
264+
$current = @{
265+
Label = $label
266+
Code = New-Object System.Collections.ArrayList
267+
Prose = New-Object System.Collections.ArrayList
268+
}
250269
} elseif (($row -like '*PS C:\>*') -or ($row -like '*C:\PS>*') -or ($row -like '>>*')) {
251-
if ($inside -eq 0) {
252-
$cleanCode.Clear()
253-
$null = $markdown.Add('')
254-
$null = $markdown.Add('```powershell')
270+
if ($null -eq $current) {
271+
$current = @{
272+
Label = ''
273+
Code = New-Object System.Collections.ArrayList
274+
Prose = New-Object System.Collections.ArrayList
275+
}
255276
}
256-
# Add formatted line with prompt (normalize C:\PS> to PS C:\>)
277+
# Normalize the prompt to a single form (C:\PS> -> PS C:\>)
257278
$formattedRow = $row.Trim() -replace 'C:\\PS>\s*', 'PS C:\> '
258279
$formattedRow = $formattedRow -replace 'PS C:\\>\s*', 'PS C:\> '
259-
$null = $markdown.Add($formattedRow)
260-
261-
# Collect clean code without prompts
262-
$cleanLine = $row.Trim() -replace '^C:\\PS>\s*', '' -replace '^PS C:\\>\s*', '' -replace '^>>\s*', ''
263-
if ($cleanLine) {
264-
$null = $cleanCode.Add($cleanLine)
265-
}
266-
$inside = 1
280+
$null = $current.Code.Add($formattedRow)
267281
} elseif ($row.Trim() -eq '' -or $row.Trim() -eq 'Description') {
268282
# Skip empty lines and Description headers
269283
} else {
270-
if ($inside -eq 1) {
271-
$inside = 0
272-
# Close code block (JavaScript will handle copy functionality)
273-
$null = $markdown.Add('```')
274-
$null = $markdown.Add('')
284+
if ($null -ne $current) {
285+
$null = $current.Prose.Add($row.Trim())
275286
}
276-
$null = $markdown.Add("$($row.Trim().Replace("`n", " `n"))<br>")
277287
}
278288
}
279289

280-
if ($inside -eq 1) {
281-
# Close final code block (JavaScript will handle copy functionality)
282-
$null = $markdown.Add('```')
290+
if ($null -ne $current) {
291+
$null = $parsedExamples.Add($current)
292+
}
293+
294+
$exampleNumber = 0
295+
foreach ($example in $parsedExamples) {
296+
$exampleNumber += 1
297+
298+
if ($example.Label) {
299+
$heading = "Example $($example.Label)"
300+
} else {
301+
$heading = "Example $exampleNumber"
302+
}
303+
304+
# Promote the first sentence of the explanation into the heading so
305+
# the heading says what the example actually does. The prose is
306+
# joined first because upstream help text hard-wraps mid-sentence,
307+
# so the first line is often a fragment ending in a comma.
308+
$joinedProse = ($example.Prose -join ' ').Trim() -replace '\s+', ' '
309+
$summaryLimit = 110
310+
$summary = ''
311+
if ($joinedProse) {
312+
$sentenceEnd = $joinedProse.IndexOf('. ')
313+
if ($sentenceEnd -gt 0 -and $sentenceEnd -le $summaryLimit) {
314+
$summary = $joinedProse.Substring(0, $sentenceEnd).TrimEnd(' ', ',', ';', ':', '.')
315+
} elseif ($joinedProse.Length -le $summaryLimit) {
316+
$summary = $joinedProse.TrimEnd(' ', ',', ';', ':', '.')
317+
} else {
318+
# Cut at the last word boundary inside the limit. The
319+
# ellipsis is appended after trimming punctuation, or the
320+
# trim would eat it again.
321+
$cut = $joinedProse.Substring(0, $summaryLimit)
322+
$lastSpace = $cut.LastIndexOf(' ')
323+
if ($lastSpace -gt 40) { $cut = $cut.Substring(0, $lastSpace) }
324+
$summary = $cut.TrimEnd(' ', ',', ';', ':', '-', '.') + '...'
325+
}
326+
}
327+
if ($summary) {
328+
$heading = "$heading`: $summary"
329+
}
330+
331+
$anchor = "example-$exampleNumber"
332+
$null = $markdown.Add('')
333+
$null = $markdown.Add("<span id=""$anchor"" class=""section-anchor""></span>")
334+
# h3 rather than the h5 these used to be: an example is a sibling of
335+
# "Required Parameters", not of a single parameter name. The
336+
# example-heading class keeps the lighter visual weight the h5 had.
337+
$null = $markdown.Add("<h3 class=""example-heading""><a class=""anchor-link"" href=""#$anchor""></a><a href=""#$anchor"" class=""heading-link"">$([System.Net.WebUtility]::HtmlEncode($heading))</a></h3>")
338+
$null = $markdown.Add('')
339+
340+
if ($example.Code.Count -gt 0) {
341+
$null = $markdown.Add('```powershell')
342+
foreach ($line in $example.Code) {
343+
$null = $markdown.Add($line)
344+
}
345+
$null = $markdown.Add('```')
346+
$null = $markdown.Add('')
347+
}
348+
349+
# When the whole explanation already fits in the heading, repeating
350+
# it verbatim underneath just pads the page.
351+
if ($joinedProse -and $joinedProse.TrimEnd(' ', '.') -ne $summary) {
352+
# Two trailing spaces is a markdown hard break, so this renders
353+
# the same as the <br> it replaces but survives into the .md
354+
# mirror as valid markdown.
355+
foreach ($line in $example.Prose) {
356+
$null = $markdown.Add("$line ")
357+
}
358+
$null = $markdown.Add('')
359+
}
283360
}
284361
}
285362

0 commit comments

Comments
 (0)