Skip to content
12 changes: 11 additions & 1 deletion examples/minimal/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@
* - Each version exposed at /v1/mcp and /v2/mcp
*/

import { createApp, defineTool, tool, type ClientToolsFromCore } from "@mcp-apps-kit/core";
import {
createApp,
defineTool,
tool,
type ClientToolsFromCore,
iconFromFile,
} from "@mcp-apps-kit/core";
import { defineReactUI } from "@mcp-apps-kit/ui-react-builder";
import { GreetingWidgetV1 } from "./ui/GreetingWidgetV1";
import { GreetingWidgetV2 } from "./ui/GreetingWidgetV2";
Expand Down Expand Up @@ -171,6 +177,10 @@ const echoToolV3 = defineTool({
const app = createApp({
name: "minimal-app",

// Server icon - displayed in MCP client UIs
// Can be a URL or base64 data URI
icon: iconFromFile("./src/logo.png").src,

// Shared config across all versions
config: {
cors: {
Expand Down
Binary file added examples/minimal/src/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 15 additions & 3 deletions packages/core/src/createApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
VersionConfig,
GlobalConfig,
VersionSpecificConfig,
Icon,
} from "./types/config";
import type { UIDef, UIDefs } from "./types/ui";
import type { Middleware } from "./middleware/types";
Expand Down Expand Up @@ -436,7 +437,9 @@ function deepMerge<T extends Record<string, unknown>>(
function mergeVersionConfig<T extends ToolDefs>(
globalConfig: GlobalConfig | undefined,
versionConfig: VersionConfig<T>,
globalPlugins: Plugin[] | undefined
globalPlugins: Plugin[] | undefined,
globalIcon?: string,
globalIcons?: Icon[]
): AppConfig<T> & { ui?: UIDefs } {
// Handle primitive config properties (null means remove, undefined means inherit)
const serverRoute =
Expand Down Expand Up @@ -488,6 +491,9 @@ function mergeVersionConfig<T extends ToolDefs>(
ui: versionConfig.ui,
config: mergedConfig,
plugins: mergedPlugins.length > 0 ? mergedPlugins : undefined,
// Propagate global icons to each version
icon: globalIcon,
icons: globalIcons,
};
}

Expand Down Expand Up @@ -820,8 +826,14 @@ function createMultiVersionApp<T extends ToolDefs>(config: VersionsConfig<T>): A

// Create app instance for each version
for (const [versionKey, versionConfig] of Object.entries(config.versions)) {
// Merge global and version-specific configs
const mergedConfig = mergeVersionConfig(config.config, versionConfig, config.plugins);
// Merge global and version-specific configs (including icons)
const mergedConfig = mergeVersionConfig(
config.config,
versionConfig,
config.plugins,
config.icon,
config.icons
);
mergedConfig.name = config.name; // Set app name from global config

// Extract colocated UIs from tool definitions
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export type {
ServerConfig,
VersionSpecificConfig,
DeepPartialWithNull,
Icon,
IconTheme,
} from "./types/config";

// OAuth types
Expand Down Expand Up @@ -203,6 +205,10 @@ export type {
OpenAIUIResourceMetadata,
} from "./utils/csp";

// Icon utilities
export { iconFromFile } from "./utils/icons";
export type { IconFromFileOptions } from "./utils/icons";

// =============================================================================
// MAIN ENTRY POINTS
// =============================================================================
Expand Down
76 changes: 74 additions & 2 deletions packages/core/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import type {
ToolContext,
UserLocation,
} from "../types/tools";
import type { AppConfig, CORSConfig, DebugConfig } from "../types/config";
import type { AppConfig, CORSConfig, DebugConfig, Icon } from "../types/config";
import type { UIDefs, UIDef } from "../types/ui";
import type { MiddlewareContext } from "../middleware/types";
import type { EventMap } from "../events/types";
Expand All @@ -38,6 +38,74 @@ import * as fs from "node:fs";
import * as path from "node:path";
import * as crypto from "node:crypto";

// =============================================================================
// ICON HELPERS
// =============================================================================

/**
* Valid sizes format pattern: "WxH" (e.g., "48x48") or "any" for scalable formats.
*/
const SIZES_PATTERN = /^\d+x\d+$/;

/**
* Validate icon sizes array format.
*/
function validateIconSizes(sizes: string[] | undefined, index: number): void {
if (!sizes) return;

for (const size of sizes) {
if (size !== "any" && !SIZES_PATTERN.test(size)) {
throw new Error(
`Invalid icon size "${size}" at index ${index}: ` +
`Sizes must be in "WxH" format (e.g., "48x48") or "any" for scalable formats.`
);
}
}
}

/**
* Validate an icon object has required fields.
*/
function validateIcon(icon: Icon, index: number): void {
if (!icon.src || typeof icon.src !== "string" || icon.src.trim() === "") {
throw new Error(`Invalid icon at index ${index}: 'src' must be a non-empty string`);
}
validateIconSizes(icon.sizes, index);
}

/**
* Normalize icon configuration into an icons array.
*
* Handles both the shorthand `icon` string and the full `icons` array.
* If both are provided, `icons` takes precedence.
*
* @internal This function is used internally by createApp. Do not use directly.
* @throws Error if any icon has an invalid or empty src
*/
export function normalizeIcons(
icon: string | undefined,
icons: Icon[] | undefined
): Icon[] | undefined {
// icons array takes precedence
if (icons && icons.length > 0) {
icons.forEach((ic, i) => {
validateIcon(ic, i);
});
return icons;
}

// Convert shorthand icon string to icons array
// Validate before falsy check for consistency (empty string should also throw)
if (icon !== undefined) {
if (typeof icon !== "string" || icon.trim() === "") {
throw new Error("Icon must be a non-empty string URL or data URI");
}
return [{ src: icon }];
}

return undefined;
}

// =============================================================================
// SERVER WRAPPER
// =============================================================================
Expand Down Expand Up @@ -83,10 +151,14 @@ export function createServerInstance<T extends ToolDefs>(
// Create protocol adapter
const adapter = createAdapter(config.config?.protocol ?? "mcp");

// Create MCP server
// Normalize icons from shorthand or array format
const icons = normalizeIcons(config.icon, config.icons);

// Create MCP server with icons if provided
const mcpServer = new McpServer({
name: config.name,
version: config.version,
...(icons && { icons }),
});

// Compute UI resource URIs with content hashes for cache busting
Expand Down
132 changes: 132 additions & 0 deletions packages/core/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,81 @@ import type { Plugin } from "../plugins/types";
import type { OAuthConfig } from "../server/oauth/types.js";
import type { UIDefs } from "./ui";

// =============================================================================
// ICON CONFIGURATION
// =============================================================================

/**
* Icon theme for light/dark mode support
*/
export type IconTheme = "light" | "dark";

/**
* Icon definition following the MCP specification.
*
* Supports both URL references and inline base64 data URIs.
*
* @see https://modelcontextprotocol.io/specification/2025-11-25/schema#implementation
*
* @example URL reference
* ```typescript
* { src: "https://example.com/icon.png", mimeType: "image/png", sizes: ["48x48"] }
* ```
*
* @example Base64 data URI
* ```typescript
* { src: "data:image/svg+xml;base64,PHN2Zy...", mimeType: "image/svg+xml", sizes: ["any"] }
* ```
*
* @example Theme-specific icon
* ```typescript
* { src: "https://example.com/icon-dark.png", theme: "dark" }
* ```
*/
export interface Icon {
/**
* Icon source URI.
*
* Can be:
* - HTTP/HTTPS URL (e.g., "https://example.com/icon.png")
* - Data URI with base64 encoding (e.g., "data:image/png;base64,...")
*/
src: string;

/**
* MIME type of the icon.
*
* Required MIME types that clients must support:
* - `"image/png"`
* - `"image/jpeg"`
*
* Optional MIME types:
* - `"image/svg+xml"`
* - `"image/webp"`
*
* @example "image/png"
*/
mimeType?: string;

/**
* Icon sizes in "WxH" format.
*
* Use `["any"]` for scalable formats like SVG.
*
* @example ["48x48", "96x96"]
* @example ["any"]
*/
sizes?: string[];

/**
* Theme this icon is designed for.
*
* When specified, clients can select the appropriate icon
* based on their current light/dark mode setting.
*/
theme?: IconTheme;
}

// =============================================================================
// PROTOCOL CONFIGURATION
// =============================================================================
Expand Down Expand Up @@ -463,6 +538,27 @@ export interface VersionsConfig<T extends ToolDefs = ToolDefs> {
* Merged with each version's plugins.
*/
plugins?: Plugin[];

/**
* Server icon URL or data URI (shorthand for single icon).
*
* Applied to all versions. For multiple icons or advanced configuration, use `icons` instead.
*
Comment thread
gabrypavanello marked this conversation as resolved.
* @example
* ```typescript
* icon: "https://example.com/icon.png"
* ```
*/
icon?: string;

/**
* Server icons for MCP client display.
*
* Applied to all versions. Follows the MCP specification for Implementation icons.
*
* @see https://modelcontextprotocol.io/specification/2025-11-25/schema#implementation
*/
icons?: Icon[];
}

/**
Expand Down Expand Up @@ -533,6 +629,42 @@ export interface AppConfig<T extends ToolDefs = ToolDefs> {
* ```
*/
plugins?: Plugin[];

/**
* Server icon URL or data URI (shorthand for single icon).
*
* For multiple icons or advanced configuration, use `icons` instead.
*
* @example URL
* ```typescript
* icon: "https://example.com/icon.png"
* ```
*
* @example Data URI
* ```typescript
* icon: "data:image/svg+xml;base64,PHN2Zy..."
* ```
*/
icon?: string;

/**
* Server icons for MCP client display.
*
* Allows specifying multiple icons with different sizes, formats, and themes.
* Follows the MCP specification for Implementation icons.
*
* @see https://modelcontextprotocol.io/specification/2025-11-25/schema#implementation
*
* @example
* ```typescript
* icons: [
* { src: "https://example.com/icon-48.png", mimeType: "image/png", sizes: ["48x48"] },
* { src: "https://example.com/icon-96.png", mimeType: "image/png", sizes: ["96x96"] },
* { src: "https://example.com/icon-dark.png", theme: "dark" }
* ]
* ```
*/
icons?: Icon[];
}

/**
Expand Down
Loading