@aparte/docs-mcp 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 aparté
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # @aparte/docs-mcp
2
+
3
+ The aparté documentation as an **MCP server**, for the coding agent that is writing your
4
+ integration. Four tools — list the topics, search, read a page, read a whole topic — over
5
+ the text the docs site publishes for models (`apartejs.dev/llms.txt` and its per-topic
6
+ files), so the answer is always the docs of the version that ships.
7
+
8
+ ```bash
9
+ npx @aparte/docs-mcp
10
+ ```
11
+
12
+ That is the whole install for a client that spawns a command. Claude Code:
13
+
14
+ ```bash
15
+ claude mcp add aparte-docs -- npx -y @aparte/docs-mcp
16
+ ```
17
+
18
+ Claude Desktop / Cursor / any client with a JSON config:
19
+
20
+ ```json
21
+ { "mcpServers": { "aparte-docs": { "command": "npx", "args": ["-y", "@aparte/docs-mcp"] } } }
22
+ ```
23
+
24
+ ## The four tools
25
+
26
+ | Tool | Ask it for |
27
+ | --- | --- |
28
+ | `list_sets` | The topics the docs are split into, one sentence each — start here. |
29
+ | `search_docs` | Pages ranked for plain words (`"approve a tool call"`, `"systemPrompt false"`, `"finish a turn in display-only"`), with a snippet. |
30
+ | `get_page` | One page's markdown, by title. |
31
+ | `get_set` | A whole topic at once (25–250 KB): tools & approval, bring your own loop, theming & the UI kit, providers, reference. |
32
+
33
+ ## Why it reads the site rather than bundling it
34
+
35
+ Every `@aparte/*` package ships at one version and the site deploys from the same commit,
36
+ so "the docs online" are the docs of the version you installed — and the package stays a
37
+ few kilobytes with nothing to go stale. It needs the network. For an offline or staging
38
+ setup, serve a docs build yourself and point the server at it:
39
+
40
+ ```bash
41
+ APARTE_DOCS_URL=http://localhost:4321 npx @aparte/docs-mcp
42
+ ```
43
+
44
+ That URL is the only origin the server ever fetches from. The index it reads lists one
45
+ URL per topic, and those are absolute production URLs even in a local build — so the
46
+ server takes their **path** and asks `APARTE_DOCS_URL` for it. A local build is therefore
47
+ read locally, and an index entry naming some other host is not a request your machine
48
+ makes. Anything that does not resolve to `http`/`https` is refused.
49
+
50
+ ## Embedding it
51
+
52
+ ```ts
53
+ import { createDocsMcpServer } from '@aparte/docs-mcp';
54
+
55
+ const server = createDocsMcpServer({ baseUrl: 'https://apartejs.dev' });
56
+ // connect it to the transport of your choice (stdio, streamable HTTP…)
57
+ ```
58
+
59
+ `DocsSource`, `parseIndex`, `splitPages` and `searchPages` are exported too, for a host
60
+ that wants the text without the protocol.
61
+
62
+ ## License
63
+
64
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { createDocsMcpServer } from "./index.js";
4
+ const server = createDocsMcpServer({ baseUrl: process.env["APARTE_DOCS_URL"] });
5
+ await server.connect(new StdioServerTransport());
6
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sources":["../src/cli.ts"],"sourcesContent":["/**\n * `npx @aparte/docs-mcp` — the server over stdio, for Claude Code, Claude Desktop, Cursor\n * and every other MCP client that spawns a command. `APARTE_DOCS_URL` points it at\n * another docs origin (a local `astro build` served on localhost, a staging site).\n */\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { createDocsMcpServer } from './index.js';\n\nconst server = createDocsMcpServer({ baseUrl: process.env['APARTE_DOCS_URL'] });\nawait server.connect(new StdioServerTransport());\n"],"names":[],"mappings":";;;AAQA,MAAM,SAAS,oBAAoB,EAAE,SAAS,QAAQ,IAAI,iBAAiB,GAAG;AAC9E,MAAM,OAAO,QAAQ,IAAI,sBAAsB;"}
@@ -0,0 +1,130 @@
1
+ /**
2
+ * @aparte/docs-mcp — the aparté documentation as an MCP server.
3
+ *
4
+ * Why it exists: two consumers' coding assistants could not FIND things the docs
5
+ * already had — the approval elicitation (one rebuilt a modal it already had), the
6
+ * UI kit of classes, `systemPrompt: false`. A model that reads a site cold misses;
7
+ * a model that can ask finds. This server gives it four questions to ask.
8
+ *
9
+ * Where the text comes from: the site itself. `apartejs.dev/llms.txt` is an index the
10
+ * docs build writes (starlight-llms-txt), listing one file per topic — tools & approval,
11
+ * bring your own loop, theming & the UI kit, providers, reference — and a full dump.
12
+ * The server fetches those on demand and caches them for the process. No snapshot is
13
+ * bundled: `main` is what npm serves, so "the docs online" are the docs of the version
14
+ * you installed, and the package stays a few kilobytes. It needs the network; point
15
+ * `baseUrl` at a local `astro build` output served on localhost to work offline.
16
+ *
17
+ * `baseUrl` is the only origin this server ever talks to. An index is remote text, and
18
+ * the URLs it lists are taken for their PATH alone — a set entry naming another host
19
+ * would otherwise have made the machine running the agent fetch it (a metadata service,
20
+ * an intranet address), and `APARTE_DOCS_URL=http://localhost:4321` would have gone on
21
+ * reading production, since a production index lists production URLs.
22
+ *
23
+ * Every page in those files starts with `# Title` then `> description` (the plugin's
24
+ * format), which is what the page splitter relies on.
25
+ *
26
+ * @packageDocumentation
27
+ */
28
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
29
+ /** Where the docs live. Override for a local build (`http://localhost:4321`). */
30
+ export declare const DEFAULT_BASE_URL = "https://apartejs.dev";
31
+ /** One entry of `llms.txt`'s "Documentation Sets" list. */
32
+ export interface DocsSet {
33
+ /** The set's label as printed in the index, e.g. "Tools, approval and asking the user". */
34
+ label: string;
35
+ /**
36
+ * The URL the index printed, as printed — usually absolute and on the production
37
+ * site. It is NOT what gets fetched: only its path is, under the configured
38
+ * `baseUrl`. See {@link DocsSource.pagesOf}.
39
+ */
40
+ url: string;
41
+ /** The one-sentence description the index gives it. */
42
+ description: string;
43
+ }
44
+ /** One page inside a set file: the `# Title` / `> description` / body triplet. */
45
+ export interface DocsPage {
46
+ title: string;
47
+ description: string;
48
+ /** The page's markdown, description line excluded. */
49
+ body: string;
50
+ }
51
+ /** A search hit: the page, its score, and a window of text around the first match. */
52
+ export interface DocsHit {
53
+ title: string;
54
+ description: string;
55
+ score: number;
56
+ snippet: string;
57
+ /** The set the page was found in. */
58
+ set: string;
59
+ }
60
+ /** `fetch`-shaped, so a test (or an offline host) can hand in its own. */
61
+ export type TextFetcher = (url: string) => Promise<string>;
62
+ export interface DocsMcpOptions {
63
+ /**
64
+ * Defaults to {@link DEFAULT_BASE_URL} when unset, empty or blank. A trailing slash
65
+ * is tolerated. This is the only origin the server fetches from: a set the index
66
+ * lists contributes its path, never its host.
67
+ */
68
+ baseUrl?: string;
69
+ /** Defaults to global `fetch` + `response.text()`; a non-2xx status throws. */
70
+ fetchText?: TextFetcher;
71
+ }
72
+ /**
73
+ * Parse the "Documentation Sets" list of an `llms.txt`: `- [label](url): description`.
74
+ * The two entries the plugin always writes (abridged / complete) are kept — a caller
75
+ * may want the full dump — and everything else in the file is ignored.
76
+ */
77
+ export declare function parseIndex(text: string): DocsSet[];
78
+ /**
79
+ * Split a set file into pages. A page starts with a line `# Title`; the plugin writes
80
+ * the page's description as a `> …` quote right under it. The `<SYSTEM>…</SYSTEM>`
81
+ * preamble of a custom set is dropped.
82
+ */
83
+ export declare function splitPages(text: string): DocsPage[];
84
+ /**
85
+ * The searchable terms of a query: lower-cased words, stopwords and one-letter words
86
+ * dropped (`"approve a tool call"` once matched every page on its "a"), and long words
87
+ * shortened to a stem so "approve" finds "approval" and "streaming" finds "stream":
88
+ * six letters or more lose their last two.
89
+ */
90
+ export declare function queryTerms(query: string): string[];
91
+ /**
92
+ * Rank pages for a query: every query term counted in the body, weighted ×5 in the
93
+ * title and ×3 in the description; a page missing every term scores 0 and is dropped.
94
+ * Plain term frequency is enough for ~60 pages — a model refines with `get_page`.
95
+ */
96
+ export declare function searchPages(pages: DocsPage[], query: string, set?: string): DocsHit[];
97
+ /** The loaded-and-cached view of the site: the index, and each set's pages on demand. */
98
+ export declare class DocsSource {
99
+ private readonly base;
100
+ private readonly fetchText;
101
+ private index;
102
+ private readonly pages;
103
+ constructor(options?: DocsMcpOptions);
104
+ /** The sets listed by `llms.txt`, fetched once. */
105
+ sets(): Promise<DocsSet[]>;
106
+ /** A set by label (case-insensitive, substring tolerated), or undefined. */
107
+ findSet(label: string): Promise<DocsSet | undefined>;
108
+ /**
109
+ * The URL to actually fetch for something the index named: its path (and query),
110
+ * under `baseUrl`. The host the index printed is discarded — an index is text from
111
+ * the network, and following it verbatim is a request made from the machine running
112
+ * the agent. It is also what made `APARTE_DOCS_URL` a half-measure: a local build
113
+ * serves the production index, whose entries are absolute production URLs, so
114
+ * pointing the server at localhost still read the live site.
115
+ *
116
+ * Anything that does not resolve to http(s) — `file:`, `data:`, a bare scheme — is
117
+ * refused rather than quietly re-based, because it is not a docs URL at all.
118
+ */
119
+ private resolve;
120
+ /** The pages of one set, fetched once per set — from `baseUrl`, whatever host the index named. */
121
+ pagesOf(set: DocsSet): Promise<DocsPage[]>;
122
+ /** The sets to search: the one named, or every topic set (not the two dumps). */
123
+ scope(setLabel?: string): Promise<DocsSet[]>;
124
+ }
125
+ /**
126
+ * Build the MCP server. Four tools, in the order a model uses them:
127
+ * `list_sets` → `search_docs` → `get_page`, and `get_set` for a whole topic at once.
128
+ */
129
+ export declare function createDocsMcpServer(options?: DocsMcpOptions): McpServer;
130
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAgBpE,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,yBAAyB,CAAC;AAEvD,2DAA2D;AAC3D,MAAM,WAAW,OAAO;IACpB,2FAA2F;IAC3F,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;CACvB;AAED,kFAAkF;AAClF,MAAM,WAAW,QAAQ;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;CAChB;AAED,sFAAsF;AACtF,MAAM,WAAW,OAAO;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,GAAG,EAAE,MAAM,CAAC;CACf;AAED,0EAA0E;AAC1E,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAE3D,MAAM,WAAW,cAAc;IAC3B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,SAAS,CAAC,EAAE,WAAW,CAAC;CAC3B;AAQD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAUlD;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,EAAE,CAmBnD;AAKD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAMlD;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,SAAK,GAAG,OAAO,EAAE,CA0BjF;AAED,yFAAyF;AACzF,qBAAa,UAAU;IACnB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,OAAO,CAAC,KAAK,CAAmC;IAChD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0C;gBAEpD,OAAO,GAAE,cAAmB;IAOxC,mDAAmD;IACnD,IAAI,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IAK1B,4EAA4E;IACtE,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;IAO1D;;;;;;;;;;OAUG;IACH,OAAO,CAAC,OAAO;IAWf,kGAAkG;IAClG,OAAO,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAQ1C,iFAAiF;IAC3E,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;CAcrD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,cAAmB,GAAG,SAAS,CA8D3E"}
package/dist/index.js ADDED
@@ -0,0 +1,227 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { createRequire } from "node:module";
4
+ const pkgVersion = (() => {
5
+ try {
6
+ return createRequire(import.meta.url)("../package.json").version ?? "0.0.0";
7
+ } catch {
8
+ return "0.0.0";
9
+ }
10
+ })();
11
+ const DEFAULT_BASE_URL = "https://apartejs.dev";
12
+ const defaultFetchText = async (url) => {
13
+ const res = await fetch(url);
14
+ if (!res.ok) throw new Error(`${url} answered ${res.status}`);
15
+ return res.text();
16
+ };
17
+ function parseIndex(text) {
18
+ const sets = [];
19
+ let inSets = false;
20
+ for (const line of text.split("\n")) {
21
+ if (/^## /.test(line)) {
22
+ inSets = /^## Documentation Sets/.test(line);
23
+ continue;
24
+ }
25
+ if (!inSets) continue;
26
+ const m = /^- \[([^\]]+)\]\(([^)]+)\)(?::\s*(.*))?$/.exec(line.trim());
27
+ if (m) sets.push({ label: m[1], url: m[2], description: (m[3] ?? "").trim() });
28
+ }
29
+ return sets;
30
+ }
31
+ function splitPages(text) {
32
+ const pages = [];
33
+ let current = null;
34
+ for (const raw of text.replace(/^<SYSTEM>[\s\S]*?<\/SYSTEM>\s*/u, "").split("\n")) {
35
+ const title = /^# (.+)$/.exec(raw);
36
+ if (title) {
37
+ if (current) pages.push(current);
38
+ current = { title: title[1].trim(), description: "", body: "" };
39
+ continue;
40
+ }
41
+ if (!current) continue;
42
+ if (!current.description && !current.body.trim() && /^> /.test(raw)) {
43
+ current.description = raw.slice(2).trim();
44
+ continue;
45
+ }
46
+ current.body += raw + "\n";
47
+ }
48
+ if (current) pages.push(current);
49
+ return pages.map((p) => ({ ...p, body: p.body.trim() }));
50
+ }
51
+ const STOPWORDS = /* @__PURE__ */ new Set(["a", "an", "the", "to", "in", "on", "of", "and", "or", "is", "it", "my", "your", "how", "do", "i", "with", "for", "from", "be", "can"]);
52
+ function queryTerms(query) {
53
+ const words = query.toLowerCase().match(/[a-z0-9_@./-]+/g) ?? [];
54
+ const terms = words.filter((w) => w.length > 1 && !STOPWORDS.has(w)).map((w) => w.length >= 6 ? w.slice(0, -2) : w);
55
+ return [...new Set(terms)];
56
+ }
57
+ function searchPages(pages, query, set = "") {
58
+ const terms = queryTerms(query);
59
+ if (!terms.length) return [];
60
+ const hits = [];
61
+ for (const page of pages) {
62
+ const title = page.title.toLowerCase();
63
+ const desc = page.description.toLowerCase();
64
+ const body = page.body.toLowerCase();
65
+ let score = 0;
66
+ let first = -1;
67
+ for (const term of terms) {
68
+ if (title.includes(term)) score += 5;
69
+ if (desc.includes(term)) score += 3;
70
+ let at = body.indexOf(term);
71
+ while (at !== -1) {
72
+ score += 1;
73
+ if (first === -1 || at < first) first = at;
74
+ at = body.indexOf(term, at + term.length);
75
+ }
76
+ }
77
+ if (!score) continue;
78
+ const start = Math.max(0, first === -1 ? 0 : first - 160);
79
+ const snippet = page.body.slice(start, start + 400).replace(/\s+/g, " ").trim();
80
+ hits.push({ title: page.title, description: page.description, score, snippet, set });
81
+ }
82
+ return hits.sort((a, b) => b.score - a.score);
83
+ }
84
+ class DocsSource {
85
+ base;
86
+ fetchText;
87
+ index = null;
88
+ pages = /* @__PURE__ */ new Map();
89
+ constructor(options = {}) {
90
+ this.base = (options.baseUrl?.trim() || DEFAULT_BASE_URL).replace(/\/+$/, "");
91
+ this.fetchText = options.fetchText ?? defaultFetchText;
92
+ }
93
+ /** The sets listed by `llms.txt`, fetched once. */
94
+ sets() {
95
+ this.index ??= this.fetchText(`${this.base}/llms.txt`).then(parseIndex);
96
+ return this.index;
97
+ }
98
+ /** A set by label (case-insensitive, substring tolerated), or undefined. */
99
+ async findSet(label) {
100
+ const wanted = label.trim().toLowerCase();
101
+ const sets = await this.sets();
102
+ return sets.find((s) => s.label.toLowerCase() === wanted) ?? sets.find((s) => s.label.toLowerCase().includes(wanted));
103
+ }
104
+ /**
105
+ * The URL to actually fetch for something the index named: its path (and query),
106
+ * under `baseUrl`. The host the index printed is discarded — an index is text from
107
+ * the network, and following it verbatim is a request made from the machine running
108
+ * the agent. It is also what made `APARTE_DOCS_URL` a half-measure: a local build
109
+ * serves the production index, whose entries are absolute production URLs, so
110
+ * pointing the server at localhost still read the live site.
111
+ *
112
+ * Anything that does not resolve to http(s) — `file:`, `data:`, a bare scheme — is
113
+ * refused rather than quietly re-based, because it is not a docs URL at all.
114
+ */
115
+ resolve(url) {
116
+ const parsed = new URL(url, `${new URL(this.base).origin}/`);
117
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
118
+ throw new Error(`Refusing to fetch "${url}": ${parsed.protocol} is not a documentation URL`);
119
+ }
120
+ return `${this.base}${parsed.pathname}${parsed.search}`;
121
+ }
122
+ /** The pages of one set, fetched once per set — from `baseUrl`, whatever host the index named. */
123
+ pagesOf(set) {
124
+ let url;
125
+ try {
126
+ url = this.resolve(set.url);
127
+ } catch (error) {
128
+ return Promise.reject(error);
129
+ }
130
+ let p = this.pages.get(url);
131
+ if (!p) {
132
+ p = this.fetchText(url).then(splitPages);
133
+ this.pages.set(url, p);
134
+ }
135
+ return p;
136
+ }
137
+ /** The sets to search: the one named, or every topic set (not the two dumps). */
138
+ async scope(setLabel) {
139
+ if (setLabel) {
140
+ const one = await this.findSet(setLabel);
141
+ return one ? [one] : [];
142
+ }
143
+ const sets = await this.sets();
144
+ const topics = sets.filter((s) => !/^(Abridged|Complete) documentation$/.test(s.label));
145
+ if (topics.length) return topics;
146
+ const full = sets.find((s) => /^Complete documentation$/.test(s.label));
147
+ return full ? [full] : sets.slice(0, 1);
148
+ }
149
+ }
150
+ function createDocsMcpServer(options = {}) {
151
+ const source = new DocsSource(options);
152
+ const server = new McpServer({ name: "aparte-docs", version: pkgVersion });
153
+ const text = (t) => ({ content: [{ type: "text", text: t }] });
154
+ server.registerTool("list_sets", {
155
+ title: "List the documentation sets",
156
+ description: "The topics the aparté docs are split into (tools & approval, bring your own loop, theming & the UI kit, providers, reference…), each with a one-sentence description. Start here.",
157
+ inputSchema: {}
158
+ }, async () => {
159
+ const sets = await source.sets();
160
+ return text(sets.map((s) => `- ${s.label}: ${s.description}`).join("\n"));
161
+ });
162
+ server.registerTool("get_set", {
163
+ title: "Read a whole set",
164
+ description: "The full text of one documentation set (every page of that topic, markdown). Use when one topic is all you need; a set is 25–250 KB.",
165
+ inputSchema: { set: z.string().describe('A set label from list_sets, or a distinctive part of it, e.g. "approval" or "theming".') }
166
+ }, async ({ set }) => {
167
+ const found = await source.findSet(set);
168
+ if (!found) return text(`No set matches "${set}". Sets: ${(await source.sets()).map((s) => s.label).join(" · ")}`);
169
+ const pages = await source.pagesOf(found);
170
+ return text(pages.map((p) => `# ${p.title}
171
+
172
+ > ${p.description}
173
+
174
+ ${p.body}`).join("\n\n"));
175
+ });
176
+ server.registerTool("search_docs", {
177
+ title: "Search the docs",
178
+ description: 'Rank the documentation pages for a query (plain words: "approve a tool call", "systemPrompt false", "finish a turn in display-only"). Returns titles, descriptions and a snippet; read a hit with get_page.',
179
+ inputSchema: {
180
+ query: z.string().describe("What you are looking for, in plain words."),
181
+ set: z.string().optional().describe("Restrict to one set (label or part of it). Default: every topic set."),
182
+ limit: z.number().int().min(1).max(20).optional().describe("How many hits. Default 8.")
183
+ }
184
+ }, async ({ query, set, limit }) => {
185
+ const scope = await source.scope(set);
186
+ if (!scope.length) return text(`No set matches "${set}".`);
187
+ const hits = [];
188
+ for (const s of scope) hits.push(...searchPages(await source.pagesOf(s), query, s.label));
189
+ const seen = /* @__PURE__ */ new Set();
190
+ const top = hits.sort((a, b) => b.score - a.score).filter((h) => !seen.has(h.title) && seen.add(h.title)).slice(0, limit ?? 8);
191
+ if (!top.length) return text(`Nothing matches "${query}". Try other words, or list_sets to see the topics.`);
192
+ return text(top.map((h) => `## ${h.title} (score ${h.score}, set: ${h.set})
193
+ ${h.description}
194
+ …${h.snippet}…`).join("\n\n"));
195
+ });
196
+ server.registerTool("get_page", {
197
+ title: "Read a page",
198
+ description: "The full markdown of one documentation page, by its title (as returned by search_docs; a distinctive part of the title is enough).",
199
+ inputSchema: {
200
+ title: z.string().describe("The page title, or a distinctive part of it."),
201
+ set: z.string().optional().describe("The set to look in, if known — faster.")
202
+ }
203
+ }, async ({ title, set }) => {
204
+ const wanted = title.trim().toLowerCase();
205
+ for (const s of await source.scope(set)) {
206
+ const pages = await source.pagesOf(s);
207
+ const page = pages.find((p) => p.title.toLowerCase() === wanted) ?? pages.find((p) => p.title.toLowerCase().includes(wanted));
208
+ if (page) return text(`# ${page.title}
209
+
210
+ > ${page.description}
211
+
212
+ ${page.body}`);
213
+ }
214
+ return text(`No page titled "${title}". search_docs finds titles.`);
215
+ });
216
+ return server;
217
+ }
218
+ export {
219
+ DEFAULT_BASE_URL,
220
+ DocsSource,
221
+ createDocsMcpServer,
222
+ parseIndex,
223
+ queryTerms,
224
+ searchPages,
225
+ splitPages
226
+ };
227
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * @aparte/docs-mcp — the aparté documentation as an MCP server.\n *\n * Why it exists: two consumers' coding assistants could not FIND things the docs\n * already had — the approval elicitation (one rebuilt a modal it already had), the\n * UI kit of classes, `systemPrompt: false`. A model that reads a site cold misses;\n * a model that can ask finds. This server gives it four questions to ask.\n *\n * Where the text comes from: the site itself. `apartejs.dev/llms.txt` is an index the\n * docs build writes (starlight-llms-txt), listing one file per topic — tools & approval,\n * bring your own loop, theming & the UI kit, providers, reference — and a full dump.\n * The server fetches those on demand and caches them for the process. No snapshot is\n * bundled: `main` is what npm serves, so \"the docs online\" are the docs of the version\n * you installed, and the package stays a few kilobytes. It needs the network; point\n * `baseUrl` at a local `astro build` output served on localhost to work offline.\n *\n * `baseUrl` is the only origin this server ever talks to. An index is remote text, and\n * the URLs it lists are taken for their PATH alone — a set entry naming another host\n * would otherwise have made the machine running the agent fetch it (a metadata service,\n * an intranet address), and `APARTE_DOCS_URL=http://localhost:4321` would have gone on\n * reading production, since a production index lists production URLs.\n *\n * Every page in those files starts with `# Title` then `> description` (the plugin's\n * format), which is what the page splitter relies on.\n *\n * @packageDocumentation\n */\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\nimport { createRequire } from 'node:module';\n\n// The version the MCP handshake reports is the PUBLISHED one: changesets bumps\n// package.json and nothing else, so a literal here went stale at every release.\n// `../package.json` resolves to this package's manifest from dist/index.js, from\n// src/index.ts under the workspace source condition, and under vitest alike.\nconst pkgVersion = (() => {\n try {\n return (createRequire(import.meta.url)('../package.json') as { version?: string }).version ?? '0.0.0';\n } catch {\n return '0.0.0';\n }\n})();\n\n/** Where the docs live. Override for a local build (`http://localhost:4321`). */\nexport const DEFAULT_BASE_URL = 'https://apartejs.dev';\n\n/** One entry of `llms.txt`'s \"Documentation Sets\" list. */\nexport interface DocsSet {\n /** The set's label as printed in the index, e.g. \"Tools, approval and asking the user\". */\n label: string;\n /**\n * The URL the index printed, as printed — usually absolute and on the production\n * site. It is NOT what gets fetched: only its path is, under the configured\n * `baseUrl`. See {@link DocsSource.pagesOf}.\n */\n url: string;\n /** The one-sentence description the index gives it. */\n description: string;\n}\n\n/** One page inside a set file: the `# Title` / `> description` / body triplet. */\nexport interface DocsPage {\n title: string;\n description: string;\n /** The page's markdown, description line excluded. */\n body: string;\n}\n\n/** A search hit: the page, its score, and a window of text around the first match. */\nexport interface DocsHit {\n title: string;\n description: string;\n score: number;\n snippet: string;\n /** The set the page was found in. */\n set: string;\n}\n\n/** `fetch`-shaped, so a test (or an offline host) can hand in its own. */\nexport type TextFetcher = (url: string) => Promise<string>;\n\nexport interface DocsMcpOptions {\n /**\n * Defaults to {@link DEFAULT_BASE_URL} when unset, empty or blank. A trailing slash\n * is tolerated. This is the only origin the server fetches from: a set the index\n * lists contributes its path, never its host.\n */\n baseUrl?: string;\n /** Defaults to global `fetch` + `response.text()`; a non-2xx status throws. */\n fetchText?: TextFetcher;\n}\n\nconst defaultFetchText: TextFetcher = async (url) => {\n const res = await fetch(url);\n if (!res.ok) throw new Error(`${url} answered ${res.status}`);\n return res.text();\n};\n\n/**\n * Parse the \"Documentation Sets\" list of an `llms.txt`: `- [label](url): description`.\n * The two entries the plugin always writes (abridged / complete) are kept — a caller\n * may want the full dump — and everything else in the file is ignored.\n */\nexport function parseIndex(text: string): DocsSet[] {\n const sets: DocsSet[] = [];\n let inSets = false;\n for (const line of text.split('\\n')) {\n if (/^## /.test(line)) { inSets = /^## Documentation Sets/.test(line); continue; }\n if (!inSets) continue;\n const m = /^- \\[([^\\]]+)\\]\\(([^)]+)\\)(?::\\s*(.*))?$/.exec(line.trim());\n if (m) sets.push({ label: m[1]!, url: m[2]!, description: (m[3] ?? '').trim() });\n }\n return sets;\n}\n\n/**\n * Split a set file into pages. A page starts with a line `# Title`; the plugin writes\n * the page's description as a `> …` quote right under it. The `<SYSTEM>…</SYSTEM>`\n * preamble of a custom set is dropped.\n */\nexport function splitPages(text: string): DocsPage[] {\n const pages: DocsPage[] = [];\n let current: DocsPage | null = null;\n for (const raw of text.replace(/^<SYSTEM>[\\s\\S]*?<\\/SYSTEM>\\s*/u, '').split('\\n')) {\n const title = /^# (.+)$/.exec(raw);\n if (title) {\n if (current) pages.push(current);\n current = { title: title[1]!.trim(), description: '', body: '' };\n continue;\n }\n if (!current) continue;\n if (!current.description && !current.body.trim() && /^> /.test(raw)) {\n current.description = raw.slice(2).trim();\n continue;\n }\n current.body += raw + '\\n';\n }\n if (current) pages.push(current);\n return pages.map((p) => ({ ...p, body: p.body.trim() }));\n}\n\n/** Words a model puts in a question and no page is about. */\nconst STOPWORDS = new Set(['a', 'an', 'the', 'to', 'in', 'on', 'of', 'and', 'or', 'is', 'it', 'my', 'your', 'how', 'do', 'i', 'with', 'for', 'from', 'be', 'can']);\n\n/**\n * The searchable terms of a query: lower-cased words, stopwords and one-letter words\n * dropped (`\"approve a tool call\"` once matched every page on its \"a\"), and long words\n * shortened to a stem so \"approve\" finds \"approval\" and \"streaming\" finds \"stream\":\n * six letters or more lose their last two.\n */\nexport function queryTerms(query: string): string[] {\n const words = query.toLowerCase().match(/[a-z0-9_@./-]+/g) ?? [];\n const terms = words\n .filter((w) => w.length > 1 && !STOPWORDS.has(w))\n .map((w) => (w.length >= 6 ? w.slice(0, -2) : w));\n return [...new Set(terms)];\n}\n\n/**\n * Rank pages for a query: every query term counted in the body, weighted ×5 in the\n * title and ×3 in the description; a page missing every term scores 0 and is dropped.\n * Plain term frequency is enough for ~60 pages — a model refines with `get_page`.\n */\nexport function searchPages(pages: DocsPage[], query: string, set = ''): DocsHit[] {\n const terms = queryTerms(query);\n if (!terms.length) return [];\n const hits: DocsHit[] = [];\n for (const page of pages) {\n const title = page.title.toLowerCase();\n const desc = page.description.toLowerCase();\n const body = page.body.toLowerCase();\n let score = 0;\n let first = -1;\n for (const term of terms) {\n if (title.includes(term)) score += 5;\n if (desc.includes(term)) score += 3;\n let at = body.indexOf(term);\n while (at !== -1) {\n score += 1;\n if (first === -1 || at < first) first = at;\n at = body.indexOf(term, at + term.length);\n }\n }\n if (!score) continue;\n const start = Math.max(0, first === -1 ? 0 : first - 160);\n const snippet = page.body.slice(start, start + 400).replace(/\\s+/g, ' ').trim();\n hits.push({ title: page.title, description: page.description, score, snippet, set });\n }\n return hits.sort((a, b) => b.score - a.score);\n}\n\n/** The loaded-and-cached view of the site: the index, and each set's pages on demand. */\nexport class DocsSource {\n private readonly base: string;\n private readonly fetchText: TextFetcher;\n private index: Promise<DocsSet[]> | null = null;\n private readonly pages = new Map<string, Promise<DocsPage[]>>();\n\n constructor(options: DocsMcpOptions = {}) {\n // `||`, not `??`: an env var exported but empty (`APARTE_DOCS_URL=\"\"`) is\n // \"unset\", and a pasted value with a stray newline fails the same way.\n this.base = (options.baseUrl?.trim() || DEFAULT_BASE_URL).replace(/\\/+$/, '');\n this.fetchText = options.fetchText ?? defaultFetchText;\n }\n\n /** The sets listed by `llms.txt`, fetched once. */\n sets(): Promise<DocsSet[]> {\n this.index ??= this.fetchText(`${this.base}/llms.txt`).then(parseIndex);\n return this.index;\n }\n\n /** A set by label (case-insensitive, substring tolerated), or undefined. */\n async findSet(label: string): Promise<DocsSet | undefined> {\n const wanted = label.trim().toLowerCase();\n const sets = await this.sets();\n return sets.find((s) => s.label.toLowerCase() === wanted)\n ?? sets.find((s) => s.label.toLowerCase().includes(wanted));\n }\n\n /**\n * The URL to actually fetch for something the index named: its path (and query),\n * under `baseUrl`. The host the index printed is discarded — an index is text from\n * the network, and following it verbatim is a request made from the machine running\n * the agent. It is also what made `APARTE_DOCS_URL` a half-measure: a local build\n * serves the production index, whose entries are absolute production URLs, so\n * pointing the server at localhost still read the live site.\n *\n * Anything that does not resolve to http(s) — `file:`, `data:`, a bare scheme — is\n * refused rather than quietly re-based, because it is not a docs URL at all.\n */\n private resolve(url: string): string {\n // Against the ORIGIN, not against `base`: a path the index gives is a path from\n // the docs root, so a `baseUrl` that carries a prefix (`https://host/docs`) must\n // prepend it once, not resolve under it and then prepend it again.\n const parsed = new URL(url, `${new URL(this.base).origin}/`);\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(`Refusing to fetch \"${url}\": ${parsed.protocol} is not a documentation URL`);\n }\n return `${this.base}${parsed.pathname}${parsed.search}`;\n }\n\n /** The pages of one set, fetched once per set — from `baseUrl`, whatever host the index named. */\n pagesOf(set: DocsSet): Promise<DocsPage[]> {\n let url: string;\n try { url = this.resolve(set.url); } catch (error) { return Promise.reject(error as Error); }\n let p = this.pages.get(url);\n if (!p) { p = this.fetchText(url).then(splitPages); this.pages.set(url, p); }\n return p;\n }\n\n /** The sets to search: the one named, or every topic set (not the two dumps). */\n async scope(setLabel?: string): Promise<DocsSet[]> {\n if (setLabel) {\n const one = await this.findSet(setLabel);\n return one ? [one] : [];\n }\n const sets = await this.sets();\n const topics = sets.filter((s) => !/^(Abridged|Complete) documentation$/.test(s.label));\n if (topics.length) return topics;\n // An index with no topic sets (an older site, a bare starlight-llms-txt): search\n // the complete dump alone — both dumps hold the same pages, twice the fetch and\n // every hit doubled.\n const full = sets.find((s) => /^Complete documentation$/.test(s.label));\n return full ? [full] : sets.slice(0, 1);\n }\n}\n\n/**\n * Build the MCP server. Four tools, in the order a model uses them:\n * `list_sets` → `search_docs` → `get_page`, and `get_set` for a whole topic at once.\n */\nexport function createDocsMcpServer(options: DocsMcpOptions = {}): McpServer {\n const source = new DocsSource(options);\n const server = new McpServer({ name: 'aparte-docs', version: pkgVersion });\n const text = (t: string) => ({ content: [{ type: 'text' as const, text: t }] });\n\n server.registerTool('list_sets', {\n title: 'List the documentation sets',\n description: 'The topics the aparté docs are split into (tools & approval, bring your own loop, theming & the UI kit, providers, reference…), each with a one-sentence description. Start here.',\n inputSchema: {},\n }, async () => {\n const sets = await source.sets();\n return text(sets.map((s) => `- ${s.label}: ${s.description}`).join('\\n'));\n });\n\n server.registerTool('get_set', {\n title: 'Read a whole set',\n description: 'The full text of one documentation set (every page of that topic, markdown). Use when one topic is all you need; a set is 25–250 KB.',\n inputSchema: { set: z.string().describe('A set label from list_sets, or a distinctive part of it, e.g. \"approval\" or \"theming\".') },\n }, async ({ set }) => {\n const found = await source.findSet(set);\n if (!found) return text(`No set matches \"${set}\". Sets: ${(await source.sets()).map((s) => s.label).join(' · ')}`);\n const pages = await source.pagesOf(found);\n return text(pages.map((p) => `# ${p.title}\\n\\n> ${p.description}\\n\\n${p.body}`).join('\\n\\n'));\n });\n\n server.registerTool('search_docs', {\n title: 'Search the docs',\n description: 'Rank the documentation pages for a query (plain words: \"approve a tool call\", \"systemPrompt false\", \"finish a turn in display-only\"). Returns titles, descriptions and a snippet; read a hit with get_page.',\n inputSchema: {\n query: z.string().describe('What you are looking for, in plain words.'),\n set: z.string().optional().describe('Restrict to one set (label or part of it). Default: every topic set.'),\n limit: z.number().int().min(1).max(20).optional().describe('How many hits. Default 8.'),\n },\n }, async ({ query, set, limit }) => {\n const scope = await source.scope(set);\n if (!scope.length) return text(`No set matches \"${set}\".`);\n const hits: DocsHit[] = [];\n for (const s of scope) hits.push(...searchPages(await source.pagesOf(s), query, s.label));\n const seen = new Set<string>();\n const top = hits.sort((a, b) => b.score - a.score).filter((h) => !seen.has(h.title) && seen.add(h.title)).slice(0, limit ?? 8);\n if (!top.length) return text(`Nothing matches \"${query}\". Try other words, or list_sets to see the topics.`);\n return text(top.map((h) => `## ${h.title} (score ${h.score}, set: ${h.set})\\n${h.description}\\n…${h.snippet}…`).join('\\n\\n'));\n });\n\n server.registerTool('get_page', {\n title: 'Read a page',\n description: 'The full markdown of one documentation page, by its title (as returned by search_docs; a distinctive part of the title is enough).',\n inputSchema: {\n title: z.string().describe('The page title, or a distinctive part of it.'),\n set: z.string().optional().describe('The set to look in, if known — faster.'),\n },\n }, async ({ title, set }) => {\n const wanted = title.trim().toLowerCase();\n for (const s of await source.scope(set)) {\n const pages = await source.pagesOf(s);\n const page = pages.find((p) => p.title.toLowerCase() === wanted) ?? pages.find((p) => p.title.toLowerCase().includes(wanted));\n if (page) return text(`# ${page.title}\\n\\n> ${page.description}\\n\\n${page.body}`);\n }\n return text(`No page titled \"${title}\". search_docs finds titles.`);\n });\n\n return server;\n}\n"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@aparte/docs-mcp",
3
+ "version": "0.16.0",
4
+ "description": "The aparté documentation as an MCP server — a coding agent searches the guides, reads a page or a whole topic (tools & approval, bring your own loop, theming & the UI kit…), always the version that ships, straight from apartejs.dev.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "bin": {
11
+ "aparte-docs-mcp": "./dist/cli.js"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "@aparte-workspace/source": "./src/index.ts",
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js",
18
+ "default": "./dist/index.js"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "dependencies": {
30
+ "@modelcontextprotocol/sdk": "^1.30.0",
31
+ "zod": "^3.25.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "20.11.0",
35
+ "typescript": "^5.4.0",
36
+ "vite": "^6.0.0"
37
+ },
38
+ "keywords": [
39
+ "aparte",
40
+ "mcp",
41
+ "model-context-protocol",
42
+ "documentation",
43
+ "llms-txt",
44
+ "ai",
45
+ "chat"
46
+ ],
47
+ "license": "MIT",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/apartejs/aparte.git",
51
+ "directory": "packages/tools/docs-mcp"
52
+ },
53
+ "bugs": {
54
+ "url": "https://github.com/apartejs/aparte/issues"
55
+ },
56
+ "scripts": {
57
+ "build": "vite build && tsc -b --emitDeclarationOnly --force",
58
+ "test": "vitest",
59
+ "test:run": "vitest run",
60
+ "test:coverage": "vitest run --coverage"
61
+ }
62
+ }