@urbicon-ui/mcp-server 6.1.4

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.
Files changed (130) hide show
  1. package/README.md +161 -0
  2. package/dist/data/catalog-loader.d.ts +37 -0
  3. package/dist/data/catalog-loader.d.ts.map +1 -0
  4. package/dist/data/catalog-loader.js +15 -0
  5. package/dist/data/catalog-loader.js.map +1 -0
  6. package/dist/data/component-loader.d.ts +2 -0
  7. package/dist/data/component-loader.d.ts.map +1 -0
  8. package/dist/data/component-loader.js +17 -0
  9. package/dist/data/component-loader.js.map +1 -0
  10. package/dist/data/recipe-loader.d.ts +4 -0
  11. package/dist/data/recipe-loader.d.ts.map +1 -0
  12. package/dist/data/recipe-loader.js +102 -0
  13. package/dist/data/recipe-loader.js.map +1 -0
  14. package/dist/data/template-loader.d.ts +8 -0
  15. package/dist/data/template-loader.d.ts.map +1 -0
  16. package/dist/data/template-loader.js +33 -0
  17. package/dist/data/template-loader.js.map +1 -0
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +57 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/resources/catalog.d.ts +3 -0
  23. package/dist/resources/catalog.d.ts.map +1 -0
  24. package/dist/resources/catalog.js +20 -0
  25. package/dist/resources/catalog.js.map +1 -0
  26. package/dist/resources/component.d.ts +3 -0
  27. package/dist/resources/component.d.ts.map +1 -0
  28. package/dist/resources/component.js +29 -0
  29. package/dist/resources/component.js.map +1 -0
  30. package/dist/resources/guides.d.ts +3 -0
  31. package/dist/resources/guides.d.ts.map +1 -0
  32. package/dist/resources/guides.js +36 -0
  33. package/dist/resources/guides.js.map +1 -0
  34. package/dist/server.d.ts +3 -0
  35. package/dist/server.d.ts.map +1 -0
  36. package/dist/server.js.map +1 -0
  37. package/dist/tools/find-components.d.ts +3 -0
  38. package/dist/tools/find-components.d.ts.map +1 -0
  39. package/dist/tools/find-components.js +21 -0
  40. package/dist/tools/find-components.js.map +1 -0
  41. package/dist/tools/get-recipe.d.ts +3 -0
  42. package/dist/tools/get-recipe.d.ts.map +1 -0
  43. package/dist/tools/get-recipe.js +48 -0
  44. package/dist/tools/get-recipe.js.map +1 -0
  45. package/dist/tools/suggest-implementation.d.ts +3 -0
  46. package/dist/tools/suggest-implementation.d.ts.map +1 -0
  47. package/dist/tools/suggest-implementation.js +178 -0
  48. package/dist/tools/suggest-implementation.js.map +1 -0
  49. package/dist/transports/http.d.ts +2 -0
  50. package/dist/transports/http.d.ts.map +1 -0
  51. package/dist/transports/http.js +77 -0
  52. package/dist/transports/http.js.map +1 -0
  53. package/dist/transports/stdio.d.ts +3 -0
  54. package/dist/transports/stdio.d.ts.map +1 -0
  55. package/dist/transports/stdio.js +6 -0
  56. package/dist/transports/stdio.js.map +1 -0
  57. package/dist/tsconfig.tsbuildinfo +1 -0
  58. package/dist/utils/format-catalog.d.ts +7 -0
  59. package/dist/utils/format-catalog.d.ts.map +1 -0
  60. package/dist/utils/format-catalog.js +93 -0
  61. package/dist/utils/format-catalog.js.map +1 -0
  62. package/dist/utils/paths.d.ts +7 -0
  63. package/dist/utils/paths.d.ts.map +1 -0
  64. package/dist/utils/paths.js +23 -0
  65. package/dist/utils/paths.js.map +1 -0
  66. package/dist/utils/search.d.ts +3 -0
  67. package/dist/utils/search.d.ts.map +1 -0
  68. package/dist/utils/search.js +44 -0
  69. package/dist/utils/search.js.map +1 -0
  70. package/package.json +42 -0
  71. package/src/data/catalog-loader.test.ts +42 -0
  72. package/src/data/catalog-loader.ts +78 -0
  73. package/src/data/component-loader.ts +68 -0
  74. package/src/data/design-system-loader.test.ts +82 -0
  75. package/src/data/design-system-loader.ts +125 -0
  76. package/src/data/icon-loader.test.ts +85 -0
  77. package/src/data/icon-loader.ts +90 -0
  78. package/src/data/recipe-loader.test.ts +49 -0
  79. package/src/data/recipe-loader.ts +131 -0
  80. package/src/data/template-loader.ts +55 -0
  81. package/src/design-linter/heuristics.ts +162 -0
  82. package/src/design-linter/index.ts +14 -0
  83. package/src/design-linter/linter.test.ts +257 -0
  84. package/src/design-linter/linter.ts +62 -0
  85. package/src/design-linter/rules.ts +348 -0
  86. package/src/design-linter/tokens.test.ts +80 -0
  87. package/src/design-linter/tokens.ts +203 -0
  88. package/src/design-linter/types.ts +66 -0
  89. package/src/design-manifest/index.ts +20 -0
  90. package/src/design-manifest/manifest.test.ts +175 -0
  91. package/src/design-manifest/manifest.ts +250 -0
  92. package/src/design-manifest/scan.test.ts +51 -0
  93. package/src/design-manifest/scan.ts +74 -0
  94. package/src/design-manifest/types.ts +40 -0
  95. package/src/design-rubric/rubric.test.ts +43 -0
  96. package/src/design-rubric/rubric.ts +140 -0
  97. package/src/eval/briefs.ts +104 -0
  98. package/src/eval/eval.test.ts +99 -0
  99. package/src/eval/index.ts +11 -0
  100. package/src/eval/score.ts +112 -0
  101. package/src/index.ts +75 -0
  102. package/src/prompts/design-prompts.test.ts +51 -0
  103. package/src/prompts/design-prompts.ts +127 -0
  104. package/src/resources/catalog.ts +23 -0
  105. package/src/resources/guides.ts +60 -0
  106. package/src/server.test.ts +69 -0
  107. package/src/server.ts +48 -0
  108. package/src/tools/find-components.ts +83 -0
  109. package/src/tools/find-icons.ts +77 -0
  110. package/src/tools/get-checklist.ts +139 -0
  111. package/src/tools/get-component.ts +204 -0
  112. package/src/tools/get-css-reference.ts +446 -0
  113. package/src/tools/get-design-context.ts +43 -0
  114. package/src/tools/get-design-principles.ts +72 -0
  115. package/src/tools/get-pattern.ts +69 -0
  116. package/src/tools/get-recipe.ts +80 -0
  117. package/src/tools/record-design-decision.ts +99 -0
  118. package/src/tools/suggest-implementation.ts +251 -0
  119. package/src/tools/sync-design-manifest.ts +92 -0
  120. package/src/tools/validate-design.ts +84 -0
  121. package/src/transports/http.ts +79 -0
  122. package/src/transports/stdio.ts +7 -0
  123. package/src/utils/format-catalog.test.ts +144 -0
  124. package/src/utils/format-catalog.ts +130 -0
  125. package/src/utils/paths.test.ts +101 -0
  126. package/src/utils/paths.ts +78 -0
  127. package/src/utils/search.test.ts +141 -0
  128. package/src/utils/search.ts +106 -0
  129. package/tsconfig.json +27 -0
  130. package/vitest.config.ts +15 -0
@@ -0,0 +1,78 @@
1
+ import { dirname, normalize, resolve, sep } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ const __dirname = dirname(fileURLToPath(import.meta.url));
5
+ const packageRoot = resolve(__dirname, '..', '..');
6
+
7
+ export function getDataDir(): string {
8
+ return process.env.DATA_DIR ?? resolve(packageRoot, '..', '..', 'apps', 'docs', 'static');
9
+ }
10
+
11
+ export function getTemplateDir(): string {
12
+ return resolve(packageRoot, '..', 'docs-gen', 'templates');
13
+ }
14
+
15
+ export function getRecipeDir(): string {
16
+ return resolve(packageRoot, '..', '..', 'apps', 'docs', 'src', 'routes', 'recipes');
17
+ }
18
+
19
+ export function getDesignSystemDir(): string {
20
+ return process.env.DESIGN_SYSTEM_DIR ?? resolve(packageRoot, '..', '..', 'design-system');
21
+ }
22
+
23
+ /**
24
+ * Root of the *consumer* project (the app being built with Urbicon UI), used by
25
+ * the design-context tools. Unlike the loaders above — which read the library's
26
+ * own shipped data — these tools read/write the consumer's `design.manifest.md`
27
+ * and scan its source, so the default is the process cwd (the consumer's repo
28
+ * when the server is launched from their editor). Override with env vars.
29
+ */
30
+ export function getProjectDir(): string {
31
+ return process.env.DESIGN_PROJECT_DIR ?? process.cwd();
32
+ }
33
+
34
+ export function getProjectManifestPath(): string {
35
+ return process.env.DESIGN_MANIFEST_PATH ?? resolve(getProjectDir(), 'design.manifest.md');
36
+ }
37
+
38
+ export function getProjectSourceDir(): string {
39
+ return process.env.DESIGN_SOURCE_DIR ?? resolve(getProjectDir(), 'src');
40
+ }
41
+
42
+ /**
43
+ * Containment check for a caller-supplied manifest path. The write tools accept
44
+ * an LLM-supplied `manifestPath`; restrict it to the project root so a model
45
+ * cannot be steered into writing `.md` files elsewhere. (The user-configured
46
+ * `DESIGN_MANIFEST_PATH` env default is trusted and not run through this.)
47
+ */
48
+ export function isWithinProjectDir(targetPath: string): boolean {
49
+ const resolved = resolve(targetPath);
50
+ const projectDir = normalize(getProjectDir());
51
+ return resolved === projectDir || resolved.startsWith(projectDir + sep);
52
+ }
53
+
54
+ export function getTemplatePath(): string {
55
+ return resolve(getTemplateDir(), 'llms-full-template.md');
56
+ }
57
+
58
+ export function getCatalogPath(): string {
59
+ return resolve(getDataDir(), 'mcp', 'component-catalog.json');
60
+ }
61
+
62
+ /** Validate that a slug contains only safe characters (lowercase alphanumeric + hyphens). */
63
+ const SAFE_SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
64
+
65
+ export function getComponentLlmPath(group: string, slug: string): string {
66
+ if (!SAFE_SLUG.test(slug)) {
67
+ throw new Error(`Invalid component slug: "${slug}"`);
68
+ }
69
+
70
+ const resolved = resolve(getDataDir(), group, slug, 'llm.txt');
71
+ const dataDir = normalize(getDataDir());
72
+
73
+ if (!resolved.startsWith(dataDir)) {
74
+ throw new Error(`Path traversal blocked for slug: "${slug}"`);
75
+ }
76
+
77
+ return resolved;
78
+ }
@@ -0,0 +1,141 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { ComponentCatalogEntry } from '../data/catalog-loader.js';
3
+ import { matchComponents } from './search.js';
4
+
5
+ function makeEntry(
6
+ overrides: Partial<ComponentCatalogEntry> & Pick<ComponentCatalogEntry, 'name' | 'slug'>
7
+ ): ComponentCatalogEntry {
8
+ return {
9
+ package: '@urbicon-ui/blocks',
10
+ group: 'primitives',
11
+ description: '',
12
+ tags: [],
13
+ import: `import { ${overrides.name} } from '@urbicon-ui/blocks';`,
14
+ llmTxtPath: '',
15
+ variants: [],
16
+ keyProps: [],
17
+ keyPropTypes: {},
18
+ slots: [],
19
+ hasExamples: false,
20
+ relatedComponents: [],
21
+ ...overrides
22
+ };
23
+ }
24
+
25
+ const Button = makeEntry({
26
+ name: 'Button',
27
+ slug: 'button',
28
+ description: 'Click to trigger an action',
29
+ tags: ['action'],
30
+ keyProps: ['intent', 'variant', 'size']
31
+ });
32
+
33
+ const Input = makeEntry({
34
+ name: 'Input',
35
+ slug: 'input',
36
+ description: 'Single-line text field',
37
+ tags: ['form'],
38
+ keyProps: ['value', 'error']
39
+ });
40
+
41
+ const Dialog = makeEntry({
42
+ name: 'Dialog',
43
+ slug: 'dialog',
44
+ description: 'Modal overlay container',
45
+ tags: ['overlay']
46
+ });
47
+
48
+ const catalog = [Button, Input, Dialog];
49
+
50
+ describe('matchComponents', () => {
51
+ it('ranks an exact name match first', () => {
52
+ const results = matchComponents(catalog, 'button');
53
+ expect(results[0]?.name).toBe('Button');
54
+ });
55
+
56
+ it('matches by case-insensitive substring of the name', () => {
57
+ const results = matchComponents(catalog, 'Butt');
58
+ expect(results.some((r) => r.name === 'Button')).toBe(true);
59
+ });
60
+
61
+ it('fuzz-matches a single-character typo', () => {
62
+ const results = matchComponents(catalog, 'Buton'); // typo: missing "t"
63
+ expect(results[0]?.name).toBe('Button');
64
+ });
65
+
66
+ it('ignores matches that are too distant', () => {
67
+ const results = matchComponents(catalog, 'xyzzy');
68
+ expect(results).toEqual([]);
69
+ });
70
+
71
+ it('scores tag matches when the tag keyword is present in the query', () => {
72
+ const results = matchComponents(catalog, 'form');
73
+ expect(results[0]?.name).toBe('Input');
74
+ });
75
+
76
+ it('filters by the explicit tags argument', () => {
77
+ const results = matchComponents(catalog, 'container', ['overlay']);
78
+ expect(results.some((r) => r.name === 'Dialog')).toBe(true);
79
+ });
80
+
81
+ it('respects the limit', () => {
82
+ const many = Array.from({ length: 10 }, (_, i) =>
83
+ makeEntry({ name: `Button${i}`, slug: `button-${i}`, description: 'click' })
84
+ );
85
+ const results = matchComponents(many, 'button', undefined, 3);
86
+ expect(results).toHaveLength(3);
87
+ });
88
+
89
+ it('drops words shorter than two characters before matching', () => {
90
+ // "a" and "x" below are too short and should be filtered;
91
+ // only "button" remains and should drive the match.
92
+ const results = matchComponents(catalog, 'a x button');
93
+ expect(results[0]?.name).toBe('Button');
94
+ });
95
+
96
+ it('uses the prop-name match as a weak signal', () => {
97
+ const results = matchComponents(catalog, 'intent');
98
+ expect(results.some((r) => r.name === 'Button')).toBe(true);
99
+ });
100
+
101
+ it('returns an empty list when the query has no usable keywords', () => {
102
+ const results = matchComponents(catalog, ', , ');
103
+ expect(results).toEqual([]);
104
+ });
105
+ });
106
+
107
+ // Regression for the DateGrid/Planner discovery fix: planning-board queries
108
+ // used to steer toward Calendar (a timed-event scheduler). With Planner in the
109
+ // catalog they must land on Planner instead — driven purely by its catalog
110
+ // description/tags/slug, no hardcoded keyword map.
111
+ describe('matchComponents — Planner discovery', () => {
112
+ const Calendar = makeEntry({
113
+ name: 'Calendar',
114
+ slug: 'calendar',
115
+ description: 'Event display and date selection with month, week and day views.',
116
+ tags: ['display'],
117
+ relatedComponents: ['DatePicker']
118
+ });
119
+ const Planner = makeEntry({
120
+ name: 'Planner',
121
+ slug: 'planner',
122
+ description:
123
+ 'Date-indexed planning board — a week, month or custom-range grid whose cells hold your domain content (meals, shifts, bookings, content slots) via a generic cell snippet.',
124
+ tags: ['display', 'layout'],
125
+ keyProps: ['items', 'getDate', 'view', 'cell'],
126
+ relatedComponents: ['Calendar', 'DatePicker']
127
+ });
128
+ const dateCatalog = [Calendar, Planner];
129
+
130
+ for (const query of ['planner', 'meal planner', 'weekly plan', 'shift schedule', 'week board']) {
131
+ it(`ranks Planner first for "${query}"`, () => {
132
+ const results = matchComponents(dateCatalog, query);
133
+ expect(results[0]?.name).toBe('Planner');
134
+ });
135
+ }
136
+
137
+ it('still ranks Calendar first for an event/appointment query', () => {
138
+ const results = matchComponents(dateCatalog, 'event calendar');
139
+ expect(results[0]?.name).toBe('Calendar');
140
+ });
141
+ });
@@ -0,0 +1,106 @@
1
+ import type { ComponentCatalogEntry } from '../data/catalog-loader.js';
2
+
3
+ interface ScoredEntry {
4
+ entry: ComponentCatalogEntry;
5
+ score: number;
6
+ }
7
+
8
+ export function matchComponents(
9
+ components: ComponentCatalogEntry[],
10
+ query: string,
11
+ tags?: string[],
12
+ limit = 5
13
+ ): ComponentCatalogEntry[] {
14
+ const keywords = query
15
+ .toLowerCase()
16
+ .split(/[\s,\-_]+/)
17
+ .filter((w) => w.length > 1);
18
+
19
+ const scored: ScoredEntry[] = components.map((entry) => {
20
+ let score = 0;
21
+ const nameLower = entry.name.toLowerCase();
22
+ const slugLower = entry.slug.toLowerCase();
23
+ const descLower = entry.description.toLowerCase();
24
+
25
+ for (const kw of keywords) {
26
+ // Exact match
27
+ if (nameLower === kw || slugLower === kw) {
28
+ score += 10;
29
+ } else if (nameLower.includes(kw) || slugLower.includes(kw)) {
30
+ score += 7;
31
+ } else {
32
+ // Fuzzy match on name/slug (Levenshtein distance <= 2)
33
+ const nameDist = levenshtein(nameLower, kw);
34
+ const slugDist = levenshtein(slugLower, kw);
35
+ const minDist = Math.min(nameDist, slugDist);
36
+ if (minDist <= 1) {
37
+ score += 6;
38
+ } else if (minDist <= 2) {
39
+ score += 3;
40
+ }
41
+ }
42
+
43
+ // Tag match
44
+ if (entry.tags.some((t) => t.toLowerCase() === kw)) {
45
+ score += 5;
46
+ }
47
+
48
+ // Description match
49
+ if (descLower.includes(kw)) {
50
+ score += 3;
51
+ }
52
+
53
+ // Prop name match
54
+ if (entry.keyProps.some((p) => p.toLowerCase().includes(kw))) {
55
+ score += 1;
56
+ }
57
+ }
58
+
59
+ if (tags && tags.length > 0) {
60
+ const entryTags = entry.tags.map((t) => t.toLowerCase());
61
+ for (const tag of tags) {
62
+ if (entryTags.includes(tag.toLowerCase())) {
63
+ score += 5;
64
+ }
65
+ }
66
+ }
67
+
68
+ return { entry, score };
69
+ });
70
+
71
+ return scored
72
+ .filter((s) => s.score > 0)
73
+ .sort((a, b) => b.score - a.score)
74
+ .slice(0, limit)
75
+ .map((s) => s.entry);
76
+ }
77
+
78
+ function levenshtein(a: string, b: string): number {
79
+ if (a.length === 0) return b.length;
80
+ if (b.length === 0) return a.length;
81
+
82
+ // Early exit for large length differences
83
+ if (Math.abs(a.length - b.length) > 2) return 3;
84
+
85
+ const matrix: number[][] = [];
86
+
87
+ for (let i = 0; i <= a.length; i++) {
88
+ matrix[i] = [i];
89
+ }
90
+ for (let j = 0; j <= b.length; j++) {
91
+ matrix[0]![j] = j;
92
+ }
93
+
94
+ for (let i = 1; i <= a.length; i++) {
95
+ for (let j = 1; j <= b.length; j++) {
96
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
97
+ matrix[i]![j] = Math.min(
98
+ matrix[i - 1]![j]! + 1,
99
+ matrix[i]![j - 1]! + 1,
100
+ matrix[i - 1]![j - 1]! + cost
101
+ );
102
+ }
103
+ }
104
+
105
+ return matrix[a.length]![b.length]!;
106
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "composite": true,
9
+ "declaration": true,
10
+ "declarationMap": true,
11
+ "sourceMap": true,
12
+ "strict": true,
13
+ "esModuleInterop": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "isolatedModules": true,
16
+ "skipLibCheck": true,
17
+ "resolveJsonModule": true,
18
+ "verbatimModuleSyntax": true,
19
+ "noImplicitReturns": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "types": ["node"],
23
+ "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
24
+ },
25
+ "include": ["src/**/*"],
26
+ "exclude": ["node_modules", "dist"]
27
+ }
@@ -0,0 +1,15 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ // zod@4's ESM layout needs to be inlined so vitest's vite-ssr runner
6
+ // resolves the named export correctly; otherwise `z.string` comes back
7
+ // as undefined inside the tool modules.
8
+ server: {
9
+ deps: {
10
+ inline: ['zod']
11
+ }
12
+ },
13
+ include: ['src/**/*.test.ts']
14
+ }
15
+ });