@greese/mcp 0.0.0-stage → 0.1.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 +21 -0
- package/README.md +62 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +7 -0
- package/dist/data.d.ts +60 -0
- package/dist/data.js +3 -0
- package/dist/data.json +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/search.d.ts +17 -0
- package/dist/search.js +58 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.js +301 -0
- package/package.json +66 -4
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/search.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { GreeseData } from './data.ts';
|
|
2
|
+
export interface Hit {
|
|
3
|
+
kind: 'component' | 'guide' | 'template' | 'theme';
|
|
4
|
+
name: string;
|
|
5
|
+
summary: string;
|
|
6
|
+
score: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Scores text against a query: every query word must appear; a match in the
|
|
10
|
+
* name counts most, then the summary, then the body. Small and predictable,
|
|
11
|
+
* which suits a few hundred documents better than an index.
|
|
12
|
+
*/
|
|
13
|
+
export declare const score: (query: string, name: string, summary: string, body?: string) => number;
|
|
14
|
+
/** The first sentence of a description (TSDoc wraps lines mid-sentence). */
|
|
15
|
+
export declare const firstSentence: (text: string) => string;
|
|
16
|
+
/** Components, guides, templates and themes that match, best first. */
|
|
17
|
+
export declare const search: (data: GreeseData, query: string, limit?: number) => Hit[];
|
package/dist/search.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const words = (text) => text.toLowerCase().match(/[a-z0-9]+/g) ?? [];
|
|
2
|
+
/**
|
|
3
|
+
* Scores text against a query: every query word must appear; a match in the
|
|
4
|
+
* name counts most, then the summary, then the body. Small and predictable,
|
|
5
|
+
* which suits a few hundred documents better than an index.
|
|
6
|
+
*/
|
|
7
|
+
export const score = (query, name, summary, body = '') => {
|
|
8
|
+
const terms = words(query);
|
|
9
|
+
if (!terms.length)
|
|
10
|
+
return 0;
|
|
11
|
+
const fields = [
|
|
12
|
+
[name.toLowerCase(), 10],
|
|
13
|
+
[summary.toLowerCase(), 3],
|
|
14
|
+
[body.toLowerCase(), 1],
|
|
15
|
+
];
|
|
16
|
+
let total = 0;
|
|
17
|
+
for (const term of terms) {
|
|
18
|
+
let best = 0;
|
|
19
|
+
for (const [text, weight] of fields) {
|
|
20
|
+
if (text === term)
|
|
21
|
+
best = Math.max(best, weight * 3);
|
|
22
|
+
else if (text.includes(term))
|
|
23
|
+
best = Math.max(best, weight);
|
|
24
|
+
}
|
|
25
|
+
if (!best)
|
|
26
|
+
return 0;
|
|
27
|
+
total += best;
|
|
28
|
+
}
|
|
29
|
+
return total;
|
|
30
|
+
};
|
|
31
|
+
/** The first sentence of a description (TSDoc wraps lines mid-sentence). */
|
|
32
|
+
export const firstSentence = (text) => {
|
|
33
|
+
const flat = text
|
|
34
|
+
.split(/\n\s*\n/)[0]
|
|
35
|
+
.replace(/\s+/g, ' ')
|
|
36
|
+
.trim();
|
|
37
|
+
return /^.*?[.!?](?=\s|$)/.exec(flat)?.[0] ?? flat;
|
|
38
|
+
};
|
|
39
|
+
/** Components, guides, templates and themes that match, best first. */
|
|
40
|
+
export const search = (data, query, limit = 10) => {
|
|
41
|
+
const hits = [];
|
|
42
|
+
const add = (kind, name, summary, body) => {
|
|
43
|
+
const s = score(query, name, summary, body);
|
|
44
|
+
if (s)
|
|
45
|
+
hits.push({ kind, name, summary: firstSentence(summary), score: s });
|
|
46
|
+
};
|
|
47
|
+
for (const c of data.components)
|
|
48
|
+
add('component', c.name, c.description, c.props.map((p) => `${p.name} ${p.description}`).join(' '));
|
|
49
|
+
for (const g of data.guides)
|
|
50
|
+
add('guide', g.name, g.title, g.text);
|
|
51
|
+
for (const t of data.templates)
|
|
52
|
+
add('template', t.id, t.description, t.source);
|
|
53
|
+
for (const t of data.themes)
|
|
54
|
+
add('theme', t.id, t.description, t.scss);
|
|
55
|
+
return hits
|
|
56
|
+
.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name))
|
|
57
|
+
.slice(0, limit);
|
|
58
|
+
};
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { ComponentDoc, GreeseData } from './data.ts';
|
|
3
|
+
/** A component as Markdown: what it is, its props, what a theme can target, and an example. */
|
|
4
|
+
export declare const componentMarkdown: (c: ComponentDoc, example?: boolean) => string;
|
|
5
|
+
/**
|
|
6
|
+
* The greese ui MCP server: tools and resources over the data baked at build
|
|
7
|
+
* time (ADR 0022). Connect it to any transport; the CLI uses stdio.
|
|
8
|
+
*/
|
|
9
|
+
export declare const createGreeseServer: (data: GreeseData) => McpServer;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { createTheme, cssVar } from '@greese/ui';
|
|
4
|
+
import { firstSentence, score, search } from "./search.js";
|
|
5
|
+
const text = (value) => ({ content: [{ type: 'text', text: value }] });
|
|
6
|
+
const error = (value) => ({ ...text(value), isError: true });
|
|
7
|
+
/** The closest names to a miss, so a typo gets a suggestion instead of a dead end. */
|
|
8
|
+
const suggest = (query, names) => {
|
|
9
|
+
const close = names
|
|
10
|
+
.map((name) => ({
|
|
11
|
+
name,
|
|
12
|
+
s: score(query, name, '') ||
|
|
13
|
+
(name.toLowerCase().startsWith(query.toLowerCase().slice(0, 3)) ? 1 : 0),
|
|
14
|
+
}))
|
|
15
|
+
.filter((c) => c.s)
|
|
16
|
+
.sort((a, b) => b.s - a.s)
|
|
17
|
+
.slice(0, 5)
|
|
18
|
+
.map((c) => c.name);
|
|
19
|
+
return close.length ? ` Did you mean: ${close.join(', ')}?` : '';
|
|
20
|
+
};
|
|
21
|
+
const findByName = (items, name, key) => items.find((item) => key(item) === name) ??
|
|
22
|
+
items.find((item) => key(item).toLowerCase() === name.toLowerCase());
|
|
23
|
+
const cell = (value) => value.replace(/\|/g, '\\|').replace(/\n+/g, ' ');
|
|
24
|
+
/** A component as Markdown: what it is, its props, what a theme can target, and an example. */
|
|
25
|
+
export const componentMarkdown = (c, example = true) => {
|
|
26
|
+
const lines = [
|
|
27
|
+
`# ${c.name}`,
|
|
28
|
+
'',
|
|
29
|
+
`Category: ${c.category}. Source: \`${c.file}\`.`,
|
|
30
|
+
'',
|
|
31
|
+
c.description,
|
|
32
|
+
'',
|
|
33
|
+
];
|
|
34
|
+
lines.push('```tsx', `import { ${c.name} } from '@greese/react'`, '```', '');
|
|
35
|
+
if (c.props.length) {
|
|
36
|
+
lines.push('## Props', '', '| Prop | Type | Default | Description |', '| --- | --- | --- | --- |');
|
|
37
|
+
for (const p of c.props)
|
|
38
|
+
lines.push(`| \`${p.name}\`${p.required ? ' (required)' : ''} | \`${cell(p.type)}\` | ${p.default === undefined ? '' : `\`${cell(p.default)}\``} | ${cell(p.description)} |`);
|
|
39
|
+
lines.push('', 'Other props pass through to the root element (DOM attributes, `ref`, `className`, `style`). Every component also takes the `greese` motion prop.', '');
|
|
40
|
+
}
|
|
41
|
+
if (c.styles) {
|
|
42
|
+
const s = c.styles;
|
|
43
|
+
lines.push('## Styling', '', `Root class: \`.${s.className}\`.`);
|
|
44
|
+
if (s.parts.length)
|
|
45
|
+
lines.push(`Parts: ${s.parts.map((p) => `\`.${s.className}-${p}\``).join(', ')}.`);
|
|
46
|
+
if (s.modifiers.length)
|
|
47
|
+
lines.push(`Modifiers: ${s.modifiers.map((m) => `\`.${s.className}--${m}\``).join(', ')}.`);
|
|
48
|
+
if (s.attributes.length)
|
|
49
|
+
lines.push(`State attributes: ${s.attributes.map((a) => `\`${a}\``).join(', ')}.`);
|
|
50
|
+
if (s.variables.length)
|
|
51
|
+
lines.push(`Variables: ${s.variables.map((v) => `\`${v}\``).join(', ')}.`);
|
|
52
|
+
lines.push('', 'Restyle it with the SCSS kit (`@include greese.component(' +
|
|
53
|
+
c.name +
|
|
54
|
+
') { … }`), which writes into `@layer greese.overrides`, or with plain CSS on these classes.', '');
|
|
55
|
+
}
|
|
56
|
+
if (example && c.example)
|
|
57
|
+
lines.push('## Example (its stories file)', '', '```tsx', c.example.trim(), '```');
|
|
58
|
+
return lines.join('\n');
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* The greese ui MCP server: tools and resources over the data baked at build
|
|
62
|
+
* time (ADR 0022). Connect it to any transport; the CLI uses stdio.
|
|
63
|
+
*/
|
|
64
|
+
export const createGreeseServer = (data) => {
|
|
65
|
+
const server = new McpServer({ name: 'greese', version: data.version }, {
|
|
66
|
+
instructions: 'greese ui is a mobile-first React 19 component library (@greese/react) styled by framework-free CSS (@greese/ui), with icons in @greese/icons and motion via the `greese` prop. Before writing UI code, look components up with get_component instead of guessing props, use tokens (list_tokens) rather than raw values, and use icons from @greese/icons (list_icons) rather than inline SVG. Templates (list_templates) are complete pages to start from.',
|
|
67
|
+
});
|
|
68
|
+
const names = data.components.map((c) => c.name);
|
|
69
|
+
server.registerTool('list_components', {
|
|
70
|
+
title: 'List components',
|
|
71
|
+
description: 'Every greese ui React component, by docs category, with a one-line summary.',
|
|
72
|
+
inputSchema: {
|
|
73
|
+
category: z
|
|
74
|
+
.string()
|
|
75
|
+
.optional()
|
|
76
|
+
.describe('Only this category: Layout, Forms, Content, Overlays, Navigation, Motion or Utilities.'),
|
|
77
|
+
},
|
|
78
|
+
annotations: { readOnlyHint: true },
|
|
79
|
+
}, ({ category }) => {
|
|
80
|
+
const chosen = data.components.filter((c) => !category || c.category.toLowerCase() === category.toLowerCase());
|
|
81
|
+
if (!chosen.length)
|
|
82
|
+
return error(`No components in category "${category}".`);
|
|
83
|
+
const byCategory = new Map();
|
|
84
|
+
for (const c of chosen)
|
|
85
|
+
byCategory.set(c.category, [...(byCategory.get(c.category) ?? []), c]);
|
|
86
|
+
const out = [...byCategory]
|
|
87
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
88
|
+
.map(([cat, list]) => `## ${cat}\n${list.map((c) => `- **${c.name}**: ${firstSentence(c.description)}`).join('\n')}`);
|
|
89
|
+
return text(out.join('\n\n'));
|
|
90
|
+
});
|
|
91
|
+
server.registerTool('get_component', {
|
|
92
|
+
title: 'Get a component',
|
|
93
|
+
description: "A component's documentation: description, every prop with its type, default and meaning, the classes, parts, modifiers and CSS variables a theme can target, and its stories file as working example code.",
|
|
94
|
+
inputSchema: {
|
|
95
|
+
name: z
|
|
96
|
+
.string()
|
|
97
|
+
.describe('The component, as exported: Button, Dialog, TextField…'),
|
|
98
|
+
example: z
|
|
99
|
+
.boolean()
|
|
100
|
+
.optional()
|
|
101
|
+
.describe('Include the example code. Defaults to true.'),
|
|
102
|
+
},
|
|
103
|
+
annotations: { readOnlyHint: true },
|
|
104
|
+
}, ({ name, example }) => {
|
|
105
|
+
const c = findByName(data.components, name, (c) => c.name);
|
|
106
|
+
return c
|
|
107
|
+
? text(componentMarkdown(c, example ?? true))
|
|
108
|
+
: error(`No component named "${name}".${suggest(name, names)}`);
|
|
109
|
+
});
|
|
110
|
+
server.registerTool('search', {
|
|
111
|
+
title: 'Search greese ui',
|
|
112
|
+
description: 'Searches components, guides, templates and themes by keyword. Every word must match.',
|
|
113
|
+
inputSchema: {
|
|
114
|
+
query: z
|
|
115
|
+
.string()
|
|
116
|
+
.min(1)
|
|
117
|
+
.describe('Words to look for: "date picker", "dark mode", "toast".'),
|
|
118
|
+
limit: z
|
|
119
|
+
.number()
|
|
120
|
+
.int()
|
|
121
|
+
.min(1)
|
|
122
|
+
.max(50)
|
|
123
|
+
.optional()
|
|
124
|
+
.describe('At most this many results. Defaults to 10.'),
|
|
125
|
+
},
|
|
126
|
+
annotations: { readOnlyHint: true },
|
|
127
|
+
}, ({ query, limit }) => {
|
|
128
|
+
const hits = search(data, query, limit ?? 10);
|
|
129
|
+
return hits.length
|
|
130
|
+
? text(hits.map((h) => `- [${h.kind}] **${h.name}**: ${h.summary}`).join('\n'))
|
|
131
|
+
: text(`Nothing matches "${query}". Try fewer or broader words.`);
|
|
132
|
+
});
|
|
133
|
+
server.registerTool('list_tokens', {
|
|
134
|
+
title: 'List design tokens',
|
|
135
|
+
description: 'The design tokens and their CSS custom properties: colors (light and dark), space, radius, font, shadow, z-index, duration, easing, breakpoints, the palette and the type scale. Without a group, lists the groups.',
|
|
136
|
+
inputSchema: {
|
|
137
|
+
group: z
|
|
138
|
+
.string()
|
|
139
|
+
.optional()
|
|
140
|
+
.describe('A group: color, space, radius, font, shadow, palette, typography, breakpoints…'),
|
|
141
|
+
},
|
|
142
|
+
annotations: { readOnlyHint: true },
|
|
143
|
+
}, ({ group }) => {
|
|
144
|
+
const groups = Object.keys(data.tokens);
|
|
145
|
+
if (!group)
|
|
146
|
+
return text(`Token groups: ${groups.join(', ')}.\n\nA token is used as \`var(--greese-<group>-<name>)\`, names in kebab-case: \`var(${cssVar('color', 'primaryStrong')})\`, \`var(${cssVar('space', '4')})\`. Palette colors are \`var(--greese-palette-<hue>-<shade>)\`.`);
|
|
147
|
+
const values = data.tokens[group];
|
|
148
|
+
if (values === undefined)
|
|
149
|
+
return error(`No token group "${group}". Groups: ${groups.join(', ')}.`);
|
|
150
|
+
const scalar = !['palette', 'typography', 'breakpoints'].includes(group);
|
|
151
|
+
const rows = scalar && values && typeof values === 'object'
|
|
152
|
+
? Object.entries(values).map(([name, value]) => `- \`${cssVar(group, name)}\`: ${JSON.stringify(value)}`)
|
|
153
|
+
: ['```json', JSON.stringify(values, null, 2), '```'];
|
|
154
|
+
return text(`# ${group}\n\n${rows.join('\n')}`);
|
|
155
|
+
});
|
|
156
|
+
server.registerTool('list_icons', {
|
|
157
|
+
title: 'List icons',
|
|
158
|
+
description: 'The icons in @greese/icons. Use these rather than inline <svg>: every greese ui icon lives in that package.',
|
|
159
|
+
inputSchema: {
|
|
160
|
+
query: z.string().optional().describe('Only icons whose name contains this.'),
|
|
161
|
+
},
|
|
162
|
+
annotations: { readOnlyHint: true },
|
|
163
|
+
}, ({ query }) => {
|
|
164
|
+
const icons = data.icons.filter((i) => !query || i.toLowerCase().includes(query.toLowerCase()));
|
|
165
|
+
if (!icons.length)
|
|
166
|
+
return text(`No icon matches "${query}". Add one to packages/icons/src/icons.tsx with createIcon().`);
|
|
167
|
+
return text(`${icons.join(', ')}\n\n\`\`\`tsx\nimport { ${icons[0]} } from '@greese/icons'\n\n<Icon label="…"><${icons[0]} /></Icon> // or as a Button's startIcon\n\`\`\``);
|
|
168
|
+
});
|
|
169
|
+
server.registerTool('create_theme', {
|
|
170
|
+
title: 'Create a theme',
|
|
171
|
+
description: "Runs @greese/ui's createTheme and returns the theme's CSS: every token, with your overrides. Setting a brand color also derives its readable content color and its strong shade.",
|
|
172
|
+
inputSchema: {
|
|
173
|
+
name: z
|
|
174
|
+
.string()
|
|
175
|
+
.regex(/^[a-z][a-z0-9-]*$/i)
|
|
176
|
+
.describe('The theme name: data-greese-theme="<name>" applies it.'),
|
|
177
|
+
options: z
|
|
178
|
+
.record(z.string(), z.unknown())
|
|
179
|
+
.optional()
|
|
180
|
+
.describe('createTheme options besides name, e.g. { "color": { "primary": { "light": "#7c3aed", "dark": "#c4b5fd" } }, "radius": { "md": "12px" } }. Groups: color, space, radius, font, shadow, duration, easing, typography, extend.'),
|
|
181
|
+
},
|
|
182
|
+
annotations: { readOnlyHint: true },
|
|
183
|
+
}, ({ name, options }) => {
|
|
184
|
+
try {
|
|
185
|
+
const theme = createTheme({ ...options, name });
|
|
186
|
+
return text(`\`\`\`css\n${theme.css}\n\`\`\`\n\nServe this after greese ui's styles and set \`data-greese-theme="${name}"\` on any element, or in React: \`<ThemeProvider theme={createTheme({ name: '${name}', … })}>\`.`);
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
return error(e instanceof Error ? e.message : String(e));
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
server.registerTool('list_templates', {
|
|
193
|
+
title: 'List templates',
|
|
194
|
+
description: 'Complete page templates (finance, admin dashboard, email client, blog, SaaS, e-commerce…) built from greese ui components.',
|
|
195
|
+
annotations: { readOnlyHint: true },
|
|
196
|
+
}, () => text(data.templates.length
|
|
197
|
+
? data.templates
|
|
198
|
+
.map((t) => `- **${t.id}**: ${firstSentence(t.description)}`)
|
|
199
|
+
.join('\n')
|
|
200
|
+
: 'No templates in this build.'));
|
|
201
|
+
server.registerTool('get_template', {
|
|
202
|
+
title: 'Get a template',
|
|
203
|
+
description: "A template's full source: a React page to copy and adapt.",
|
|
204
|
+
inputSchema: {
|
|
205
|
+
id: z.string().describe('The template, as list_templates names it.'),
|
|
206
|
+
},
|
|
207
|
+
annotations: { readOnlyHint: true },
|
|
208
|
+
}, ({ id }) => {
|
|
209
|
+
const t = findByName(data.templates, id, (t) => t.id);
|
|
210
|
+
return t
|
|
211
|
+
? text(`# ${t.id}\n\n${t.description}\n\n\`\`\`tsx\n${t.source.trim()}\n\`\`\`${data.templateStyles
|
|
212
|
+
? `\n\nThe \`tpl-*\` classes it uses (templates.scss, shared by every template):\n\n\`\`\`scss\n${data.templateStyles.trim()}\n\`\`\``
|
|
213
|
+
: ''}`)
|
|
214
|
+
: error(`No template "${id}".${suggest(id, data.templates.map((t) => t.id))}`);
|
|
215
|
+
});
|
|
216
|
+
server.registerTool('list_themes', {
|
|
217
|
+
title: 'List themes',
|
|
218
|
+
description: "greese ui's theme files, one layer each (ADR 0025): color themes (brand colors and neutrals), shape themes (clip-path outlines and radii), motion themes and color variants.",
|
|
219
|
+
annotations: { readOnlyHint: true },
|
|
220
|
+
}, () => text(data.themes.length
|
|
221
|
+
? data.themes
|
|
222
|
+
.map((t) => `- **${t.id}**: ${firstSentence(t.description)}`)
|
|
223
|
+
.join('\n')
|
|
224
|
+
: 'No themes in this build.'));
|
|
225
|
+
server.registerTool('get_theme', {
|
|
226
|
+
title: 'Get a shape theme',
|
|
227
|
+
description: "A shape theme's source: kit SCSS (compile after greese ui's styles, or read as a recipe) and its compiled CSS (@greese/ui/themes/<id>.css).",
|
|
228
|
+
inputSchema: {
|
|
229
|
+
id: z.string().describe('The theme, as list_themes names it.'),
|
|
230
|
+
format: z
|
|
231
|
+
.enum(['scss', 'css', 'both'])
|
|
232
|
+
.optional()
|
|
233
|
+
.describe('Which source. Defaults to scss.'),
|
|
234
|
+
},
|
|
235
|
+
annotations: { readOnlyHint: true },
|
|
236
|
+
}, ({ id, format }) => {
|
|
237
|
+
const t = findByName(data.themes, id, (t) => t.id);
|
|
238
|
+
if (!t)
|
|
239
|
+
return error(`No theme "${id}".${suggest(id, data.themes.map((t) => t.id))}`);
|
|
240
|
+
const want = format ?? 'scss';
|
|
241
|
+
const parts = [
|
|
242
|
+
`# ${t.id}\n\n${t.description}\n\nUse it: \`import '@greese/ui/themes/${t.id}.css'\` after greese ui's styles.`,
|
|
243
|
+
];
|
|
244
|
+
if (want !== 'css')
|
|
245
|
+
parts.push(`\`\`\`scss\n${t.scss.trim()}\n\`\`\``);
|
|
246
|
+
if (want !== 'scss')
|
|
247
|
+
parts.push(`\`\`\`css\n${t.css.trim()}\n\`\`\``);
|
|
248
|
+
return text(parts.join('\n\n'));
|
|
249
|
+
});
|
|
250
|
+
server.registerResource('guide', new ResourceTemplate('greese://guides/{name}', {
|
|
251
|
+
list: () => ({
|
|
252
|
+
resources: data.guides.map((g) => ({
|
|
253
|
+
uri: `greese://guides/${g.name}`,
|
|
254
|
+
name: g.name,
|
|
255
|
+
title: g.title,
|
|
256
|
+
mimeType: 'text/markdown',
|
|
257
|
+
})),
|
|
258
|
+
}),
|
|
259
|
+
}), {
|
|
260
|
+
title: 'greese ui guides',
|
|
261
|
+
description: 'Getting started, theming, styling, animation, utilities…',
|
|
262
|
+
mimeType: 'text/markdown',
|
|
263
|
+
}, (uri, { name }) => {
|
|
264
|
+
const g = data.guides.find((g) => g.name === name);
|
|
265
|
+
return {
|
|
266
|
+
contents: [
|
|
267
|
+
{
|
|
268
|
+
uri: uri.href,
|
|
269
|
+
mimeType: 'text/markdown',
|
|
270
|
+
text: g?.text ?? `No guide "${String(name)}".`,
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
server.registerResource('component', new ResourceTemplate('greese://components/{name}', {
|
|
276
|
+
list: () => ({
|
|
277
|
+
resources: data.components.map((c) => ({
|
|
278
|
+
uri: `greese://components/${c.name}`,
|
|
279
|
+
name: c.name,
|
|
280
|
+
description: firstSentence(c.description),
|
|
281
|
+
mimeType: 'text/markdown',
|
|
282
|
+
})),
|
|
283
|
+
}),
|
|
284
|
+
}), {
|
|
285
|
+
title: 'greese ui components',
|
|
286
|
+
description: 'One document per component: props, classes, example.',
|
|
287
|
+
mimeType: 'text/markdown',
|
|
288
|
+
}, (uri, { name }) => {
|
|
289
|
+
const c = data.components.find((c) => c.name === name);
|
|
290
|
+
return {
|
|
291
|
+
contents: [
|
|
292
|
+
{
|
|
293
|
+
uri: uri.href,
|
|
294
|
+
mimeType: 'text/markdown',
|
|
295
|
+
text: c ? componentMarkdown(c) : `No component "${String(name)}".`,
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
};
|
|
299
|
+
});
|
|
300
|
+
return server;
|
|
301
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@greese/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "An MCP server for greese ui: components and their props, classes and examples, tokens, icons, guides, themes and page templates, for coding assistants.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"greese",
|
|
9
|
+
"design-system",
|
|
10
|
+
"ai"
|
|
11
|
+
],
|
|
12
|
+
"author": "Chris Moody",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"homepage": "https://chris-moody.github.io/greese/",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/chris-moody/greese.git",
|
|
18
|
+
"directory": "packages/mcp"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/chris-moody/greese/issues"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"bin": {
|
|
25
|
+
"greese-mcp": "./dist/cli.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"default": "./dist/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./package.json": "./package.json"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=24"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
46
|
+
"build": "npm run clean && tsc -p tsconfig.build.json && node scripts/build-data.ts",
|
|
47
|
+
"check-types": "tsc --noEmit",
|
|
48
|
+
"lint": "eslint src scripts --max-warnings 0",
|
|
49
|
+
"test": "vitest run"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@greese/ui": "*",
|
|
53
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
54
|
+
"zod": "^4.6.6"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@greese/eslint-config": "*",
|
|
58
|
+
"@greese/icons": "*",
|
|
59
|
+
"@greese/react": "*",
|
|
60
|
+
"@greese/typescript-config": "*",
|
|
61
|
+
"@types/node": "^24.0.0",
|
|
62
|
+
"eslint": "^9.39.0",
|
|
63
|
+
"react-docgen-typescript": "^2.4.0",
|
|
64
|
+
"typescript": "~6.0.3",
|
|
65
|
+
"typescript-eslint": "^8.26.0",
|
|
66
|
+
"vitest": "^5.0.5"
|
|
67
|
+
}
|
|
68
|
+
}
|