@c2n/mcp 0.0.7

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/render.js ADDED
@@ -0,0 +1,181 @@
1
+ /** Compact markdown renderers for tool results. */
2
+ import { groupCssProperties } from "./lib/group-css.js";
3
+ export const MAX_CHARS = 12_000;
4
+ export function truncate(text, max = MAX_CHARS) {
5
+ return text.length <= max ? text : `${text.slice(0, max)}\n\n… (truncated at ${max} characters; narrow the request with a filter, limit or offset)`;
6
+ }
7
+ function table(headers, rows) {
8
+ const esc = (v) => v.replace(/\|/g, '\\|').replace(/\n/g, ' ');
9
+ return [`| ${headers.join(' | ')} |`, `| ${headers.map(() => '---').join(' | ')} |`, ...rows.map((r) => `| ${r.map(esc).join(' | ')} |`)].join('\n');
10
+ }
11
+ function code(value) {
12
+ return value === undefined || value === '' ? '—' : `\`${value}\``;
13
+ }
14
+ export function renderComponentList(components, installed) {
15
+ const rows = components.map((c) => [
16
+ c.tagPattern ? `\`${c.tagPattern}\` (${c.icons?.length ?? 0} icons)` : c.elements.map((e) => `\`${e.tag}\``).join(', '),
17
+ c.package,
18
+ c.category,
19
+ c.description || c.intro || '',
20
+ installed(c.package)?.version ?? '—',
21
+ ]);
22
+ return `${components.length} components\n\n${table(['Tag', 'Package', 'Category', 'Description', 'Installed'], rows)}\n\nNext: \`get_component\` for the API, \`get_examples\` for markup, \`get_theme\` before writing CSS.`;
23
+ }
24
+ export function renderComponent(component, options) {
25
+ const { include, installed, concrete } = options;
26
+ const out = [];
27
+ const primary = concrete ?? { tag: component.elements[0].tag, modulePath: component.elements[0].modulePath, className: component.elements[0].className };
28
+ out.push(`# ${component.title} (${primary.tag})`);
29
+ out.push(component.description || component.intro || '');
30
+ out.push('');
31
+ out.push(`- Package: \`${component.package}\` ${installed ? `(installed ${installed.version})` : `— not installed. \`${component.install.npm}\``}`);
32
+ out.push(`- Status: ${component.status} · Category: ${component.category} · Docs: ${component.docsUrl}`);
33
+ out.push(`- Register: \`import '${primary.modulePath}'\` · Class: \`import { ${primary.className} } from '${primary.modulePath}'\``);
34
+ if (component.tagPattern)
35
+ out.push(`- Icon set: tags follow \`${component.tagPattern}\`, ${component.icons?.length} icons (use \`search_components\` with the icon name).`);
36
+ for (const element of component.elements) {
37
+ const api = installed?.elements?.get(element.tag) ?? element;
38
+ const heading = component.elements.length > 1 || component.tagPattern ? `\n## ${element.tag}\n` : '';
39
+ out.push(heading);
40
+ if (element.description && component.elements.length > 1)
41
+ out.push(element.description);
42
+ if (include.has('attributes') && api.attributes.length) {
43
+ out.push('\n### Attributes\n');
44
+ out.push(table(['Name', 'Type', 'Default', 'Description'], api.attributes.map((a) => [code(a.name), code(a.type), code(a.default), a.description ?? ''])));
45
+ }
46
+ if (include.has('slots') && api.slots.length) {
47
+ out.push('\n### Slots\n');
48
+ out.push(table(['Slot', 'Description'], api.slots.map((s) => [code(s.name || '(default)'), s.description ?? ''])));
49
+ }
50
+ if (include.has('events') && api.events.length) {
51
+ out.push('\n### Events\n');
52
+ out.push(table(['Event', 'Type', 'Description'], api.events.map((e) => [code(e.name), code(e.type), e.description ?? ''])));
53
+ }
54
+ if (include.has('parts') && api.cssParts.length) {
55
+ out.push('\n### CSS parts\n');
56
+ out.push('Use with `::part(name)` when CSS custom properties do not cover the required change.\n');
57
+ out.push(table(['Part', 'Description'], api.cssParts.map((part) => [code(part.name), part.description ?? ''])));
58
+ }
59
+ if (include.has('css') && api.cssProperties.length) {
60
+ out.push('\n### CSS variables\n');
61
+ out.push('Set them on the element, a class or any ancestor. `token` = the `--c2-theme--*` token the base theme maps the variable to.\n');
62
+ out.push(renderCssGroups(api.cssProperties.map((p) => ('part' in p ? p : withParsedName(p)))));
63
+ }
64
+ }
65
+ if (include.has('composition')) {
66
+ const { internal, slotted, usedBy } = component.composition;
67
+ if (internal.length || slotted.length || usedBy.length) {
68
+ out.push('\n## Composition\n');
69
+ if (internal.length)
70
+ out.push(`- Renders internally: ${internal.map((t) => `\`${t}\``).join(', ')} (their variables can be set on this element)`);
71
+ if (slotted.length)
72
+ out.push(`- Expects slotted: ${slotted.map((t) => `\`${t}\``).join(', ')}`);
73
+ if (usedBy.length)
74
+ out.push(`- Used by: ${usedBy.map((t) => `\`${t}\``).join(', ')}`);
75
+ }
76
+ }
77
+ if (include.has('presets') && component.presets?.items.length) {
78
+ out.push('\n## Presets\n');
79
+ out.push(component.presets.items.map((p) => `- **${p.name}**${p.description ? ` — ${p.description}` : ''} (${Object.keys(p.css).length} variables)`).join('\n'));
80
+ out.push('\nUse `get_presets` for the values, `generate_variant` with `preset` to turn one into code.');
81
+ }
82
+ out.push(`\nExamples: \`get_examples\` with \`tag: "${primary.tag}"\` (${component.examples.length} available${component.hasGallery ? ', including gallery variants' : ''}).`);
83
+ return out.filter((l) => l !== undefined).join('\n');
84
+ }
85
+ function withParsedName(p) {
86
+ const match = /^--(c2-[a-z0-9-]+?)(?:__(.+?))?--(-?[a-z-]+)$/.exec(p.name);
87
+ return {
88
+ name: p.name,
89
+ type: typeof p.type === 'string' ? p.type : (p.type?.text ?? ''),
90
+ default: p.default,
91
+ part: match?.[2]?.split('__')[0] ?? '',
92
+ state: match?.[2]?.split('__').slice(1).join('/') || undefined,
93
+ property: match?.[3] ?? p.name,
94
+ description: p.description,
95
+ };
96
+ }
97
+ export function renderCssGroups(properties) {
98
+ const out = [];
99
+ for (const group of groupCssProperties(properties)) {
100
+ out.push(`#### ${group.part || 'root'}`);
101
+ for (const { state, properties: props } of group.states) {
102
+ if (state)
103
+ out.push(`*${state}*`);
104
+ out.push(props
105
+ .map((p) => `- \`${p.name}\`${p.default ? ` = \`${p.default}\`` : ''}${p.type ? ` (${p.type})` : ''}${p.token ? ` → \`${p.token}\`` : ''}${p.description ? ` — ${p.description}` : ''}`)
106
+ .join('\n'));
107
+ }
108
+ }
109
+ return out.join('\n');
110
+ }
111
+ export function renderExamples(component, examples, total, offset) {
112
+ const out = [`# ${component.title} examples (${offset + 1}–${offset + examples.length} of ${total})`, ''];
113
+ for (const ex of examples) {
114
+ out.push(`#### ${ex.label}${ex.kind === 'gallery' ? ` (gallery${ex.section ? ` · ${ex.section}` : ''}${ex.isDefault ? ' · default without component overrides' : ''})` : ex.kind === 'usage' ? ' (usage)' : ''}`);
115
+ if (ex.tags?.length)
116
+ out.push(`Components: ${ex.tags.map((tag) => `\`${tag}\``).join(', ')}`);
117
+ if (ex.description)
118
+ out.push(`Demonstrates: ${ex.description}`);
119
+ if (ex.useWhen)
120
+ out.push(`Use when: ${ex.useWhen}`);
121
+ if (ex.accessibility)
122
+ out.push(`Accessibility: ${ex.accessibility}`);
123
+ out.push('```html');
124
+ out.push(ex.html.length > 4000 ? `${ex.html.slice(0, 4000)}\n<!-- … truncated -->` : ex.html);
125
+ out.push('```');
126
+ if (ex.css) {
127
+ out.push('```css');
128
+ out.push(ex.css);
129
+ out.push('```');
130
+ }
131
+ out.push('');
132
+ }
133
+ if (offset + examples.length < total)
134
+ out.push(`More: call again with \`offset: ${offset + examples.length}\`.`);
135
+ return out.join('\n');
136
+ }
137
+ export function renderPresets(component, names) {
138
+ if (!component.presets)
139
+ return `${component.title} has no curated presets. Use \`get_examples\` (gallery variants) instead.`;
140
+ const items = names ? component.presets.items.filter((p) => p.name.toLowerCase().includes(names.toLowerCase())) : component.presets.items;
141
+ const out = [`# ${component.title} presets`, '', 'Base markup:', '```html', component.presets.html, '```', ''];
142
+ for (const preset of items) {
143
+ out.push(`## ${preset.name}${preset.description ? ` — ${preset.description}` : ''}`);
144
+ out.push('```css');
145
+ out.push(Object.entries(preset.css)
146
+ .map(([k, v]) => `${k}: ${v};`)
147
+ .join('\n'));
148
+ out.push('```');
149
+ if (preset.attributes && Object.keys(preset.attributes).length)
150
+ out.push(`Attributes: ${Object.entries(preset.attributes)
151
+ .map(([k, v]) => `\`${k}${v ? `="${v}"` : ''}\``)
152
+ .join(', ')}`);
153
+ out.push('');
154
+ }
155
+ return out.join('\n');
156
+ }
157
+ export function renderTheme(theme, registry, component) {
158
+ const out = [];
159
+ if (component) {
160
+ out.push(`# Theme mapping for ${component.title}`, '');
161
+ const rows = [];
162
+ for (const element of component.elements)
163
+ for (const p of element.cssProperties)
164
+ if (p.token)
165
+ rows.push([code(p.name), code(p.token), code(p.default)]);
166
+ out.push(rows.length ? table(['Component variable', 'Token', 'Default'], rows) : 'No variable of this component is mapped to a token; set its variables directly.');
167
+ out.push('', 'Variables without a token keep the component default; set them on a class for a variant.');
168
+ return out.join('\n');
169
+ }
170
+ out.push('# @c2n/theme', '');
171
+ out.push(`\`${theme.install.npm}\` then \`${theme.install.imports[0]}\` once at the app root (or \`${theme.install.imports[2]}\` alone and bridge your own tokens).`);
172
+ out.push('', `Dark mode: ${theme.darkMode}`, '');
173
+ out.push('## Tokens', '');
174
+ out.push(table(['Token', 'Light', 'Dark', 'Used by', 'Description'], theme.tokens.map((t) => [code(t.name), t.light === null ? '*unset*' : code(t.light), t.dark ? code(t.dark) : 'same', String(t.usedBy), t.description])));
175
+ out.push('', '## Map your own tokens', '', '```css', ':root {', ' --c2-theme--color-primary: var(--brand-600);', ' --c2-theme--color-surface: var(--surface);', ' --c2-theme--color-on-surface: var(--text);', ' --c2-theme--color-outline: var(--border);', ' --c2-theme--radius-md: var(--radius);', ' --c2-theme--font-family: var(--font-sans);', '}', '```');
176
+ out.push('', `Component variables always win over the base theme; \`get_theme\` with a \`tag\` lists one component's mapping. ${Object.keys(registry.components).length} components covered.`);
177
+ return out.join('\n');
178
+ }
179
+ export function elementApiNames(element) {
180
+ return { css: element.cssProperties.map((p) => p.name), attributes: element.attributes.map((a) => a.name) };
181
+ }
@@ -0,0 +1,8 @@
1
+ /** Tokenised scoring over component names, descriptions, attributes, slots, example labels and icon names. */
2
+ import type { ComponentEntry, Registry } from './registry-types.ts';
3
+ export interface SearchHit {
4
+ component: ComponentEntry;
5
+ score: number;
6
+ matches: string[];
7
+ }
8
+ export declare function searchComponents(registry: Registry, query: string, limit?: number): SearchHit[];
package/dist/search.js ADDED
@@ -0,0 +1,52 @@
1
+ function tokens(text) {
2
+ return text
3
+ .toLowerCase()
4
+ .split(/[^a-z0-9]+/)
5
+ .filter((t) => t.length > 1);
6
+ }
7
+ export function searchComponents(registry, query, limit = 10) {
8
+ const terms = tokens(query);
9
+ if (terms.length === 0)
10
+ return [];
11
+ const hits = [];
12
+ for (const component of Object.values(registry.components)) {
13
+ let score = 0;
14
+ const matches = new Set();
15
+ const fields = [
16
+ ['title', component.title, 6],
17
+ ['id', component.id, 6],
18
+ ['tag', component.elements.map((e) => e.tag).join(' '), 6],
19
+ ['description', `${component.description} ${component.intro ?? ''}`, 3],
20
+ ['category', component.category, 2],
21
+ ];
22
+ for (const element of component.elements) {
23
+ for (const a of element.attributes)
24
+ fields.push([`attr:${a.name}`, `${a.name} ${a.description ?? ''}`, 2]);
25
+ for (const s of element.slots)
26
+ fields.push([`slot:${s.name || 'default'}`, `${s.name} ${s.description ?? ''}`, 2]);
27
+ for (const e of element.events)
28
+ fields.push([`event:${e.name}`, `${e.name} ${e.description ?? ''}`, 2]);
29
+ }
30
+ for (const ex of component.examples)
31
+ if (ex.kind !== 'preview')
32
+ fields.push([`example:${ex.label}`, `${ex.label} ${ex.section ?? ''} ${ex.description ?? ''} ${ex.useWhen ?? ''} ${ex.accessibility ?? ''}`, 1]);
33
+ for (const icon of component.icons ?? [])
34
+ fields.push([`icon:${icon}`, icon, 3]);
35
+ for (const [label, text, weight] of fields) {
36
+ const haystack = tokens(text);
37
+ for (const term of terms) {
38
+ if (haystack.some((h) => h === term)) {
39
+ score += weight * 2;
40
+ matches.add(label);
41
+ }
42
+ else if (haystack.some((h) => h.includes(term) || term.includes(h))) {
43
+ score += weight;
44
+ matches.add(label);
45
+ }
46
+ }
47
+ }
48
+ if (score > 0)
49
+ hits.push({ component, score, matches: [...matches].slice(0, 6) });
50
+ }
51
+ return hits.sort((a, b) => b.score - a.score || a.component.title.localeCompare(b.component.title)).slice(0, limit);
52
+ }
@@ -0,0 +1,4 @@
1
+ /** The c2n MCP server: tools and resources over the bundled registry. */
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import type { Registry } from './registry-types.ts';
4
+ export declare function createServer(registry?: Registry): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,276 @@
1
+ /** The c2n MCP server: tools and resources over the bundled registry. */
2
+ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { z } from 'zod';
4
+ import { installedPackage } from "./installed.js";
5
+ import { generateCode } from "./lib/generate-code.js";
6
+ import { loadRegistry, resolveElement, suggest } from "./registry.js";
7
+ import { MAX_CHARS, elementApiNames, renderComponent, renderComponentList, renderExamples, renderPresets, renderTheme, truncate } from "./render.js";
8
+ import { searchComponents } from "./search.js";
9
+ const GUIDE_TOPICS = ['workflow', 'theming', 'variant-components', 'frameworks'];
10
+ const readOnly = { readOnlyHint: true, openWorldHint: false };
11
+ function text(value) {
12
+ return { content: [{ type: 'text', text: truncate(value) }] };
13
+ }
14
+ function json(value) {
15
+ return { content: [{ type: 'text', text: truncate(JSON.stringify(value, null, 2), MAX_CHARS * 4) }] };
16
+ }
17
+ function notFound(registry, tag) {
18
+ const candidates = [...Object.keys(registry.tagIndex), ...Object.keys(registry.components)];
19
+ const hints = suggest(candidates, tag);
20
+ return {
21
+ ...text(`Unknown component "${tag}".${hints.length ? ` Did you mean ${hints.map((h) => `\`${h}\``).join(', ')}?` : ''} Use \`list_components\` or \`search_components\`.`),
22
+ isError: true,
23
+ };
24
+ }
25
+ export function createServer(registry = loadRegistry()) {
26
+ const server = new McpServer({ name: 'c2n', version: registry.c2nVersion }, {
27
+ instructions: [
28
+ 'c2n web components (Lit custom elements, tag prefix c2-, npm scope @c2n).',
29
+ 'Start with list_components or search_components, then get_component for the API and get_examples for markup.',
30
+ 'Call get_theme before writing CSS: apps set ~35 --c2-theme--* tokens once; prefer per-component variables on a class or element, and use only documented ::part() hooks when variables cannot express the change.',
31
+ 'When a look repeats, call generate_variant (css | html | lit) instead of repeating inline styles.',
32
+ 'get_workflow_guide explains the application workflow, theming, variant components and framework notes.',
33
+ ].join(' '),
34
+ });
35
+ const formatSchema = z.enum(['markdown', 'json']).default('markdown').describe('Output format');
36
+ const tagSchema = z
37
+ .string()
38
+ .min(1)
39
+ .describe('Component tag, id or package: `c2-button`, `button`, `@c2n/button`, `c2-tab`, or a concrete icon tag such as `c2-feather-arrow-right`');
40
+ server.registerTool('list_components', {
41
+ title: 'List components',
42
+ description: 'Lists every c2n component (tag, package, category, one-line description, installed version in the current project).',
43
+ inputSchema: {
44
+ category: z
45
+ .enum(registry.categories)
46
+ .optional()
47
+ .describe('Only this category'),
48
+ keyword: z.string().optional().describe('Substring filter on tag, title or description'),
49
+ installedOnly: z.boolean().optional().describe('Only packages installed in the current project'),
50
+ format: formatSchema,
51
+ },
52
+ annotations: readOnly,
53
+ }, ({ category, keyword, installedOnly, format }) => {
54
+ const needle = keyword?.toLowerCase();
55
+ const components = Object.values(registry.components).filter((c) => (!category || c.category === category) &&
56
+ (!needle || `${c.id} ${c.title} ${c.description} ${c.elements.map((e) => e.tag).join(' ')}`.toLowerCase().includes(needle)) &&
57
+ (!installedOnly || installedPackage(c.package)));
58
+ if (format === 'json')
59
+ return json(components.map((c) => ({
60
+ id: c.id,
61
+ tags: c.elements.map((e) => e.tag),
62
+ tagPattern: c.tagPattern,
63
+ package: c.package,
64
+ category: c.category,
65
+ description: c.description,
66
+ installed: installedPackage(c.package)?.version ?? null,
67
+ })));
68
+ return text(renderComponentList(components, installedPackage));
69
+ });
70
+ server.registerTool('get_component', {
71
+ title: 'Get component API',
72
+ description: 'Full API of one component: install/import lines, attributes, slots, events, native CSS parts, CSS variables grouped by semantic target and state (with the theme token each follows), composition and preset names. Examples are served by get_examples.',
73
+ inputSchema: {
74
+ tag: tagSchema,
75
+ include: z
76
+ .array(z.enum(['attributes', 'slots', 'events', 'parts', 'css', 'composition', 'presets']))
77
+ .optional()
78
+ .describe('Sections to include (default: all)'),
79
+ format: formatSchema,
80
+ },
81
+ annotations: readOnly,
82
+ }, ({ tag, include, format }) => {
83
+ const resolved = resolveElement(registry, tag);
84
+ if (!resolved)
85
+ return notFound(registry, tag);
86
+ const installed = installedPackage(resolved.component.package);
87
+ if (format === 'json')
88
+ return json({
89
+ ...resolved.component,
90
+ examples: undefined,
91
+ installed: installed?.version ?? null,
92
+ resolved: { tag: resolved.tag, modulePath: resolved.modulePath, className: resolved.className },
93
+ });
94
+ return text(renderComponent(resolved.component, {
95
+ include: new Set(include ?? ['attributes', 'slots', 'events', 'parts', 'css', 'composition', 'presets']),
96
+ installed,
97
+ concrete: { tag: resolved.tag, modulePath: resolved.modulePath, className: resolved.className },
98
+ }));
99
+ });
100
+ server.registerTool('search_components', {
101
+ title: 'Search components',
102
+ description: 'Free-text search across component names, descriptions, attributes, slots, events, example labels and icon names. Use it to find the component for a UI need ("dropdown", "dialog", "avatar with status", "arrow icon").',
103
+ inputSchema: { query: z.string().min(2).describe('What you are looking for'), limit: z.number().int().min(1).max(25).default(10) },
104
+ annotations: readOnly,
105
+ }, ({ query, limit }) => {
106
+ const hits = searchComponents(registry, query, limit);
107
+ if (hits.length === 0)
108
+ return text(`No component matches "${query}". Try \`list_components\`.`);
109
+ return text(`${hits.length} result(s) for "${query}"\n\n` +
110
+ hits
111
+ .map((h) => `- **${h.component.title}** ${h.component.tagPattern ? `\`${h.component.tagPattern}\`` : h.component.elements.map((e) => `\`${e.tag}\``).join(', ')} (${h.component.package}) — ${h.component.description}\n matched: ${h.matches.join(', ')}`)
112
+ .join('\n'));
113
+ });
114
+ server.registerTool('get_examples', {
115
+ title: 'Get examples',
116
+ description: 'Markup examples of a component: usage rows from the docs, styled gallery variants (with intent, use-when and accessibility metadata when curated) and the landing preview.',
117
+ inputSchema: {
118
+ tag: tagSchema,
119
+ kind: z.enum(['usage', 'gallery', 'preview']).optional().describe('Only this kind'),
120
+ label: z.string().optional().describe('Substring filter on the example label'),
121
+ section: z.string().optional().describe('Gallery section filter (e.g. "Variants", "Sizes")'),
122
+ query: z.string().optional().describe('Search label, section, description, use case, accessibility note and component tags'),
123
+ limit: z.number().int().min(1).max(20).default(8),
124
+ offset: z.number().int().min(0).default(0),
125
+ format: formatSchema,
126
+ },
127
+ annotations: readOnly,
128
+ }, ({ tag, kind, label, section, query, limit, offset, format }) => {
129
+ const resolved = resolveElement(registry, tag);
130
+ if (!resolved)
131
+ return notFound(registry, tag);
132
+ const needle = query?.toLowerCase();
133
+ const all = resolved.component.examples.filter((e) => {
134
+ const searchable = `${e.label} ${e.section ?? ''} ${e.description ?? ''} ${e.useWhen ?? ''} ${e.accessibility ?? ''} ${(e.tags ?? []).join(' ')}`.toLowerCase();
135
+ return ((!kind || e.kind === kind) &&
136
+ (!label || e.label.toLowerCase().includes(label.toLowerCase())) &&
137
+ (!section || (e.section ?? '').toLowerCase().includes(section.toLowerCase())) &&
138
+ (!needle || searchable.includes(needle)));
139
+ });
140
+ const page = all.slice(offset, offset + limit);
141
+ if (format === 'json')
142
+ return json({ total: all.length, offset, examples: page });
143
+ return text(all.length ? renderExamples(resolved.component, page, all.length, offset) : `No examples match for ${resolved.tag}.`);
144
+ });
145
+ server.registerTool('get_presets', {
146
+ title: 'Get presets',
147
+ description: 'Curated presets of a component ("inspiration" looks): base markup plus each preset as CSS variable values and attributes.',
148
+ inputSchema: { tag: tagSchema, name: z.string().optional().describe('Substring filter on the preset name'), format: formatSchema },
149
+ annotations: readOnly,
150
+ }, ({ tag, name, format }) => {
151
+ const resolved = resolveElement(registry, tag);
152
+ if (!resolved)
153
+ return notFound(registry, tag);
154
+ if (format === 'json')
155
+ return json(resolved.component.presets ?? null);
156
+ return text(renderPresets(resolved.component, name));
157
+ });
158
+ server.registerTool('get_theme', {
159
+ title: 'Get theme tokens',
160
+ description: "The @c2n/theme design tokens (--c2-theme--*) with light/dark defaults, how to install and map your own tokens, and dark mode. With `tag`: which of that component's variables follow which token.",
161
+ inputSchema: { tag: tagSchema.optional(), format: formatSchema },
162
+ annotations: readOnly,
163
+ }, ({ tag, format }) => {
164
+ const resolved = tag ? resolveElement(registry, tag) : undefined;
165
+ if (tag && !resolved)
166
+ return notFound(registry, tag);
167
+ if (format === 'json')
168
+ return json(resolved
169
+ ? {
170
+ tag: resolved.tag,
171
+ mapping: Object.fromEntries(resolved.component.elements.flatMap((e) => e.cssProperties.filter((p) => p.token).map((p) => [p.name, p.token]))),
172
+ }
173
+ : registry.theme);
174
+ return text(renderTheme(registry.theme, registry, resolved?.component));
175
+ });
176
+ server.registerTool('generate_variant', {
177
+ title: 'Generate a variant component',
178
+ description: 'Turns CSS variable / attribute overrides of a component into copy-paste code: a CSS class, HTML + <style>, a Lit subclass registered under your own tag, or JSON. Validates variable and attribute names against the component API. Start from a preset with `preset`.',
179
+ inputSchema: {
180
+ tag: tagSchema,
181
+ name: z
182
+ .string()
183
+ .regex(/^[a-z][a-z0-9]*(-[a-z0-9]+)+$/, 'kebab-case with at least one hyphen, e.g. app-danger-button')
184
+ .describe('Class / tag name of the variant (kebab-case, must contain a hyphen, not c2-*)'),
185
+ css: z
186
+ .record(z.string(), z.string())
187
+ .optional()
188
+ .describe('Component CSS variables to fix, e.g. {"--c2-button__container--background-color": "var(--c2-theme--color-error)"}'),
189
+ attributes: z.record(z.string(), z.string()).optional().describe('Attributes to fix, e.g. {"running": "true"}'),
190
+ preset: z.string().optional().describe('Name of a curated preset to start from (its values are merged under `css`)'),
191
+ html: z.string().optional().describe('Base markup; defaults to the preset markup or `<tag>Label</tag>`'),
192
+ format: z.enum(['css', 'html', 'lit', 'json', 'all']).default('all'),
193
+ },
194
+ annotations: readOnly,
195
+ }, ({ tag, name, css, attributes, preset, html, format }) => {
196
+ const resolved = resolveElement(registry, tag);
197
+ if (!resolved)
198
+ return notFound(registry, tag);
199
+ if (name.startsWith('c2-'))
200
+ return { ...text('Variant names must not start with `c2-`; use your own prefix (app-, site-, my-).'), isError: true };
201
+ const presetItem = preset ? resolved.component.presets?.items.find((p) => p.name.toLowerCase() === preset.toLowerCase()) : undefined;
202
+ if (preset && !presetItem)
203
+ return {
204
+ ...text(`Unknown preset "${preset}" for ${resolved.tag}. Available: ${(resolved.component.presets?.items ?? []).map((p) => p.name).join(', ') || 'none'}.`),
205
+ isError: true,
206
+ };
207
+ const changes = { css: { ...(presetItem?.css ?? {}), ...(css ?? {}) }, attributes: { ...(presetItem?.attributes ?? {}), ...(attributes ?? {}) } };
208
+ const api = elementApiNames(resolved.element);
209
+ const warnings = [];
210
+ for (const variable of Object.keys(changes.css)) {
211
+ if (!api.css.includes(variable) && !variable.startsWith('--c2-theme--')) {
212
+ const hint = suggest(api.css, variable, 2);
213
+ warnings.push(`Unknown variable \`${variable}\`${hint.length ? ` (did you mean ${hint.map((h) => `\`${h}\``).join(', ')}?)` : ''}`);
214
+ }
215
+ }
216
+ for (const attribute of Object.keys(changes.attributes)) {
217
+ if (!api.attributes.includes(attribute) && !attribute.startsWith('data-') && !attribute.startsWith('aria-'))
218
+ warnings.push(`Unknown attribute \`${attribute}\` (attributes: ${api.attributes.join(', ') || 'none'})`);
219
+ }
220
+ const baseHtml = html ?? resolved.component.presets?.html ?? `<${resolved.tag}>Label</${resolved.tag}>`;
221
+ const input = { tag: resolved.tag, html: baseHtml, changes, name, className: resolved.className, modulePath: resolved.modulePath };
222
+ const formats = format === 'all' ? ['css', 'html', 'lit'] : [format];
223
+ const out = [];
224
+ if (warnings.length)
225
+ out.push(`> ${warnings.join('\n> ')}\n`);
226
+ for (const f of formats) {
227
+ out.push(`## ${f === 'lit' ? 'Lit subclass' : f === 'css' ? 'CSS class variant' : f === 'html' ? 'HTML + style' : 'JSON preset'}`);
228
+ out.push(`\`\`\`${f === 'lit' ? 'ts' : f}`);
229
+ out.push(generateCode(f, input));
230
+ out.push('```');
231
+ }
232
+ return text(out.join('\n'));
233
+ });
234
+ server.registerTool('get_workflow_guide', {
235
+ title: 'Get workflow guide',
236
+ description: 'The c2n application workflow guides: `workflow` (theme once, use tags, name what repeats), `theming`, `variant-components`, `frameworks` (plain HTML, Lit, Astro, React, Vue, SSR).',
237
+ inputSchema: { topic: z.enum(GUIDE_TOPICS).default('workflow') },
238
+ annotations: readOnly,
239
+ }, ({ topic }) => text(registry.guides[topic]));
240
+ server.registerResource('components', 'c2n://components', { title: 'c2n components', description: 'All components: id, tags, package, category, description', mimeType: 'application/json' }, (uri) => ({
241
+ contents: [
242
+ {
243
+ uri: uri.href,
244
+ mimeType: 'application/json',
245
+ text: JSON.stringify(Object.values(registry.components).map((c) => ({
246
+ id: c.id,
247
+ tags: c.elements.map((e) => e.tag),
248
+ tagPattern: c.tagPattern,
249
+ package: c.package,
250
+ category: c.category,
251
+ description: c.description,
252
+ })), null, 2),
253
+ },
254
+ ],
255
+ }));
256
+ server.registerResource('component', new ResourceTemplate('c2n://components/{tag}', {
257
+ list: () => ({ resources: Object.keys(registry.tagIndex).map((tag) => ({ uri: `c2n://components/${tag}`, name: tag, mimeType: 'application/json' })) }),
258
+ }), { title: 'c2n component', description: 'Full registry entry of one component (API, composition, presets, examples)', mimeType: 'application/json' }, (uri, { tag }) => {
259
+ const resolved = resolveElement(registry, String(tag));
260
+ if (!resolved)
261
+ throw new Error(`Unknown component ${String(tag)}`);
262
+ return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(resolved.component, null, 2) }] };
263
+ });
264
+ server.registerResource('theme', 'c2n://theme', { title: '@c2n/theme tokens', description: 'Design tokens and the component variable → token mapping', mimeType: 'application/json' }, (uri) => ({
265
+ contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(registry.theme, null, 2) }],
266
+ }));
267
+ server.registerResource('guide', new ResourceTemplate('c2n://guide/{topic}', {
268
+ list: () => ({ resources: GUIDE_TOPICS.map((topic) => ({ uri: `c2n://guide/${topic}`, name: topic, mimeType: 'text/markdown' })) }),
269
+ }), { title: 'c2n guide', description: 'Application workflow guides', mimeType: 'text/markdown' }, (uri, { topic }) => {
270
+ const guide = registry.guides[String(topic)];
271
+ if (!guide)
272
+ throw new Error(`Unknown guide ${String(topic)}`);
273
+ return { contents: [{ uri: uri.href, mimeType: 'text/markdown', text: guide }] };
274
+ });
275
+ return server;
276
+ }
package/package.json ADDED
@@ -0,0 +1,105 @@
1
+ {
2
+ "name": "@c2n/mcp",
3
+ "version": "0.0.7",
4
+ "description": "MCP server exposing the c2n web components (API, examples, presets, theme tokens, variant generation) to AI agents",
5
+ "type": "module",
6
+ "bin": {
7
+ "c2n-mcp": "dist/cli.js"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./registry.json": "./data/registry.json"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "data"
19
+ ],
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "keywords": [
24
+ "mcp",
25
+ "model context protocol",
26
+ "web components",
27
+ "lit",
28
+ "c2n"
29
+ ],
30
+ "license": "MIT",
31
+ "author": "code2nguyen@gmail.com",
32
+ "publishConfig": {
33
+ "registry": "https://registry.npmjs.org",
34
+ "access": "public"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/code2nguyen/web-components.git"
39
+ },
40
+ "scripts": {
41
+ "build": "wireit",
42
+ "build:registry": "wireit",
43
+ "build:tsc": "wireit",
44
+ "predev": "npm run build:registry",
45
+ "start": "node dist/cli.js",
46
+ "dev": "node src/cli.ts",
47
+ "preinspect": "npm run build:registry",
48
+ "inspect": "npx -y @modelcontextprotocol/inspector node src/cli.ts",
49
+ "presmoke": "npm run build",
50
+ "prepack": "npm run build",
51
+ "smoke": "node scripts/smoke.ts"
52
+ },
53
+ "wireit": {
54
+ "build:registry": {
55
+ "dependencies": [
56
+ "../../..:build"
57
+ ],
58
+ "command": "node scripts/build-registry.ts",
59
+ "files": [
60
+ "scripts/build-registry.ts",
61
+ "src/registry-types.ts",
62
+ "src/lib/css-var-name.ts",
63
+ "../../components/*/custom-elements.json",
64
+ "../../icons/*/custom-elements.json",
65
+ "../../../open-packages/*/custom-elements.json",
66
+ "../../../apps/ui/src/content/**/*.mdx",
67
+ "../../../apps/ui/src/data/component-presets.ts",
68
+ "../../../apps/ui/src/data/component-previews.ts",
69
+ "../theme/dist/tokens.json",
70
+ "../skill/skills/c2n-components/references/*.md",
71
+ "!../skill/skills/c2n-components/references/component-catalog.md",
72
+ "../../../lerna.json",
73
+ "../../../package.json"
74
+ ],
75
+ "output": [
76
+ "data/registry.json"
77
+ ]
78
+ },
79
+ "build:tsc": {
80
+ "command": "tsc -p tsconfig.json",
81
+ "files": [
82
+ "src/**/*.ts",
83
+ "tsconfig.json"
84
+ ],
85
+ "output": [
86
+ "dist/**"
87
+ ]
88
+ },
89
+ "build": {
90
+ "dependencies": [
91
+ "build:registry",
92
+ "build:tsc"
93
+ ]
94
+ }
95
+ },
96
+ "dependencies": {
97
+ "@modelcontextprotocol/sdk": "1.30.0",
98
+ "zod": "4.5.4"
99
+ },
100
+ "devDependencies": {
101
+ "@c2n/config": "*",
102
+ "custom-elements-manifest": "2.1.0"
103
+ },
104
+ "gitHead": "c8db398a27f7cd417d665fdf5da455fee2d4e910"
105
+ }