Skip to content
Open
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
57 changes: 50 additions & 7 deletions apps/react-mcp/src/mcp/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import {getDocsTool} from "./get-docs";
import {getThemeVariablesTool} from "./get-theme-variables";
import {listComponentsTool} from "./list-components";

/** Default timeout for API requests in milliseconds (30 seconds). */
const DEFAULT_FETCH_TIMEOUT_MS = 30_000;

// All available tools
const tools: Tool[] = [
listComponentsTool,
Expand All @@ -21,17 +24,57 @@ const tools: Tool[] = [
];

/**
* Initialize all tools with the server
* Fetches shared context once and passes it to all tools
* Fetch shared context with a timeout to prevent hanging on unresponsive APIs.
*/
async function getSharedContextWithTimeout(
apiBaseUrl: string,
timeoutMs: number = DEFAULT_FETCH_TIMEOUT_MS,
) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);

try {
return await getSharedContext(apiBaseUrl, {signal: controller.signal});
} finally {
clearTimeout(timeout);
}
}

/**
* Initialize all tools with the server.
* Fetches shared context once and passes it to all tools.
* If the API is unreachable, the server starts with degraded functionality
* rather than crashing entirely.
*/
export async function initializeTools(server: McpServer, config: ToolConfig = {}): Promise<void> {
// Fetch shared context once for all tools
const sharedContext = await getSharedContext(
config.apiBaseUrl || process.env.HEROUI_API_URL || "https://mcp-api.heroui.com",
);
const apiBaseUrl = config.apiBaseUrl || process.env.HEROUI_API_URL || "https://mcp-api.heroui.com";

// Fetch shared context with graceful fallback — don't crash the server if API is down
let sharedContext: Awaited<ReturnType<typeof getSharedContext>>;

try {
sharedContext = await getSharedContextWithTimeout(apiBaseUrl);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const isTimeout = error instanceof Error && error.name === "AbortError";

// eslint-disable-next-line no-console
console.error(
`Warning: Failed to fetch shared context from ${apiBaseUrl}: ${
isTimeout ? `Request timed out after ${DEFAULT_FETCH_TIMEOUT_MS / 1000}s` : message
}`,
);
// eslint-disable-next-line no-console
console.error(
"MCP server starting with limited functionality. Tools requiring component lists may return errors.",
);

// Provide a minimal fallback so the server can still start
sharedContext = {componentList: []} as Awaited<ReturnType<typeof getSharedContext>>;
}

const finalConfig: ToolConfig = {
apiBaseUrl: config.apiBaseUrl || process.env.HEROUI_API_URL || "https://mcp-api.heroui.com",
apiBaseUrl,
...config,
};

Expand Down