@public-ui/mcp 3.0.7-rc.3 → 4.0.0-alpha.8

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/dist/mcp.mjs ADDED
@@ -0,0 +1,291 @@
1
+ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3
+ import express from 'express';
4
+ import { createRequire } from 'node:module';
5
+ import { z } from 'zod';
6
+ import { getSampleIndexMetadata, getAllEntries, getEntryById } from './data.mjs';
7
+ import { searchEntries } from './search.mjs';
8
+ import 'node:fs';
9
+ import 'node:url';
10
+ import 'fuse.js';
11
+
12
+ const KIND_OPTIONS = ["doc", "sample", "scenario", "spec"];
13
+ function isValidKind(value) {
14
+ return typeof value === "string" && KIND_OPTIONS.includes(value);
15
+ }
16
+ function normalizeTags(tags) {
17
+ return Array.isArray(tags) ? tags : [];
18
+ }
19
+ function formatTagsForText(tags) {
20
+ const normalized = normalizeTags(tags);
21
+ return normalized.length > 0 ? normalized.join(", ") : "none";
22
+ }
23
+ const require = createRequire(import.meta.url);
24
+ const {
25
+ version: PACKAGE_VERSION = "0.0.0",
26
+ name: PACKAGE_NAME = "@public-ui/mcp",
27
+ description: PACKAGE_DESCRIPTION
28
+ } = require("../package.json");
29
+ const ENABLE_LOGGING = process.env.MCP_LOGGING === "true" || process.env.MCP_LOGGING === "1";
30
+ function log(type, message, data) {
31
+ if (!ENABLE_LOGGING) return;
32
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
33
+ const prefix = `[${timestamp}] [${type.toUpperCase()}]`;
34
+ if (data) {
35
+ console.error(`${prefix} ${message}`, JSON.stringify(data, null, 2));
36
+ } else {
37
+ console.error(`${prefix} ${message}`);
38
+ }
39
+ }
40
+ function createKolibriMcpServer() {
41
+ const server = new McpServer({
42
+ name: PACKAGE_NAME,
43
+ version: PACKAGE_VERSION
44
+ });
45
+ return configureServer(server);
46
+ }
47
+ function configureServer(server) {
48
+ server.registerTool(
49
+ "search",
50
+ {
51
+ title: "Search KoliBri Samples and Docs",
52
+ description: 'Search for KoliBri component samples, scenarios, specifications, and documentation using fuzzy search. Parameters: query (optional string), kind (optional select: "doc", "sample", "scenario", or "spec"), limit (optional number, default 10).',
53
+ inputSchema: {
54
+ query: z.string().optional().default(""),
55
+ kind: z.enum(KIND_OPTIONS).optional(),
56
+ limit: z.number().optional()
57
+ },
58
+ outputSchema: {
59
+ query: z.string(),
60
+ totalResults: z.number()
61
+ }
62
+ },
63
+ ({ query, kind, limit }) => {
64
+ log("tool", "search called", { query, kind, limit });
65
+ const queryStr = typeof query === "string" ? query : "";
66
+ const allEntries = getAllEntries();
67
+ const searchOptions = {
68
+ limit: typeof limit === "number" ? limit : 10
69
+ };
70
+ if (isValidKind(kind)) {
71
+ searchOptions.kind = kind;
72
+ }
73
+ const results = searchEntries(allEntries, queryStr, searchOptions);
74
+ log("tool", "search completed", {
75
+ query: queryStr,
76
+ resultCount: results.length,
77
+ options: searchOptions
78
+ });
79
+ const structuredContent = {
80
+ query: queryStr,
81
+ totalResults: results.length,
82
+ results: results.map(({ item, score }) => ({
83
+ id: item.id,
84
+ kind: item.kind,
85
+ name: item.name,
86
+ group: item.group ?? "N/A",
87
+ description: item.description ?? "N/A",
88
+ tags: normalizeTags(item.tags),
89
+ score: score ?? 1,
90
+ path: item.path ?? "N/A"
91
+ }))
92
+ };
93
+ const resultText = results.length ? results.map(({ item, score }, index) => {
94
+ const matchScore = ((score ?? 1) * 100).toFixed(1);
95
+ const pathLine = item.path ? `
96
+ Path: ${item.path}` : "";
97
+ return `${index + 1}. [${item.kind}] ${item.id}: ${item.name}
98
+ Description: ${item.description ?? "N/A"}
99
+ Match score: ${matchScore}%
100
+ Tags: ${formatTagsForText(item.tags)}${pathLine}`;
101
+ }).join("\n\n") : "No matches found.";
102
+ const tipText = results.length ? "\n\n\u{1F4A1} Tip: Use 'fetch' with any ID above to see full entry details." : "";
103
+ return {
104
+ content: [
105
+ {
106
+ type: "text",
107
+ text: `Found ${results.length} result(s) for "${queryStr}":
108
+
109
+ ${resultText}${tipText}`
110
+ }
111
+ ],
112
+ structuredContent
113
+ };
114
+ }
115
+ );
116
+ server.registerTool(
117
+ "fetch",
118
+ {
119
+ title: "Get Sample or Doc Entry",
120
+ description: 'Get a specific sample, specification, or documentation entry by its ID. Parameter: id (required string, e.g. "button/basic", "spec/button", or "docs/getting-started")',
121
+ inputSchema: {
122
+ id: z.string()
123
+ },
124
+ outputSchema: {
125
+ id: z.string(),
126
+ kind: z.string(),
127
+ name: z.string()
128
+ }
129
+ },
130
+ ({ id }) => {
131
+ log("tool", "fetch called", { id });
132
+ const idStr = String(id ?? "");
133
+ if (!idStr) {
134
+ log("error", "fetch failed: empty id");
135
+ throw new Error("ID parameter is required");
136
+ }
137
+ const entry = getEntryById(idStr);
138
+ if (!entry) {
139
+ log("error", "fetch failed: entry not found", { id: idStr });
140
+ throw new Error(`Entry with ID "${idStr}" not found`);
141
+ }
142
+ log("tool", "fetch completed", { id: idStr, kind: entry.kind });
143
+ const output = {
144
+ id: entry.id,
145
+ kind: entry.kind,
146
+ name: entry.name,
147
+ group: entry.group ?? "N/A",
148
+ description: entry.description ?? "N/A",
149
+ tags: entry.tags ?? [],
150
+ code: entry.code ?? "No code available",
151
+ path: entry.path ?? "N/A"
152
+ };
153
+ return {
154
+ content: [
155
+ {
156
+ type: "text",
157
+ text: `# ${entry.name}
158
+
159
+ ID: ${entry.id}
160
+ Kind: ${entry.kind}
161
+ Group: ${entry.group ?? "N/A"}
162
+ Description: ${entry.description ?? "N/A"}
163
+ Tags: ${entry.tags?.join(", ") ?? "none"}
164
+
165
+ ## Code
166
+
167
+ \`\`\`
168
+ ${entry.code ?? "No code available"}
169
+ \`\`\``
170
+ }
171
+ ],
172
+ structuredContent: output
173
+ };
174
+ }
175
+ );
176
+ server.registerResource(
177
+ "info",
178
+ new ResourceTemplate("kolibri://info", { list: void 0 }),
179
+ {
180
+ title: "KoliBri MCP Server Info",
181
+ description: "Get information about the KoliBri MCP Server and available samples"
182
+ },
183
+ (uri) => {
184
+ log("resource", "info accessed", { uri: uri.href });
185
+ const metadata = getSampleIndexMetadata();
186
+ const infoText = `# KoliBri MCP Server v${PACKAGE_VERSION}
187
+
188
+ ${PACKAGE_DESCRIPTION ?? ""}
189
+
190
+ ## Sample Index
191
+ - Generated: ${metadata.generatedAt ?? "unknown"}
192
+ - Build mode: ${metadata.buildMode}
193
+ - Total entries: ${metadata.counts.total}
194
+ - Documentation: ${metadata.counts.totalDocs}
195
+ - Specifications: ${metadata.counts.totalSpecs ?? 0}
196
+ - Samples: ${metadata.counts.totalSamples}
197
+ - Scenarios: ${metadata.counts.totalScenarios ?? 0}
198
+
199
+ ## Repository
200
+ - Branch: ${metadata.repo.branch ?? "N/A"}
201
+ - Commit: ${metadata.repo.commit ?? "N/A"}
202
+ - URL: ${metadata.repo.repoUrl ?? "N/A"}
203
+ `;
204
+ return {
205
+ contents: [
206
+ {
207
+ uri: uri.href,
208
+ mimeType: "text/markdown",
209
+ text: infoText
210
+ }
211
+ ]
212
+ };
213
+ }
214
+ );
215
+ server.registerResource(
216
+ "best-practices",
217
+ new ResourceTemplate("kolibri://best-practices", { list: void 0 }),
218
+ {
219
+ title: "KoliBri Best Practices",
220
+ description: "Essential guidelines for working with KoliBri Web Components"
221
+ },
222
+ (uri) => {
223
+ log("resource", "best-practices accessed", { uri: uri.href });
224
+ const practicesText = `# KoliBri Web Components - Best Practices
225
+
226
+ ## Essential Guidelines
227
+
228
+ 1. **Component Registration**
229
+ Always register KoliBri Web Components in the browser runtime before rendering them.
230
+
231
+ 2. **Integration Setup**
232
+ Choose the integration guide that matches your project setup to load and bundle the components correctly.
233
+
234
+ 3. **Icon Font Assets**
235
+ Bundle the KoliBri icon font assets (for example codicon.css and codicon.ttf) so kol-icon glyphs can render.
236
+
237
+ 4. **Form Validation**
238
+ Wrap input elements with <kol-form> and feed its _errorList to surface validation issues via the generated error summary.
239
+
240
+ ## Additional Resources
241
+
242
+ Use the 'search' tool to find specific component examples and implementation details.
243
+ Use the 'fetch' tool to retrieve full code samples for specific components.
244
+ `;
245
+ return {
246
+ contents: [
247
+ {
248
+ uri: uri.href,
249
+ mimeType: "text/markdown",
250
+ text: practicesText
251
+ }
252
+ ]
253
+ };
254
+ }
255
+ );
256
+ return server;
257
+ }
258
+ if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("/mcp.ts") || process.argv[1]?.endsWith("/mcp.cjs") || process.argv[1]?.endsWith("/mcp.mjs")) {
259
+ const server = createKolibriMcpServer();
260
+ const app = express();
261
+ app.use(express.json());
262
+ app.post("/mcp", async (req, res) => {
263
+ const transport = new StreamableHTTPServerTransport({
264
+ sessionIdGenerator: void 0,
265
+ enableJsonResponse: true
266
+ });
267
+ res.on("close", () => {
268
+ void transport.close();
269
+ });
270
+ await server.connect(transport);
271
+ await transport.handleRequest(req, res, req.body);
272
+ });
273
+ const port = parseInt(process.env.PORT || "3000");
274
+ void app.listen(port, () => {
275
+ console.log(`KoliBri MCP Server v${PACKAGE_VERSION} running on http://localhost:${port}/mcp`);
276
+ const metadata = getSampleIndexMetadata();
277
+ console.log(
278
+ `Loaded ${metadata.counts.total} entries (${metadata.counts.totalDocs} docs, ${metadata.counts.totalSamples} samples, ${metadata.counts.totalScenarios ?? 0} scenarios)`
279
+ );
280
+ if (ENABLE_LOGGING) {
281
+ console.log("\u{1F50D} Logging is ENABLED (MCP_LOGGING=true)");
282
+ } else {
283
+ console.log("\u{1F4A1} Logging is disabled. Set MCP_LOGGING=true to enable request logging");
284
+ }
285
+ }).on("error", (error) => {
286
+ console.error("Server error:", error);
287
+ process.exit(1);
288
+ });
289
+ }
290
+
291
+ export { createKolibriMcpServer };
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ const Fuse = require('fuse.js');
4
+
5
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
6
+
7
+ const Fuse__default = /*#__PURE__*/_interopDefaultCompat(Fuse);
8
+
9
+ const FUSE_OPTIONS = {
10
+ includeScore: true,
11
+ shouldSort: true,
12
+ threshold: 0.4,
13
+ // Default threshold for single-word queries
14
+ minMatchCharLength: 2,
15
+ // useExtendedSearch is set dynamically based on query type
16
+ keys: [
17
+ { name: "id", weight: 0.2 },
18
+ { name: "name", weight: 0.2 },
19
+ { name: "group", weight: 0.15 },
20
+ { name: "description", weight: 0.1 },
21
+ { name: "tags", weight: 0.05 }
22
+ ]
23
+ };
24
+ function searchEntries(entries, query, options = {}) {
25
+ const normalizedQuery = query.trim();
26
+ const filteredEntries = options.kind ? entries.filter((e) => e.kind === options.kind) : entries;
27
+ if (!normalizedQuery) {
28
+ return filteredEntries.map((item) => ({ item, score: 1 }));
29
+ }
30
+ const words = normalizedQuery.split(/\s+/).filter((w) => w.length > 0);
31
+ const isMultiWord = words.length > 1;
32
+ const fuseOptions = {
33
+ ...FUSE_OPTIONS,
34
+ // Use higher threshold for multi-word queries to find more results
35
+ threshold: options.threshold ?? (isMultiWord ? 0.6 : FUSE_OPTIONS.threshold),
36
+ // Only use extended search for multi-word queries
37
+ useExtendedSearch: isMultiWord
38
+ };
39
+ const fuse = new Fuse__default(filteredEntries, fuseOptions);
40
+ const searchQuery = isMultiWord ? words.map((w) => `'${w}`).join(" | ") : normalizedQuery;
41
+ const results = fuse.search(searchQuery);
42
+ const searchResults = results.map((result) => ({
43
+ item: result.item,
44
+ score: result.score ?? 1
45
+ }));
46
+ if (options.limit && options.limit > 0) {
47
+ return searchResults.slice(0, options.limit);
48
+ }
49
+ return searchResults;
50
+ }
51
+
52
+ exports.searchEntries = searchEntries;
@@ -0,0 +1,16 @@
1
+ import { SampleEntry } from './data.cjs';
2
+
3
+ type EntryKind = SampleEntry['kind'];
4
+ interface SearchOptions {
5
+ threshold?: number;
6
+ limit?: number;
7
+ kind?: EntryKind;
8
+ }
9
+ interface SearchResult {
10
+ item: SampleEntry;
11
+ score: number;
12
+ }
13
+ declare function searchEntries(entries: SampleEntry[], query: string, options?: SearchOptions): SearchResult[];
14
+
15
+ export { searchEntries };
16
+ export type { SearchOptions };
@@ -0,0 +1,16 @@
1
+ import { SampleEntry } from './data.mjs';
2
+
3
+ type EntryKind = SampleEntry['kind'];
4
+ interface SearchOptions {
5
+ threshold?: number;
6
+ limit?: number;
7
+ kind?: EntryKind;
8
+ }
9
+ interface SearchResult {
10
+ item: SampleEntry;
11
+ score: number;
12
+ }
13
+ declare function searchEntries(entries: SampleEntry[], query: string, options?: SearchOptions): SearchResult[];
14
+
15
+ export { searchEntries };
16
+ export type { SearchOptions };
@@ -0,0 +1,16 @@
1
+ import { SampleEntry } from './data.js';
2
+
3
+ type EntryKind = SampleEntry['kind'];
4
+ interface SearchOptions {
5
+ threshold?: number;
6
+ limit?: number;
7
+ kind?: EntryKind;
8
+ }
9
+ interface SearchResult {
10
+ item: SampleEntry;
11
+ score: number;
12
+ }
13
+ declare function searchEntries(entries: SampleEntry[], query: string, options?: SearchOptions): SearchResult[];
14
+
15
+ export { searchEntries };
16
+ export type { SearchOptions };
@@ -0,0 +1,46 @@
1
+ import Fuse from 'fuse.js';
2
+
3
+ const FUSE_OPTIONS = {
4
+ includeScore: true,
5
+ shouldSort: true,
6
+ threshold: 0.4,
7
+ // Default threshold for single-word queries
8
+ minMatchCharLength: 2,
9
+ // useExtendedSearch is set dynamically based on query type
10
+ keys: [
11
+ { name: "id", weight: 0.2 },
12
+ { name: "name", weight: 0.2 },
13
+ { name: "group", weight: 0.15 },
14
+ { name: "description", weight: 0.1 },
15
+ { name: "tags", weight: 0.05 }
16
+ ]
17
+ };
18
+ function searchEntries(entries, query, options = {}) {
19
+ const normalizedQuery = query.trim();
20
+ const filteredEntries = options.kind ? entries.filter((e) => e.kind === options.kind) : entries;
21
+ if (!normalizedQuery) {
22
+ return filteredEntries.map((item) => ({ item, score: 1 }));
23
+ }
24
+ const words = normalizedQuery.split(/\s+/).filter((w) => w.length > 0);
25
+ const isMultiWord = words.length > 1;
26
+ const fuseOptions = {
27
+ ...FUSE_OPTIONS,
28
+ // Use higher threshold for multi-word queries to find more results
29
+ threshold: options.threshold ?? (isMultiWord ? 0.6 : FUSE_OPTIONS.threshold),
30
+ // Only use extended search for multi-word queries
31
+ useExtendedSearch: isMultiWord
32
+ };
33
+ const fuse = new Fuse(filteredEntries, fuseOptions);
34
+ const searchQuery = isMultiWord ? words.map((w) => `'${w}`).join(" | ") : normalizedQuery;
35
+ const results = fuse.search(searchQuery);
36
+ const searchResults = results.map((result) => ({
37
+ item: result.item,
38
+ score: result.score ?? 1
39
+ }));
40
+ if (options.limit && options.limit > 0) {
41
+ return searchResults.slice(0, options.limit);
42
+ }
43
+ return searchResults;
44
+ }
45
+
46
+ export { searchEntries };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@public-ui/mcp",
3
- "version": "3.0.7-rc.3",
3
+ "version": "4.0.0-alpha.8",
4
4
  "license": "EUPL-1.2",
5
5
  "homepage": "https://public-ui.github.io",
6
6
  "repository": {
@@ -32,7 +32,8 @@
32
32
  "main": "dist/index.cjs",
33
33
  "module": "dist/index.mjs",
34
34
  "bin": {
35
- "kolibri-mcp": "dist/cli.mjs"
35
+ "@public-ui/mcp": "dist/cli.cjs",
36
+ "kolibri-mcp": "dist/cli.cjs"
36
37
  },
37
38
  "exports": {
38
39
  ".": {
@@ -40,18 +41,59 @@
40
41
  "require": "./dist/index.cjs"
41
42
  }
42
43
  },
44
+ "dependencies": {
45
+ "@modelcontextprotocol/sdk": "1.24.3",
46
+ "express": "5.2.1",
47
+ "fuse.js": "7.1.0",
48
+ "zod": "3.25.76",
49
+ "@public-ui/components": "4.0.0-alpha.8"
50
+ },
43
51
  "devDependencies": {
44
- "prettier": "3.6.2",
52
+ "@modelcontextprotocol/inspector": "0.17.5",
53
+ "@types/express": "5.0.6",
54
+ "@types/node": "24.10.1",
55
+ "@typescript-eslint/eslint-plugin": "7.18.0",
56
+ "@typescript-eslint/parser": "7.18.0",
57
+ "eslint": "8.57.1",
58
+ "eslint-config-prettier": "9.1.2",
59
+ "eslint-plugin-html": "8.1.3",
60
+ "eslint-plugin-jsdoc": "50.8.0",
61
+ "eslint-plugin-json": "3.1.0",
62
+ "knip": "5.71.0",
63
+ "nodemon": "3.1.11",
64
+ "prettier": "3.7.4",
65
+ "prettier-plugin-organize-imports": "4.3.0",
66
+ "tsx": "4.21.0",
67
+ "typescript": "5.9.3",
45
68
  "unbuild": "3.6.1"
46
69
  },
47
70
  "files": [
48
71
  "dist/",
72
+ "shared/",
49
73
  "README.md"
50
74
  ],
51
75
  "scripts": {
52
- "build": "node prebuild.js && unbuild",
53
- "format": "prettier --check src",
76
+ "build:deps": "pnpm --filter @public-ui/mcp^... build",
77
+ "generate-index": "node ./scripts/generate-sample-index.mjs",
78
+ "prebuild": "pnpm generate-index",
79
+ "build": "unbuild",
80
+ "predev": "pnpm generate-index",
81
+ "dev": "nodemon",
82
+ "format": "prettier -c src test public",
83
+ "lint": "pnpm lint:eslint && pnpm lint:tsc",
84
+ "lint:eslint": "eslint src test 'public/**/*.html'",
85
+ "lint:tsc": "tsc --noemit",
86
+ "preinspect": "pnpm generate-index",
87
+ "inspect": "mcp-inspector tsx src/cli.ts",
54
88
  "prestart": "pnpm build",
55
- "start": "node dist/index.mjs"
89
+ "start": "node dist/mcp.cjs",
90
+ "start:stdio": "node dist/cli.cjs",
91
+ "pretest": "pnpm build",
92
+ "test": "node --test test/*.test.js",
93
+ "test:api": "node test-vercel-api.mjs",
94
+ "unused": "knip",
95
+ "vercel:dev": "vercel dev",
96
+ "vercel:deploy": "vercel",
97
+ "vercel:deploy:prod": "vercel --prod"
56
98
  }
57
99
  }
@@ -0,0 +1 @@
1
+ # Keep this directory in git, but exclude generated JSON files