Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions packages/ghost-mcp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@ghost/mcp",
"version": "0.1.0",
"description": "MCP server for the Ghost UI component registry",
"license": "Apache-2.0",
"type": "module",
"bin": {
"ghost-mcp": "./dist/bin.js"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsc --build && node scripts/copy-assets.mjs"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0",
"zod": "^3.25.0"
}
}
18 changes: 18 additions & 0 deletions packages/ghost-mcp/scripts/copy-assets.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const assetsDir = path.resolve(__dirname, "../dist/assets");

fs.mkdirSync(assetsDir, { recursive: true });

fs.copyFileSync(
path.resolve(__dirname, "../../ghost-ui/registry.json"),
path.join(assetsDir, "registry.json"),
);

fs.copyFileSync(
path.resolve(__dirname, "../../ghost-ui/.shadcn/skills.md"),
path.join(assetsDir, "skills.md"),
);
7 changes: 7 additions & 0 deletions packages/ghost-mcp/src/bin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env node
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createServer } from "./server.js";

const server = createServer();
const transport = new StdioServerTransport();
await server.connect(transport);
148 changes: 148 additions & 0 deletions packages/ghost-mcp/src/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const assetsDir = path.resolve(__dirname, "assets");

export interface RegistryFile {
type: string;
target: string;
path: string;
}

export interface RegistryCssVars {
theme?: Record<string, string>;
light?: Record<string, string>;
dark?: Record<string, string>;
}

export interface RegistryItem {
name: string;
type: string;
title?: string;
description?: string;
author?: string;
style?: string;
iconLibrary?: string;
baseColor?: string;
dependencies?: string[];
devDependencies?: string[];
registryDependencies?: string[];
files: RegistryFile[];
categories?: string[];
cssVars?: RegistryCssVars;
}

export interface Registry {
$schema: string;
name: string;
homepage: string;
items: RegistryItem[];
}

const registryRaw = fs.readFileSync(
path.join(assetsDir, "registry.json"),
"utf-8",
);
const registry: Registry = JSON.parse(registryRaw);

const skillsContent = fs.readFileSync(
path.join(assetsDir, "skills.md"),
"utf-8",
);

const itemsByName = new Map<string, RegistryItem>();
for (const item of registry.items) {
itemsByName.set(item.name, item);
}

export interface ItemSummary {
name: string;
type: string;
categories: string[];
dependencies: string[];
registryDependencies: string[];
description?: string;
}

export function searchItems(
query?: string,
category?: string,
aiOnly?: boolean,
): ItemSummary[] {
let items = registry.items;

if (category) {
const lower = category.toLowerCase();
items = items.filter((i) =>
(i.categories ?? []).some((c) => c.toLowerCase() === lower),
);
}

if (aiOnly) {
items = items.filter((i) => (i.categories ?? []).includes("ai"));
}

if (query) {
const lower = query.toLowerCase();
items = items.filter((i) => i.name.toLowerCase().includes(lower));
}

return items.map((i) => ({
name: i.name,
type: i.type,
categories: i.categories ?? [],
dependencies: i.dependencies ?? [],
registryDependencies: i.registryDependencies ?? [],
description: i.description,
}));
}

export function getRegistryItem(name: string): RegistryItem | undefined {
return itemsByName.get(name);
}

export function getCategoriesWithCounts(): Record<
string,
{ name: string; count: number }
> {
const result: Record<string, { name: string; count: number }> = {};
for (const item of registry.items) {
for (const cat of item.categories ?? []) {
if (result[cat]) {
result[cat].count++;
} else {
result[cat] = { name: cat, count: 1 };
}
}
}
return result;
}

export function getThemePreset(name: string): RegistryCssVars | undefined {
const item = registry.items.find(
(i) => i.type === "registry:theme" && i.name === name,
);
return item?.cssVars;
}

export function getComponentSource(name: string): string | null {
const item = itemsByName.get(name);
if (!item || item.files.length === 0) return null;

const filePath = item.files[0].path;
const ghostUiDir = path.resolve(__dirname, "../../ghost-ui");
const fullPath = path.join(ghostUiDir, filePath);

try {
return fs.readFileSync(fullPath, "utf-8");
} catch {
return null;
}
}

export function getSkills(): string {
return skillsContent;
}
1 change: 1 addition & 0 deletions packages/ghost-mcp/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { createServer } from "./server.js";
50 changes: 50 additions & 0 deletions packages/ghost-mcp/src/resources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { getSkills } from "./data.js";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const assetsDir = path.resolve(__dirname, "assets");

export function registerResources(server: McpServer): void {
server.resource(
"Ghost UI Registry",
"ghost://registry",
{ mimeType: "application/json" },
async () => {
const content = fs.readFileSync(
path.join(assetsDir, "registry.json"),
"utf-8",
);
return {
contents: [
{
uri: "ghost://registry",
mimeType: "application/json",
text: content,
},
],
};
},
);

server.resource(
"Ghost UI Skills",
"ghost://skills",
{ mimeType: "text/markdown" },
async () => {
const content = getSkills();
return {
contents: [
{
uri: "ghost://skills",
mimeType: "text/markdown",
text: content,
},
],
};
},
);
}
15 changes: 15 additions & 0 deletions packages/ghost-mcp/src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerResources } from "./resources.js";
import { registerTools } from "./tools.js";

export function createServer(): McpServer {
const server = new McpServer({
name: "ghost-ui",
version: "0.1.0",
});

registerTools(server);
registerResources(server);

return server;
}
Loading
Loading