Jamf Docs MCP Server
# Jamf Docs MCP Server
[](https://github.com/GET-Technology-Inc/jamf-docs-mcp-server/actions/workflows/ci.yml)
[](https://app.codecov.io/gh/GET-Technology-Inc/jamf-docs-mcp-server)
[](https://www.npmjs.com/package/@get-technology-inc/jamf-docs-mcp-server)
[](https://opensource.org/licenses/MIT)
An MCP server that gives AI assistants (Claude, Cursor, etc.) direct access to Jamf official documentation. Ask Jamf-related questions and get answers based on the latest docs from learn.jamf.com.
**Supported Products** (28): Jamf Pro, Jamf School, Jamf Connect, Jamf Protect, Jamf Now, Jamf Safe Internet, Jamf Insights, RapidIdentity, Jamf Trust, Jamf Routines, Self Service+, Jamf App Catalog, Jamf Account, Jamf Security Cloud, Elevate, Composer, Jamf Parent, Jamf Teacher, Jamf Setup and Reset, Jamf Assessment, Title Editor, Jamf Infrastructure Manager, Jamf AD CS Connector, Jamf PKI Proxy, Jamf Migrate, Jamf Remote Assist, Jamf Cloud Distribution Service, Healthcare Listener
[中文文件](docs/README.zh-TW.md)
## Installation
`@modelcontextprotocol/server` is a **peer dependency**, and the CLI imports it
at startup (`dist/index.js` → `@modelcontextprotocol/server/stdio`). If it is
missing from the install tree the process exits immediately with
`ERR_MODULE_NOT_FOUND` — the install itself succeeds, so the failure only
shows up when you run the server.
Most install paths bring it in automatically:
| Install method | Peer installed |
| --- | --- |
| `npx -y @get-technology-inc/jamf-docs-mcp-server` | Yes |
| `npm install` (npm 7+, default settings) | Yes |
| `pnpm add` (pnpm 10) | Yes |
| `npm install --legacy-peer-deps` | **No** |
| Yarn 1 (classic) | **No** |
If you use one of the last two — or if you are vendoring the package — install
the SDK alongside it:
```bash
npm install @get-technology-inc/jamf-docs-mcp-server @modelcontextprotocol/server@^2
```
**Why it is a peer dependency, not a regular one.** This package hands
`McpServer` instances to its consumers, and those consumers pass them to
`createMcpHandler` from their own SDK copy. If the two resolve to *different*
copies of the SDK, an instance built by one module's `Protocol` is inspected by
another's, and every 2026-07-28 request fails with
`Cannot read properties of undefined (reading 'includes')` — an HTTP 500 with
no useful diagnostic. Declaring the SDK as a peer states the single-copy
requirement instead of relying on the consumer's tree happening to hoist it;
listing it under `dependencies` as well would reintroduce exactly the duplicate
it exists to prevent.
Node.js 24 or newer is required. 5.x is the last release line that supports
Node.js 20 and 22. On those versions an unpinned
`npx -y @get-technology-inc/jamf-docs-mcp-server` resolves to the newest 5.x
on its own, because npm prefers the newest release whose `engines` field
accepts the running Node: the server keeps working, but stops receiving
updates until Node is upgraded. Use
`@get-technology-inc/jamf-docs-mcp-server@5` to make that explicit.
## Quick Start
### Claude Desktop
Edit `claude_desktop_config.json`:
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
```json
{
"mcpServers": {
"jamf-docs": {
"command": "npx",
"args": ["-y", "@get-technology-inc/jamf-docs-mcp-server"]
}
}
}
```
Restart Claude Desktop to apply.
### Claude Code (CLI)
```bash
claude mcp add jamf-docs -- npx -y @get-technology-inc/jamf-docs-mcp-server
```
### Cursor
Edit `~/.cursor/mcp.json`:
```json
{
"mcpServers": {
"jamf-docs": {
"command": "npx",
"args": ["-y", "@get-technology-inc/jamf-docs-mcp-server"]
}
}
}
```
### Verify Installation
Test with MCP Inspector:
```bash
npx @modelcontextprotocol/inspector npx -y @get-technology-inc/jamf-docs-mcp-server
```
## Usage Examples
Once configured, just ask your AI assistant:
- "How do I configure SSO in Jamf Pro?"
- "What are the system requirements for Jamf Protect?"
- "Explain the MDM enrollment process"
- "What changed in the latest Jamf Connect release notes?"
## Available Tools
| Tool | Description |
|------|-------------|
| `jamf_docs_list_products` | List all supported products, topics, and document type filters |
| `jamf_docs_search` | Search documentation by keyword with filtering and pagination |
| `jamf_docs_get_article` | Retrieve full content of a specific documentation article |
| `jamf_docs_get_toc` | Browse the table of contents for a product or any single publication |
| `jamf_docs_batch_get_articles` | Fetch multiple articles in one call (up to 10 URLs) |
| `jamf_docs_glossary_lookup` | Look up Jamf terminology and definitions (fuzzy matching) |
### jamf_docs_list_products
Returns all available Jamf products and their IDs, available topic filters, and document type filters.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `outputMode` | `"full"` \| `"compact"` | `"full"` | Detail level of the response |
| `responseFormat` | `"markdown"` \| `"json"` | `"markdown"` | Output format |
| `maxTokens` | number (100–50000) | `10000` | Maximum tokens in response. Higher than the other tools' `5000` because the answer is a whole catalogue |
This is the one tool without a `language` parameter. Every tool's input schema
is strict, so an unrecognised key is rejected
(`Unrecognized key: "language"`) rather than silently ignored.
Each publication's `locales` are the languages Jamf publishes it in. For a
`jamf-support-*` publication, they are the languages whose support.jamf.com home
page lists it. `jamf_docs_get_toc` serves a publication in any other language as
its en-US edition, with a `localeNote`.
If a source cannot be read, the reply lists what it could and says so:
`incomplete` names each unavailable source, and the markdown reply says the same
at the top. `maps-registry` is the list of documentation maps, from learn.jamf.com
or a custom `MapsProvider`, where the publication list and the product versions
both come from, so either can then be missing or a compiled-in default.
`jamf-support` is support.jamf.com, whose `jamf-support-*` publications are then
missing, or, when only some of its languages cannot be read, leave those out of
their `locales`. No `incomplete` means every source answered.
### jamf_docs_search
Searches across all Jamf product documentation.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `query` | string (2–200 chars, or one Chinese, Japanese or Korean character) | required | Search keywords |
| `product` | string | — | Filter by product ID (e.g., `jamf-pro`) |
| `topic` | string | — | Filter by topic category (e.g., `enrollment`, `security`) |
| `docType` | string | — | Filter by document type: `documentation`, `release-notes`, `training`, `solution-guide`, `glossary`, `getting-started` |
| `version` | string (at most 50 chars) | — | Filter by version (e.g., `"11.13.0"`) or `"current"` |
| `language` | string | `en-US` | Documentation language/locale |
| `limit` | number (1–50) | `10` | Results per page |
| `page` | number (1–100) | `1` | Page number for pagination |
| `maxTokens` | number (100–50000) | `5000` | Maximum tokens in response |
| `outputMode` | `"full"` \| `"compact"` | `"full"` | Detail level; use `"compact"` for token-efficient output |
| `responseFormat` | `"markdown"` \| `"json"` | `"markdown"` | Output format |
Pages are cut to `maxTokens`. A page holds up to `limit` results, as many as fit, and the next page starts at the first one that did not fit. Every result is on exactly one page, but which page depends on `limit` and `maxTokens`, so keep them the same while paging. The markdown footer names them beside the next `page` when they are not the default, and `structuredContent` echoes back what to resend: `filters`, `limit` and `maxTokens`. The MCP App's "Show more" resends all of them. A result larger than `maxTokens` on its own is alone on its page, with its snippet cut to fit and ended with `…`. Only that page has `tokenInfo.truncated: true`, and `truncatedResult` names the result and what it costs whole (`estimatedTokens`); `outputMode: "full"` markdown names the `maxTokens` to repeat the call with. If `limit` and `maxTokens` make more pages than `page` accepts (100), page 100 offers no next page and `paginationNote` says what reaches the rest.
Jamf's search also ranks courses and learning paths of the Jamf Training Catalog (trainingcatalog.jamf.com) among the documentation. They come back as results like any other, at the rank Jamf's search gives them and under the same `product` and `topic` filters, marked `external: true` (**External** in markdown). `jamf_docs_get_article` cannot read them: open the `url` in a browser. They have no `mapId` + `contentId` pair. Jamf classifies them as training content, so their `docType` is `training`, and `docType: "training"` returns them among the training topics, at the rank Jamf's search gives them; any other `docType`, or a specific `version`, leaves them out.
"No results found" means the search ran and nothing in the product documentation matched (with `responseFormat: "json"`, the JSON body with `total: 0`). The reply still lists what matched outside the product documentation (`otherSources`) and queries to try instead (`suggestions`). A query with Chinese, Japanese, Korean or Thai words, searched in a language whose documentation is not written in them (such as `en-US`, the default, which is in English), is suggested none of those words, and the reply says to search with the English terms or which `language` to search instead (in JSON, `localeNote`). Fluid Topics finds a page with any one word of a query that has no phrase in double quotes and no word marked `+` or `-`. So such a query that found nothing is not suggested fewer of its own words, which find nothing either, and the markdown reply says to check the spelling or try other terms. Curly quotes (“ ” „ ‟), the full-width ", the corner brackets 「」 and 『』 and the guillemets « » are sent to Fluid Topics as straight double quotes, so a phrase quoted with them is searched as a phrase. Two exceptions: 「」, 『』 or « » around a query's only word are sent as typed, since a phrase of one word matches only the form typed (in de-DE, `"Konfigurationsprofil"` has 1,717 results and `Konfigurationsprofil` 7,141); and when no page has a phrase quoted with them, the search runs once more with those marks as typed, which matches the words loosely, and says so in `queryNote`. A straight-quoted phrase that finds nothing is suggested its words without the quotes. Full-width letters and digits (SSO, 15) are sent as ASCII, which in most languages Fluid Topics does not read them as, and are matched as ASCII against the other sources' titles. If the search cannot be completed (learn.jamf.com is unreachable, times out, or answers with an error, or a custom search backend, a `SearchProvider`, fails), the tool returns an error (`isError: true`) that says what failed, that this is not a "no results", and whether trying again may help. Pages outside the product documentation that matched follow it, in a second text block.
### jamf_docs_get_article
Fetches and converts a documentation article to clean markdown or JSON. Address
the article either by `url`, or by the `mapId` + `contentId` pair that search
results and the table of contents carry — one of the two is required, and a
call with neither is an error.
Passing both, as a search result allows, is fine. On learn.jamf.com the pair
decides which article is fetched, and the result's `url` is that article's own
address, read from its metadata or else the table of contents; a note says so
when the `url` you passed does not match it. Only if neither has the address
does the result keep the `url` you passed. A concepts.jamf.com or
support.jamf.com `url` is fetched by URL, and a note says the pair was ignored.
A support.jamf.com collection URL, which `jamf_docs_get_toc` lists each
subcollection by, returns the collection's list of articles. A concepts.jamf.com
URL whose path starts with no locale code, which the site answers with a page
that only redirects to the en page, returns that page, under its own URL. The
site root, `https://concepts.jamf.com/`, which picks a language in the browser,
returns the en home page, `/en/`. With `language`, either returns that
language's edition where the site has one.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `url` | string | — | Full `https://` URL on `learn.jamf.com`, `docs.jamf.com`, `concepts.jamf.com` or `support.jamf.com`, at most 2,048 characters |
| `mapId` | string | — | Fluid Topics map ID (from search results or the TOC). Use with `contentId`, instead of `url` or alongside it |
| `contentId` | string | — | Fluid Topics content ID (from search results or the TOC). Use with `mapId`, instead of `url` or alongside it |
| `section` | string (at most 200 chars) | — | Extract only a named section (e.g., `"Prerequisites"`). A section that matches no heading is not an error: the reply lists the article's sections, or says it has none, and lists its sub-topics with their URLs — on learn.jamf.com, what a page shows as sections are mostly sub-topics |
| `summaryOnly` | boolean | `false` | Return only article outline, and the article's sub-topics when it has any — token-efficient way to preview before fetching full content |
| `includeRelated` | boolean | `false` | Include links to related articles |
| `language` | string | locale in the URL | Documentation language/locale. Overrides the locale in `url`. A learn.jamf.com publication Jamf does not publish in it is served in en-US, and a concepts.jamf.com or support.jamf.com page with no edition in it as `url` names it, both with a note; `contentLocale` says which language was served. No effect on a `mapId` + `contentId` pair (each map is already one language) |
| `maxTokens` | number (100–50000) | `5000` | Maximum tokens in response |
| `outputMode` | `"full"` \| `"compact"` | `"full"` | Detail level; `"compact"` shows a ~500-token preview with available sections list |
| `responseFormat` | `"markdown"` \| `"json"` | `"markdown"` | Output format |
`maxTokens` bounds every reply: the article or one section, a `summaryOnly` outline, a missed section's reply, and any note about how the call was resolved, all counted in `tokenInfo.tokenCount`. When content exceeds `maxTokens`, the tool truncates the response and lists the remaining sections with their token counts, as many as fit. An outline or a list of sections or sub-topics cut to fit says how many it left out, and `truncated` is `true`. Use the `section` parameter on a follow-up call to retrieve a specific part.
### jamf_docs_get_toc
Retrieves the navigation structure (table of contents) for a product, or for any
single publication — release notes, technical papers, courses, evaluation and
configuration guides. Exactly one of `product` and `publication` is required;
passing both, or neither, is an error. (Before 5.1, `product` was required and
`publication` did not exist.)
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `product` | string | — | Product ID (see supported products below) |
| `publication` | string (1–200 chars) | — | Bundle family ID of one publication, e.g. `jamf-pro-release-notes` or `technical-paper-laps`. `jamf_docs_list_products` lists them |
| `version` | string (at most 50 chars) | latest | Specific version to fetch (e.g., `"11.13.0"`) or `"current"` |
| `language` | string | `en-US` | Documentation language/locale. A product or publication Jamf does not publish in it is served as its en-US edition, with a `localeNote` |
| `page` | number (1–100) | `1` | Page number for paginated TOC |
| `maxTokens` | number (100–50000) | `5000` | Maximum tokens in response |
| `outputMode` | `"full"` \| `"compact"` | `"full"` | Use `"compact"` for a flat list without nested children |
| `responseFormat` | `"markdown"` \| `"json"` | `"markdown"` | Output format |
Pages are cut to `maxTokens`. A page holds up to 10 top-level entries, each with everything under it, as many as fit, and the next page starts at the first one that did not fit. Every top-level entry is on exactly one page, but which page depends on `maxTokens` (and on the tree, so on `version` and `language`), so keep them the same while paging. The markdown footer names `maxTokens` beside the next `page` when it is not the default, and `structuredContent` echoes back what to resend: `productId` or `publicationId`, `version`, `language` (when one was asked for) and `maxTokens`. The MCP App's "Show more" resends all of them. A top-level entry larger than `maxTokens` on its own is alone on its page, cut to the entries under it that fit. Only that page has `tokenInfo.truncated: true`, and `truncatedEntry` says how many of its entries are shown and what the whole entry costs (`estimatedTokens`); `outputMode: "full"` markdown names the `maxTokens` to repeat the call with. (`"compact"` lists top-level entries only, so a cut there hides nothing it would show.) If `maxTokens` makes more pages than `page` accepts (100), page 100 offers no next page and `paginationNote` names a `maxTokens` that reaches the rest.
### jamf_docs_batch_get_articles
Fetches multiple documentation articles in a single call. Each URL is fetched concurrently, and invalid domains are reported as per-article errors without failing the entire batch.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `urls` | string[] (1–10) | required | Array of Jamf documentation URLs (same hosts as `jamf_docs_get_article`), each at most 2,048 characters |
| `concurrency` | number (1–5) | `3` | Maximum parallel requests |
| `language` | string | locale in each URL | Documentation language/locale. Overrides the locale in each URL. A learn.jamf.com publication Jamf does not publish in it is served in en-US, and a concepts.jamf.com or support.jamf.com page with no edition in it as its URL names it, both with a note; each result's `contentLocale` says which language was served |
| `maxTokens` | number (100–50000) | `5000` | Total token budget across all articles |
| `outputMode` | `"full"` \| `"compact"` | `"full"` | Detail level per article |
| `responseFormat` | `"markdown"` \| `"json"` | `"markdown"` | Output format |
### jamf_docs_glossary_lookup
Looks up a term in the Jamf official glossary and returns matching definitions using fuzzy matching. A term of 4 characters or fewer is treated as an abbreviation and must be a whole word of the entry's name, so `DEP` does not match `zero-touch deployment`. A 4-character term may be a plural, or miss a letter or swap two (`MDMs`, `LDPA`). Glossary content is currently English-only; non-English `language` values are accepted but results will be in English. A reply with no match says so when the term was asked in another language or is written in a script other than Latin, such as Chinese (in JSON, `warning`). Full-width letters and digits in a term (`MDM`) are matched as ASCII.
"No glossary entries found" means the glossary was read and no entry matches (with `responseFormat: "json"`, the JSON body with `totalMatches: 0`). If entries match but not even the first fits in `maxTokens`, the reply says how many matched and the `maxTokens` the first one needs; `truncatedContent` lists each entry left out and its estimated tokens. If the glossary cannot be read (learn.jamf.com is unreachable, times out, or answers with an error, or a custom maps list, a `MapsProvider`, fails), the tool returns an error (`isError: true`) that says what failed and whether trying again may help. If some matching entries cannot be fetched, the reply answers from the others and says so: `incomplete` names the entries it may be missing. If one of those would have led the answer, because its title names the term more closely than any entry that was fetched, the reply is an error instead, so a lesser entry is never given in its place.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `term` | string (2–100 chars) | required | Glossary term to look up |
| `product` | string | — | Accepted, but does not filter: Jamf publishes one platform-wide glossary with no product classification |
| `language` | string | `en-US` | Documentation language/locale (glossary is English-only) |
| `maxTokens` | number (100–50000) | `5000` | Maximum tokens in response |
| `outputMode` | `"full"` \| `"compact"` | `"full"` | Detail level |
| `responseFormat` | `"markdown"` \| `"json"` | `"markdown"` | Output format |
## MCP Resources
Static and dynamic reference data accessible without tool calls:
| Resource | URI | Description |
|----------|-----|-------------|
| Products list | `jamf://products` | All available Jamf products with IDs and version info (fetched dynamically from API) |
| Topics list | `jamf://topics` | Topic categories for filtering documentation searches |
| Product TOC | `jamf://products/{productId}/toc` | The whole table of contents of a product's current documentation (template resource; see below) |
| Product versions | `jamf://products/{productId}/versions` | Available documentation versions for a specific product (template resource) |
Template resources support tab-completion on `productId` in compatible clients. A `productId` of more than 100 characters is refused as invalid params, and one that names no product gets a body that says so and lists the product IDs.
`jamf://products/{productId}/toc` holds the whole table of contents of the product's current en-US documentation, nested as `jamf_docs_get_toc` returns it in JSON, up to 20000 tokens counted from entry titles as that tool counts `maxTokens`. That is a bound on the titles, not on the size of the JSON, which also carries each entry's URL and ids. On 2026-09-28 every product fitted: the largest, Jamf Pro, was about 8,400 such tokens (794 entries, 267 KB of JSON). `complete` says whether `toc` is the whole tree. When it is not, `shownEntries` counts the entries it holds and `missing` says what is left out and, where `jamf_docs_get_toc` can return it, which call does. `mapId` is the map the entries were read from: with an entry's `contentId` it is the pair `jamf_docs_get_article` fetches that entry by. A map is one version in one language, so the pair needs no version or language beside it. `mapId` is absent when no one map is known for the entries; each entry's `url` still fetches it.
## MCP Prompts
Pre-built prompt workflows that guide the AI through multi-step documentation tasks:
### `jamf_troubleshoot`
Guides the AI through a structured troubleshooting workflow: searching for relevant documentation, previewing articles with `summaryOnly`, then providing a root-cause diagnosis and step-by-step resolution.
| Argument | Type | Description |
|----------|------|-------------|
| `problem` | string (required, at most 2,000 chars) | Description of the issue to troubleshoot |
| `product` | string (optional, at most 100 chars) | Jamf product ID to scope the search |
### `jamf_setup_guide`
Directs the AI to generate a step-by-step setup guide for a Jamf feature, including prerequisites, configuration steps, and verification.
| Argument | Type | Description |
|----------|------|-------------|
| `feature` | string (required, at most 2,000 chars) | The feature or capability to set up |
| `product` | string (optional, at most 100 chars) | Jamf product ID to scope the search |
### `jamf_compare_versions`
Instructs the AI to compare table-of-contents structures and key articles between two product versions, summarizing new features, removed capabilities, and migration considerations.
| Argument | Type | Description |
|----------|------|-------------|
| `product` | string (required, at most 100 chars) | Jamf product ID |
| `version_a` | string (required, at most 50 chars) | First version to compare (e.g., `"11.13.0"`) |
| `version_b` | string (required, at most 50 chars) | Second version to compare (e.g., `"11.32.0"`) |
## Supported Products
| Product ID | Name | Description |
|------------|------|-------------|
| `jamf-pro` | Jamf Pro | Apple device management for enterprise |
| `jamf-school` | Jamf School | Apple device management for education |
| `jamf-connect` | Jamf Connect | Identity and access management |
| `jamf-protect` | Jamf Protect | Endpoint security for Apple |
| `jamf-now` | Jamf Now | Simple Apple device management for small businesses |
| `jamf-safe-internet` | Jamf Safe Internet | Content filtering and web security for education and business |
| `jamf-insights` | Jamf Insights | Analytics and reporting platform for Apple fleet |
| `jamf-rapididentity` | RapidIdentity | Identity and access management platform |
| `jamf-trust` | Jamf Trust | Zero-trust network access for Apple devices |
| `jamf-routines` | Jamf Routines | Automated workflow orchestration for device management |
| `self-service-plus` | Self Service+ | Next-generation self-service portal for macOS and mobile |
| `jamf-app-catalog` | Jamf App Catalog | Curated application catalog for managed deployments |
| `jamf-account` | Jamf Account | Identity, licensing, and platform services portal |
| `jamf-security-cloud` | Jamf Security Cloud | Cloud security portal for Jamf Connect and Jamf Protect |
| `elevate` | Elevate | Guided remediation and device health portal |
| `composer` | Composer | macOS package building and editing |
| `jamf-parent` | Jamf Parent | Parental device controls for school-issued devices |
| `jamf-teacher` | Jamf Teacher | Classroom device management for teachers |
| `jamf-setup-reset` | Jamf Setup and Reset | Device personalisation and wipe-and-reprovision apps |
| `jamf-assessment` | Jamf Assessment | Locked-down assessment mode for education devices |
| `title-editor` | Title Editor | Custom software title patch definitions |
| `jamf-infrastructure-manager` | Jamf Infrastructure Manager | On-premises proxy for LDAP and other internal services |
| `jamf-adcs-connector` | Jamf AD CS Connector | Certificate issuance via Active Directory Certificate Services |
| `jamf-pki-proxy` | Jamf PKI Proxy | Proxy for certificate authorities behind a firewall |
| `jamf-migrate` | Jamf Migrate | Migrating macOS devices between Jamf Pro instances |
| `jamf-remote-assist` | Jamf Remote Assist | Remote screen sharing and support sessions |
| `jamf-cloud-distribution-service` | Jamf Cloud Distribution Service | Jamf-hosted package distribution (JCDS) |
| `healthcare-listener` | Healthcare Listener | Integration with healthcare information systems |
## Key Features
- **Compact Mode**: Use `outputMode: "compact"` for token-efficient responses; articles show a ~500-token preview with an available sections list
- **Summary Only**: Use `summaryOnly: true` on `jamf_docs_get_article` to preview an article outline before fetching full content
- **Section Extraction**: Use `section: "Prerequisites"` to retrieve only the part of an article you need
- **Batch Fetching**: Use `jamf_docs_batch_get_articles` to fetch up to 10 articles in one call with concurrent requests
- **Glossary Lookup**: Use `jamf_docs_glossary_lookup` to look up Jamf terminology with fuzzy matching
- **Multi-language**: Every tool except `jamf_docs_list_products` accepts a `language` parameter for localized documentation: `en-US` (default), `ja-JP`, `zh-TW`, `de-DE`, `es-ES`, `fr-FR`, `nl-NL`, `th-TH`, `it-IT`, `pt-BR`, `zh-CN`
- **Document Type Filter**: Use `docType` on `jamf_docs_search` to narrow results to `documentation`, `release-notes`, `training`, `solution-guide`, `glossary`, or `getting-started`
- **Version Query**: Use the `version` parameter to query documentation for a specific product version
- **Pagination**: Search results support `page` and `limit`; table of contents supports `page`; product lists are not paginated
- **Search Suggestions**: Receive helpful suggestions when a search returns no results
- **Token Management**: All tools accept a `maxTokens` parameter (100–50000; default 5000, or 10000 for `jamf_docs_list_products`) to control response size
## MCP Apps (interactive viewer)
Hosts that negotiate the MCP Apps extension (`io.modelcontextprotocol/ui`) render
`jamf_docs_search`, `jamf_docs_get_toc` and `jamf_docs_get_article` results as an
interactive viewer instead of plain markdown: search hits are clickable through to
the article, TOC entries open in place, and articles carry section navigation and a
back stack. All three tools reference one self-contained `ui://` resource, whose
URI carries a hash of the bundle it names (`ui://jamf-docs/app-<hash>.html`).
Hosts that do not negotiate the extension ignore the metadata and get exactly the
markdown they always did.
The resource is served with a 24-hour public cache hint, which is safe precisely
because the URI is content-addressed: a given URI names one exact bundle forever,
and a new bundle arrives under a new URI rather than replacing an old one. Hosts
pick up a changed viewer on their next `tools/list` refresh.
### Design
The viewer paints no background and owns no colour. It reads the host's theme,
design tokens and fonts out of `hostContext` (`applyDocumentTheme`,
`applyHostStyleVariables`, `applyHostFonts`) and builds every rule from those,
so the panel is a piece of the host's surface rather than a web page embedded in
one. `app-ui/styles.ts` carries a `:root` fallback for each token, because hosts
may send any subset of the 76 style variables.
Before 5.10 it did the opposite: a hardcoded palette with a
`prefers-color-scheme` dark block. That media query reports the **operating
system**, not the host — so a user running Claude in dark mode on a light OS got
a white slab in a dark conversation, and no tuning of the greys could fix it.
### Inline and fullscreen
The two display modes render different things, because Claude's
[design guidelines](https://claude.com/docs/connectors/building/mcp-apps/design-guidelines)
make them different surfaces. An inline card is a compact summary that fits its
own content; fullscreen is where a documentation browser lives.
| | Inline | Fullscreen |
| --------------------------------- | ----------------------------- | ---------- |
| Search hits | 3 | the page |
| Table-of-contents rows | 8 | the page |
| Article prose | 3 blocks, stopping before any table or code block | whole |
| Breadcrumbs, Back, parent link | — | ✓ |
| Section rail, neighbour list | — | ✓ |
| Type-ahead filter, paging | — | ✓ |
| Actions | 1 | as needed |
Opening a result from an inline panel sends `ui/request-display-mode` **before**
the `tools/call`, so the article arrives in the mode built for it. A host that
declines, or that offers no fullscreen, still gets the article in place.
Two documented constraints drive this.
**Inline apps must not scroll internally.** On a touch device the conversation
view owns vertical panning, so a vertical gesture starting inside an inline app
is handed to the conversation — an internal scroll container does not scroll at
all, and everything past the host's cap becomes *unreachable* rather than below
the fold. Bounding what is rendered instead keeps search at ~518px, the table of
contents at ~373px and an article at ~554px, all fully visible.
**Inline apps must not drill in.** "Drill-ins, breadcrumbs, or multiple views"
are named patterns to avoid, which is why every piece of navigation chrome above
is fullscreen-only.
The article preview is counted in blocks rather than characters. A character
budget does not predict height: 900 characters of prose is three short
paragraphs, and 900 characters containing a settings table is 700px of
panel.
Style variables in `app-ui/styles.ts` are transcribed from the same guidelines.
### Article navigation
`jamf_docs_get_article` publishes `navigation` — `parent`, `siblings`,
`children`, and the true totals alongside the (capped) lists.
This matters more than it sounds. Fluid Topics serves **one topic per API call**
while learn.jamf.com concatenates a topic and its children into a single page,
so every `<h2>` a reader sees on the site is a separate topic here — verified at
9 of 9 on "Computer Configuration Profiles", whose API payload contains zero
heading tags of any level. Without `navigation`, a client showing that page has
the introduction and no route to the nine procedures the page consists of on the
website. The viewer renders them as *In this section*.
It is derived from the map's TOC index, the same index the breadcrumb and the
internal links are resolved from. An article loads that index once and shares
it between all three.
### Developing the viewer
```bash
npm run dev:app-ui # http://127.0.0.1:5173 — edit app-ui/*.ts, the frame reloads
```
`app-ui/dev/harness.ts` is a local MCP Apps host built on the SDK's `AppBridge`,
which accepts `Client | null` — a host with no server behind it is a supported
mode. It speaks the real protocol to the real `App` class and answers
`tools/call` out of `app-ui/dev/fixtures.json`, real `structuredContent`
captured from the live tools by `npm run fixtures:app-ui`.
It exists because the viewer only runs inside a host, so there is nothing to
look at without one, and the alternative loop — build, launch the Inspector,
click through — is tens of seconds per edit. It also does three things no
inspector can: strip `hostContext.styles` entirely (the check that proves the
`:root` fallbacks are coherent as a set), resize the container continuously
across the `@container` breakpoints, and lie about `deviceCapabilities.hover`.
Before shipping, run the viewer through the real thing as well — it is the only
place the actual `ui://` resource, CSP and `_meta` are exercised:
```bash
npm run build && npm run test:inspector
```
> [!WARNING]
> **The MCP Apps viewer is broken in 4.0.0 — upgrade past it.** The build step that
> inlines the UI bundle into the HTML document used a replacement string, so every
> `$` pattern in the minified JavaScript was expanded instead of copied. The
> document that shipped is not parseable JavaScript, and a host that renders it gets
> `SyntaxError: missing ) after argument list` and a blank panel. Nothing else in
> 4.0.0 is affected — tools, resources and prompts return the same results either
> way, since a host that cannot render the app falls back to the markdown. Fixed in
> 4.0.1.
>
> **4.0.1 alone did not reach every host.** Up to and including 4.0.1 the resource
> lived at a fixed `ui://jamf-docs/app.html` with a 24-hour public cache hint, so a
> host that had read the broken 4.0.0 bundle kept serving it from cache for up to a
> day after the server was upgraded — the corrected bundle was published under the
> same URI and never fetched. Upgrading past 4.0.1 fixes the distribution as well as
> the bundle: the URI now changes with the content, so a host holding the 4.0.0 copy
> simply stops asking for it. No manual cache clearing is needed, and the remaining
> delay is the one-hour `tools/list` hint rather than 24 hours.
## HTTP/SSE Transport Mode
In addition to the default `stdio` transport, the server supports an HTTP transport for use as a remote or shared MCP endpoint.
### Starting the HTTP Server
```bash
# Using the npm script (defaults: localhost:3000)
npm run start:http
# Using the built binary directly with custom options
node dist/index.js --transport http --port 8080 --host 127.0.0.1
```
### CLI Arguments
| Argument | Default | Description |
|----------|---------|-------------|
| `--transport` | `stdio` | Transport mode: `stdio` or `http` |
| `--port` | `3000` | Port to listen on, a whole number from 1 to 65535. Any other value stops the server with an error |
| `--host` | `127.0.0.1` | Host to bind to. A blank value stops the server with an error |
Each can also be written `--port=8080`. An argument the server does not know, such as a
misspelt `--prot`, or an option given no value, is ignored, with a warning on stderr.
> **Security note**: The default host `127.0.0.1` restricts access to localhost only. Binding to `0.0.0.0` exposes the server to the network; only do this in controlled environments.
### HTTP Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/mcp` | `POST` | MCP JSON-RPC endpoint (streamable HTTP transport) |
| `/health` | `GET` | Health check — returns `{"status":"ok","version":"<current>"}` |
### Connecting an MCP Client via HTTP
Claude Desktop or other MCP clients that support HTTP transport can connect with:
```json
{
"mcpServers": {
"jamf-docs": {
"url": "http://127.0.0.1:3000/mcp"
}
}
}
```
### CORS Configuration
By default the server does not set CORS headers (cross-origin requests are blocked). To allow specific origins, set the `CORS_ALLOWED_ORIGINS` environment variable:
```bash
CORS_ALLOWED_ORIGINS=https://myapp.example.com node dist/index.js --transport http
```
Multiple origins are separated by commas.
### Rate Limiting
The HTTP server applies per-IP token-bucket rate limiting. The default is 60 requests per minute. Override with the `RATE_LIMIT_RPM` environment variable.
By default the "IP" is the TCP peer address. Behind a reverse proxy every
request arrives from the proxy, so all clients share one `RATE_LIMIT_RPM`
bucket and one busy client throttles everyone. Set `TRUST_PROXY=true` (or `1`)
to take the client address from `X-Forwarded-For` instead. Only those two exact
values turn it on — `TRUE`, `yes` or anything else leaves it off. When it is on:
- The **rightmost** entry is used: the address appended by the proxy directly
in front of the server. Entries to its left are whatever the client sent and
can be forged, so they are ignored. That is right for exactly one proxy hop;
behind two (a CDN in front of nginx, say) the rightmost entry is the outer
proxy, and clients arriving through it share a bucket again.
- A request without `X-Forwarded-For` falls back to the peer address.
`X-Real-IP` and `Forwarded` are not read, so the proxy must send
`X-Forwarded-For` for the setting to have any effect.
- The address feeds the rate limiter and nothing else.
Leave it off when clients reach the server directly. With it on and no proxy in
front, a client can send any `X-Forwarded-For` it likes and get a fresh bucket
on every request, which defeats the limit.
## Configuration
All settings are optional. Set them as environment variables before launching the server.
A setting that is empty, or only whitespace, counts as unset. A numeric setting must be a
whole number within its range. Any other value is ignored, with a warning on stderr, and
the default is used.
### Cache Settings
| Variable | Default | Range | Description |
|----------|---------|-------|-------------|
| `CACHE_DIR` | `.cache` | — | Cache directory. Must be a directory used only for this cache; see below |
| `CACHE_TTL_SEARCH` | `1800000` (30 min) | 1 min–30 days | TTL for search result cache entries |
| `CACHE_TTL_ARTICLE` | `86400000` (24 hr) | 1 min–30 days | TTL for articles, the glossary and each map's topic index; see below |
| `CACHE_TTL_PRODUCTS` | `604800000` (7 days) | 1 min–30 days | TTL for the product list: the maps list the product catalogue is read from, support.jamf.com's collections and the search indexes of concepts.jamf.com and support.jamf.com; see below |
| `CACHE_TTL_TOC` | `86400000` (24 hr) | 1 min–30 days | TTL for tables of contents, from any source, and the map TOC index `navigation` is read from; see below |
| `CACHE_MAX_ENTRIES` | `500` | 10–10000 | Maximum number of entries kept in the in-memory cache |
`CACHE_DIR` must be a directory used only for this cache. The server writes
entries there as `<hash>.json`, and every start deletes the ones that have
expired or can't be read. Files with other names are left alone, but don't
point it at a project root or a directory other tools use.
- A relative path, including the default `.cache`, resolves against the
server's working directory. It must stay inside that directory. If the
host starts the server from `/`, set an absolute `CACHE_DIR`: on macOS
`/.cache` can't be created, so nothing reaches the disk.
- A path inside `/etc`, `/usr`, `/var`, `/sys`, `/proc`, `/dev`, `/sbin` or
`/bin` is rejected, relative or absolute. Symlinks are resolved first, so
on macOS `/private/etc/…` is rejected just like `/etc/…`. An unset or
blank `CACHE_DIR` isn't checked.
- Your home directory and the server's temp directory (`TMPDIR`) are allowed
even when they're under one of those. On macOS so is your per-user temp
directory, under `/private/var/folders`, even when the host does not pass
the server `TMPDIR`, as an MCP SDK stdio client does not. On ostree systems
such as Fedora Silverblue `/home` is a link to `/var/home`.
- A rejected value falls back to `.cache`, with a warning on stderr. That is
relative to the working directory too, so if the working directory is
inside a system directory, the fallback is as well.
What each TTL covers:
- `CACHE_TTL_SEARCH`: the search results `jamf_docs_search` reads from
learn.jamf.com.
- `CACHE_TTL_ARTICLE`: each article `jamf_docs_get_article` and
`jamf_docs_batch_get_articles` fetch, from any source, with the breadcrumb
and internal links built when it was fetched; the glossary's term list and
definitions; each learn.jamf.com map's topic index, which a page URL is
resolved with; and the `jamf://topics` list.
- `CACHE_TTL_PRODUCTS`: the list of learn.jamf.com maps, which products,
versions and publications are read from on every call, so the products and
versions `jamf_docs_list_products` and `jamf://products` list are never
older than it; support.jamf.com's list of collections; and the title
indexes `jamf_docs_search` searches concepts.jamf.com and support.jamf.com
with. The maps list's age counts from when it was fetched, also for a
server that starts later and reads it from the cache. A title index built
while a page its titles are listed on could not be read is kept for a
minute.
- `CACHE_TTL_TOC`: each table of contents `jamf_docs_get_toc` and
`jamf://products/{productId}/toc` serve, whether it is read from a
learn.jamf.com map, concepts.jamf.com's sitemap or a support.jamf.com
collection page; the titles concepts.jamf.com's section index pages list,
which its tables of contents and title indexes use; and the map TOC index
an article's `navigation` is read from. The glossary's term list is read
from a map's table of contents too, and is kept with the glossary, for
`CACHE_TTL_ARTICLE`.
A support.jamf.com collection page is requested once for both of its uses:
whichever of `jamf_docs_get_article` and a table of contents or title index
reads it first keeps what the other reads from it too, each for its own TTL.
An article's breadcrumb and internal links are built from the map TOC index
when the article is fetched, and kept with it for `CACHE_TTL_ARTICLE`. Its
`navigation` is read from the index on every call, so with `CACHE_TTL_TOC`
shorter than `CACHE_TTL_ARTICLE`, a reply can carry a breadcrumb older than
its `navigation`.
6.0.12 and earlier did not read `CACHE_TTL_TOC`. In 6.0.12 the Fluid Topics
tables of contents and the map TOC index were kept for `CACHE_TTL_ARTICLE`,
and the concepts.jamf.com and support.jamf.com ones for `CACHE_TTL_PRODUCTS`,
7 days by default where they now get 24 hours. The products and versions
`jamf_docs_list_products` lists were cached apart from the maps list, for
`CACHE_TTL_ARTICLE`, so a version Jamf published could show under
`publications` up to 24 hours before it showed under `products`. A server
that read the maps list from the cache kept it for `CACHE_TTL_PRODUCTS` from
that read, not from when the list was fetched, so it could serve a list up to
twice that old.
### Request Settings
Applied to every outbound request to a documentation host.
| Variable | Default | Range | Description |
|----------|---------|-------|-------------|
| `REQUEST_TIMEOUT` | `15000` | 1000–60000 ms | Per-attempt HTTP timeout |
| `MAX_RETRIES` | `0` | 0–10 | Retry attempts after the first. Only 429, 5xx, network errors and timeouts are retried, with exponential backoff and `Retry-After` honoured. `0` disables retrying |
| `RETRY_DELAY` | `1000` | 100–30000 ms | Base for the backoff between retries. No effect while `MAX_RETRIES` is `0` |
| `RATE_LIMIT_DELAY` | `0` | 0–10000 ms | Minimum gap between outbound requests, retries included. `0` lets parallel fetches stay parallel |
| `USER_AGENT` | `jamf-docs-mcp-server/<version> (+<repo url>)` | Latin-1 | Sent on every request so this client is identifiable to Jamf. An HTTP header can carry no character above U+00FF and no control character but a tab. Line breaks are removed; a value with any other such character is ignored, with a warning on stderr |
### HTTP Transport Settings
| Variable | Default | Range | Description |
|----------|---------|-------|-------------|
| `RATE_LIMIT_RPM` | `60` | 1–10000 | Inbound requests per minute per IP (HTTP transport only) |
| `CORS_ALLOWED_ORIGINS` | `` (empty) | — | Comma-separated list of allowed CORS origins (HTTP transport only) |
| `TRUST_PROXY` | off | `true` or `1` | Take the client IP for the per-IP rate limit from the rightmost `X-Forwarded-For` entry instead of the socket. Enable only behind a reverse proxy — see [Rate Limiting](#rate-limiting) (HTTP transport only) |
## Development
```bash
git clone https://github.com/GET-Technology-Inc/jamf-docs-mcp-server.git
cd jamf-docs-mcp-server
npm install
npm run dev # stdio mode with file watching
npm run start:http # HTTP transport mode
```
### Available Scripts
| Script | Description |
|--------|-------------|
| `npm run build` | Compile TypeScript to `dist/` |
| `npm run dev` | Development mode with auto-reload (stdio) |
| `npm run dev:app-ui` | Live preview of the MCP Apps viewer at http://127.0.0.1:5173 |
| `npm run fixtures:app-ui` | Re-capture the viewer's fixtures from the live tools |
| `npm run start:http` | Start HTTP/SSE transport mode |
| `npm test` | Pre-PR run: unit, integration and e2e |
| `npm run test:unit` | Unit tests only — hermetic, no network |
| `npm run test:integration` | Integration tests only (live Jamf APIs) |
| `npm run test:contract` | Upstream contract suites. Run out of band by `upstream-contract.yml`, not part of the merge gate |
| `npm run test:e2e` | End-to-end tests only (live Jamf APIs) |
| `npm run test:all` | Everything, contract suites included |
| `npm run test:coverage` | Coverage over the unit tier, as CI reports it |
| `npm run test:inspector` | Launch MCP Inspector against local build |
| `npm run lint` | Lint source files |
| `npm run typecheck` | TypeScript type check without emitting |
## License
MIT - Copyright (c) 2025 GET Technology Inc.
## Disclaimer
This is an unofficial tool and is not affiliated with Jamf.
## Links
- [Jamf Documentation](https://learn.jamf.com)
- [MCP Specification](https://spec.modelcontextprotocol.io/)
TDQS
Scored across 6 tools
Each tool occupies a distinct role in the docs workflow: catalogue listing, search, single fetch, batch fetch, TOC browsing, and glossary lookup. The only mild overlap is between jamf_docs_list_products (catalogue of products/publications/filters) and jamf_docs_get_toc (navigation structure), but the descriptions explicitly clarify that one is discovery metadata and the other is document hierarchy. jamf_docs_get_article vs jamf_docs_batch_get_articles is unambiguous given the batch's explicit concurrency/bulk framing.
All tools share a clear jamf_docs_ prefix and are snake_case, which is strong. Most follow verb_noun (list_products, get_article, get_toc), but glossary_lookup inverts to noun_verb and batch_get_articles inserts a modifier prefix, so it is not perfectly uniform.
Six tools is well-scoped for a documentation server: discovery, search, retrieval (single and bulk), structure browsing, and glossary each earn their place. No redundant or filler tools are present.
The doc lifecycle is well covered: discover products/publications, list versions via list_products or error hints, browse TOC, search, fetch single or batch articles, and look up glossary terms. Minor gaps include no dedicated version-listing or category-browsing operation, but agents can work around these via list_products and the TOC.