@reburn-ui/mcp-server 0.0.0-stage → 9.8.1

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,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Moritz Weber
4
+ Copyright (c) 2021 Vitaly Rtishchev
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,51 @@
1
- # Temporary Holding Version
1
+ # @reburn-ui/mcp-server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ MCP server for the reburn-ui documentation, a port of `@mantine/mcp-server`. It reads the static
4
+ data the docs build writes to `/mcp` (`src/docs/llms/compile-mcp-data.ts`) and serves it over
5
+ stdio.
6
+
7
+ This folder is its own package, not part of the published `reburn-ui` package: it is a Node CLI
8
+ with its own dependencies (`@modelcontextprotocol/server`, `zod`) that `reburn-ui` users don't
9
+ need, and `svelte-package` only builds `src/`. The docs build imports the data types from
10
+ `src/types.ts`, so the format stays in one place.
11
+
12
+ ## Usage
13
+
14
+ ```bash
15
+ npx @reburn-ui/mcp-server
16
+ ```
17
+
18
+ From this repository:
19
+
20
+ ```bash
21
+ bun install # in mcp-server/
22
+ bun src/index.ts
23
+ ```
24
+
25
+ `bun run build` bundles `dist/index.js` for the bin (`bin/reburn-ui-mcp-server.js`, Node 22+),
26
+ `bun run check` type-checks the sources.
27
+
28
+ ## Configuration
29
+
30
+ - `REBURN_MCP_DATA_URL` – static MCP data base URL (`https://mantine-reburn-ui.vercel.app/mcp` by
31
+ default), e.g. `http://localhost:4174/mcp` for a local preview of the docs
32
+ - `REBURN_MCP_TIMEOUT_MS` – HTTP timeout in milliseconds (`10000` by default)
33
+
34
+ ## Tools
35
+
36
+ - `search_docs` – search all documentation; returns ranked matches, often pointing at a specific section
37
+ - `get_item_doc` – outline by default, one section with `section`, whole page with `full: true`
38
+ - `get_item_props` – component props, or a hook's TypeScript signature
39
+ - `list_items` – filter by `kind`, `group`, `category`, `package` or `query`
40
+ - `get_api` – resolve a symbol (`useDisclosure`, `ButtonBuilder`) to its signature or section
41
+
42
+ ## Testing
43
+
44
+ From the repository root, after `bun run build`:
45
+
46
+ ```bash
47
+ bun run mcp:smoke # TypeScript source with bun
48
+ bun run mcp:smoke --built # bundled bin with node (run `bun run build` in mcp-server first)
49
+ ```
50
+
51
+ It uses the preview on port 4174, or starts and stops one, and calls all five tools.
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ // Port of `@mantine/mcp-server` `bin/mcp-server.cjs` (ESM here, the package is `"type": "module"`)
3
+ import { readFileSync } from 'node:fs';
4
+
5
+ const args = process.argv.slice(2);
6
+
7
+ if (args.includes('--help') || args.includes('-h')) {
8
+ console.log(`Usage: reburn-ui-mcp-server
9
+
10
+ Starts the reburn-ui MCP server.
11
+
12
+ Environment:
13
+ REBURN_MCP_DATA_URL /mcp URL of the reburn-ui docs
14
+ (https://mantine-reburn-ui.vercel.app/mcp by default)
15
+ REBURN_MCP_TIMEOUT_MS HTTP timeout in milliseconds (10000 by default)
16
+
17
+ Options:
18
+ -h, --help Show help
19
+ -v, --version Show version`);
20
+ process.exit(0);
21
+ }
22
+
23
+ if (args.includes('--version') || args.includes('-v')) {
24
+ const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
25
+ console.log(packageJson.version);
26
+ process.exit(0);
27
+ }
28
+
29
+ const unknownOption = args.find((arg) => arg.startsWith('-'));
30
+
31
+ if (unknownOption) {
32
+ console.error(`Unknown option: ${unknownOption}`);
33
+ console.error('Run reburn-ui-mcp-server --help for usage.');
34
+ process.exit(1);
35
+ }
36
+
37
+ await import('../dist/index.js');
package/dist/index.js ADDED
@@ -0,0 +1,569 @@
1
+ // src/server.ts
2
+ import { McpServer } from "@modelcontextprotocol/server";
3
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
4
+ // package.json
5
+ var package_default = {
6
+ name: "@reburn-ui/mcp-server",
7
+ version: "9.8.1",
8
+ description: "MCP server for reburn-ui documentation",
9
+ keywords: [
10
+ "ai",
11
+ "docs",
12
+ "mantine",
13
+ "mcp",
14
+ "model-context-protocol",
15
+ "reburn-ui",
16
+ "svelte"
17
+ ],
18
+ homepage: "https://github.com/Mo310/mantine-reburn-ui/tree/main/mcp-server#readme",
19
+ license: "MIT",
20
+ repository: {
21
+ type: "git",
22
+ url: "git+https://github.com/Mo310/mantine-reburn-ui.git",
23
+ directory: "mcp-server"
24
+ },
25
+ bin: {
26
+ "reburn-ui-mcp-server": "bin/reburn-ui-mcp-server.js"
27
+ },
28
+ files: [
29
+ "bin",
30
+ "dist"
31
+ ],
32
+ type: "module",
33
+ main: "./dist/index.js",
34
+ publishConfig: {
35
+ access: "public"
36
+ },
37
+ scripts: {
38
+ build: "bun build src/index.ts --target node --packages external --outfile dist/index.js",
39
+ check: "tsc --noEmit",
40
+ prepack: "bun run build",
41
+ start: "bun src/index.ts"
42
+ },
43
+ dependencies: {
44
+ "@modelcontextprotocol/server": "2.1.0",
45
+ zod: "^4.6.5"
46
+ },
47
+ devDependencies: {
48
+ "@types/node": "^22.0.0",
49
+ typescript: "^6.0.3"
50
+ },
51
+ engines: {
52
+ node: ">=22"
53
+ }
54
+ };
55
+
56
+ // src/search.ts
57
+ var STOPWORDS = new Set([
58
+ "a",
59
+ "an",
60
+ "the",
61
+ "and",
62
+ "or",
63
+ "of",
64
+ "to",
65
+ "in",
66
+ "on",
67
+ "for",
68
+ "with",
69
+ "how",
70
+ "do",
71
+ "i",
72
+ "my",
73
+ "is",
74
+ "it",
75
+ "can",
76
+ "use",
77
+ "using",
78
+ "when",
79
+ "what",
80
+ "that",
81
+ "this",
82
+ "be",
83
+ "as",
84
+ "at",
85
+ "by",
86
+ "from",
87
+ "are",
88
+ "get"
89
+ ]);
90
+ function normalize(value) {
91
+ return value.trim().toLowerCase();
92
+ }
93
+ function tokenize(value) {
94
+ return normalize(value).split(/[^a-z0-9.-]+/).filter(Boolean);
95
+ }
96
+ function queryTokens(query) {
97
+ const raw = tokenize(query);
98
+ const filtered = raw.filter((token) => token.length > 2 && !STOPWORDS.has(token));
99
+ return filtered.length > 0 ? filtered : raw;
100
+ }
101
+ function createSearchScorer(index, sections) {
102
+ const sectionDocs = sections.map((section) => normalize(`${section.heading} ${section.snippet}`));
103
+ const itemDocs = index.map((item) => normalize(item.searchText));
104
+ const corpus = [...sectionDocs, ...itemDocs];
105
+ const corpusSize = corpus.length;
106
+ const documentFrequencyCache = new Map;
107
+ function documentFrequency(token) {
108
+ const cached = documentFrequencyCache.get(token);
109
+ if (cached !== undefined) {
110
+ return cached;
111
+ }
112
+ let count = 0;
113
+ for (const doc of corpus) {
114
+ if (doc.includes(token)) {
115
+ count += 1;
116
+ }
117
+ }
118
+ documentFrequencyCache.set(token, count);
119
+ return count;
120
+ }
121
+ function idf(token) {
122
+ return Math.log((corpusSize + 1) / (documentFrequency(token) + 1));
123
+ }
124
+ function coverage(haystack, tokens) {
125
+ if (tokens.length === 0) {
126
+ return 0;
127
+ }
128
+ let matchedWeight = 0;
129
+ let totalWeight = 0;
130
+ for (const token of tokens) {
131
+ const weight = idf(token);
132
+ totalWeight += weight;
133
+ if (haystack.includes(token)) {
134
+ matchedWeight += weight;
135
+ }
136
+ }
137
+ return totalWeight > 0 ? matchedWeight / totalWeight : 0;
138
+ }
139
+ return {
140
+ scoreItem(item, query) {
141
+ const q = normalize(query);
142
+ if (!q) {
143
+ return 0;
144
+ }
145
+ const tokens = queryTokens(query);
146
+ const name = normalize(item.name);
147
+ const haystack = normalize(item.searchText);
148
+ const coverageScore = coverage(haystack, tokens);
149
+ let bonus = 0;
150
+ if (name === q) {
151
+ bonus = 60;
152
+ } else if (name.startsWith(q)) {
153
+ bonus = 40;
154
+ } else if (name.includes(q)) {
155
+ bonus = 25;
156
+ }
157
+ if (tokens.some((token) => name.includes(token))) {
158
+ bonus += 15;
159
+ }
160
+ return coverageScore * 55 + bonus;
161
+ },
162
+ scoreSection(section, query) {
163
+ const q = normalize(query);
164
+ if (!q) {
165
+ return 0;
166
+ }
167
+ const tokens = queryTokens(query);
168
+ const heading = normalize(section.heading);
169
+ const haystack = normalize(`${section.heading} ${section.snippet}`);
170
+ const coverageScore = coverage(haystack, tokens);
171
+ let bonus = 0;
172
+ if (heading === q) {
173
+ bonus += 40;
174
+ } else if (heading.includes(q)) {
175
+ bonus += 25;
176
+ }
177
+ if (tokens.some((token) => heading.includes(token))) {
178
+ bonus += 15;
179
+ }
180
+ return coverageScore * 60 + bonus;
181
+ }
182
+ };
183
+ }
184
+ function editDistance(a, b) {
185
+ const rows = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
186
+ for (let j = 0;j <= b.length; j += 1) {
187
+ rows[0][j] = j;
188
+ }
189
+ for (let i = 1;i <= a.length; i += 1) {
190
+ for (let j = 1;j <= b.length; j += 1) {
191
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
192
+ rows[i][j] = Math.min(rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + cost);
193
+ }
194
+ }
195
+ return rows[a.length][b.length];
196
+ }
197
+ function suggestNames(index, query, limit = 3) {
198
+ const q = normalize(query);
199
+ const threshold = Math.max(2, Math.floor(q.length / 3));
200
+ return index.map((item) => ({ name: item.name, distance: editDistance(q, normalize(item.name)) })).filter((entry) => entry.distance <= threshold).sort((a, b) => a.distance - b.distance).slice(0, limit).map((entry) => entry.name);
201
+ }
202
+
203
+ // src/data-client.ts
204
+ function normalize2(value) {
205
+ return value.trim().toLowerCase();
206
+ }
207
+ function compact(value) {
208
+ return value.toLowerCase().replace(/[^a-z0-9]/g, "");
209
+ }
210
+ function hasSearchTag(item, symbol) {
211
+ const requested = compact(symbol);
212
+ return !!requested && (item.searchTags || "").split(",").some((tag) => compact(tag) === requested);
213
+ }
214
+ function findSymbolSection(sections, symbol) {
215
+ const requested = compact(symbol);
216
+ return sections.find((s) => compact(s.heading).startsWith(requested)) || sections.find((s) => compact(s.heading).includes(requested));
217
+ }
218
+ function clampLimit(limit, fallback, max) {
219
+ if (typeof limit !== "number" || Number.isNaN(limit)) {
220
+ return fallback;
221
+ }
222
+ return Math.max(1, Math.min(max, Math.floor(limit)));
223
+ }
224
+ var INDEX_REVALIDATE_INTERVAL_MS = 5 * 60 * 1000;
225
+
226
+ class ReburnMcpDataClient {
227
+ baseUrl;
228
+ timeoutMs;
229
+ indexCache = null;
230
+ indexEtag = null;
231
+ indexCheckedAt = 0;
232
+ sectionsCache = null;
233
+ docCache = new Map;
234
+ scorer = null;
235
+ constructor(baseUrl, timeoutMs = 1e4) {
236
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
237
+ this.timeoutMs = timeoutMs;
238
+ }
239
+ async fetchJson(relativePath, etag) {
240
+ const controller = new AbortController;
241
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
242
+ try {
243
+ const url = `${this.baseUrl}/${relativePath.replace(/^\/+/, "")}`;
244
+ const headers = etag ? { "If-None-Match": etag } : undefined;
245
+ const response = await fetch(url, { signal: controller.signal, headers });
246
+ if (response.status === 304) {
247
+ return { data: null, etag };
248
+ }
249
+ if (!response.ok) {
250
+ throw new Error(`Failed to fetch ${url}: ${response.status}`);
251
+ }
252
+ return { data: await response.json(), etag: response.headers.get("etag") };
253
+ } finally {
254
+ clearTimeout(timer);
255
+ }
256
+ }
257
+ async revalidateIndex() {
258
+ try {
259
+ const { data, etag } = await this.fetchJson("index.json", this.indexEtag);
260
+ if (data) {
261
+ this.indexCache = data;
262
+ this.indexEtag = etag ?? null;
263
+ this.sectionsCache = null;
264
+ this.docCache.clear();
265
+ this.scorer = null;
266
+ }
267
+ } catch (error) {
268
+ console.error("Failed to revalidate reburn-ui MCP index, serving cached data", error);
269
+ }
270
+ }
271
+ async getIndex() {
272
+ if (!this.indexCache) {
273
+ const { data, etag } = await this.fetchJson("index.json");
274
+ this.indexCache = data ?? [];
275
+ this.indexEtag = etag ?? null;
276
+ this.indexCheckedAt = Date.now();
277
+ return this.indexCache;
278
+ }
279
+ const now = Date.now();
280
+ if (now - this.indexCheckedAt >= INDEX_REVALIDATE_INTERVAL_MS) {
281
+ this.indexCheckedAt = now;
282
+ await this.revalidateIndex();
283
+ }
284
+ return this.indexCache;
285
+ }
286
+ async getSections() {
287
+ if (this.sectionsCache) {
288
+ return this.sectionsCache;
289
+ }
290
+ const { data } = await this.fetchJson("sections.json");
291
+ this.sectionsCache = data ?? [];
292
+ return this.sectionsCache;
293
+ }
294
+ async getScorer() {
295
+ if (this.scorer) {
296
+ return this.scorer;
297
+ }
298
+ const index = await this.getIndex();
299
+ const sections = await this.getSections();
300
+ this.scorer = createSearchScorer(index, sections);
301
+ return this.scorer;
302
+ }
303
+ async getDocData(id) {
304
+ const cached = this.docCache.get(id);
305
+ if (cached) {
306
+ return cached;
307
+ }
308
+ const { data } = await this.fetchJson(`docs/${id}.json`);
309
+ if (!data) {
310
+ throw new Error(`Missing document data for ${id}`);
311
+ }
312
+ this.docCache.set(id, data);
313
+ return data;
314
+ }
315
+ async listItems(args = {}) {
316
+ const limit = clampLimit(args.limit, 30, 500);
317
+ const query = args.query ? normalize2(args.query) : null;
318
+ const index = await this.getIndex();
319
+ return index.filter((item) => args.kind ? item.kind === args.kind : true).filter((item) => args.group ? item.group === args.group : true).filter((item) => args.category ? normalize2(item.category) === normalize2(args.category) : true).filter((item) => args.package ? normalize2(item.package || "") === normalize2(args.package) : true).filter((item) => query ? item.searchText.includes(query) : true).slice(0, limit);
320
+ }
321
+ async findItem(args) {
322
+ const requested = normalize2(args.name);
323
+ const index = await this.getIndex();
324
+ const candidates = args.kind ? index.filter((item) => item.kind === args.kind) : index;
325
+ return candidates.find((item) => normalize2(item.id) === requested) || candidates.find((item) => normalize2(item.name) === requested) || candidates.find((item) => normalize2(item.name).startsWith(requested)) || candidates.find((item) => item.searchText.includes(requested)) || null;
326
+ }
327
+ async suggestFor(name) {
328
+ return suggestNames(await this.getIndex(), name);
329
+ }
330
+ async search(args) {
331
+ const limit = clampLimit(args.limit, 20, 100);
332
+ const query = args.query.trim();
333
+ if (!query) {
334
+ return [];
335
+ }
336
+ const index = await this.getIndex();
337
+ const sections = await this.getSections();
338
+ const scorer = await this.getScorer();
339
+ const allowed = index.filter((item) => args.kind ? item.kind === args.kind : true).filter((item) => args.package ? normalize2(item.package || "") === normalize2(args.package) : true);
340
+ const allowedIds = new Map(allowed.map((item) => [item.id, item]));
341
+ const sectionHits = sections.filter((section) => allowedIds.has(section.id)).map((section) => ({ section, score: scorer.scoreSection(section, query) })).filter((entry) => entry.score > 0);
342
+ const pageHits = allowed.map((item) => ({ item, score: scorer.scoreItem(item, query) })).filter((entry) => entry.score > 0);
343
+ const merged = new Map;
344
+ pageHits.forEach((hit) => {
345
+ merged.set(hit.item.id, { item: hit.item, score: hit.score });
346
+ });
347
+ sectionHits.forEach((hit) => {
348
+ const item = allowedIds.get(hit.section.id);
349
+ const key = `${hit.section.id}#${hit.section.slug}`;
350
+ merged.set(key, { item, section: hit.section, score: hit.score });
351
+ });
352
+ return Array.from(merged.values()).sort((a, b) => b.score - a.score).slice(0, limit).map((entry) => ({
353
+ id: entry.item.id,
354
+ name: entry.item.name,
355
+ kind: entry.item.kind,
356
+ package: entry.item.package,
357
+ route: entry.item.route,
358
+ docsUrl: entry.item.docsUrl,
359
+ description: entry.item.description,
360
+ section: entry.section?.slug,
361
+ heading: entry.section?.heading,
362
+ snippet: entry.section?.snippet,
363
+ score: Number(entry.score.toFixed(2))
364
+ }));
365
+ }
366
+ async getDoc(args) {
367
+ const item = await this.findItem(args);
368
+ if (!item) {
369
+ return null;
370
+ }
371
+ const data = await this.getDocData(item.id);
372
+ if (args.full) {
373
+ const body = [data.intro, ...data.sections.map((s) => `## ${s.heading}
374
+
375
+ ${s.body}`)].join(`
376
+
377
+ `);
378
+ return { item, mode: "full", markdown: body };
379
+ }
380
+ if (args.section) {
381
+ const requested = normalize2(args.section);
382
+ const section = data.sections.find((s) => s.slug === requested) || data.sections.find((s) => normalize2(s.heading) === requested);
383
+ if (!section) {
384
+ return {
385
+ item,
386
+ mode: "section-not-found",
387
+ availableSections: data.sections.map((s) => s.slug)
388
+ };
389
+ }
390
+ return { item, mode: "section", heading: section.heading, markdown: section.body };
391
+ }
392
+ return {
393
+ item,
394
+ mode: "outline",
395
+ intro: data.intro,
396
+ sections: data.sections.map((s) => ({ slug: s.slug, heading: s.heading })),
397
+ source: data.source,
398
+ docs: data.docs
399
+ };
400
+ }
401
+ findApiSection(sections) {
402
+ return sections.find((s) => /api|return type|overview/i.test(s.heading));
403
+ }
404
+ async getProps(args) {
405
+ const item = await this.findItem(args);
406
+ if (!item) {
407
+ return null;
408
+ }
409
+ const data = await this.getDocData(item.id);
410
+ if (data.props.length > 0) {
411
+ return { item, kind: "props", props: data.props };
412
+ }
413
+ if (data.signature) {
414
+ return {
415
+ item,
416
+ kind: "signature",
417
+ signature: data.signature,
418
+ exportedTypeNames: data.exportedTypeNames
419
+ };
420
+ }
421
+ const apiSection = this.findApiSection(data.sections);
422
+ return {
423
+ item,
424
+ kind: "sections",
425
+ message: `${item.name} has no generated props or signature. Use get_item_doc with a section instead.`,
426
+ suggestedSection: apiSection?.slug,
427
+ availableSections: data.sections.map((s) => s.slug)
428
+ };
429
+ }
430
+ async getApi(symbol) {
431
+ const index = await this.getIndex();
432
+ const requested = normalize2(symbol);
433
+ const candidates = index.filter((item) => item.hasSignature || item.propsCount > 0 || item.kind === "hook");
434
+ const directMatch = candidates.find((item) => normalize2(item.name).replace(/-/g, "") === requested.replace(/-/g, "")) || candidates.find((item) => item.searchText.includes(requested));
435
+ const match = directMatch || index.find((item) => hasSearchTag(item, symbol));
436
+ if (!match) {
437
+ return null;
438
+ }
439
+ const data = await this.getDocData(match.id);
440
+ const hasExportedTypeNames = !!data.exportedTypeNames && data.exportedTypeNames.length > 0;
441
+ if (!data.signature && !hasExportedTypeNames) {
442
+ const apiSection = !directMatch && findSymbolSection(data.sections, symbol) || this.findApiSection(data.sections);
443
+ return {
444
+ item: match,
445
+ signature: data.signature,
446
+ exportedTypeNames: data.exportedTypeNames,
447
+ propsCount: data.props.length,
448
+ suggestedSection: apiSection?.slug,
449
+ availableSections: data.sections.map((s) => s.slug)
450
+ };
451
+ }
452
+ return {
453
+ item: match,
454
+ signature: data.signature,
455
+ exportedTypeNames: data.exportedTypeNames,
456
+ propsCount: data.props.length
457
+ };
458
+ }
459
+ }
460
+
461
+ // src/tools.ts
462
+ import * as z from "zod";
463
+ var kindSchema = z.enum(["component", "hook", "guide", "faq"]);
464
+ var groupSchema = z.enum([
465
+ "gettingStarted",
466
+ "components",
467
+ "hooks",
468
+ "dates",
469
+ "charts",
470
+ "others",
471
+ "headless"
472
+ ]);
473
+ var nameSchema = z.string().trim().min(1);
474
+ function text(payload) {
475
+ return {
476
+ content: [
477
+ {
478
+ type: "text",
479
+ text: typeof payload === "string" ? payload : JSON.stringify(payload, null, 2)
480
+ }
481
+ ]
482
+ };
483
+ }
484
+ function error(message) {
485
+ return { isError: true, content: [{ type: "text", text: message }] };
486
+ }
487
+ function registerTools(server, client) {
488
+ server.registerTool("search_docs", {
489
+ title: "Search reburn-ui documentation",
490
+ description: "Search all reburn-ui documentation — components, headless builders, hooks, dates, charts, extensions and guides. Returns ranked matches; a match may point at a specific section, which can then be fetched with get_item_doc.",
491
+ inputSchema: z.object({
492
+ query: z.string().describe('Free text query, e.g. "close a modal on outside click"'),
493
+ kind: kindSchema.optional(),
494
+ package: z.string().optional().describe('Filter by package, e.g. "reburn-ui/builders"'),
495
+ limit: z.number().optional().describe("Maximum number of results (default 20, max 100)")
496
+ })
497
+ }, async (args) => text(await client.search(args)));
498
+ server.registerTool("get_item_doc", {
499
+ title: "Get reburn-ui documentation",
500
+ description: "Get documentation for a reburn-ui component, headless builder, hook or guide page. Without a section it returns an outline (description plus section list) — pass a section to fetch just that part, or full:true for the whole page.",
501
+ inputSchema: z.object({
502
+ name: nameSchema.describe('Item name or id, e.g. "use-disclosure", "Button", "core-button-headless"'),
503
+ kind: kindSchema.optional(),
504
+ section: z.string().optional().describe('Section slug from the outline, e.g. "usage"'),
505
+ full: z.boolean().optional().describe("Return the entire page; can be very large")
506
+ })
507
+ }, async (args) => {
508
+ const result = await client.getDoc(args);
509
+ if (!result) {
510
+ const suggestions = await client.suggestFor(args.name);
511
+ return error(suggestions.length > 0 ? `No exact match for "${args.name}". Did you mean: ${suggestions.join(", ")}?` : `No reburn-ui documentation found for "${args.name}". Try search_docs first.`);
512
+ }
513
+ return text(result);
514
+ });
515
+ server.registerTool("get_item_props", {
516
+ title: "Get reburn-ui props or signature",
517
+ description: "Get props for a reburn-ui component, or the TypeScript signature for a hook. Pages documenting an API in prose point at the relevant section instead.",
518
+ inputSchema: z.object({ name: nameSchema, kind: kindSchema.optional() })
519
+ }, async (args) => {
520
+ const result = await client.getProps(args);
521
+ if (!result) {
522
+ const suggestions = await client.suggestFor(args.name);
523
+ return error(suggestions.length > 0 ? `No exact match for "${args.name}". Did you mean: ${suggestions.join(", ")}?` : `No reburn-ui documentation found for "${args.name}".`);
524
+ }
525
+ return text(result);
526
+ });
527
+ server.registerTool("list_items", {
528
+ title: "List reburn-ui documentation items",
529
+ description: "List reburn-ui documentation items, filtered by kind, nav group, category, package or a text query.",
530
+ inputSchema: z.object({
531
+ kind: kindSchema.optional(),
532
+ group: groupSchema.optional(),
533
+ category: z.string().optional(),
534
+ package: z.string().optional(),
535
+ query: z.string().optional(),
536
+ limit: z.number().optional().describe("Maximum number of results (default 30, max 500)")
537
+ })
538
+ }, async (args) => text(await client.listItems(args)));
539
+ server.registerTool("get_api", {
540
+ title: "Resolve a reburn-ui symbol",
541
+ description: "Resolve a reburn-ui symbol (useDisclosure, ButtonBuilder) to its TypeScript signature and exported types, without knowing which page documents it.",
542
+ inputSchema: z.object({ symbol: nameSchema })
543
+ }, async ({ symbol }) => {
544
+ const result = await client.getApi(symbol);
545
+ if (!result) {
546
+ const suggestions = await client.suggestFor(symbol);
547
+ return error(suggestions.length > 0 ? `No API found for "${symbol}". Did you mean: ${suggestions.join(", ")}?` : `No API found for "${symbol}". Try search_docs.`);
548
+ }
549
+ return text(result);
550
+ });
551
+ }
552
+
553
+ // src/server.ts
554
+ var DEFAULT_DATA_URL = "https://mantine-reburn-ui.vercel.app/mcp";
555
+ function startServer() {
556
+ const timeoutMs = Number(process.env.REBURN_MCP_TIMEOUT_MS || 1e4);
557
+ const dataUrl = process.env.REBURN_MCP_DATA_URL || DEFAULT_DATA_URL;
558
+ serveStdio(() => {
559
+ const server = new McpServer({ name: package_default.name, version: package_default.version });
560
+ registerTools(server, new ReburnMcpDataClient(dataUrl, timeoutMs));
561
+ return server;
562
+ });
563
+ }
564
+
565
+ // src/index.ts
566
+ startServer();
567
+ export {
568
+ startServer
569
+ };
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
- "name": "@reburn-ui/mcp-server",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@reburn-ui/mcp-server",
3
+ "version": "9.8.1",
4
+ "description": "MCP server for reburn-ui documentation",
5
+ "keywords": [
6
+ "ai",
7
+ "docs",
8
+ "mantine",
9
+ "mcp",
10
+ "model-context-protocol",
11
+ "reburn-ui",
12
+ "svelte"
13
+ ],
14
+ "homepage": "https://github.com/Mo310/mantine-reburn-ui/tree/main/mcp-server#readme",
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/Mo310/mantine-reburn-ui.git",
19
+ "directory": "mcp-server"
20
+ },
21
+ "bin": {
22
+ "reburn-ui-mcp-server": "bin/reburn-ui-mcp-server.js"
23
+ },
24
+ "files": [
25
+ "bin",
26
+ "dist"
27
+ ],
28
+ "type": "module",
29
+ "main": "./dist/index.js",
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "scripts": {
34
+ "build": "bun build src/index.ts --target node --packages external --outfile dist/index.js",
35
+ "check": "tsc --noEmit",
36
+ "prepack": "bun run build",
37
+ "start": "bun src/index.ts"
38
+ },
39
+ "dependencies": {
40
+ "@modelcontextprotocol/server": "2.1.0",
41
+ "zod": "^4.6.5"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^22.0.0",
45
+ "typescript": "^6.0.3"
46
+ },
47
+ "engines": {
48
+ "node": ">=22"
49
+ }
50
+ }