Jamf Docs MCP Server

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.

Documentation

Jamf Docs MCP Server

CI codecov npm version License: 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

中文文件

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 methodPeer installed
npx -y @get-technology-inc/jamf-docs-mcp-serverYes
npm install (npm 7+, default settings)Yes
pnpm add (pnpm 10)Yes
npm install --legacy-peer-depsNo
Yarn 1 (classic)No

If you use one of the last two — or if you are vendoring the package — install the SDK alongside it:

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
{
  "mcpServers": {
    "jamf-docs": {
      "command": "npx",
      "args": ["-y", "@get-technology-inc/jamf-docs-mcp-server"]
    }
  }
}

Restart Claude Desktop to apply.

Claude Code (CLI)

claude mcp add jamf-docs -- npx -y @get-technology-inc/jamf-docs-mcp-server

Cursor

Edit ~/.cursor/mcp.json:

{
  "mcpServers": {
    "jamf-docs": {
      "command": "npx",
      "args": ["-y", "@get-technology-inc/jamf-docs-mcp-server"]
    }
  }
}

Verify Installation

Test with MCP Inspector:

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

ToolDescription
jamf_docs_list_productsList all supported products, topics, and document type filters
jamf_docs_searchSearch documentation by keyword with filtering and pagination
jamf_docs_get_articleRetrieve full content of a specific documentation article
jamf_docs_get_tocBrowse the table of contents for a product or any single publication
jamf_docs_batch_get_articlesFetch multiple articles in one call (up to 10 URLs)
jamf_docs_glossary_lookupLook 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.

ParameterTypeDefaultDescription
outputMode"full" | "compact""full"Detail level of the response
responseFormat"markdown" | "json""markdown"Output format
maxTokensnumber (100–50000)10000Maximum 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.

ParameterTypeDefaultDescription
querystring (2–200 chars, or one Chinese, Japanese or Korean character)requiredSearch keywords
productstring—Filter by product ID (e.g., jamf-pro)
topicstring—Filter by topic category (e.g., enrollment, security)
docTypestring—Filter by document type: documentation, release-notes, training, solution-guide, glossary, getting-started
versionstring (at most 50 chars)—Filter by version (e.g., "11.13.0") or "current"
languagestringen-USDocumentation language/locale
limitnumber (1–50)10Results per page
pagenumber (1–100)1Page number for pagination
maxTokensnumber (100–50000)5000Maximum 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.

ParameterTypeDefaultDescription
urlstring—Full https:// URL on learn.jamf.com, docs.jamf.com, concepts.jamf.com or support.jamf.com, at most 2,048 characters
mapIdstring—Fluid Topics map ID (from search results or the TOC). Use with contentId, instead of url or alongside it
contentIdstring—Fluid Topics content ID (from search results or the TOC). Use with mapId, instead of url or alongside it
sectionstring (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
summaryOnlybooleanfalseReturn only article outline, and the article's sub-topics when it has any — token-efficient way to preview before fetching full content
includeRelatedbooleanfalseInclude links to related articles
languagestringlocale in the URLDocumentation 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)
maxTokensnumber (100–50000)5000Maximum 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.)

ParameterTypeDefaultDescription
productstring—Product ID (see supported products below)
publicationstring (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
versionstring (at most 50 chars)latestSpecific version to fetch (e.g., "11.13.0") or "current"
languagestringen-USDocumentation language/locale. A product or publication Jamf does not publish in it is served as its en-US edition, with a localeNote
pagenumber (1–100)1Page number for paginated TOC
maxTokensnumber (100–50000)5000Maximum 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.

ParameterTypeDefaultDescription
urlsstring[] (1–10)requiredArray of Jamf documentation URLs (same hosts as jamf_docs_get_article), each at most 2,048 characters
concurrencynumber (1–5)3Maximum parallel requests
languagestringlocale in each URLDocumentation 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
maxTokensnumber (100–50000)5000Total 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.

ParameterTypeDefaultDescription
termstring (2–100 chars)requiredGlossary term to look up
productstring—Accepted, but does not filter: Jamf publishes one platform-wide glossary with no product classification
languagestringen-USDocumentation language/locale (glossary is English-only)
maxTokensnumber (100–50000)5000Maximum 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:

ResourceURIDescription
Products listjamf://productsAll available Jamf products with IDs and version info (fetched dynamically from API)
Topics listjamf://topicsTopic categories for filtering documentation searches
Product TOCjamf://products/{productId}/tocThe whole table of contents of a product's current documentation (template resource; see below)
Product versionsjamf://products/{productId}/versionsAvailable 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.

ArgumentTypeDescription
problemstring (required, at most 2,000 chars)Description of the issue to troubleshoot
productstring (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.

ArgumentTypeDescription
featurestring (required, at most 2,000 chars)The feature or capability to set up
productstring (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.

ArgumentTypeDescription
productstring (required, at most 100 chars)Jamf product ID
version_astring (required, at most 50 chars)First version to compare (e.g., "11.13.0")
version_bstring (required, at most 50 chars)Second version to compare (e.g., "11.32.0")

Supported Products

Product IDNameDescription
jamf-proJamf ProApple device management for enterprise
jamf-schoolJamf SchoolApple device management for education
jamf-connectJamf ConnectIdentity and access management
jamf-protectJamf ProtectEndpoint security for Apple
jamf-nowJamf NowSimple Apple device management for small businesses
jamf-safe-internetJamf Safe InternetContent filtering and web security for education and business
jamf-insightsJamf InsightsAnalytics and reporting platform for Apple fleet
jamf-rapididentityRapidIdentityIdentity and access management platform
jamf-trustJamf TrustZero-trust network access for Apple devices
jamf-routinesJamf RoutinesAutomated workflow orchestration for device management
self-service-plusSelf Service+Next-generation self-service portal for macOS and mobile
jamf-app-catalogJamf App CatalogCurated application catalog for managed deployments
jamf-accountJamf AccountIdentity, licensing, and platform services portal
jamf-security-cloudJamf Security CloudCloud security portal for Jamf Connect and Jamf Protect
elevateElevateGuided remediation and device health portal
composerComposermacOS package building and editing
jamf-parentJamf ParentParental device controls for school-issued devices
jamf-teacherJamf TeacherClassroom device management for teachers
jamf-setup-resetJamf Setup and ResetDevice personalisation and wipe-and-reprovision apps
jamf-assessmentJamf AssessmentLocked-down assessment mode for education devices
title-editorTitle EditorCustom software title patch definitions
jamf-infrastructure-managerJamf Infrastructure ManagerOn-premises proxy for LDAP and other internal services
jamf-adcs-connectorJamf AD CS ConnectorCertificate issuance via Active Directory Certificate Services
jamf-pki-proxyJamf PKI ProxyProxy for certificate authorities behind a firewall
jamf-migrateJamf MigrateMigrating macOS devices between Jamf Pro instances
jamf-remote-assistJamf Remote AssistRemote screen sharing and support sessions
jamf-cloud-distribution-serviceJamf Cloud Distribution ServiceJamf-hosted package distribution (JCDS)
healthcare-listenerHealthcare ListenerIntegration 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 make them different surfaces. An inline card is a compact summary that fits its own content; fullscreen is where a documentation browser lives.

InlineFullscreen
Search hits3the page
Table-of-contents rows8the page
Article prose3 blocks, stopping before any table or code blockwhole
Breadcrumbs, Back, parent link—✓
Section rail, neighbour list—✓
Type-ahead filter, paging—✓
Actions1as 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

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:

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

# 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

ArgumentDefaultDescription
--transportstdioTransport mode: stdio or http
--port3000Port to listen on, a whole number from 1 to 65535. Any other value stops the server with an error
--host127.0.0.1Host 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

EndpointMethodDescription
/mcpPOSTMCP JSON-RPC endpoint (streamable HTTP transport)
/healthGETHealth 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:

{
  "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:

CORS_ALLOWED_ORIGINS=https://myapp.example.com node dist/index.js --transport http

Multiple origins are separated by commas. An entry of the form <scheme>://* allows every origin of that scheme, for browser extensions whose IDs differ per install:

CORS_ALLOWED_ORIGINS=https://myapp.example.com,moz-extension://*,chrome-extension://*

http://* and https://* are not honoured; list websites by full origin, or use * to allow every origin.

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

VariableDefaultRangeDescription
CACHE_DIR.cache—Cache directory. Must be a directory used only for this cache; see below
CACHE_TTL_SEARCH1800000 (30 min)1 min–30 daysTTL for search result cache entries
CACHE_TTL_ARTICLE86400000 (24 hr)1 min–30 daysTTL for articles, the glossary and each map's topic index; see below
CACHE_TTL_PRODUCTS604800000 (7 days)1 min–30 daysTTL 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_TOC86400000 (24 hr)1 min–30 daysTTL for tables of contents, from any source, and the map TOC index navigation is read from; see below
CACHE_MAX_ENTRIES50010–10000Maximum 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.

VariableDefaultRangeDescription
REQUEST_TIMEOUT150001000–60000 msPer-attempt HTTP timeout
MAX_RETRIES00–10Retry attempts after the first. Only 429, 5xx, network errors and timeouts are retried, with exponential backoff and Retry-After honoured. 0 disables retrying
RETRY_DELAY1000100–30000 msBase for the backoff between retries. No effect while MAX_RETRIES is 0
RATE_LIMIT_DELAY00–10000 msMinimum gap between outbound requests, retries included. 0 lets parallel fetches stay parallel
USER_AGENTjamf-docs-mcp-server/<version> (+<repo url>)Latin-1Sent 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

VariableDefaultRangeDescription
RATE_LIMIT_RPM601–10000Inbound requests per minute per IP (HTTP transport only)
CORS_ALLOWED_ORIGINS`` (empty)—Comma-separated list of allowed CORS origins; <scheme>://* allows a whole non-HTTP scheme such as chrome-extension://* (HTTP transport only)
TRUST_PROXYofftrue or 1Take 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 (HTTP transport only)

Development

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

ScriptDescription
npm run buildCompile TypeScript to dist/
npm run devDevelopment mode with auto-reload (stdio)
npm run dev:app-uiLive preview of the MCP Apps viewer at http://127.0.0.1:5173
npm run fixtures:app-uiRe-capture the viewer's fixtures from the live tools
npm run start:httpStart HTTP/SSE transport mode
npm testPre-PR run: unit, integration and e2e
npm run test:unitUnit tests only — hermetic, no network
npm run test:integrationIntegration tests only (live Jamf APIs)
npm run test:contractUpstream contract suites. Run out of band by upstream-contract.yml, not part of the merge gate
npm run test:e2eEnd-to-end tests only (live Jamf APIs)
npm run test:allEverything, contract suites included
npm run test:coverageCoverage over the unit tier, as CI reports it
npm run test:inspectorLaunch MCP Inspector against local build
npm run lintLint source files
npm run typecheckTypeScript 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