kodza-mcp 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,39 @@
1
+ Kodza License
2
+
3
+ Copyright (c) 2025-2026 fcwebdesign (Kodza)
4
+
5
+ Kodza components are original works. Their code (the HTML, CSS and JavaScript of
6
+ each component, the React version Kodza provides, and what your AI receives through
7
+ the Kodza MCP server) is free to use.
8
+
9
+ You may:
10
+ - use them in any number of projects, personal or commercial, including client work;
11
+ - copy, modify and adapt them, and combine them with your own code;
12
+ - ship them as part of a website, an application, a theme, a template or a plugin
13
+ you build, as long as that product offers more than the components themselves;
14
+ - install and run the Kodza MCP server, and let an AI assistant fetch the
15
+ components and add them to your project;
16
+ - keep them in your own or your team's snippets;
17
+ - show a component or two in a free article, tutorial or code playground,
18
+ with a link to Kodza.
19
+ No attribution is required in your projects. A link back is always appreciated.
20
+
21
+ You may not:
22
+ - sell, sublicense or redistribute the components themselves, alone, in a bundle
23
+ or as a ported version (React, Vue, Svelte...), Kodza's own React version
24
+ included, free or paid, modified or not;
25
+ - offer them through another catalogue, registry, marketplace or MCP server;
26
+ - make them the core of a paid course, membership or tutorial series;
27
+ - repackage them with superficial changes, or present them as your own work;
28
+ - use the Kodza name and logo to promote something that is not Kodza.
29
+
30
+ Not covered by this license: the Kodza website and its design, the demo pages and
31
+ their copy, the preview images and videos, the house images in /media (placeholders:
32
+ swap them for your own before you ship), and the Kodza name and logo.
33
+ All rights reserved.
34
+
35
+ Copies that break these terms will be reported and taken down.
36
+
37
+ THE COMPONENTS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
38
+ IMPLIED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
39
+ LIABILITY ARISING FROM THE COMPONENTS OR THEIR USE.
package/README.md CHANGED
@@ -1,3 +1,143 @@
1
- # Temporary Holding Version
1
+ # kodza-mcp
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
+ An MCP server for [Kodza](https://kodza.dev): free, premium UI components and effects in
4
+ plain HTML, CSS and vanilla JavaScript. Your AI assistant finds the component, gets its exact code
5
+ and knows how to wire it into a plain page, WordPress, Shopify or React. No framework, no build
6
+ step, no API key.
7
+
8
+ ## Install
9
+
10
+ Kodza runs at `https://kodza.dev/api/mcp`. Nothing to install: give your assistant the address.
11
+
12
+ **Claude Code**
13
+
14
+ ```bash
15
+ claude mcp add -t http kodza https://kodza.dev/api/mcp
16
+ ```
17
+
18
+ **Codex** (the CLI, the IDE extension and the app share `~/.codex/config.toml`)
19
+
20
+ ```bash
21
+ codex mcp add kodza --url https://kodza.dev/api/mcp
22
+ ```
23
+
24
+ **Cursor** (`.cursor/mcp.json` for one project, `~/.cursor/mcp.json` for all)
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "kodza": { "url": "https://kodza.dev/api/mcp" }
30
+ }
31
+ }
32
+ ```
33
+
34
+ From a project file, the server starts switched off: turn it on in *Customize → MCPs*.
35
+
36
+ **VS Code**
37
+
38
+ ```bash
39
+ code --add-mcp '{"name":"kodza","type":"http","url":"https://kodza.dev/api/mcp"}'
40
+ ```
41
+
42
+ That one is for every project. For one project only, in `.vscode/mcp.json`:
43
+
44
+ ```json
45
+ {
46
+ "servers": {
47
+ "kodza": { "type": "http", "url": "https://kodza.dev/api/mcp" }
48
+ }
49
+ }
50
+ ```
51
+
52
+ ### Run it locally
53
+
54
+ Rather keep the search on your machine, or using a client that only starts local servers (like
55
+ Claude Desktop's `claude_desktop_config.json`)? The same server runs through npx (Node 18 or newer):
56
+
57
+ ```json
58
+ {
59
+ "mcpServers": {
60
+ "kodza": { "command": "npx", "args": ["-y", "kodza-mcp"] }
61
+ }
62
+ }
63
+ ```
64
+
65
+ In a terminal: `claude mcp add kodza -- npx -y kodza-mcp`, `codex mcp add kodza -- npx -y kodza-mcp`.
66
+
67
+ **Server won't start?** `spawn npx ENOENT`, or `No such file or directory` in VS Code: the editor
68
+ can't find your Node. Give `command` the full path from `which npx`, and the server
69
+ `"env": { "PATH": "<that folder>:/usr/bin:/bin" }`: `npx` needs `node` right next to it.
70
+
71
+ ### Then ask
72
+
73
+ Ask for what you need: *"add a button that fills from the cursor"*, *"a sticky header that
74
+ hides while I read, in my WordPress theme"*, *"a date picker for this form"*.
75
+
76
+ ## Tools
77
+
78
+ | Tool | What it does |
79
+ |---|---|
80
+ | `search_components` | Finds components by what they do or look like (`query`), or lists a `category`. Returns slugs. |
81
+ | `get_component` | One component: description, API (CSS variables and their defaults, modifier classes, `data-` attributes, events), what each key does (for the components that take the keyboard), its dark-mode values, how to install it (`target`: `html`, `artifact` for a Claude artifact, `wordpress`, `shopify`, `react`), its files and the full code. `inline: false` leaves the CSS and JS out of the code, for an assistant that downloads the files. |
82
+
83
+ ## Exact files, not retyped code
84
+
85
+ An assistant that can run commands should not retype a component: a CSS with its theme guard
86
+ runs to several hundred lines, retyping it costs thousands of tokens and can drop or change a
87
+ line. Each component's CSS, JS and HTML are served as files, byte for byte what the Kodza site
88
+ copies, and `get_component` gives the commands:
89
+
90
+ ```bash
91
+ curl -fsSL --create-dirs -o kodza/kodza-drawer.css https://kodza.dev/registry/drawer/kodza-drawer.css
92
+ curl -fsSL --create-dirs -o kodza/kodza-drawer.js https://kodza.dev/registry/drawer/kodza-drawer.js
93
+ ```
94
+
95
+ The install notes then load those files (a `<link>` and a `<script defer>`, `wp_enqueue_style`
96
+ and `wp_enqueue_script`, Shopify `assets/`, Next.js `public/`). Only the HTML is pasted and
97
+ adapted. The code stays inline by default, for assistants that can't run anything (a chat, a
98
+ Claude artifact); `inline: false` drops the CSS and JS blocks to save the reading too.
99
+
100
+ ## How Kodza components behave
101
+
102
+ - Three pieces, pasted as they are: HTML, CSS, and often a small vanilla JS file.
103
+ - The CSS only targets `kodza-` classes: it never touches the rest of the page or the theme.
104
+ - Restyle with the `--kz-…` variables, the modifier classes and the `data-` attributes.
105
+ - Dark mode: every component comes with its values for a dark background, ready for
106
+ `@media (prefers-color-scheme: dark)` or your own dark selector.
107
+ - They take the page's font. The Kodza look is Geist (Google Fonts).
108
+ - Any number of instances per page; one copy of the JS serves them all.
109
+ - Content added after the page load (AJAX, SPA, a builder preview):
110
+ `document.dispatchEvent(new CustomEvent('kodza:init', { detail: { root } }))` starts it,
111
+ `'kodza:destroy'` stops it and puts the original markup back.
112
+ - `prefers-reduced-motion`, keyboard and WCAG AA are checked on every component.
113
+
114
+ ## What Kodza counts
115
+
116
+ Kodza counts which components AI assistants fetch, to know what is actually used: the component,
117
+ the assistant's name (as it gives it when connecting, like `claude-code` or `cursor-vscode`) and
118
+ the target, when one was asked for (`html`, `artifact`, `wordpress`, `shopify`, `react`). That's
119
+ all.
120
+
121
+ Through kodza.dev/api/mcp, your search words reach the server to run the search, and are thrown
122
+ away with the answer: never stored, never logged. Your files and your code stay on your machine:
123
+ the tools only take a few words and a slug. With the local server (npx), the search itself runs on
124
+ your machine.
125
+
126
+ ## Options
127
+
128
+ `KODZA_REGISTRY` points the local server at another copy of the catalogue: a URL, or a local folder
129
+ that contains `registry/` and `components/` (the library's build output). Default:
130
+ `https://kodza.dev`. The download commands then point at that copy too (`file://` for a folder).
131
+
132
+ ```bash
133
+ KODZA_REGISTRY=http://localhost:5180 npx -y kodza-mcp
134
+ ```
135
+
136
+ Hosting your own copy: `http.js` exports `handle(request, { source })`, which takes a web
137
+ `Request` and returns its `Response` (the server is stateless, with no dependency).
138
+
139
+ ## License
140
+
141
+ The Kodza License, like the rest of Kodza: use the components in any project, personal,
142
+ client or commercial, no attribution required; don't resell or redistribute them as such.
143
+ Full text in [LICENSE](LICENSE).
package/http.js ADDED
@@ -0,0 +1,97 @@
1
+ // Kodza MCP server, the hosted one (kodza.dev/api/mcp): the same tools as the local server
2
+ // (kodza.js), over Streamable HTTP, without state and without the MCP SDK. Each POST carries one
3
+ // JSON-RPC message (or a batch) and gets its answer as plain JSON; there is no event stream to
4
+ // open (GET answers 405), no session to keep, nothing for the user to install.
5
+ //
6
+ // The assistant's name, given once when it connects, comes back to it inside its session id: the
7
+ // server keeps nothing, and later calls still say who asks (README, "What Kodza counts"). Nothing
8
+ // the assistant sends is stored or logged here.
9
+ import { randomUUID } from 'node:crypto';
10
+ import { INSTRUCTIONS, TOOLS, VERSION, registry } from './kodza.js';
11
+
12
+ // The protocol versions it speaks, newest first: these two tools travel the same way in each
13
+ const PROTOCOLS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05'];
14
+
15
+ // Assistants that run in a browser (an inspector, a web app) call it from another origin
16
+ const CORS = {
17
+ 'access-control-allow-origin': '*',
18
+ 'access-control-allow-methods': 'GET, POST, DELETE, OPTIONS',
19
+ 'access-control-allow-headers': 'content-type, accept, authorization, mcp-session-id, mcp-protocol-version, last-event-id',
20
+ 'access-control-expose-headers': 'mcp-session-id',
21
+ };
22
+
23
+ const reply = (status, body, extra = {}) =>
24
+ new Response(body === undefined ? null : JSON.stringify(body), {
25
+ status,
26
+ headers: { ...CORS, ...(body !== undefined && { 'content-type': 'application/json' }), ...extra },
27
+ });
28
+ const ok = (id, result) => ({ jsonrpc: '2.0', id, result });
29
+ const failure = (id, code, message) => ({ jsonrpc: '2.0', id: id ?? null, error: { code, message } });
30
+
31
+ // The session id: random, then the assistant's name (ASCII, in base64url)
32
+ const ascii = (s) => String(s || '').replace(/[^\x20-\x7e]/g, '').slice(0, 64);
33
+ const sessionFor = (client) => `${randomUUID()}${client ? `.${Buffer.from(client).toString('base64url')}` : ''}`;
34
+ const clientOf = (session) => {
35
+ const encoded = String(session || '').split('.')[1];
36
+ return encoded ? ascii(Buffer.from(encoded, 'base64url').toString()) : '';
37
+ };
38
+
39
+ // One HTTP request (a web Request, as Vercel and Node give it) to its Response
40
+ export async function handle(request, { source = 'https://kodza.dev' } = {}) {
41
+ if (request.method === 'OPTIONS') return reply(204);
42
+ if (request.method !== 'POST') {
43
+ return reply(405, failure(null, -32000, 'Method not allowed: this server answers POST only.'), { allow: 'POST, OPTIONS' });
44
+ }
45
+ let input;
46
+ try {
47
+ input = JSON.parse(await request.text());
48
+ } catch {
49
+ return reply(400, failure(null, -32700, 'Parse error'));
50
+ }
51
+ const batch = Array.isArray(input);
52
+ const messages = batch ? input : [input];
53
+ if (!messages.length) return reply(400, failure(null, -32600, 'Invalid request'));
54
+
55
+ const kodza = registry(source);
56
+ let client = clientOf(request.headers.get('mcp-session-id'));
57
+ let session = null;
58
+
59
+ async function answer({ id, method, params }) {
60
+ const p = params && typeof params === 'object' ? params : {};
61
+ switch (method) {
62
+ case 'initialize': {
63
+ client = ascii(p.clientInfo?.name);
64
+ session = sessionFor(client);
65
+ return ok(id, {
66
+ protocolVersion: PROTOCOLS.includes(p.protocolVersion) ? p.protocolVersion : PROTOCOLS[0],
67
+ capabilities: { tools: { listChanged: false } },
68
+ serverInfo: { name: 'kodza', title: 'Kodza', version: VERSION },
69
+ instructions: INSTRUCTIONS,
70
+ });
71
+ }
72
+ case 'ping':
73
+ return ok(id, {});
74
+ case 'tools/list':
75
+ return ok(id, { tools: TOOLS });
76
+ case 'tools/call': {
77
+ const result = await kodza.call(p.name, p.arguments, client);
78
+ return result ? ok(id, result) : failure(id, -32602, `Unknown tool: ${p.name}`);
79
+ }
80
+ default:
81
+ return failure(id, -32601, `Method not found: ${method}`);
82
+ }
83
+ }
84
+
85
+ const answers = [];
86
+ for (const message of messages) {
87
+ if (!message || typeof message !== 'object' || message.jsonrpc !== '2.0') {
88
+ answers.push(failure(message?.id, -32600, 'Invalid request'));
89
+ continue;
90
+ }
91
+ // Notifications, and the client's own responses, get no answer
92
+ if (typeof message.method !== 'string' || message.id === undefined) continue;
93
+ answers.push(await answer(message));
94
+ }
95
+ if (!answers.length) return reply(202);
96
+ return reply(200, batch ? answers : answers[0], session ? { 'mcp-session-id': session } : {});
97
+ }
package/index.js ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ // Kodza MCP server, the local one: lets an AI coding assistant find a Kodza component and get its
3
+ // exact code, its API (CSS variables, modifiers, data attributes) and how to wire it into a plain
4
+ // HTML page, WordPress, Shopify or React. Run by npx, it talks to the assistant over stdio; the
5
+ // search runs on the user's machine. The tools themselves are in kodza.js, shared with the hosted
6
+ // server (http.js).
7
+ //
8
+ // KODZA_REGISTRY points it at another copy of the catalogue: a URL, or a local folder (handy while
9
+ // working on the library).
10
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
11
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
13
+ import { INSTRUCTIONS, TOOLS, VERSION, registry } from './kodza.js';
14
+
15
+ const kodza = registry(process.env.KODZA_REGISTRY || 'https://kodza.dev');
16
+
17
+ const server = new Server(
18
+ { name: 'kodza', title: 'Kodza', version: VERSION },
19
+ { capabilities: { tools: {} }, instructions: INSTRUCTIONS }
20
+ );
21
+
22
+ server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: TOOLS }));
23
+
24
+ server.setRequestHandler(CallToolRequestSchema, async ({ params }) => {
25
+ const result = await kodza.call(params.name, params.arguments, server.getClientVersion()?.name);
26
+ return result || { content: [{ type: 'text', text: `Unknown tool "${params.name}".` }], isError: true };
27
+ });
28
+
29
+ await server.connect(new StdioServerTransport());
package/kodza.js ADDED
@@ -0,0 +1,234 @@
1
+ // The Kodza tools, shared by the two servers: index.js (stdio, run by npx on the user's machine)
2
+ // and http.js (Streamable HTTP, hosted on kodza.dev). No dependency: the hosted one runs without
3
+ // the MCP SDK.
4
+ //
5
+ // The data is the static registry published with the site (registry/index.json, one
6
+ // registry/<slug>.json and one components/<slug>.md per component, and the code to copy as
7
+ // files in registry/<slug>/). The source is its URL, or a local folder (handy while working on
8
+ // the library).
9
+ import fs from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import { pathToFileURL } from 'node:url';
12
+
13
+ export const VERSION = '0.1.0';
14
+ export const TARGETS = ['html', 'artifact', 'wordpress', 'shopify', 'react'];
15
+ const INDEX_TTL = 30 * 60 * 1000;
16
+ // Where a missing component is requested and voted for (a public repo, no code)
17
+ const REQUEST_URL = 'https://github.com/kodza-dev/requests/discussions/new?category=requests';
18
+
19
+ export const INSTRUCTIONS = `Kodza is a free library of premium UI components and effects in plain HTML, CSS and vanilla JavaScript (no framework, no build step), made to be pasted into any site: a plain HTML page, WordPress, Shopify, or a React app.
20
+ Use it when the user wants a polished UI element or effect (buttons, links, text effects, sliders and galleries, cards, navigation, scroll effects, preloaders and page transitions, forms, overlays, feedback, data display) without adding a framework: search_components to find one, then get_component for its exact code.
21
+ Rules for the code you get: if you can run shell commands, download the CSS and JS files with the commands of the component's Files section (byte for byte what the Kodza site copies) instead of retyping them, and call get_component with inline: false to leave them out of the answer; only the HTML is yours to adapt and paste. Without a shell, paste the code as it is (HTML, CSS, JS), including the generated theme guard at the end of the CSS (the :not(#_) lines keep the site's own CSS from restyling buttons and fields); restyle through its --kz-* CSS variables, modifier classes and data- attributes rather than by editing its CSS; one copy of the JS per page serves every instance; for content inserted after page load, dispatch kodza:init on document with { detail: { root } } (and kodza:destroy before removing it).
22
+ Components take the page's font; the Kodza look is Geist (Google Fonts). Each component comes with its dark-mode values (the "Dark mode" section): use them when the page has a dark theme. Building a Claude artifact (claude.ai)? Ask get_component with target "artifact": the artifact blocks images from other sites, among other limits.`;
23
+
24
+ export const TOOLS = [
25
+ {
26
+ name: 'search_components',
27
+ title: 'Search Kodza components',
28
+ description: 'Find Kodza UI components (plain HTML/CSS/vanilla JS) by what they do or look like, e.g. "button fill hover", "image slider", "sticky header", "date picker". Leave query empty to list a whole category. Returns slugs to pass to get_component.',
29
+ inputSchema: {
30
+ type: 'object',
31
+ properties: {
32
+ query: { type: 'string', description: 'Words describing the component or effect, in English' },
33
+ category: { type: 'string', description: 'buttons, links, text, images, cards, navigation, scroll, transitions, forms, overlays, feedback or data' },
34
+ limit: { type: 'integer', minimum: 1, maximum: 50, description: 'How many results, 12 by default' },
35
+ },
36
+ },
37
+ annotations: { readOnlyHint: true, openWorldHint: false },
38
+ },
39
+ {
40
+ name: 'get_component',
41
+ title: 'Get a Kodza component',
42
+ description: 'The full code of one Kodza component (HTML, CSS and, when it has one, vanilla JS), its API (CSS variables with defaults, modifier classes, data attributes, events), what each key does when it takes the keyboard, its dark-mode values and how to install it. Its CSS and JS are also served as exact files, with the curl commands to download them: if you can run shell commands, download them instead of retyping the code, and pass inline: false to leave them out of the answer. Give target to get only the instructions for that stack.',
43
+ inputSchema: {
44
+ type: 'object',
45
+ properties: {
46
+ slug: { type: 'string', description: 'The component slug, from search_components (e.g. "button-fill")' },
47
+ target: { type: 'string', enum: TARGETS, description: 'Where it goes: html (a plain page), artifact (a Claude artifact on claude.ai), wordpress, shopify or react. All of them when omitted' },
48
+ inline: { type: 'boolean', description: 'false: leave the CSS and JS out of the Code section (you download those files with the given commands). true by default, for assistants that cannot run commands' },
49
+ },
50
+ required: ['slug'],
51
+ },
52
+ annotations: { readOnlyHint: true, openWorldHint: false },
53
+ },
54
+ ];
55
+
56
+ // The arguments against the tool's schema: the message of the first mistake, or null
57
+ function invalid(tool, args) {
58
+ const { properties, required = [] } = tool.inputSchema;
59
+ for (const key of required) if (args[key] === undefined) return `${key} is required`;
60
+ for (const [key, value] of Object.entries(args)) {
61
+ const spec = properties[key];
62
+ if (!spec || value === undefined) continue;
63
+ const type = spec.type === 'integer' ? Number.isInteger(value) && 'integer' : typeof value;
64
+ if (type !== spec.type) return `${key} must be ${spec.type === 'integer' ? 'an' : 'a'} ${spec.type}`;
65
+ if (spec.enum && !spec.enum.includes(value)) return `${key} must be one of ${spec.enum.join(', ')}`;
66
+ if (value < spec.minimum || value > spec.maximum) return `${key} must be between ${spec.minimum} and ${spec.maximum}`;
67
+ }
68
+ return null;
69
+ }
70
+
71
+ // What the Kodza site counts (README, "What Kodza counts"): the assistant's name, as its client
72
+ // gives it when connecting, and the target asked for. Never the query, never the user's code
73
+ const ascii = (s) => String(s || '').replace(/[^\x20-\x7e]/g, '').slice(0, 64);
74
+ const headers = (client, target) => ({
75
+ 'user-agent': `kodza-mcp/${VERSION}`,
76
+ ...(ascii(client) && { 'x-kodza-client': ascii(client) }),
77
+ ...(target && { 'x-kodza-target': target }),
78
+ });
79
+
80
+ // ---------------------------------------------------------------- one registry
81
+
82
+ const registries = new Map();
83
+
84
+ // The tools on one source, kept (with its index) for the life of the process
85
+ export function registry(source) {
86
+ const key = String(source).replace(/\/$/, '');
87
+ if (!registries.has(key)) registries.set(key, open(key));
88
+ return registries.get(key);
89
+ }
90
+
91
+ function open(source) {
92
+ const remote = /^https?:\/\//.test(source);
93
+ // Where the component files are downloaded from: the source itself (a local folder as file://)
94
+ const filesBase = remote ? source : pathToFileURL(path.resolve(source)).href;
95
+ let index = null;
96
+ let loadedAt = 0;
97
+
98
+ async function read(file, client, target) {
99
+ if (remote) {
100
+ const res = await fetch(`${source}/${file}`, { headers: headers(client, target) });
101
+ if (res.status === 404) return null;
102
+ if (!res.ok) throw new Error(`Kodza registry: ${res.status} on ${file}`);
103
+ return res.text();
104
+ }
105
+ try {
106
+ return await fs.readFile(path.join(source, file), 'utf8');
107
+ } catch (err) {
108
+ if (err.code === 'ENOENT') return null;
109
+ throw err;
110
+ }
111
+ }
112
+
113
+ async function getIndex(client) {
114
+ if (!index || Date.now() - loadedAt > INDEX_TTL) {
115
+ const text = await read('registry/index.json', client);
116
+ try {
117
+ index = JSON.parse(text);
118
+ } catch {
119
+ throw new Error(`No Kodza registry at ${source}`);
120
+ }
121
+ loadedAt = Date.now();
122
+ }
123
+ return index;
124
+ }
125
+
126
+ async function search({ query = '', category, limit = 12 }, client) {
127
+ const { components, categories } = await getIndex(client);
128
+ if (category && !categories.some((c) => c.id === category)) {
129
+ return `Unknown category "${category}". Categories: ${categories.map((c) => `${c.id} (${c.count})`).join(', ')}.`;
130
+ }
131
+ const pool = components.filter((c) => !category || c.category === category);
132
+ const terms = queryTerms(query);
133
+ const found = terms.length
134
+ ? pool.map((c) => ({ c, s: score(c, terms) })).filter((r) => r.s > 0).sort((a, b) => b.s - a.s).map((r) => r.c)
135
+ : pool;
136
+ if (!found.length) {
137
+ return `Nothing in Kodza matches "${query}"${category ? ` in ${category}` : ''}. Try fewer or broader words, or browse a category: ${categories.map((c) => c.id).join(', ')}.\n\nStill missing? The user can request it, and vote for it, here: ${REQUEST_URL}`;
138
+ }
139
+ const shown = found.slice(0, Math.max(1, Math.min(limit, 50)));
140
+ return [
141
+ `${found.length} Kodza component${found.length > 1 ? 's' : ''}${terms.length ? ` for "${query}"` : ''}${category ? ` in ${category}` : ''}${found.length > shown.length ? ` (first ${shown.length})` : ''}:`,
142
+ '',
143
+ ...shown.map(line),
144
+ '',
145
+ 'Next: get_component with a slug for the code, the API and how to install it.',
146
+ ].join('\n');
147
+ }
148
+
149
+ async function getComponent({ slug, target, inline = true }, client) {
150
+ const clean = String(slug).trim().toLowerCase().replace(/^\/+|\/+$/g, '').replace(/^components\//, '');
151
+ // The index decides what exists (a host may answer any path with its home page)
152
+ const { components, site } = await getIndex(client);
153
+ let markdown = components.some((c) => c.slug === clean) ? await read(`components/${clean}.md`, client, target) : null;
154
+ if (markdown?.startsWith('# ')) {
155
+ // Files from the registry actually read, not from the public site
156
+ const from = `${String(site || '').replace(/\/$/, '')}/registry/${clean}/`;
157
+ if (site && !from.startsWith(`${filesBase}/`)) markdown = markdown.replaceAll(from, `${filesBase}/registry/${clean}/`);
158
+ return forTarget(inline ? markdown : withoutFileCode(markdown), target);
159
+ }
160
+ const hint = await search({ query: clean.replace(/[-_]/g, ' '), limit: 5 }, client);
161
+ return `No Kodza component "${slug}".\n\n${hint}`;
162
+ }
163
+
164
+ const run = { search_components: search, get_component: getComponent };
165
+
166
+ // One tools/call: its result, or null for a tool Kodza doesn't have. A wrong argument or a
167
+ // registry out of reach is a tool error the assistant can read, with nothing it sent in it
168
+ async function call(name, args, client) {
169
+ const tool = TOOLS.find((t) => t.name === name);
170
+ if (!tool) return null;
171
+ const given = args && typeof args === 'object' && !Array.isArray(args) ? args : {};
172
+ const wrong = invalid(tool, given);
173
+ if (wrong) return { content: [{ type: 'text', text: `Invalid arguments for ${name}: ${wrong}.` }], isError: true };
174
+ try {
175
+ return { content: [{ type: 'text', text: await run[name](given, client) }] };
176
+ } catch (err) {
177
+ const known = /^(Kodza registry|No Kodza registry)/.test(err.message);
178
+ return { content: [{ type: 'text', text: `${known ? err.message : 'Kodza registry out of reach'}. Try again in a moment.` }], isError: true };
179
+ }
180
+ }
181
+
182
+ return { call };
183
+ }
184
+
185
+ // ---------------------------------------------------------------- search
186
+
187
+ const STOP = new Set('a an the and or of for to in on with my our your some any that this it is be like nice cool simple component components effect effects ui kodza'.split(' '));
188
+ // Lowercase words, without accents, cut to a rough stem (loader, loading, loads → load)
189
+ const stem = (w) => (w.length > 4 ? w.replace(/(ings?|ers?|ed|es|s)$/, '') : w);
190
+ const words = (s) => s.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').split(/[^a-z0-9]+/).filter(Boolean).map(stem);
191
+ const queryTerms = (s) => s.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 1 && !STOP.has(w)).map(stem);
192
+
193
+ // How well a component answers the query: words matched first, then where they matched
194
+ function score(component, terms) {
195
+ const title = words(component.title);
196
+ const slug = words(component.slug);
197
+ const tags = component.tags.flatMap(words);
198
+ const description = words(component.description);
199
+ const hit = (list, term) => list.some((w) => w === term || (term.length > 3 && (w.startsWith(term) || term.startsWith(w) && w.length > 3)));
200
+ let matched = 0;
201
+ let points = 0;
202
+ for (const term of terms) {
203
+ const p = (hit(title, term) ? 6 : 0) + (hit(slug, term) ? 3 : 0) + (hit(tags, term) ? 4 : 0) + (hit(description, term) ? 1 : 0) + (component.category === term ? 2 : 0);
204
+ if (p) matched++;
205
+ points += p;
206
+ }
207
+ // At least half of the words have to match somewhere
208
+ return matched * 2 >= terms.length ? matched * 100 + points : 0;
209
+ }
210
+
211
+ const line = (c) => `- \`${c.slug}\` — **${c.title}** (${c.category}${c.js ? '' : ', CSS only'}): ${c.description}`;
212
+
213
+ // ---------------------------------------------------------------- one component
214
+
215
+ // Keep only the integration notes for the target the assistant is working with
216
+ function forTarget(markdown, target) {
217
+ if (!target) return markdown;
218
+ // An artifact is a plain page with its own limits: it gets both sections
219
+ const labels = { html: ['Plain HTML page'], artifact: ['Plain HTML page', 'Claude artifact'], wordpress: ['WordPress'], shopify: ['Shopify'], react: ['React / Next.js'] };
220
+ return markdown.replace(/## Use it\n([\s\S]*?)(?=\n## Code)/, (block) => {
221
+ const keep = block.split(/\n(?=### )/).filter((part) => !part.startsWith('### ') || labels[target].some((label) => part.startsWith(`### ${label}`)));
222
+ return keep.join('\n');
223
+ });
224
+ }
225
+
226
+ // Without the CSS and JS blocks of the Code section: the assistant downloads those files
227
+ function withoutFileCode(markdown) {
228
+ const at = markdown.indexOf('\n## Code\n');
229
+ if (at < 0 || !markdown.includes('\n## Files\n')) return markdown;
230
+ const left = [];
231
+ const code = markdown.slice(at).replace(/\n```(css|js)\n[\s\S]*?\n```\n/g, (block, lang) => (left.push(lang.toUpperCase()), ''));
232
+ const note = `\nThe ${left.join(' and the ')} ${left.length > 1 ? 'are' : 'is'} left out here (inline: false): download ${left.length > 1 ? 'them' : 'it'} with the commands of the Files section. The HTML below is the part to adapt and paste.\n`;
233
+ return markdown.slice(0, at) + code.replace('\n## Code\n', `\n## Code\n${note}`);
234
+ }
package/package.json CHANGED
@@ -1,6 +1,34 @@
1
1
  {
2
2
  "name": "kodza-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Kodza: free premium UI components in plain HTML, CSS and vanilla JS, ready for AI coding assistants.",
5
+ "mcpName": "dev.kodza/kodza",
6
+ "homepage": "https://kodza.dev/docs/",
7
+ "type": "module",
8
+ "bin": {
9
+ "kodza-mcp": "index.js"
10
+ },
11
+ "files": [
12
+ "index.js",
13
+ "kodza.js",
14
+ "http.js",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "license": "SEE LICENSE IN LICENSE",
22
+ "keywords": [
23
+ "mcp",
24
+ "ui",
25
+ "components",
26
+ "vanilla",
27
+ "css",
28
+ "wordpress"
29
+ ],
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.32.1",
32
+ "zod": "^4.6.7"
33
+ }
34
+ }