Skip to content
Draft
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
13 changes: 11 additions & 2 deletions packages/mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,25 @@
"author": "HashiCorp Design Systems <design-systems@hashicorp.com>",
"type": "module",
"scripts": {
"test": "pnpm build && node --test \"dist/**/tests.js\"",
"typecheck": "pnpm tsc --noEmit",
"lint": "pnpm eslint --quiet .",
"build": "pnpm tsc -p tsconfig.json",
"start": "node ./dist/index.js"
"build:watch": "pnpm tsc -p tsconfig.json --watch --preserveWatchOutput",
"serve": "node ./dist/index.js",
"serve:watch": "node --watch --watch-path=./dist ./dist/index.js",
"start": "pnpm build && pnpm concurrently --kill-others-on-fail --names build,server \"pnpm build:watch\" \"pnpm serve:watch\"",
"start:dev": "pnpm concurrently --kill-others-on-fail --names server,inspector \"pnpm start\" \"npx @modelcontextprotocol/inspector node ./dist/index.js\""
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0"
"@hashicorp/flight-icons": "workspace:^5.0.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"zod": "^4.4.3"
},
"devDependencies": {
"concurrently": "^9.2.1",
"@eslint/js": "^9.27.0",
"@modelcontextprotocol/inspector": "^0.22.0",
"@types/node": "^22.10.2",
"eslint": "^9.27.0",
"globals": "^16.1.0",
Expand Down
129 changes: 119 additions & 10 deletions packages/mcp/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,130 @@
/**
* Copyright IBM Corp. 2021, 2025
* SPDX-License-Identifier: MPL-2.0
*/

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { dirname, resolve } from "node:path";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { registerResources } from "./resources/index.js";

const server = new McpServer({
name: "helios-design-system-mcp",
version: "0.0.0",
});
const currentFilePath = fileURLToPath(import.meta.url);
const currentDirectoryPath = dirname(currentFilePath);
const packageJsonPath = resolve(currentDirectoryPath, "../package.json");
const defaultServerVersion = "0.0.0";

const main = async (): Promise<void> => {
const transport = new StdioServerTransport();
const getServerVersion = (): string => {
try {
const rawPackageJson = readFileSync(packageJsonPath, "utf8");
const parsedPackageJson = JSON.parse(rawPackageJson) as {
version?: unknown;
};

if (typeof parsedPackageJson.version === "string") {
return parsedPackageJson.version;
}
} catch (error: unknown) {
console.error("Unable to read MCP package version:", error);
}

return defaultServerVersion;
};

const buildServer = (): McpServer => {
const server = new McpServer({
name: "helios-design-system-mcp",
version: getServerVersion(),
});

await server.connect(transport);
registerResources(server);

console.error("Helios MCP server running on stdio");
return server;
};

void main().catch((error: unknown) => {
console.error("Fatal error in MCP server:", error);
const installLifecycleHandlers = (
server: McpServer,
): { shutdown: (reason: string, error?: unknown) => Promise<void> } => {
let isShuttingDown = false;

const shutdown = async (reason: string, error?: unknown): Promise<void> => {
if (isShuttingDown) {
return;
}

isShuttingDown = true;

if (error) {
process.exitCode = 1;

console.error(`Shutting down MCP server due to ${reason}:`, error);
} else {
console.error(`Shutting down MCP server (${reason})`);
}

try {
await server.close();

console.error("MCP server shutdown complete");
} catch (closeError: unknown) {
process.exitCode = 1;

console.error("Failed to close MCP server cleanly:", closeError);
}
};

const onSigint = (): void => {
void shutdown("SIGINT");
};

const onSigterm = (): void => {
void shutdown("SIGTERM");
};

const onUnhandledRejection = (reason: unknown): void => {
void shutdown("unhandledRejection", reason);
};

const onUncaughtException = (error: Error): void => {
void shutdown("uncaughtException", error);
};

process.on("SIGINT", onSigint);
process.on("SIGTERM", onSigterm);
process.on("unhandledRejection", onUnhandledRejection);
process.on("uncaughtException", onUncaughtException);

return {
shutdown,
};
};

const main = async (): Promise<void> => {
let shutdown:
| ((reason: string, error?: unknown) => Promise<void>)
| undefined;

try {
const server = buildServer();
shutdown = installLifecycleHandlers(server).shutdown;
const transport = new StdioServerTransport();

await server.connect(transport);
// STDIO servers must never write to stdout; use stderr for diagnostics.
console.error("Helios Design System MCP server running on stdio");
} catch (error: unknown) {
if (shutdown) {
await shutdown("startup-failure", error);
} else {
console.error("Failed to initialize MCP server:", error);
}

throw error;
}
};

main().catch((error: unknown) => {
console.error("Fatal error starting MCP server:", error);
process.exit(1);
});
116 changes: 116 additions & 0 deletions packages/mcp/src/resources/flight-icons/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: MPL-2.0
*/

import {
ResourceTemplate,
ReadResourceTemplateCallback,
} from "@modelcontextprotocol/sdk/server/mcp.js";

import {
toJsonResourceResponse,
withSafeResourceHandler,
} from "../../utils/resources.js";
import { loadIconCatalog } from "../../stores/flight-icons/store.js";

import type { McpResource } from "../types.js";

export const ICONS_URI = "hds://icons";
export const ICON_URI_TEMPLATE = `${ICONS_URI}/{iconName}`;

const iconStore = loadIconCatalog();

export default [
{
name: "get_hds_icons",
uri: ICONS_URI,
config: {
title: "HDS icon catalog index",
description: "Canonical list of Flight icons with summary metadata.",
mimeType: "application/json",
},
readCallback: withSafeResourceHandler(
"get_hds_icons",
async () => {
const meta = iconStore.getMeta();

const payload = {
totalIconCount: meta.totalIconCount,
totalAssetCount: meta.totalAssetCount,
categories: meta.categories,
icons: iconStore.listIcons().map((icon) => ({
iconName: icon.iconName,
description: icon.description,
category: icon.category,
sizes: icon.sizes,
hasMapping: icon.hasMapping,
})),
};

return toJsonResourceResponse(ICONS_URI, payload);
},
ICONS_URI,
),
},
{
name: "get_hds_icon",
template: new ResourceTemplate(ICON_URI_TEMPLATE, {
list: undefined,
complete: {
iconName: (value) => {
const normalizedValue = value.trim().toLowerCase();

return iconStore
.listIcons()
.map((icon) => icon.iconName)
.filter((iconName) => {
if (normalizedValue === "") {
return true;
}

return iconName.toLowerCase().includes(normalizedValue);
})
.slice(0, 25);
},
},
}),
config: {
title: "HDS icon catalog entry",
description: "Detailed metadata for a specific Flight icon.",
mimeType: "application/json",
},
readCallback: withSafeResourceHandler(
"get_hds_icon",
async (_uri, variables) => {
const iconName = variables["iconName"];

if (typeof iconName !== "string" || iconName.trim() === "") {
throw new Error(
'Resource variable "iconName" must be a non-empty string.',
);
}

const icon = iconStore.getIconByName(iconName);

const payload =
icon === null
? {
found: false,
requestedIconName: iconName,
message: "Icon not found for provided iconName.",
}
: {
found: true,
requestedIconName: iconName,
icon,
};

return toJsonResourceResponse(
`${ICONS_URI}/${encodeURIComponent(iconName)}`,
payload,
);
},
) as ReadResourceTemplateCallback,
},
] as McpResource[];
Loading