@tesseron/docs-mcp 0.2.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 +114 -0
- package/README.md +95 -0
- package/dist/docs-index.json +1 -0
- package/dist/index.cjs +232 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +68 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +222 -0
- package/dist/index.js.map +1 -0
- package/dist/tesseron-docs-mcp.cjs +23314 -0
- package/package.json +71 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var MiniSearch = require('minisearch');
|
|
4
|
+
var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
5
|
+
var zod = require('zod');
|
|
6
|
+
var fs = require('fs');
|
|
7
|
+
var path = require('path');
|
|
8
|
+
var url = require('url');
|
|
9
|
+
|
|
10
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
11
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
|
+
|
|
13
|
+
var MiniSearch__default = /*#__PURE__*/_interopDefault(MiniSearch);
|
|
14
|
+
|
|
15
|
+
// src/search.ts
|
|
16
|
+
var SNIPPET_RADIUS = 120;
|
|
17
|
+
var SNIPPET_MAX = SNIPPET_RADIUS * 2;
|
|
18
|
+
var DocsIndex = class {
|
|
19
|
+
mini;
|
|
20
|
+
bySlug;
|
|
21
|
+
constructor(docs) {
|
|
22
|
+
this.bySlug = new Map(docs.map((d) => [d.slug, d]));
|
|
23
|
+
this.mini = new MiniSearch__default.default({
|
|
24
|
+
idField: "slug",
|
|
25
|
+
fields: ["title", "description", "bodyText"],
|
|
26
|
+
storeFields: ["slug", "title", "description", "section"],
|
|
27
|
+
searchOptions: {
|
|
28
|
+
boost: { title: 3, description: 2 },
|
|
29
|
+
fuzzy: 0.15,
|
|
30
|
+
prefix: true
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
this.mini.addAll(docs);
|
|
34
|
+
}
|
|
35
|
+
search(query, limit = 8) {
|
|
36
|
+
const raw = this.mini.search(query);
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const r of raw.slice(0, Math.max(1, Math.min(limit, 20)))) {
|
|
39
|
+
const slug = String(r["slug"]);
|
|
40
|
+
const entry = this.bySlug.get(slug);
|
|
41
|
+
if (!entry) continue;
|
|
42
|
+
out.push({
|
|
43
|
+
slug,
|
|
44
|
+
title: String(r["title"]),
|
|
45
|
+
description: String(r["description"] ?? ""),
|
|
46
|
+
section: String(r["section"] ?? ""),
|
|
47
|
+
score: typeof r["score"] === "number" ? r["score"] : 0,
|
|
48
|
+
snippet: makeSnippet(entry, query)
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
function makeSnippet(entry, query) {
|
|
55
|
+
const text = entry.bodyText;
|
|
56
|
+
if (!text) return entry.description;
|
|
57
|
+
const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 1);
|
|
58
|
+
if (terms.length === 0) return truncate(text, SNIPPET_MAX);
|
|
59
|
+
const lower = text.toLowerCase();
|
|
60
|
+
let bestIdx = -1;
|
|
61
|
+
for (const t of terms) {
|
|
62
|
+
const idx = lower.indexOf(t);
|
|
63
|
+
if (idx !== -1 && (bestIdx === -1 || idx < bestIdx)) bestIdx = idx;
|
|
64
|
+
}
|
|
65
|
+
if (bestIdx === -1) return entry.description || truncate(text, SNIPPET_MAX);
|
|
66
|
+
const start = Math.max(0, bestIdx - SNIPPET_RADIUS);
|
|
67
|
+
const end = Math.min(text.length, bestIdx + SNIPPET_RADIUS);
|
|
68
|
+
let slice = text.slice(start, end).replace(/\s+/g, " ").trim();
|
|
69
|
+
if (start > 0) slice = `\u2026${slice}`;
|
|
70
|
+
if (end < text.length) slice = `${slice}\u2026`;
|
|
71
|
+
return slice;
|
|
72
|
+
}
|
|
73
|
+
function truncate(s, n) {
|
|
74
|
+
const single = s.replace(/\s+/g, " ").trim();
|
|
75
|
+
return single.length <= n ? single : `${single.slice(0, n - 1)}\u2026`;
|
|
76
|
+
}
|
|
77
|
+
var RESOURCE_SCHEME = "tesseron-docs";
|
|
78
|
+
function createDocsMcpServer(options) {
|
|
79
|
+
const { snapshot } = options;
|
|
80
|
+
const bySlug = new Map(snapshot.docs.map((d) => [d.slug, d]));
|
|
81
|
+
const index = new DocsIndex(snapshot.docs);
|
|
82
|
+
const server = new mcp_js.McpServer({
|
|
83
|
+
name: options.name ?? "tesseron-docs",
|
|
84
|
+
version: options.version ?? snapshot.version
|
|
85
|
+
});
|
|
86
|
+
server.registerTool(
|
|
87
|
+
"list_docs",
|
|
88
|
+
{
|
|
89
|
+
description: "List every Tesseron documentation page with title, slug, section, short description, and related slugs. Cheap, call before searching when you want the full catalogue.",
|
|
90
|
+
inputSchema: {}
|
|
91
|
+
},
|
|
92
|
+
() => {
|
|
93
|
+
const docs = snapshot.docs.map((d) => ({
|
|
94
|
+
slug: d.slug,
|
|
95
|
+
title: d.title,
|
|
96
|
+
section: d.section,
|
|
97
|
+
description: d.description,
|
|
98
|
+
related: d.related
|
|
99
|
+
}));
|
|
100
|
+
return {
|
|
101
|
+
content: [{ type: "text", text: JSON.stringify({ count: docs.length, docs }, null, 2) }]
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
server.registerTool(
|
|
106
|
+
"search_docs",
|
|
107
|
+
{
|
|
108
|
+
description: "Full-text search across Tesseron docs (BM25, title- and description-weighted). Returns ranked hits with short snippets. Call `read_doc(slug)` on promising hits for full content.",
|
|
109
|
+
inputSchema: {
|
|
110
|
+
query: zod.z.string().min(1).describe("Free-form query. Supports multiple terms; fuzzy + prefix matching are on."),
|
|
111
|
+
limit: zod.z.number().int().min(1).max(20).optional().describe("Maximum hits to return. Default 8, hard cap 20.")
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
({ query, limit }) => {
|
|
115
|
+
const hits = index.search(query, limit ?? 8);
|
|
116
|
+
return {
|
|
117
|
+
content: [
|
|
118
|
+
{
|
|
119
|
+
type: "text",
|
|
120
|
+
text: JSON.stringify({ query, count: hits.length, hits }, null, 2)
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
server.registerTool(
|
|
127
|
+
"read_doc",
|
|
128
|
+
{
|
|
129
|
+
description: "Return the full markdown body of a Tesseron docs page plus its structured frontmatter (title, description, section, related). Slug format: `<section>/<basename>` without extension (e.g. `protocol/handshake`).",
|
|
130
|
+
inputSchema: {
|
|
131
|
+
slug: zod.z.string().min(1).describe("Page slug. Use `list_docs` or `search_docs` to discover valid slugs.")
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
({ slug }) => {
|
|
135
|
+
const entry = bySlug.get(slug);
|
|
136
|
+
if (!entry) {
|
|
137
|
+
return {
|
|
138
|
+
isError: true,
|
|
139
|
+
content: [
|
|
140
|
+
{
|
|
141
|
+
type: "text",
|
|
142
|
+
text: `No doc found for slug "${slug}". Call list_docs to see valid slugs.`
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const payload = {
|
|
148
|
+
slug: entry.slug,
|
|
149
|
+
title: entry.title,
|
|
150
|
+
description: entry.description,
|
|
151
|
+
section: entry.section,
|
|
152
|
+
related: entry.related,
|
|
153
|
+
body: entry.bodyRaw
|
|
154
|
+
};
|
|
155
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
server.registerResource(
|
|
159
|
+
"tesseron-doc",
|
|
160
|
+
new mcp_js.ResourceTemplate(`${RESOURCE_SCHEME}://{+slug}`, {
|
|
161
|
+
list: () => ({
|
|
162
|
+
resources: snapshot.docs.map((d) => ({
|
|
163
|
+
uri: `${RESOURCE_SCHEME}://${d.slug}`,
|
|
164
|
+
name: d.title,
|
|
165
|
+
description: d.description,
|
|
166
|
+
mimeType: "text/markdown"
|
|
167
|
+
}))
|
|
168
|
+
})
|
|
169
|
+
}),
|
|
170
|
+
{
|
|
171
|
+
title: "Tesseron documentation page",
|
|
172
|
+
description: "Full markdown body of a single Tesseron docs page, keyed by slug."
|
|
173
|
+
},
|
|
174
|
+
(uri, variables) => {
|
|
175
|
+
const slug = Array.isArray(variables["slug"]) ? variables["slug"][0] : variables["slug"];
|
|
176
|
+
if (!slug) {
|
|
177
|
+
throw new Error(`Missing slug in resource URI ${uri.href}`);
|
|
178
|
+
}
|
|
179
|
+
const entry = bySlug.get(slug);
|
|
180
|
+
if (!entry) {
|
|
181
|
+
throw new Error(`No doc found for slug "${slug}".`);
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
contents: [
|
|
185
|
+
{
|
|
186
|
+
uri: uri.href,
|
|
187
|
+
mimeType: "text/markdown",
|
|
188
|
+
text: entry.bodyRaw
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
return server;
|
|
195
|
+
}
|
|
196
|
+
var SNAPSHOT_FILENAME = "docs-index.json";
|
|
197
|
+
var ENV_SNAPSHOT_PATH = "TESSERON_DOCS_SNAPSHOT";
|
|
198
|
+
function thisModuleDir() {
|
|
199
|
+
if (typeof __dirname !== "undefined") return __dirname;
|
|
200
|
+
return path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
|
|
201
|
+
}
|
|
202
|
+
function resolveSnapshotPath(override) {
|
|
203
|
+
const envPath = process.env[ENV_SNAPSHOT_PATH];
|
|
204
|
+
if (envPath) return envPath;
|
|
205
|
+
if (override) return override;
|
|
206
|
+
const here = thisModuleDir();
|
|
207
|
+
const bundled = path.resolve(here, SNAPSHOT_FILENAME);
|
|
208
|
+
if (fs.existsSync(bundled)) return bundled;
|
|
209
|
+
const devFallback = path.resolve(here, "..", "dist", SNAPSHOT_FILENAME);
|
|
210
|
+
return devFallback;
|
|
211
|
+
}
|
|
212
|
+
function loadSnapshot(override) {
|
|
213
|
+
const path = resolveSnapshotPath(override);
|
|
214
|
+
if (!fs.existsSync(path)) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`[tesseron-docs-mcp] snapshot not found at ${path}. Run "pnpm --filter @tesseron/docs-mcp build" to generate it, or set ${ENV_SNAPSHOT_PATH}=/abs/path/to/docs-index.json.`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
const raw = fs.readFileSync(path, "utf8");
|
|
220
|
+
const parsed = JSON.parse(raw);
|
|
221
|
+
if (!Array.isArray(parsed.docs)) {
|
|
222
|
+
throw new Error(`[tesseron-docs-mcp] invalid snapshot at ${path}: missing "docs" array.`);
|
|
223
|
+
}
|
|
224
|
+
return parsed;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
exports.DocsIndex = DocsIndex;
|
|
228
|
+
exports.createDocsMcpServer = createDocsMcpServer;
|
|
229
|
+
exports.loadSnapshot = loadSnapshot;
|
|
230
|
+
exports.resolveSnapshotPath = resolveSnapshotPath;
|
|
231
|
+
//# sourceMappingURL=index.cjs.map
|
|
232
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/search.ts","../src/server.ts","../src/snapshot.ts"],"names":["MiniSearch","McpServer","z","ResourceTemplate","dirname","fileURLToPath","resolve","existsSync","readFileSync"],"mappings":"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","file":"index.cjs","sourcesContent":["import MiniSearch from 'minisearch';\nimport type { DocEntry } from './content/types.js';\n\nexport type SearchHit = {\n slug: string;\n title: string;\n description: string;\n section: string;\n score: number;\n snippet: string;\n};\n\nconst SNIPPET_RADIUS = 120;\nconst SNIPPET_MAX = SNIPPET_RADIUS * 2;\n\nexport class DocsIndex {\n private readonly mini: MiniSearch<DocEntry>;\n private readonly bySlug: Map<string, DocEntry>;\n\n constructor(docs: DocEntry[]) {\n this.bySlug = new Map(docs.map((d) => [d.slug, d]));\n this.mini = new MiniSearch<DocEntry>({\n idField: 'slug',\n fields: ['title', 'description', 'bodyText'],\n storeFields: ['slug', 'title', 'description', 'section'],\n searchOptions: {\n boost: { title: 3, description: 2 },\n fuzzy: 0.15,\n prefix: true,\n },\n });\n this.mini.addAll(docs);\n }\n\n search(query: string, limit = 8): SearchHit[] {\n const raw = this.mini.search(query);\n const out: SearchHit[] = [];\n for (const r of raw.slice(0, Math.max(1, Math.min(limit, 20)))) {\n const slug = String(r['slug']);\n const entry = this.bySlug.get(slug);\n if (!entry) continue;\n out.push({\n slug,\n title: String(r['title']),\n description: String(r['description'] ?? ''),\n section: String(r['section'] ?? ''),\n score: typeof r['score'] === 'number' ? r['score'] : 0,\n snippet: makeSnippet(entry, query),\n });\n }\n return out;\n }\n}\n\n/**\n * Extract a ~240-char snippet centred on the best match for `query` within\n * `entry.bodyText`. Falls back to `description` when no body match is found.\n */\nexport function makeSnippet(entry: DocEntry, query: string): string {\n const text = entry.bodyText;\n if (!text) return entry.description;\n\n const terms = query\n .toLowerCase()\n .split(/\\s+/)\n .filter((t) => t.length > 1);\n if (terms.length === 0) return truncate(text, SNIPPET_MAX);\n\n const lower = text.toLowerCase();\n let bestIdx = -1;\n for (const t of terms) {\n const idx = lower.indexOf(t);\n if (idx !== -1 && (bestIdx === -1 || idx < bestIdx)) bestIdx = idx;\n }\n if (bestIdx === -1) return entry.description || truncate(text, SNIPPET_MAX);\n\n const start = Math.max(0, bestIdx - SNIPPET_RADIUS);\n const end = Math.min(text.length, bestIdx + SNIPPET_RADIUS);\n let slice = text.slice(start, end).replace(/\\s+/g, ' ').trim();\n if (start > 0) slice = `…${slice}`;\n if (end < text.length) slice = `${slice}…`;\n return slice;\n}\n\nfunction truncate(s: string, n: number): string {\n const single = s.replace(/\\s+/g, ' ').trim();\n return single.length <= n ? single : `${single.slice(0, n - 1)}…`;\n}\n","import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\nimport type { DocEntry, Snapshot } from './content/types.js';\nimport { DocsIndex } from './search.js';\n\nconst RESOURCE_SCHEME = 'tesseron-docs';\n\nexport type ServerOptions = {\n /** Snapshot produced by `scripts/build-snapshot.ts`. */\n snapshot: Snapshot;\n /** Optional override of the reported server name (for test isolation). */\n name?: string;\n /** Optional override of the reported server version. */\n version?: string;\n};\n\n/**\n * Build an `McpServer` wired up with `list_docs`, `search_docs`, `read_doc`\n * tools and `tesseron-docs://<slug>` resources.\n */\nexport function createDocsMcpServer(options: ServerOptions): McpServer {\n const { snapshot } = options;\n const bySlug = new Map<string, DocEntry>(snapshot.docs.map((d) => [d.slug, d]));\n const index = new DocsIndex(snapshot.docs);\n\n const server = new McpServer({\n name: options.name ?? 'tesseron-docs',\n version: options.version ?? snapshot.version,\n });\n\n server.registerTool(\n 'list_docs',\n {\n description:\n 'List every Tesseron documentation page with title, slug, section, short description, and related slugs. Cheap, call before searching when you want the full catalogue.',\n inputSchema: {},\n },\n () => {\n const docs = snapshot.docs.map((d) => ({\n slug: d.slug,\n title: d.title,\n section: d.section,\n description: d.description,\n related: d.related,\n }));\n return {\n content: [{ type: 'text', text: JSON.stringify({ count: docs.length, docs }, null, 2) }],\n };\n },\n );\n\n server.registerTool(\n 'search_docs',\n {\n description:\n 'Full-text search across Tesseron docs (BM25, title- and description-weighted). Returns ranked hits with short snippets. Call `read_doc(slug)` on promising hits for full content.',\n inputSchema: {\n query: z\n .string()\n .min(1)\n .describe('Free-form query. Supports multiple terms; fuzzy + prefix matching are on.'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(20)\n .optional()\n .describe('Maximum hits to return. Default 8, hard cap 20.'),\n },\n },\n ({ query, limit }) => {\n const hits = index.search(query, limit ?? 8);\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify({ query, count: hits.length, hits }, null, 2),\n },\n ],\n };\n },\n );\n\n server.registerTool(\n 'read_doc',\n {\n description:\n 'Return the full markdown body of a Tesseron docs page plus its structured frontmatter (title, description, section, related). Slug format: `<section>/<basename>` without extension (e.g. `protocol/handshake`).',\n inputSchema: {\n slug: z\n .string()\n .min(1)\n .describe('Page slug. Use `list_docs` or `search_docs` to discover valid slugs.'),\n },\n },\n ({ slug }) => {\n const entry = bySlug.get(slug);\n if (!entry) {\n return {\n isError: true,\n content: [\n {\n type: 'text',\n text: `No doc found for slug \"${slug}\". Call list_docs to see valid slugs.`,\n },\n ],\n };\n }\n const payload = {\n slug: entry.slug,\n title: entry.title,\n description: entry.description,\n section: entry.section,\n related: entry.related,\n body: entry.bodyRaw,\n };\n return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };\n },\n );\n\n server.registerResource(\n 'tesseron-doc',\n new ResourceTemplate(`${RESOURCE_SCHEME}://{+slug}`, {\n list: () => ({\n resources: snapshot.docs.map((d) => ({\n uri: `${RESOURCE_SCHEME}://${d.slug}`,\n name: d.title,\n description: d.description,\n mimeType: 'text/markdown',\n })),\n }),\n }),\n {\n title: 'Tesseron documentation page',\n description: 'Full markdown body of a single Tesseron docs page, keyed by slug.',\n },\n (uri, variables) => {\n const slug = Array.isArray(variables['slug'])\n ? variables['slug'][0]\n : (variables['slug'] as string | undefined);\n if (!slug) {\n throw new Error(`Missing slug in resource URI ${uri.href}`);\n }\n const entry = bySlug.get(slug);\n if (!entry) {\n throw new Error(`No doc found for slug \"${slug}\".`);\n }\n return {\n contents: [\n {\n uri: uri.href,\n mimeType: 'text/markdown',\n text: entry.bodyRaw,\n },\n ],\n };\n },\n );\n\n return server;\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { DocEntry, Snapshot } from './content/types.js';\n\nconst SNAPSHOT_FILENAME = 'docs-index.json';\nconst ENV_SNAPSHOT_PATH = 'TESSERON_DOCS_SNAPSHOT';\n\nfunction thisModuleDir(): string {\n if (typeof __dirname !== 'undefined') return __dirname;\n return dirname(fileURLToPath(import.meta.url));\n}\n\n/**\n * Locate the bundled snapshot JSON. Lookup order:\n * 1. `TESSERON_DOCS_SNAPSHOT` environment variable, if set.\n * 2. Explicit `override` argument.\n * 3. Alongside the compiled bin (production / `npx` use).\n * 4. `../dist/docs-index.json` (when running via `tsx src/cli.ts` in dev).\n */\nexport function resolveSnapshotPath(override?: string): string {\n const envPath = process.env[ENV_SNAPSHOT_PATH];\n if (envPath) return envPath;\n if (override) return override;\n\n const here = thisModuleDir();\n const bundled = resolve(here, SNAPSHOT_FILENAME);\n if (existsSync(bundled)) return bundled;\n\n const devFallback = resolve(here, '..', 'dist', SNAPSHOT_FILENAME);\n return devFallback;\n}\n\nexport function loadSnapshot(override?: string): Snapshot {\n const path = resolveSnapshotPath(override);\n if (!existsSync(path)) {\n throw new Error(\n `[tesseron-docs-mcp] snapshot not found at ${path}. ` +\n 'Run \"pnpm --filter @tesseron/docs-mcp build\" to generate it, ' +\n `or set ${ENV_SNAPSHOT_PATH}=/abs/path/to/docs-index.json.`,\n );\n }\n const raw = readFileSync(path, 'utf8');\n const parsed = JSON.parse(raw) as Snapshot;\n if (!Array.isArray(parsed.docs)) {\n throw new Error(`[tesseron-docs-mcp] invalid snapshot at ${path}: missing \"docs\" array.`);\n }\n return parsed;\n}\n\n/** Index docs by slug for O(1) lookup in `read_doc`. */\nexport function indexBySlug(docs: DocEntry[]): Map<string, DocEntry> {\n const map = new Map<string, DocEntry>();\n for (const d of docs) map.set(d.slug, d);\n return map;\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
|
|
3
|
+
type DocEntry = {
|
|
4
|
+
/** Slug relative to `docs/src/content/docs/`, no extension. E.g. `protocol/handshake`. */
|
|
5
|
+
slug: string;
|
|
6
|
+
/** `title` field from frontmatter, or the filename stem as fallback. */
|
|
7
|
+
title: string;
|
|
8
|
+
/** `description` field from frontmatter; empty string if absent. */
|
|
9
|
+
description: string;
|
|
10
|
+
/** Top-level folder segment of the slug (`protocol`, `sdk`, `examples`, etc.). */
|
|
11
|
+
section: string;
|
|
12
|
+
/** Sibling-page slugs from the `related:` frontmatter list. */
|
|
13
|
+
related: string[];
|
|
14
|
+
/** Raw markdown/MDX body, exactly as authored. Returned by `read_doc`. */
|
|
15
|
+
bodyRaw: string;
|
|
16
|
+
/** Cleaned plain-text body used for full-text indexing. MDX imports and JSX removed. */
|
|
17
|
+
bodyText: string;
|
|
18
|
+
};
|
|
19
|
+
type Snapshot = {
|
|
20
|
+
/** Short git SHA of the docs tree at build time, or `dev` when built locally. */
|
|
21
|
+
version: string;
|
|
22
|
+
/** ISO timestamp when the snapshot was generated. */
|
|
23
|
+
generatedAt: string;
|
|
24
|
+
/** Total number of docs pages in the snapshot. */
|
|
25
|
+
count: number;
|
|
26
|
+
docs: DocEntry[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type SearchHit = {
|
|
30
|
+
slug: string;
|
|
31
|
+
title: string;
|
|
32
|
+
description: string;
|
|
33
|
+
section: string;
|
|
34
|
+
score: number;
|
|
35
|
+
snippet: string;
|
|
36
|
+
};
|
|
37
|
+
declare class DocsIndex {
|
|
38
|
+
private readonly mini;
|
|
39
|
+
private readonly bySlug;
|
|
40
|
+
constructor(docs: DocEntry[]);
|
|
41
|
+
search(query: string, limit?: number): SearchHit[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type ServerOptions = {
|
|
45
|
+
/** Snapshot produced by `scripts/build-snapshot.ts`. */
|
|
46
|
+
snapshot: Snapshot;
|
|
47
|
+
/** Optional override of the reported server name (for test isolation). */
|
|
48
|
+
name?: string;
|
|
49
|
+
/** Optional override of the reported server version. */
|
|
50
|
+
version?: string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Build an `McpServer` wired up with `list_docs`, `search_docs`, `read_doc`
|
|
54
|
+
* tools and `tesseron-docs://<slug>` resources.
|
|
55
|
+
*/
|
|
56
|
+
declare function createDocsMcpServer(options: ServerOptions): McpServer;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Locate the bundled snapshot JSON. Lookup order:
|
|
60
|
+
* 1. `TESSERON_DOCS_SNAPSHOT` environment variable, if set.
|
|
61
|
+
* 2. Explicit `override` argument.
|
|
62
|
+
* 3. Alongside the compiled bin (production / `npx` use).
|
|
63
|
+
* 4. `../dist/docs-index.json` (when running via `tsx src/cli.ts` in dev).
|
|
64
|
+
*/
|
|
65
|
+
declare function resolveSnapshotPath(override?: string): string;
|
|
66
|
+
declare function loadSnapshot(override?: string): Snapshot;
|
|
67
|
+
|
|
68
|
+
export { type DocEntry, DocsIndex, type SearchHit, type Snapshot, createDocsMcpServer, loadSnapshot, resolveSnapshotPath };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
|
|
3
|
+
type DocEntry = {
|
|
4
|
+
/** Slug relative to `docs/src/content/docs/`, no extension. E.g. `protocol/handshake`. */
|
|
5
|
+
slug: string;
|
|
6
|
+
/** `title` field from frontmatter, or the filename stem as fallback. */
|
|
7
|
+
title: string;
|
|
8
|
+
/** `description` field from frontmatter; empty string if absent. */
|
|
9
|
+
description: string;
|
|
10
|
+
/** Top-level folder segment of the slug (`protocol`, `sdk`, `examples`, etc.). */
|
|
11
|
+
section: string;
|
|
12
|
+
/** Sibling-page slugs from the `related:` frontmatter list. */
|
|
13
|
+
related: string[];
|
|
14
|
+
/** Raw markdown/MDX body, exactly as authored. Returned by `read_doc`. */
|
|
15
|
+
bodyRaw: string;
|
|
16
|
+
/** Cleaned plain-text body used for full-text indexing. MDX imports and JSX removed. */
|
|
17
|
+
bodyText: string;
|
|
18
|
+
};
|
|
19
|
+
type Snapshot = {
|
|
20
|
+
/** Short git SHA of the docs tree at build time, or `dev` when built locally. */
|
|
21
|
+
version: string;
|
|
22
|
+
/** ISO timestamp when the snapshot was generated. */
|
|
23
|
+
generatedAt: string;
|
|
24
|
+
/** Total number of docs pages in the snapshot. */
|
|
25
|
+
count: number;
|
|
26
|
+
docs: DocEntry[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type SearchHit = {
|
|
30
|
+
slug: string;
|
|
31
|
+
title: string;
|
|
32
|
+
description: string;
|
|
33
|
+
section: string;
|
|
34
|
+
score: number;
|
|
35
|
+
snippet: string;
|
|
36
|
+
};
|
|
37
|
+
declare class DocsIndex {
|
|
38
|
+
private readonly mini;
|
|
39
|
+
private readonly bySlug;
|
|
40
|
+
constructor(docs: DocEntry[]);
|
|
41
|
+
search(query: string, limit?: number): SearchHit[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type ServerOptions = {
|
|
45
|
+
/** Snapshot produced by `scripts/build-snapshot.ts`. */
|
|
46
|
+
snapshot: Snapshot;
|
|
47
|
+
/** Optional override of the reported server name (for test isolation). */
|
|
48
|
+
name?: string;
|
|
49
|
+
/** Optional override of the reported server version. */
|
|
50
|
+
version?: string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Build an `McpServer` wired up with `list_docs`, `search_docs`, `read_doc`
|
|
54
|
+
* tools and `tesseron-docs://<slug>` resources.
|
|
55
|
+
*/
|
|
56
|
+
declare function createDocsMcpServer(options: ServerOptions): McpServer;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Locate the bundled snapshot JSON. Lookup order:
|
|
60
|
+
* 1. `TESSERON_DOCS_SNAPSHOT` environment variable, if set.
|
|
61
|
+
* 2. Explicit `override` argument.
|
|
62
|
+
* 3. Alongside the compiled bin (production / `npx` use).
|
|
63
|
+
* 4. `../dist/docs-index.json` (when running via `tsx src/cli.ts` in dev).
|
|
64
|
+
*/
|
|
65
|
+
declare function resolveSnapshotPath(override?: string): string;
|
|
66
|
+
declare function loadSnapshot(override?: string): Snapshot;
|
|
67
|
+
|
|
68
|
+
export { type DocEntry, DocsIndex, type SearchHit, type Snapshot, createDocsMcpServer, loadSnapshot, resolveSnapshotPath };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import MiniSearch from 'minisearch';
|
|
2
|
+
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { existsSync, readFileSync } from 'fs';
|
|
5
|
+
import { resolve, dirname } from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
// src/search.ts
|
|
9
|
+
var SNIPPET_RADIUS = 120;
|
|
10
|
+
var SNIPPET_MAX = SNIPPET_RADIUS * 2;
|
|
11
|
+
var DocsIndex = class {
|
|
12
|
+
mini;
|
|
13
|
+
bySlug;
|
|
14
|
+
constructor(docs) {
|
|
15
|
+
this.bySlug = new Map(docs.map((d) => [d.slug, d]));
|
|
16
|
+
this.mini = new MiniSearch({
|
|
17
|
+
idField: "slug",
|
|
18
|
+
fields: ["title", "description", "bodyText"],
|
|
19
|
+
storeFields: ["slug", "title", "description", "section"],
|
|
20
|
+
searchOptions: {
|
|
21
|
+
boost: { title: 3, description: 2 },
|
|
22
|
+
fuzzy: 0.15,
|
|
23
|
+
prefix: true
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
this.mini.addAll(docs);
|
|
27
|
+
}
|
|
28
|
+
search(query, limit = 8) {
|
|
29
|
+
const raw = this.mini.search(query);
|
|
30
|
+
const out = [];
|
|
31
|
+
for (const r of raw.slice(0, Math.max(1, Math.min(limit, 20)))) {
|
|
32
|
+
const slug = String(r["slug"]);
|
|
33
|
+
const entry = this.bySlug.get(slug);
|
|
34
|
+
if (!entry) continue;
|
|
35
|
+
out.push({
|
|
36
|
+
slug,
|
|
37
|
+
title: String(r["title"]),
|
|
38
|
+
description: String(r["description"] ?? ""),
|
|
39
|
+
section: String(r["section"] ?? ""),
|
|
40
|
+
score: typeof r["score"] === "number" ? r["score"] : 0,
|
|
41
|
+
snippet: makeSnippet(entry, query)
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
function makeSnippet(entry, query) {
|
|
48
|
+
const text = entry.bodyText;
|
|
49
|
+
if (!text) return entry.description;
|
|
50
|
+
const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 1);
|
|
51
|
+
if (terms.length === 0) return truncate(text, SNIPPET_MAX);
|
|
52
|
+
const lower = text.toLowerCase();
|
|
53
|
+
let bestIdx = -1;
|
|
54
|
+
for (const t of terms) {
|
|
55
|
+
const idx = lower.indexOf(t);
|
|
56
|
+
if (idx !== -1 && (bestIdx === -1 || idx < bestIdx)) bestIdx = idx;
|
|
57
|
+
}
|
|
58
|
+
if (bestIdx === -1) return entry.description || truncate(text, SNIPPET_MAX);
|
|
59
|
+
const start = Math.max(0, bestIdx - SNIPPET_RADIUS);
|
|
60
|
+
const end = Math.min(text.length, bestIdx + SNIPPET_RADIUS);
|
|
61
|
+
let slice = text.slice(start, end).replace(/\s+/g, " ").trim();
|
|
62
|
+
if (start > 0) slice = `\u2026${slice}`;
|
|
63
|
+
if (end < text.length) slice = `${slice}\u2026`;
|
|
64
|
+
return slice;
|
|
65
|
+
}
|
|
66
|
+
function truncate(s, n) {
|
|
67
|
+
const single = s.replace(/\s+/g, " ").trim();
|
|
68
|
+
return single.length <= n ? single : `${single.slice(0, n - 1)}\u2026`;
|
|
69
|
+
}
|
|
70
|
+
var RESOURCE_SCHEME = "tesseron-docs";
|
|
71
|
+
function createDocsMcpServer(options) {
|
|
72
|
+
const { snapshot } = options;
|
|
73
|
+
const bySlug = new Map(snapshot.docs.map((d) => [d.slug, d]));
|
|
74
|
+
const index = new DocsIndex(snapshot.docs);
|
|
75
|
+
const server = new McpServer({
|
|
76
|
+
name: options.name ?? "tesseron-docs",
|
|
77
|
+
version: options.version ?? snapshot.version
|
|
78
|
+
});
|
|
79
|
+
server.registerTool(
|
|
80
|
+
"list_docs",
|
|
81
|
+
{
|
|
82
|
+
description: "List every Tesseron documentation page with title, slug, section, short description, and related slugs. Cheap, call before searching when you want the full catalogue.",
|
|
83
|
+
inputSchema: {}
|
|
84
|
+
},
|
|
85
|
+
() => {
|
|
86
|
+
const docs = snapshot.docs.map((d) => ({
|
|
87
|
+
slug: d.slug,
|
|
88
|
+
title: d.title,
|
|
89
|
+
section: d.section,
|
|
90
|
+
description: d.description,
|
|
91
|
+
related: d.related
|
|
92
|
+
}));
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: "text", text: JSON.stringify({ count: docs.length, docs }, null, 2) }]
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
server.registerTool(
|
|
99
|
+
"search_docs",
|
|
100
|
+
{
|
|
101
|
+
description: "Full-text search across Tesseron docs (BM25, title- and description-weighted). Returns ranked hits with short snippets. Call `read_doc(slug)` on promising hits for full content.",
|
|
102
|
+
inputSchema: {
|
|
103
|
+
query: z.string().min(1).describe("Free-form query. Supports multiple terms; fuzzy + prefix matching are on."),
|
|
104
|
+
limit: z.number().int().min(1).max(20).optional().describe("Maximum hits to return. Default 8, hard cap 20.")
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
({ query, limit }) => {
|
|
108
|
+
const hits = index.search(query, limit ?? 8);
|
|
109
|
+
return {
|
|
110
|
+
content: [
|
|
111
|
+
{
|
|
112
|
+
type: "text",
|
|
113
|
+
text: JSON.stringify({ query, count: hits.length, hits }, null, 2)
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
server.registerTool(
|
|
120
|
+
"read_doc",
|
|
121
|
+
{
|
|
122
|
+
description: "Return the full markdown body of a Tesseron docs page plus its structured frontmatter (title, description, section, related). Slug format: `<section>/<basename>` without extension (e.g. `protocol/handshake`).",
|
|
123
|
+
inputSchema: {
|
|
124
|
+
slug: z.string().min(1).describe("Page slug. Use `list_docs` or `search_docs` to discover valid slugs.")
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
({ slug }) => {
|
|
128
|
+
const entry = bySlug.get(slug);
|
|
129
|
+
if (!entry) {
|
|
130
|
+
return {
|
|
131
|
+
isError: true,
|
|
132
|
+
content: [
|
|
133
|
+
{
|
|
134
|
+
type: "text",
|
|
135
|
+
text: `No doc found for slug "${slug}". Call list_docs to see valid slugs.`
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const payload = {
|
|
141
|
+
slug: entry.slug,
|
|
142
|
+
title: entry.title,
|
|
143
|
+
description: entry.description,
|
|
144
|
+
section: entry.section,
|
|
145
|
+
related: entry.related,
|
|
146
|
+
body: entry.bodyRaw
|
|
147
|
+
};
|
|
148
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
server.registerResource(
|
|
152
|
+
"tesseron-doc",
|
|
153
|
+
new ResourceTemplate(`${RESOURCE_SCHEME}://{+slug}`, {
|
|
154
|
+
list: () => ({
|
|
155
|
+
resources: snapshot.docs.map((d) => ({
|
|
156
|
+
uri: `${RESOURCE_SCHEME}://${d.slug}`,
|
|
157
|
+
name: d.title,
|
|
158
|
+
description: d.description,
|
|
159
|
+
mimeType: "text/markdown"
|
|
160
|
+
}))
|
|
161
|
+
})
|
|
162
|
+
}),
|
|
163
|
+
{
|
|
164
|
+
title: "Tesseron documentation page",
|
|
165
|
+
description: "Full markdown body of a single Tesseron docs page, keyed by slug."
|
|
166
|
+
},
|
|
167
|
+
(uri, variables) => {
|
|
168
|
+
const slug = Array.isArray(variables["slug"]) ? variables["slug"][0] : variables["slug"];
|
|
169
|
+
if (!slug) {
|
|
170
|
+
throw new Error(`Missing slug in resource URI ${uri.href}`);
|
|
171
|
+
}
|
|
172
|
+
const entry = bySlug.get(slug);
|
|
173
|
+
if (!entry) {
|
|
174
|
+
throw new Error(`No doc found for slug "${slug}".`);
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
contents: [
|
|
178
|
+
{
|
|
179
|
+
uri: uri.href,
|
|
180
|
+
mimeType: "text/markdown",
|
|
181
|
+
text: entry.bodyRaw
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
return server;
|
|
188
|
+
}
|
|
189
|
+
var SNAPSHOT_FILENAME = "docs-index.json";
|
|
190
|
+
var ENV_SNAPSHOT_PATH = "TESSERON_DOCS_SNAPSHOT";
|
|
191
|
+
function thisModuleDir() {
|
|
192
|
+
if (typeof __dirname !== "undefined") return __dirname;
|
|
193
|
+
return dirname(fileURLToPath(import.meta.url));
|
|
194
|
+
}
|
|
195
|
+
function resolveSnapshotPath(override) {
|
|
196
|
+
const envPath = process.env[ENV_SNAPSHOT_PATH];
|
|
197
|
+
if (envPath) return envPath;
|
|
198
|
+
if (override) return override;
|
|
199
|
+
const here = thisModuleDir();
|
|
200
|
+
const bundled = resolve(here, SNAPSHOT_FILENAME);
|
|
201
|
+
if (existsSync(bundled)) return bundled;
|
|
202
|
+
const devFallback = resolve(here, "..", "dist", SNAPSHOT_FILENAME);
|
|
203
|
+
return devFallback;
|
|
204
|
+
}
|
|
205
|
+
function loadSnapshot(override) {
|
|
206
|
+
const path = resolveSnapshotPath(override);
|
|
207
|
+
if (!existsSync(path)) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`[tesseron-docs-mcp] snapshot not found at ${path}. Run "pnpm --filter @tesseron/docs-mcp build" to generate it, or set ${ENV_SNAPSHOT_PATH}=/abs/path/to/docs-index.json.`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
const raw = readFileSync(path, "utf8");
|
|
213
|
+
const parsed = JSON.parse(raw);
|
|
214
|
+
if (!Array.isArray(parsed.docs)) {
|
|
215
|
+
throw new Error(`[tesseron-docs-mcp] invalid snapshot at ${path}: missing "docs" array.`);
|
|
216
|
+
}
|
|
217
|
+
return parsed;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export { DocsIndex, createDocsMcpServer, loadSnapshot, resolveSnapshotPath };
|
|
221
|
+
//# sourceMappingURL=index.js.map
|
|
222
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/search.ts","../src/server.ts","../src/snapshot.ts"],"names":[],"mappings":";;;;;;;;AAYA,IAAM,cAAA,GAAiB,GAAA;AACvB,IAAM,cAAc,cAAA,GAAiB,CAAA;AAE9B,IAAM,YAAN,MAAgB;AAAA,EACJ,IAAA;AAAA,EACA,MAAA;AAAA,EAEjB,YAAY,IAAA,EAAkB;AAC5B,IAAA,IAAA,CAAK,MAAA,GAAS,IAAI,GAAA,CAAI,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA,KAAM,CAAC,CAAA,CAAE,IAAA,EAAM,CAAC,CAAC,CAAC,CAAA;AAClD,IAAA,IAAA,CAAK,IAAA,GAAO,IAAI,UAAA,CAAqB;AAAA,MACnC,OAAA,EAAS,MAAA;AAAA,MACT,MAAA,EAAQ,CAAC,OAAA,EAAS,aAAA,EAAe,UAAU,CAAA;AAAA,MAC3C,WAAA,EAAa,CAAC,MAAA,EAAQ,OAAA,EAAS,eAAe,SAAS,CAAA;AAAA,MACvD,aAAA,EAAe;AAAA,QACb,KAAA,EAAO,EAAE,KAAA,EAAO,CAAA,EAAG,aAAa,CAAA,EAAE;AAAA,QAClC,KAAA,EAAO,IAAA;AAAA,QACP,MAAA,EAAQ;AAAA;AACV,KACD,CAAA;AACD,IAAA,IAAA,CAAK,IAAA,CAAK,OAAO,IAAI,CAAA;AAAA,EACvB;AAAA,EAEA,MAAA,CAAO,KAAA,EAAe,KAAA,GAAQ,CAAA,EAAgB;AAC5C,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,IAAA,CAAK,MAAA,CAAO,KAAK,CAAA;AAClC,IAAA,MAAM,MAAmB,EAAC;AAC1B,IAAA,KAAA,MAAW,CAAA,IAAK,GAAA,CAAI,KAAA,CAAM,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,KAAA,EAAO,EAAE,CAAC,CAAC,CAAA,EAAG;AAC9D,MAAA,MAAM,IAAA,GAAO,MAAA,CAAO,CAAA,CAAE,MAAM,CAAC,CAAA;AAC7B,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,IAAI,CAAA;AAClC,MAAA,IAAI,CAAC,KAAA,EAAO;AACZ,MAAA,GAAA,CAAI,IAAA,CAAK;AAAA,QACP,IAAA;AAAA,QACA,KAAA,EAAO,MAAA,CAAO,CAAA,CAAE,OAAO,CAAC,CAAA;AAAA,QACxB,WAAA,EAAa,MAAA,CAAO,CAAA,CAAE,aAAa,KAAK,EAAE,CAAA;AAAA,QAC1C,OAAA,EAAS,MAAA,CAAO,CAAA,CAAE,SAAS,KAAK,EAAE,CAAA;AAAA,QAClC,KAAA,EAAO,OAAO,CAAA,CAAE,OAAO,MAAM,QAAA,GAAW,CAAA,CAAE,OAAO,CAAA,GAAI,CAAA;AAAA,QACrD,OAAA,EAAS,WAAA,CAAY,KAAA,EAAO,KAAK;AAAA,OAClC,CAAA;AAAA,IACH;AACA,IAAA,OAAO,GAAA;AAAA,EACT;AACF;AAMO,SAAS,WAAA,CAAY,OAAiB,KAAA,EAAuB;AAClE,EAAA,MAAM,OAAO,KAAA,CAAM,QAAA;AACnB,EAAA,IAAI,CAAC,IAAA,EAAM,OAAO,KAAA,CAAM,WAAA;AAExB,EAAA,MAAM,KAAA,GAAQ,KAAA,CACX,WAAA,EAAY,CACZ,KAAA,CAAM,KAAK,CAAA,CACX,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,MAAA,GAAS,CAAC,CAAA;AAC7B,EAAA,IAAI,MAAM,MAAA,KAAW,CAAA,EAAG,OAAO,QAAA,CAAS,MAAM,WAAW,CAAA;AAEzD,EAAA,MAAM,KAAA,GAAQ,KAAK,WAAA,EAAY;AAC/B,EAAA,IAAI,OAAA,GAAU,EAAA;AACd,EAAA,KAAA,MAAW,KAAK,KAAA,EAAO;AACrB,IAAA,MAAM,GAAA,GAAM,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA;AAC3B,IAAA,IAAI,QAAQ,EAAA,KAAO,OAAA,KAAY,EAAA,IAAM,GAAA,GAAM,UAAU,OAAA,GAAU,GAAA;AAAA,EACjE;AACA,EAAA,IAAI,YAAY,EAAA,EAAI,OAAO,MAAM,WAAA,IAAe,QAAA,CAAS,MAAM,WAAW,CAAA;AAE1E,EAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,UAAU,cAAc,CAAA;AAClD,EAAA,MAAM,MAAM,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,MAAA,EAAQ,UAAU,cAAc,CAAA;AAC1D,EAAA,IAAI,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,KAAA,EAAO,GAAG,EAAE,OAAA,CAAQ,MAAA,EAAQ,GAAG,CAAA,CAAE,IAAA,EAAK;AAC7D,EAAA,IAAI,KAAA,GAAQ,CAAA,EAAG,KAAA,GAAQ,CAAA,MAAA,EAAI,KAAK,CAAA,CAAA;AAChC,EAAA,IAAI,GAAA,GAAM,IAAA,CAAK,MAAA,EAAQ,KAAA,GAAQ,GAAG,KAAK,CAAA,MAAA,CAAA;AACvC,EAAA,OAAO,KAAA;AACT;AAEA,SAAS,QAAA,CAAS,GAAW,CAAA,EAAmB;AAC9C,EAAA,MAAM,SAAS,CAAA,CAAE,OAAA,CAAQ,MAAA,EAAQ,GAAG,EAAE,IAAA,EAAK;AAC3C,EAAA,OAAO,MAAA,CAAO,MAAA,IAAU,CAAA,GAAI,MAAA,GAAS,CAAA,EAAG,OAAO,KAAA,CAAM,CAAA,EAAG,CAAA,GAAI,CAAC,CAAC,CAAA,MAAA,CAAA;AAChE;AClFA,IAAM,eAAA,GAAkB,eAAA;AAejB,SAAS,oBAAoB,OAAA,EAAmC;AACrE,EAAA,MAAM,EAAE,UAAS,GAAI,OAAA;AACrB,EAAA,MAAM,MAAA,GAAS,IAAI,GAAA,CAAsB,QAAA,CAAS,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA,KAAM,CAAC,CAAA,CAAE,IAAA,EAAM,CAAC,CAAC,CAAC,CAAA;AAC9E,EAAA,MAAM,KAAA,GAAQ,IAAI,SAAA,CAAU,QAAA,CAAS,IAAI,CAAA;AAEzC,EAAA,MAAM,MAAA,GAAS,IAAI,SAAA,CAAU;AAAA,IAC3B,IAAA,EAAM,QAAQ,IAAA,IAAQ,eAAA;AAAA,IACtB,OAAA,EAAS,OAAA,CAAQ,OAAA,IAAW,QAAA,CAAS;AAAA,GACtC,CAAA;AAED,EAAA,MAAA,CAAO,YAAA;AAAA,IACL,WAAA;AAAA,IACA;AAAA,MACE,WAAA,EACE,wKAAA;AAAA,MACF,aAAa;AAAC,KAChB;AAAA,IACA,MAAM;AACJ,MAAA,MAAM,IAAA,GAAO,QAAA,CAAS,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,QACrC,MAAM,CAAA,CAAE,IAAA;AAAA,QACR,OAAO,CAAA,CAAE,KAAA;AAAA,QACT,SAAS,CAAA,CAAE,OAAA;AAAA,QACX,aAAa,CAAA,CAAE,WAAA;AAAA,QACf,SAAS,CAAA,CAAE;AAAA,OACb,CAAE,CAAA;AACF,MAAA,OAAO;AAAA,QACL,SAAS,CAAC,EAAE,IAAA,EAAM,MAAA,EAAQ,MAAM,IAAA,CAAK,SAAA,CAAU,EAAE,KAAA,EAAO,KAAK,MAAA,EAAQ,IAAA,IAAQ,IAAA,EAAM,CAAC,GAAG;AAAA,OACzF;AAAA,IACF;AAAA,GACF;AAEA,EAAA,MAAA,CAAO,YAAA;AAAA,IACL,aAAA;AAAA,IACA;AAAA,MACE,WAAA,EACE,mLAAA;AAAA,MACF,WAAA,EAAa;AAAA,QACX,KAAA,EAAO,EACJ,MAAA,EAAO,CACP,IAAI,CAAC,CAAA,CACL,SAAS,2EAA2E,CAAA;AAAA,QACvF,KAAA,EAAO,CAAA,CACJ,MAAA,EAAO,CACP,KAAI,CACJ,GAAA,CAAI,CAAC,CAAA,CACL,IAAI,EAAE,CAAA,CACN,QAAA,EAAS,CACT,SAAS,iDAAiD;AAAA;AAC/D,KACF;AAAA,IACA,CAAC,EAAE,KAAA,EAAO,KAAA,EAAM,KAAM;AACpB,MAAA,MAAM,IAAA,GAAO,KAAA,CAAM,MAAA,CAAO,KAAA,EAAO,SAAS,CAAC,CAAA;AAC3C,MAAA,OAAO;AAAA,QACL,OAAA,EAAS;AAAA,UACP;AAAA,YACE,IAAA,EAAM,MAAA;AAAA,YACN,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,EAAE,KAAA,EAAO,KAAA,EAAO,IAAA,CAAK,MAAA,EAAQ,IAAA,EAAK,EAAG,IAAA,EAAM,CAAC;AAAA;AACnE;AACF,OACF;AAAA,IACF;AAAA,GACF;AAEA,EAAA,MAAA,CAAO,YAAA;AAAA,IACL,UAAA;AAAA,IACA;AAAA,MACE,WAAA,EACE,kNAAA;AAAA,MACF,WAAA,EAAa;AAAA,QACX,IAAA,EAAM,EACH,MAAA,EAAO,CACP,IAAI,CAAC,CAAA,CACL,SAAS,sEAAsE;AAAA;AACpF,KACF;AAAA,IACA,CAAC,EAAE,IAAA,EAAK,KAAM;AACZ,MAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,GAAA,CAAI,IAAI,CAAA;AAC7B,MAAA,IAAI,CAAC,KAAA,EAAO;AACV,QAAA,OAAO;AAAA,UACL,OAAA,EAAS,IAAA;AAAA,UACT,OAAA,EAAS;AAAA,YACP;AAAA,cACE,IAAA,EAAM,MAAA;AAAA,cACN,IAAA,EAAM,0BAA0B,IAAI,CAAA,qCAAA;AAAA;AACtC;AACF,SACF;AAAA,MACF;AACA,MAAA,MAAM,OAAA,GAAU;AAAA,QACd,MAAM,KAAA,CAAM,IAAA;AAAA,QACZ,OAAO,KAAA,CAAM,KAAA;AAAA,QACb,aAAa,KAAA,CAAM,WAAA;AAAA,QACnB,SAAS,KAAA,CAAM,OAAA;AAAA,QACf,SAAS,KAAA,CAAM,OAAA;AAAA,QACf,MAAM,KAAA,CAAM;AAAA,OACd;AACA,MAAA,OAAO,EAAE,OAAA,EAAS,CAAC,EAAE,MAAM,MAAA,EAAQ,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,OAAA,EAAS,IAAA,EAAM,CAAC,CAAA,EAAG,CAAA,EAAE;AAAA,IAC/E;AAAA,GACF;AAEA,EAAA,MAAA,CAAO,gBAAA;AAAA,IACL,cAAA;AAAA,IACA,IAAI,gBAAA,CAAiB,CAAA,EAAG,eAAe,CAAA,UAAA,CAAA,EAAc;AAAA,MACnD,MAAM,OAAO;AAAA,QACX,SAAA,EAAW,QAAA,CAAS,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,UACnC,GAAA,EAAK,CAAA,EAAG,eAAe,CAAA,GAAA,EAAM,EAAE,IAAI,CAAA,CAAA;AAAA,UACnC,MAAM,CAAA,CAAE,KAAA;AAAA,UACR,aAAa,CAAA,CAAE,WAAA;AAAA,UACf,QAAA,EAAU;AAAA,SACZ,CAAE;AAAA,OACJ;AAAA,KACD,CAAA;AAAA,IACD;AAAA,MACE,KAAA,EAAO,6BAAA;AAAA,MACP,WAAA,EAAa;AAAA,KACf;AAAA,IACA,CAAC,KAAK,SAAA,KAAc;AAClB,MAAA,MAAM,IAAA,GAAO,KAAA,CAAM,OAAA,CAAQ,SAAA,CAAU,MAAM,CAAC,CAAA,GACxC,SAAA,CAAU,MAAM,CAAA,CAAE,CAAC,CAAA,GAClB,UAAU,MAAM,CAAA;AACrB,MAAA,IAAI,CAAC,IAAA,EAAM;AACT,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,6BAAA,EAAgC,GAAA,CAAI,IAAI,CAAA,CAAE,CAAA;AAAA,MAC5D;AACA,MAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,GAAA,CAAI,IAAI,CAAA;AAC7B,MAAA,IAAI,CAAC,KAAA,EAAO;AACV,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,uBAAA,EAA0B,IAAI,CAAA,EAAA,CAAI,CAAA;AAAA,MACpD;AACA,MAAA,OAAO;AAAA,QACL,QAAA,EAAU;AAAA,UACR;AAAA,YACE,KAAK,GAAA,CAAI,IAAA;AAAA,YACT,QAAA,EAAU,eAAA;AAAA,YACV,MAAM,KAAA,CAAM;AAAA;AACd;AACF,OACF;AAAA,IACF;AAAA,GACF;AAEA,EAAA,OAAO,MAAA;AACT;AC3JA,IAAM,iBAAA,GAAoB,iBAAA;AAC1B,IAAM,iBAAA,GAAoB,wBAAA;AAE1B,SAAS,aAAA,GAAwB;AAC/B,EAAA,IAAI,OAAO,SAAA,KAAc,WAAA,EAAa,OAAO,SAAA;AAC7C,EAAA,OAAO,OAAA,CAAQ,aAAA,CAAc,MAAA,CAAA,IAAA,CAAY,GAAG,CAAC,CAAA;AAC/C;AASO,SAAS,oBAAoB,QAAA,EAA2B;AAC7D,EAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,GAAA,CAAI,iBAAiB,CAAA;AAC7C,EAAA,IAAI,SAAS,OAAO,OAAA;AACpB,EAAA,IAAI,UAAU,OAAO,QAAA;AAErB,EAAA,MAAM,OAAO,aAAA,EAAc;AAC3B,EAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,IAAA,EAAM,iBAAiB,CAAA;AAC/C,EAAA,IAAI,UAAA,CAAW,OAAO,CAAA,EAAG,OAAO,OAAA;AAEhC,EAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,IAAA,EAAM,IAAA,EAAM,QAAQ,iBAAiB,CAAA;AACjE,EAAA,OAAO,WAAA;AACT;AAEO,SAAS,aAAa,QAAA,EAA6B;AACxD,EAAA,MAAM,IAAA,GAAO,oBAAoB,QAAQ,CAAA;AACzC,EAAA,IAAI,CAAC,UAAA,CAAW,IAAI,CAAA,EAAG;AACrB,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,0CAAA,EAA6C,IAAI,CAAA,sEAAA,EAErC,iBAAiB,CAAA,8BAAA;AAAA,KAC/B;AAAA,EACF;AACA,EAAA,MAAM,GAAA,GAAM,YAAA,CAAa,IAAA,EAAM,MAAM,CAAA;AACrC,EAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAC7B,EAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,MAAA,CAAO,IAAI,CAAA,EAAG;AAC/B,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,wCAAA,EAA2C,IAAI,CAAA,uBAAA,CAAyB,CAAA;AAAA,EAC1F;AACA,EAAA,OAAO,MAAA;AACT","file":"index.js","sourcesContent":["import MiniSearch from 'minisearch';\nimport type { DocEntry } from './content/types.js';\n\nexport type SearchHit = {\n slug: string;\n title: string;\n description: string;\n section: string;\n score: number;\n snippet: string;\n};\n\nconst SNIPPET_RADIUS = 120;\nconst SNIPPET_MAX = SNIPPET_RADIUS * 2;\n\nexport class DocsIndex {\n private readonly mini: MiniSearch<DocEntry>;\n private readonly bySlug: Map<string, DocEntry>;\n\n constructor(docs: DocEntry[]) {\n this.bySlug = new Map(docs.map((d) => [d.slug, d]));\n this.mini = new MiniSearch<DocEntry>({\n idField: 'slug',\n fields: ['title', 'description', 'bodyText'],\n storeFields: ['slug', 'title', 'description', 'section'],\n searchOptions: {\n boost: { title: 3, description: 2 },\n fuzzy: 0.15,\n prefix: true,\n },\n });\n this.mini.addAll(docs);\n }\n\n search(query: string, limit = 8): SearchHit[] {\n const raw = this.mini.search(query);\n const out: SearchHit[] = [];\n for (const r of raw.slice(0, Math.max(1, Math.min(limit, 20)))) {\n const slug = String(r['slug']);\n const entry = this.bySlug.get(slug);\n if (!entry) continue;\n out.push({\n slug,\n title: String(r['title']),\n description: String(r['description'] ?? ''),\n section: String(r['section'] ?? ''),\n score: typeof r['score'] === 'number' ? r['score'] : 0,\n snippet: makeSnippet(entry, query),\n });\n }\n return out;\n }\n}\n\n/**\n * Extract a ~240-char snippet centred on the best match for `query` within\n * `entry.bodyText`. Falls back to `description` when no body match is found.\n */\nexport function makeSnippet(entry: DocEntry, query: string): string {\n const text = entry.bodyText;\n if (!text) return entry.description;\n\n const terms = query\n .toLowerCase()\n .split(/\\s+/)\n .filter((t) => t.length > 1);\n if (terms.length === 0) return truncate(text, SNIPPET_MAX);\n\n const lower = text.toLowerCase();\n let bestIdx = -1;\n for (const t of terms) {\n const idx = lower.indexOf(t);\n if (idx !== -1 && (bestIdx === -1 || idx < bestIdx)) bestIdx = idx;\n }\n if (bestIdx === -1) return entry.description || truncate(text, SNIPPET_MAX);\n\n const start = Math.max(0, bestIdx - SNIPPET_RADIUS);\n const end = Math.min(text.length, bestIdx + SNIPPET_RADIUS);\n let slice = text.slice(start, end).replace(/\\s+/g, ' ').trim();\n if (start > 0) slice = `…${slice}`;\n if (end < text.length) slice = `${slice}…`;\n return slice;\n}\n\nfunction truncate(s: string, n: number): string {\n const single = s.replace(/\\s+/g, ' ').trim();\n return single.length <= n ? single : `${single.slice(0, n - 1)}…`;\n}\n","import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\nimport type { DocEntry, Snapshot } from './content/types.js';\nimport { DocsIndex } from './search.js';\n\nconst RESOURCE_SCHEME = 'tesseron-docs';\n\nexport type ServerOptions = {\n /** Snapshot produced by `scripts/build-snapshot.ts`. */\n snapshot: Snapshot;\n /** Optional override of the reported server name (for test isolation). */\n name?: string;\n /** Optional override of the reported server version. */\n version?: string;\n};\n\n/**\n * Build an `McpServer` wired up with `list_docs`, `search_docs`, `read_doc`\n * tools and `tesseron-docs://<slug>` resources.\n */\nexport function createDocsMcpServer(options: ServerOptions): McpServer {\n const { snapshot } = options;\n const bySlug = new Map<string, DocEntry>(snapshot.docs.map((d) => [d.slug, d]));\n const index = new DocsIndex(snapshot.docs);\n\n const server = new McpServer({\n name: options.name ?? 'tesseron-docs',\n version: options.version ?? snapshot.version,\n });\n\n server.registerTool(\n 'list_docs',\n {\n description:\n 'List every Tesseron documentation page with title, slug, section, short description, and related slugs. Cheap, call before searching when you want the full catalogue.',\n inputSchema: {},\n },\n () => {\n const docs = snapshot.docs.map((d) => ({\n slug: d.slug,\n title: d.title,\n section: d.section,\n description: d.description,\n related: d.related,\n }));\n return {\n content: [{ type: 'text', text: JSON.stringify({ count: docs.length, docs }, null, 2) }],\n };\n },\n );\n\n server.registerTool(\n 'search_docs',\n {\n description:\n 'Full-text search across Tesseron docs (BM25, title- and description-weighted). Returns ranked hits with short snippets. Call `read_doc(slug)` on promising hits for full content.',\n inputSchema: {\n query: z\n .string()\n .min(1)\n .describe('Free-form query. Supports multiple terms; fuzzy + prefix matching are on.'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(20)\n .optional()\n .describe('Maximum hits to return. Default 8, hard cap 20.'),\n },\n },\n ({ query, limit }) => {\n const hits = index.search(query, limit ?? 8);\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify({ query, count: hits.length, hits }, null, 2),\n },\n ],\n };\n },\n );\n\n server.registerTool(\n 'read_doc',\n {\n description:\n 'Return the full markdown body of a Tesseron docs page plus its structured frontmatter (title, description, section, related). Slug format: `<section>/<basename>` without extension (e.g. `protocol/handshake`).',\n inputSchema: {\n slug: z\n .string()\n .min(1)\n .describe('Page slug. Use `list_docs` or `search_docs` to discover valid slugs.'),\n },\n },\n ({ slug }) => {\n const entry = bySlug.get(slug);\n if (!entry) {\n return {\n isError: true,\n content: [\n {\n type: 'text',\n text: `No doc found for slug \"${slug}\". Call list_docs to see valid slugs.`,\n },\n ],\n };\n }\n const payload = {\n slug: entry.slug,\n title: entry.title,\n description: entry.description,\n section: entry.section,\n related: entry.related,\n body: entry.bodyRaw,\n };\n return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };\n },\n );\n\n server.registerResource(\n 'tesseron-doc',\n new ResourceTemplate(`${RESOURCE_SCHEME}://{+slug}`, {\n list: () => ({\n resources: snapshot.docs.map((d) => ({\n uri: `${RESOURCE_SCHEME}://${d.slug}`,\n name: d.title,\n description: d.description,\n mimeType: 'text/markdown',\n })),\n }),\n }),\n {\n title: 'Tesseron documentation page',\n description: 'Full markdown body of a single Tesseron docs page, keyed by slug.',\n },\n (uri, variables) => {\n const slug = Array.isArray(variables['slug'])\n ? variables['slug'][0]\n : (variables['slug'] as string | undefined);\n if (!slug) {\n throw new Error(`Missing slug in resource URI ${uri.href}`);\n }\n const entry = bySlug.get(slug);\n if (!entry) {\n throw new Error(`No doc found for slug \"${slug}\".`);\n }\n return {\n contents: [\n {\n uri: uri.href,\n mimeType: 'text/markdown',\n text: entry.bodyRaw,\n },\n ],\n };\n },\n );\n\n return server;\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { DocEntry, Snapshot } from './content/types.js';\n\nconst SNAPSHOT_FILENAME = 'docs-index.json';\nconst ENV_SNAPSHOT_PATH = 'TESSERON_DOCS_SNAPSHOT';\n\nfunction thisModuleDir(): string {\n if (typeof __dirname !== 'undefined') return __dirname;\n return dirname(fileURLToPath(import.meta.url));\n}\n\n/**\n * Locate the bundled snapshot JSON. Lookup order:\n * 1. `TESSERON_DOCS_SNAPSHOT` environment variable, if set.\n * 2. Explicit `override` argument.\n * 3. Alongside the compiled bin (production / `npx` use).\n * 4. `../dist/docs-index.json` (when running via `tsx src/cli.ts` in dev).\n */\nexport function resolveSnapshotPath(override?: string): string {\n const envPath = process.env[ENV_SNAPSHOT_PATH];\n if (envPath) return envPath;\n if (override) return override;\n\n const here = thisModuleDir();\n const bundled = resolve(here, SNAPSHOT_FILENAME);\n if (existsSync(bundled)) return bundled;\n\n const devFallback = resolve(here, '..', 'dist', SNAPSHOT_FILENAME);\n return devFallback;\n}\n\nexport function loadSnapshot(override?: string): Snapshot {\n const path = resolveSnapshotPath(override);\n if (!existsSync(path)) {\n throw new Error(\n `[tesseron-docs-mcp] snapshot not found at ${path}. ` +\n 'Run \"pnpm --filter @tesseron/docs-mcp build\" to generate it, ' +\n `or set ${ENV_SNAPSHOT_PATH}=/abs/path/to/docs-index.json.`,\n );\n }\n const raw = readFileSync(path, 'utf8');\n const parsed = JSON.parse(raw) as Snapshot;\n if (!Array.isArray(parsed.docs)) {\n throw new Error(`[tesseron-docs-mcp] invalid snapshot at ${path}: missing \"docs\" array.`);\n }\n return parsed;\n}\n\n/** Index docs by slug for O(1) lookup in `read_doc`. */\nexport function indexBySlug(docs: DocEntry[]): Map<string, DocEntry> {\n const map = new Map<string, DocEntry>();\n for (const d of docs) map.set(d.slug, d);\n return map;\n}\n"]}
|