@webspire/mcp 0.9.0 → 0.11.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/README.md +95 -60
- package/css/webspire-components.css +48 -0
- package/data/registry.json +4075 -286
- package/dist/registration.js +128 -1
- package/dist/registry.d.ts +9 -3
- package/dist/registry.js +67 -24
- package/dist/search.d.ts +9 -1
- package/dist/search.js +45 -0
- package/dist/types.d.ts +23 -0
- package/package.json +2 -2
package/dist/registration.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { loadFonts, searchSnippets } from './registry.js';
|
|
3
|
-
import { CONTENT_NEED_KEYS, CONTENT_NEED_LABEL, composePage, DOMAIN_KEYS, DOMAIN_LABEL, normalizeContentNeed, recommendFonts, searchCanvasEffects, searchPatterns, TONE_KEYS, TONE_LABEL, UX_GOAL_KEYS, UX_GOAL_LABEL, } from './search.js';
|
|
3
|
+
import { CONTENT_NEED_KEYS, CONTENT_NEED_LABEL, composePage, DOMAIN_KEYS, DOMAIN_LABEL, normalizeContentNeed, recommendFonts, searchCanvasEffects, searchMotionRecipes, searchPatterns, TONE_KEYS, TONE_LABEL, UX_GOAL_KEYS, UX_GOAL_LABEL, } from './search.js';
|
|
4
4
|
function formatSnippetBrief(s) {
|
|
5
5
|
return [
|
|
6
6
|
`**${s.title}** (${s.id})`,
|
|
@@ -785,6 +785,133 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
785
785
|
content: [{ type: 'text', text }],
|
|
786
786
|
};
|
|
787
787
|
});
|
|
788
|
+
// --- Motion Recipe Tools ---
|
|
789
|
+
function formatMotionBrief(r) {
|
|
790
|
+
return [
|
|
791
|
+
`**${r.title}** (${r.id})`,
|
|
792
|
+
` ${r.summary}`,
|
|
793
|
+
` Family: ${r.family} | Technology: ${r.technology} | Triggers: ${r.triggers.join(', ')}`,
|
|
794
|
+
r.tags.length > 0 ? ` Tags: ${r.tags.join(', ')}` : '',
|
|
795
|
+
]
|
|
796
|
+
.filter(Boolean)
|
|
797
|
+
.join('\n');
|
|
798
|
+
}
|
|
799
|
+
function formatMotionFull(r) {
|
|
800
|
+
const deps = r.dependencies
|
|
801
|
+
.map((d) => ` ${d.name} v${d.version} — ${d.cdn}`)
|
|
802
|
+
.join('\n');
|
|
803
|
+
return [
|
|
804
|
+
`# ${r.title}`,
|
|
805
|
+
'',
|
|
806
|
+
r.description ?? r.summary,
|
|
807
|
+
'',
|
|
808
|
+
'## Identity',
|
|
809
|
+
`ID: ${r.id}`,
|
|
810
|
+
`Family: ${r.family}`,
|
|
811
|
+
`Technology: ${r.technology}`,
|
|
812
|
+
`Triggers: ${r.triggers.join(', ')}`,
|
|
813
|
+
'',
|
|
814
|
+
r.tags.length > 0 ? `## Tags\n${r.tags.join(', ')}` : '',
|
|
815
|
+
deps ? `## Dependencies\n${deps}` : '',
|
|
816
|
+
'',
|
|
817
|
+
'## HTML',
|
|
818
|
+
'```html',
|
|
819
|
+
r.html,
|
|
820
|
+
'```',
|
|
821
|
+
]
|
|
822
|
+
.filter((l) => l !== undefined)
|
|
823
|
+
.join('\n');
|
|
824
|
+
}
|
|
825
|
+
server.tool('list_motion_recipes', 'List all available Motion Recipes grouped by family (reveal, scroll, transition, micro, layout).', {}, async () => {
|
|
826
|
+
const registry = await getRegistry();
|
|
827
|
+
const recipes = registry.motionRecipes ?? [];
|
|
828
|
+
const counts = {};
|
|
829
|
+
for (const r of recipes) {
|
|
830
|
+
counts[r.family] = (counts[r.family] ?? 0) + 1;
|
|
831
|
+
}
|
|
832
|
+
const result = Object.entries(counts)
|
|
833
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
834
|
+
.map(([fam, count]) => `${fam}: ${count} recipe${count > 1 ? 's' : ''}`)
|
|
835
|
+
.join('\n');
|
|
836
|
+
return {
|
|
837
|
+
content: [
|
|
838
|
+
{
|
|
839
|
+
type: 'text',
|
|
840
|
+
text: `${recipes.length} motion recipes across ${Object.keys(counts).length} families:\n\n${result}`,
|
|
841
|
+
},
|
|
842
|
+
],
|
|
843
|
+
};
|
|
844
|
+
});
|
|
845
|
+
server.tool('search_motion_recipes', 'Search Webspire Motion Recipes by keyword, family, technology, or trigger. Returns matching recipes with descriptions.', {
|
|
846
|
+
query: z
|
|
847
|
+
.string()
|
|
848
|
+
.describe('Search query, e.g. "scroll reveal", "fade up", "stagger", "clip-path"'),
|
|
849
|
+
family: z
|
|
850
|
+
.string()
|
|
851
|
+
.optional()
|
|
852
|
+
.describe('Filter by family: reveal, scroll, transition, micro, layout'),
|
|
853
|
+
technology: z
|
|
854
|
+
.string()
|
|
855
|
+
.optional()
|
|
856
|
+
.describe('Filter by technology: gsap, motion-one, both'),
|
|
857
|
+
trigger: z
|
|
858
|
+
.string()
|
|
859
|
+
.optional()
|
|
860
|
+
.describe('Filter by trigger: scroll, hover, load, click'),
|
|
861
|
+
}, async ({ query, family, technology, trigger }) => {
|
|
862
|
+
const registry = await getRegistry();
|
|
863
|
+
const recipes = registry.motionRecipes ?? [];
|
|
864
|
+
if (recipes.length === 0) {
|
|
865
|
+
return {
|
|
866
|
+
content: [
|
|
867
|
+
{
|
|
868
|
+
type: 'text',
|
|
869
|
+
text: 'No motion recipes available in registry.',
|
|
870
|
+
},
|
|
871
|
+
],
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
const results = searchMotionRecipes(recipes, { query, family, technology, trigger });
|
|
875
|
+
if (results.length === 0) {
|
|
876
|
+
return {
|
|
877
|
+
content: [
|
|
878
|
+
{
|
|
879
|
+
type: 'text',
|
|
880
|
+
text: 'No motion recipes found matching your query.',
|
|
881
|
+
},
|
|
882
|
+
],
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
return {
|
|
886
|
+
content: [
|
|
887
|
+
{
|
|
888
|
+
type: 'text',
|
|
889
|
+
text: `Found ${results.length} motion recipe${results.length > 1 ? 's' : ''}:\n\n${results.map(formatMotionBrief).join('\n\n')}\n\nUse \`get_motion_recipe(id)\` for full HTML source.`,
|
|
890
|
+
},
|
|
891
|
+
],
|
|
892
|
+
};
|
|
893
|
+
});
|
|
894
|
+
server.tool('get_motion_recipe', 'Get full HTML source and dependency info for a specific Motion Recipe. Returns a self-contained HTML file.', {
|
|
895
|
+
id: z.string().describe('Recipe ID, e.g. "reveal/fade-up", "reveal/clip-reveal"'),
|
|
896
|
+
}, async ({ id }) => {
|
|
897
|
+
const registry = await getRegistry();
|
|
898
|
+
const recipes = registry.motionRecipes ?? [];
|
|
899
|
+
const recipe = recipes.find((r) => r.id === id);
|
|
900
|
+
if (!recipe) {
|
|
901
|
+
const available = recipes.map((r) => r.id).join(', ');
|
|
902
|
+
return {
|
|
903
|
+
content: [
|
|
904
|
+
{
|
|
905
|
+
type: 'text',
|
|
906
|
+
text: `Motion recipe "${id}" not found. Available: ${available}`,
|
|
907
|
+
},
|
|
908
|
+
],
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
return {
|
|
912
|
+
content: [{ type: 'text', text: formatMotionFull(recipe) }],
|
|
913
|
+
};
|
|
914
|
+
});
|
|
788
915
|
// --- Canvas Effect Tools ---
|
|
789
916
|
server.tool('search_canvas_effects', 'Search Webspire Canvas effects by keyword, category, or capability. Returns matching effects with their descriptions.', {
|
|
790
917
|
query: z
|
package/dist/registry.d.ts
CHANGED
|
@@ -7,9 +7,15 @@ export interface RegistryOptions {
|
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
9
|
* Load the registry with fallback chain:
|
|
10
|
-
* 1. Explicit --registry-file path
|
|
11
|
-
* 2.
|
|
12
|
-
* 3. Remote fetch from
|
|
10
|
+
* 1. Explicit --registry-file path (dev / CI override)
|
|
11
|
+
* 2. In-memory cache (5-min TTL, cleared when MCP process restarts)
|
|
12
|
+
* 3. Remote fetch from webspire.de — if newer than disk cache, writes to disk
|
|
13
|
+
* 4. Disk cache (~/.cache/webspire/registry.json) — persists across restarts,
|
|
14
|
+
* always holds the last successfully fetched remote version
|
|
15
|
+
* 5. Bundled data/registry.json — frozen at npm publish time, last resort
|
|
16
|
+
*
|
|
17
|
+
* The date comparison (registry.generated) ensures the disk cache is only
|
|
18
|
+
* overwritten when the remote actually has newer content.
|
|
13
19
|
*/
|
|
14
20
|
export declare function loadRegistry(options?: RegistryOptions): Promise<Registry>;
|
|
15
21
|
/**
|
package/dist/registry.js
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
3
4
|
import { dirname, resolve } from 'node:path';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { searchSnippets as searchEntries } from './search.js';
|
|
6
7
|
const DEFAULT_REGISTRY_URL = 'https://webspire.de/registry.json';
|
|
7
8
|
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* Resolve the path to the bundled registry.json shipped with this package.
|
|
11
|
-
* Works both from src/ (development) and dist/ (published).
|
|
12
|
-
*/
|
|
9
|
+
const DISK_CACHE_PATH = resolve(homedir(), '.cache', 'webspire', 'registry.json');
|
|
10
|
+
let memCache = null;
|
|
13
11
|
function getBundledRegistryPath() {
|
|
14
12
|
try {
|
|
15
13
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
16
|
-
// From dist/ → ../data/registry.json, from src/ → ../data/registry.json
|
|
17
14
|
const candidate = resolve(thisDir, '..', 'data', 'registry.json');
|
|
18
15
|
return existsSync(candidate) ? candidate : undefined;
|
|
19
16
|
}
|
|
@@ -21,11 +18,39 @@ function getBundledRegistryPath() {
|
|
|
21
18
|
return undefined;
|
|
22
19
|
}
|
|
23
20
|
}
|
|
21
|
+
async function readDiskCache() {
|
|
22
|
+
try {
|
|
23
|
+
const raw = await readFile(DISK_CACHE_PATH, 'utf-8');
|
|
24
|
+
return JSON.parse(raw);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function writeDiskCache(registry) {
|
|
31
|
+
try {
|
|
32
|
+
await mkdir(dirname(DISK_CACHE_PATH), { recursive: true });
|
|
33
|
+
await writeFile(DISK_CACHE_PATH, JSON.stringify(registry), 'utf-8');
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Best-effort — disk write failures are non-fatal
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Returns true if registry a was generated after registry b. */
|
|
40
|
+
function isNewer(a, b) {
|
|
41
|
+
return new Date(a.generated ?? 0).getTime() > new Date(b.generated ?? 0).getTime();
|
|
42
|
+
}
|
|
24
43
|
/**
|
|
25
44
|
* Load the registry with fallback chain:
|
|
26
|
-
* 1. Explicit --registry-file path
|
|
27
|
-
* 2.
|
|
28
|
-
* 3. Remote fetch from
|
|
45
|
+
* 1. Explicit --registry-file path (dev / CI override)
|
|
46
|
+
* 2. In-memory cache (5-min TTL, cleared when MCP process restarts)
|
|
47
|
+
* 3. Remote fetch from webspire.de — if newer than disk cache, writes to disk
|
|
48
|
+
* 4. Disk cache (~/.cache/webspire/registry.json) — persists across restarts,
|
|
49
|
+
* always holds the last successfully fetched remote version
|
|
50
|
+
* 5. Bundled data/registry.json — frozen at npm publish time, last resort
|
|
51
|
+
*
|
|
52
|
+
* The date comparison (registry.generated) ensures the disk cache is only
|
|
53
|
+
* overwritten when the remote actually has newer content.
|
|
29
54
|
*/
|
|
30
55
|
export async function loadRegistry(options) {
|
|
31
56
|
// 1. Explicit file path
|
|
@@ -33,24 +58,42 @@ export async function loadRegistry(options) {
|
|
|
33
58
|
const raw = await readFile(options.filePath, 'utf-8');
|
|
34
59
|
return JSON.parse(raw);
|
|
35
60
|
}
|
|
36
|
-
// 2.
|
|
61
|
+
// 2. In-memory cache (avoids re-fetching within the same session)
|
|
62
|
+
if (memCache && Date.now() - memCache.loadedAt < CACHE_TTL_MS) {
|
|
63
|
+
return memCache.registry;
|
|
64
|
+
}
|
|
65
|
+
// 3. Remote fetch — compare with disk cache, write if newer
|
|
66
|
+
try {
|
|
67
|
+
const url = options?.url ?? DEFAULT_REGISTRY_URL;
|
|
68
|
+
const response = await fetch(url);
|
|
69
|
+
if (response.ok) {
|
|
70
|
+
const remote = (await response.json());
|
|
71
|
+
memCache = { registry: remote, loadedAt: Date.now() };
|
|
72
|
+
// Write to disk only when remote is actually newer than what's cached
|
|
73
|
+
const disk = await readDiskCache();
|
|
74
|
+
if (!disk || isNewer(remote, disk)) {
|
|
75
|
+
await writeDiskCache(remote);
|
|
76
|
+
}
|
|
77
|
+
return remote;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Network unavailable — fall through to disk cache
|
|
82
|
+
}
|
|
83
|
+
// 4. Disk cache — last successfully fetched version, survives process restarts
|
|
84
|
+
const disk = await readDiskCache();
|
|
85
|
+
if (disk) {
|
|
86
|
+
memCache = { registry: disk, loadedAt: Date.now() };
|
|
87
|
+
return disk;
|
|
88
|
+
}
|
|
89
|
+
// 5. Bundled registry — frozen at npm publish time
|
|
37
90
|
const bundledPath = getBundledRegistryPath();
|
|
38
|
-
if (bundledPath
|
|
91
|
+
if (bundledPath) {
|
|
39
92
|
const raw = await readFile(bundledPath, 'utf-8');
|
|
40
93
|
return JSON.parse(raw);
|
|
41
94
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return cache.registry;
|
|
45
|
-
}
|
|
46
|
-
const url = options?.url ?? DEFAULT_REGISTRY_URL;
|
|
47
|
-
const response = await fetch(url);
|
|
48
|
-
if (!response.ok) {
|
|
49
|
-
throw new Error(`Failed to fetch registry from ${url}: ${response.statusText}`);
|
|
50
|
-
}
|
|
51
|
-
const registry = (await response.json());
|
|
52
|
-
cache = { registry, loadedAt: Date.now() };
|
|
53
|
-
return registry;
|
|
95
|
+
throw new Error('Registry unavailable: network unreachable and no local cache found. ' +
|
|
96
|
+
'Try reinstalling @webspire/mcp or check your internet connection.');
|
|
54
97
|
}
|
|
55
98
|
/**
|
|
56
99
|
* Load the bundled fonts.json shipped with this package.
|
package/dist/search.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CanvasEffectEntry, FontRecommendation, FontsData, PatternEntry, SnippetEntry } from './types.js';
|
|
1
|
+
import type { CanvasEffectEntry, FontRecommendation, FontsData, MotionRecipeEntry, PatternEntry, SnippetEntry } from './types.js';
|
|
2
2
|
export interface SnippetSearchOptions {
|
|
3
3
|
query: string;
|
|
4
4
|
category?: string;
|
|
@@ -61,3 +61,11 @@ export interface CanvasSearchOptions {
|
|
|
61
61
|
limit?: number;
|
|
62
62
|
}
|
|
63
63
|
export declare function searchCanvasEffects(effects: CanvasEffectEntry[], options: CanvasSearchOptions): CanvasEffectEntry[];
|
|
64
|
+
export interface MotionSearchOptions {
|
|
65
|
+
query: string;
|
|
66
|
+
family?: string;
|
|
67
|
+
technology?: string;
|
|
68
|
+
trigger?: string;
|
|
69
|
+
limit?: number;
|
|
70
|
+
}
|
|
71
|
+
export declare function searchMotionRecipes(recipes: MotionRecipeEntry[], options: MotionSearchOptions): MotionRecipeEntry[];
|
package/dist/search.js
CHANGED
|
@@ -999,3 +999,48 @@ export function searchCanvasEffects(effects, options) {
|
|
|
999
999
|
.sort((a, b) => b.score - a.score);
|
|
1000
1000
|
return scored.slice(0, limit).map(({ effect }) => effect);
|
|
1001
1001
|
}
|
|
1002
|
+
export function searchMotionRecipes(recipes, options) {
|
|
1003
|
+
const { query, family, technology, trigger, limit = 10 } = options;
|
|
1004
|
+
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
1005
|
+
const stems = words.map(stem);
|
|
1006
|
+
const expanded = expandWithSynonyms(words);
|
|
1007
|
+
if (stems.length === 0 && !family && !technology && !trigger)
|
|
1008
|
+
return recipes.slice(0, limit);
|
|
1009
|
+
const filtered = recipes.filter((r) => {
|
|
1010
|
+
if (family && r.family !== family)
|
|
1011
|
+
return false;
|
|
1012
|
+
if (technology && r.technology !== technology)
|
|
1013
|
+
return false;
|
|
1014
|
+
if (trigger && !r.triggers.includes(trigger))
|
|
1015
|
+
return false;
|
|
1016
|
+
return true;
|
|
1017
|
+
});
|
|
1018
|
+
if (stems.length === 0)
|
|
1019
|
+
return filtered.slice(0, limit);
|
|
1020
|
+
const scored = filtered
|
|
1021
|
+
.map((recipe) => {
|
|
1022
|
+
let score = 0;
|
|
1023
|
+
const idLower = recipe.id.toLowerCase();
|
|
1024
|
+
for (const term of stems) {
|
|
1025
|
+
if (idLower.includes(term))
|
|
1026
|
+
score += 5;
|
|
1027
|
+
}
|
|
1028
|
+
const title = recipe.title.toLowerCase();
|
|
1029
|
+
score += stems.filter((w) => title.includes(w)).length * 3;
|
|
1030
|
+
const summary = recipe.summary.toLowerCase();
|
|
1031
|
+
score += stems.filter((w) => summary.includes(w)).length * 2;
|
|
1032
|
+
const tagStr = recipe.tags.join(' ').toLowerCase();
|
|
1033
|
+
score += stems.filter((w) => tagStr.includes(w)).length * 2;
|
|
1034
|
+
const tech = recipe.technology.toLowerCase();
|
|
1035
|
+
score += stems.filter((w) => tech.includes(w)).length * 2;
|
|
1036
|
+
const synonymOnly = expanded.filter((t) => !stems.includes(t));
|
|
1037
|
+
if (synonymOnly.length > 0) {
|
|
1038
|
+
const allText = [title, summary, tagStr].join(' ').toLowerCase();
|
|
1039
|
+
score += synonymOnly.filter((w) => allText.includes(w)).length;
|
|
1040
|
+
}
|
|
1041
|
+
return { recipe, score };
|
|
1042
|
+
})
|
|
1043
|
+
.filter(({ score }) => score > 0)
|
|
1044
|
+
.sort((a, b) => b.score - a.score);
|
|
1045
|
+
return scored.slice(0, limit).map(({ recipe }) => recipe);
|
|
1046
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -226,6 +226,28 @@ export interface CanvasEffectEntry {
|
|
|
226
226
|
solves: string[];
|
|
227
227
|
js: string;
|
|
228
228
|
}
|
|
229
|
+
export interface MotionRecipeEntry {
|
|
230
|
+
id: string;
|
|
231
|
+
title: string;
|
|
232
|
+
summary: string;
|
|
233
|
+
description?: string;
|
|
234
|
+
family: 'reveal' | 'scroll' | 'transition' | 'micro' | 'layout';
|
|
235
|
+
variant: string;
|
|
236
|
+
technology: 'gsap' | 'motion-one' | 'both';
|
|
237
|
+
triggers: Array<'scroll' | 'hover' | 'load' | 'click'>;
|
|
238
|
+
tags: string[];
|
|
239
|
+
dependencies: Array<{
|
|
240
|
+
name: string;
|
|
241
|
+
cdn: string;
|
|
242
|
+
version: string;
|
|
243
|
+
}>;
|
|
244
|
+
governance: {
|
|
245
|
+
status: 'draft' | 'review' | 'published' | 'deprecated';
|
|
246
|
+
quality: 'experimental' | 'stable' | 'flagship';
|
|
247
|
+
updatedAt: string;
|
|
248
|
+
};
|
|
249
|
+
html: string;
|
|
250
|
+
}
|
|
229
251
|
export interface Registry {
|
|
230
252
|
version: string;
|
|
231
253
|
generated: string;
|
|
@@ -233,6 +255,7 @@ export interface Registry {
|
|
|
233
255
|
patterns?: PatternEntry[];
|
|
234
256
|
templates?: TemplateEntry[];
|
|
235
257
|
canvasEffects?: CanvasEffectEntry[];
|
|
258
|
+
motionRecipes?: MotionRecipeEntry[];
|
|
236
259
|
fonts?: FontsData;
|
|
237
260
|
mountCanvas?: string;
|
|
238
261
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webspire/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for Webspire — AI-native discovery of CSS snippets, UI patterns, canvas effects, page templates, and font recommendations",
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"description": "MCP server for Webspire — AI-native discovery of CSS snippets, UI patterns, canvas effects, page templates, motion recipes, and font recommendations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./dist/index.js",
|