@kitn.ai/ui
Version:
Framework-agnostic, Shadow-DOM web components for building AI chat interfaces — works in React, Vue, Angular, Svelte, or plain HTML. Authored in SolidJS.
165 lines (144 loc) • 6.63 kB
text/typescript
import { z } from 'zod';
import type { Tool } from './types';
import { getElement, listElements } from '../manifest';
/**
* component_reference — look up AI/UI (kai-*) web components, their props,
* events, attributes, and CSS custom properties from the live Custom Elements
* Manifest (dist/custom-elements.json).
*
* Backed by manifest.ts which resolves the CEM in both the bundled bin
* (dist/mcp.es.js sibling) and the Vitest source context (walks up to find
* dist/custom-elements.json in the repo root).
*/
/** Types that warrant the JS-property contract note. */
const JS_ONLY_TYPE_PATTERNS = /\[\]|\{|Record</;
function isJsOnlyType(typeText: string | undefined): boolean {
return typeText ? JS_ONLY_TYPE_PATTERNS.test(typeText) : false;
}
function formatReference(tag: string): string {
const el = getElement(tag);
if (!el) {
const all = listElements();
const sample = all.slice(0, 5).join(', ');
return (
`Unknown element: ${tag}\n\n` +
`Valid tags include: ${sample} (and ${Math.max(0, all.length - 5)} more).\n` +
`Call component_reference with no name (or name: "list") to list all ${all.length} elements.`
);
}
const lines: string[] = [];
// ── Header ─────────────────────────────────────────────────────────────────
lines.push(`## <${tag}>`);
if (el.description) {
lines.push('', el.description.trim());
}
// ── Contract note ──────────────────────────────────────────────────────────
lines.push(
'',
'### AI/UI contract',
'`kai-*` elements accept **array and object data as JavaScript properties** ' +
'(set in JavaScript via `el.property = value`, not as HTML attributes). ' +
'Events are native CustomEvents — listen with `el.addEventListener("event-name", handler)` ' +
'and read `event.detail` for the payload.',
);
// ── Public JS properties ───────────────────────────────────────────────────
const publicFields = (el.members ?? []).filter(
(m) => m.kind === 'field' && m.privacy === 'public',
);
if (publicFields.length > 0) {
lines.push('', '### Props (JavaScript properties)');
for (const field of publicFields) {
const type = field.type?.text ?? 'unknown';
const jsOnly = isJsOnlyType(type);
const desc = field.description?.trim() ?? '';
const note = jsOnly ? ' ⚑ set as a JS property, not an HTML attribute' : '';
lines.push(`- **${field.name}** \`${type}\`${note}`);
if (desc) {
lines.push(` ${desc}`);
}
}
}
// ── HTML attributes ────────────────────────────────────────────────────────
const attrs = el.attributes ?? [];
if (attrs.length > 0) {
lines.push('', '### Attributes (HTML-safe)');
for (const attr of attrs) {
const type = attr.type?.text ?? 'unknown';
const desc = attr.description?.trim() ?? '';
lines.push(`- **${attr.name}** \`${type}\``);
if (desc) {
lines.push(` ${desc}`);
}
}
}
// ── Events ────────────────────────────────────────────────────────────────
const events = el.events ?? [];
if (events.length > 0) {
lines.push('', '### Events (CustomEvent, listen via addEventListener)');
for (const ev of events) {
const desc = ev.description?.trim() ?? '';
lines.push(`- **${ev.name}** — ${desc}`);
}
}
// ── CSS custom properties ─────────────────────────────────────────────────
const cssProps = el.cssProperties ?? [];
if (cssProps.length > 0) {
lines.push('', '### CSS custom properties');
for (const prop of cssProps) {
const desc = prop.description?.trim() ?? '';
const def = prop.default ? ` (default: ${prop.default})` : '';
lines.push(`- **${prop.name}**${def}${desc ? ` — ${desc}` : ''}`);
}
}
// ── Composition slots ──────────────────────────────────────────────────────
const slots = el.slots ?? [];
if (slots.length > 0) {
lines.push(
'',
'### Composition slots',
'Project your own markup into these named regions — add a light-DOM child ' +
'with a `slot="…"` attribute (e.g. `<div slot="sidebar">…</div>`).',
);
for (const slot of slots) {
const desc = slot.description?.trim() ?? '';
lines.push(`- **${slot.name}**${desc ? ` — ${desc}` : ''}`);
}
}
// ── Styleable parts (::part) ───────────────────────────────────────────────
const parts = el.cssParts ?? [];
if (parts.length > 0) {
lines.push(
'',
'### Styleable parts (`::part`)',
'Restyle these from outside the Shadow DOM via `' + tag + '::part(name) { … }`.',
);
for (const part of parts) {
const desc = part.description?.trim() ?? '';
lines.push(`- **${part.name}**${desc ? ` — ${desc}` : ''}`);
if (part.recipe) {
lines.push(' ```css', ` ${part.recipe}`, ' ```');
}
}
}
return lines.join('\n');
}
export const reference: Tool = {
name: 'component_reference',
description:
'Look up AI/UI (kai-*) web components: their tags, props, events, and usage examples.',
inputSchema: z.object({ name: z.string().optional() }),
handler: async (args: Record<string, unknown>) => {
const name = typeof args.name === 'string' ? args.name.trim() : undefined;
let text: string;
if (!name || name === 'list') {
const tags = listElements();
text =
`AI/UI elements (${tags.length} total):\n\n` +
tags.map((t) => ` ${t}`).join('\n') +
'\n\nCall component_reference with a specific name (e.g. { name: "kai-chat" }) for full API details.';
} else {
text = formatReference(name);
}
return { content: [{ type: 'text' as const, text }] };
},
};