/**
 * @fileoverview Server manifest — the single source of truth consumed by the
 * HTTP transport's three surfaces: the bespoke `/mcp` status JSON, the
 * SEP-1649 Server Card at `/.well-known/mcp.json`, and the HTML landing page
 * at `/`. `buildServerManifest()` is a pure function: feed it config + the
 * definition arrays that were already passed to `createApp()` and it produces
 * everything the surfaces need to render without re-reading registries or
 * inspecting Zod schemas a second time.
 *
 * Landing-page content limits (tagline/logo/links/repoRoot shape) are
 * enforced at build time here and surfaced through the definition linter —
 * surprising input fails loudly at startup instead of quietly at render.
 * @module src/core/serverManifest
 */
import { SUPPORTED_PROTOCOL_VERSIONS } from '@modelcontextprotocol/sdk/types.js';
import { toJSONSchema } from 'zod/v4/core';
import { FRAMEWORK_NAME, FRAMEWORK_VERSION } from '../config/index.js';
import { getDisabledMetadata, } from '../mcp-server/tools/utils/disabled-tool.js';
import { configurationError } from '../types-global/errors.js';
/**
 * Tab IDs for the connect card; each may have a derived snippet or operator
 * override. Order here is the render order on the landing page: Claude leads
 * because it's the most common conversion path for MCP visitors; named
 * clients follow alphabetically, then transport/protocol fallbacks. Default
 * selected tab is `claude` (wired in the connect-card renderer, not by index).
 */
export const CONNECT_SNIPPET_TABS = [
    'claude',
    'codex',
    'cursor',
    'gemini',
    'stdio',
    'http',
    'curl',
];
// ---------------------------------------------------------------------------
// Content limits (enforced by the linter)
// ---------------------------------------------------------------------------
/** Max tagline length. Longer strings break the hero layout. */
export const LANDING_MAX_TAGLINE_LENGTH = 120;
/** Max logo size when supplied as a data URI (bytes). */
export const LANDING_MAX_LOGO_BYTES = 24 * 1024;
/** Max footer link count. Beyond this the cluster wraps unreadably. */
export const LANDING_MAX_LINKS = 6;
/** Max `envExample` entries. Beyond this the connect card grows awkwardly tall. */
export const LANDING_MAX_ENV_EXAMPLE = 12;
/** `repoRoot` must match `https://github.com/{owner}/{repo}` with no trailing path. */
export const GITHUB_REPO_ROOT_PATTERN = /^https:\/\/github\.com\/([^/\s]+)\/([^/\s?#]+?)\/?$/;
/** Max length for `landing.theme.accent`. Anything longer is almost certainly an injection attempt. */
export const LANDING_ACCENT_MAX_LENGTH = 128;
/**
 * Reject CSS values that could escape the custom-property declaration context
 * (`--accent: ${value};`). The renderer HTML-escapes the value, but that's the
 * wrong escape for a CSS context — `;`, `{`, `}`, comment markers, and
 * backslash escapes all survive HTML escaping and can break out. This is the
 * real defense; it's a content shape check, not a full CSS color parser, so
 * new color spaces (`oklch`, `color-mix`, future additions) pass through
 * without regex churn.
 */
export function isSafeCssColor(value) {
    const trimmed = value.trim();
    if (trimmed.length === 0 || trimmed.length > LANDING_ACCENT_MAX_LENGTH)
        return false;
    if (/[;{}\\<>]/.test(trimmed))
        return false;
    if (trimmed.includes('/*') || trimmed.includes('*/'))
        return false;
    return /^[#a-zA-Z]/.test(trimmed);
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const PRE_RELEASE_PATTERNS = [
    [/-alpha/i, 'alpha'],
    [/-beta/i, 'beta'],
    [/-rc/i, 'rc'],
];
/** Classifies a semver string — anything pre-1.0 or with a `-tag` is a pre-release. */
export function classifyPreRelease(version) {
    for (const [pattern, label] of PRE_RELEASE_PATTERNS) {
        if (pattern.test(version))
            return { isPreRelease: true, label };
    }
    // `0.x.y` signals unstable per semver convention
    if (/^0\./.test(version))
        return { isPreRelease: true, label: 'pre-1.0' };
    return { isPreRelease: false };
}
/** Parses `https://github.com/{owner}/{repo}` into structured form. */
export function detectGitHubRepo(url) {
    if (!url)
        return;
    const match = GITHUB_REPO_ROOT_PATTERN.exec(url.trim());
    if (!match)
        return;
    const [, owner, repo] = match;
    if (!owner || !repo)
        return;
    return { url: `https://github.com/${owner}/${repo}`, owner, repo };
}
/** Converts snake_case or kebab-case to Title Case. */
export function deriveTitleFromName(name) {
    return name
        .replace(/[_-]/g, ' ')
        .replace(/\b\w/g, (ch) => ch.toUpperCase())
        .trim();
}
/** snake_case → kebab-case (for source-link path derivation). */
export function snakeToKebab(name) {
    return name.replace(/_/g, '-').toLowerCase();
}
/** Safe Zod → JSON Schema. Returns undefined if the schema isn't serializable. */
function safeToJsonSchema(schema) {
    if (!schema || typeof schema !== 'object')
        return;
    try {
        return toJSONSchema(schema);
    }
    catch {
        return;
    }
}
/** Extract required field names from a Zod object schema. Silent on failure. */
function extractRequiredFields(schema) {
    if (!schema || typeof schema !== 'object')
        return [];
    try {
        const asObj = schema;
        if (!asObj.shape || typeof asObj.shape !== 'object')
            return [];
        const fields = [];
        for (const [key, field] of Object.entries(asObj.shape)) {
            if (typeof field?.isOptional === 'function' && !field.isOptional()) {
                fields.push(key);
            }
        }
        return fields;
    }
    catch {
        return [];
    }
}
/** Detect whether a tool's `_meta.ui.resourceUri` flags it as an MCP App tool. */
function isMcpAppTool(def) {
    const meta = def._meta;
    return typeof meta?.ui?.resourceUri === 'string' && meta.ui.resourceUri.length > 0;
}
/**
 * Build a canonical source-link URL for a definition by convention:
 *   `${repoRoot}/blob/main/src/mcp-server/<kind>/definitions/<kebab-name>.<suffix>.ts`
 *
 * Returns undefined if `repoRoot` isn't set or the name is empty.
 */
export function deriveSourceUrl(repoRoot, kind, name) {
    if (!repoRoot || !name)
        return;
    const suffix = kind === 'tools' ? 'tool' : kind === 'resources' ? 'resource' : 'prompt';
    const fileName = `${snakeToKebab(name)}.${suffix}.ts`;
    return `${repoRoot.url}/blob/main/src/mcp-server/${kind}/definitions/${fileName}`;
}
/** Normalize `landing.links` to a concrete array with `external` defaulted. */
function normalizeLinks(links) {
    if (!Array.isArray(links) || links.length === 0)
        return [];
    return links.slice(0, LANDING_MAX_LINKS).map((link) => ({
        href: link.href,
        label: link.label,
        external: link.external ?? /^https?:\/\//i.test(link.href),
    }));
}
/** Discover an npm package auto-link from consumer package.json metadata. */
function detectNpmPackage(pkgName) {
    if (!pkgName)
        return;
    // Only link scoped packages or names that look like published packages.
    // Avoids false-positive links for internal `my-mcp-server`-style names.
    if (!pkgName.startsWith('@') && !pkgName.includes('/'))
        return;
    return {
        name: pkgName,
        url: `https://www.npmjs.com/package/${encodeURIComponent(pkgName)}`,
    };
}
/**
 * Normalize `landing.envExample` into the ordered array shape the renderer
 * consumes. Preserves insertion order (important for the rendered JSON block),
 * truncates to {@link LANDING_MAX_ENV_EXAMPLE}, and drops non-string entries
 * silently — the linter surfaces structural issues separately.
 */
function normalizeEnvExample(envExample) {
    if (!envExample || typeof envExample !== 'object')
        return [];
    const entries = [];
    for (const [key, value] of Object.entries(envExample)) {
        if (typeof key !== 'string' || key.length === 0)
            continue;
        if (typeof value !== 'string')
            continue;
        entries.push({ key, value });
        if (entries.length >= LANDING_MAX_ENV_EXAMPLE)
            break;
    }
    return entries;
}
/**
 * Build the server manifest. Pure — same inputs produce the same output.
 * Called once at startup from `composeServices()`.
 */
export function buildServerManifest(input) {
    const { config, description, extensions, icons, landing = {}, prompts, resources, title, tools, websiteUrl, } = input;
    const protocolVersions = SUPPORTED_PROTOCOL_VERSIONS;
    const latestProtocol = protocolVersions[0] ?? '2025-06-18';
    // Auto-derive repoRoot from homepage when the consumer didn't set it explicitly.
    const repoRoot = detectGitHubRepo(landing.repoRoot ?? config.mcpServerHomepage);
    const accent = landing.theme?.accent ?? '#6366f1'; // indigo-500 — used by tokens when unset
    if (!isSafeCssColor(accent)) {
        throw configurationError(`landing.theme.accent "${accent}" contains characters that are unsafe to interpolate into CSS. Use a hex color (e.g. "#6366f1"), a named color (e.g. "indigo"), or a functional notation like "rgb(99 102 241)" or "oklch(0.7 0.2 280)".`, { field: 'landing.theme.accent', value: accent });
    }
    const attribution = landing.attribution ?? true;
    // Default the inventory gate to auth presence: an auth-enabled server
    // shouldn't disclose more through the landing page than through GET /mcp.
    // Explicit landing.requireAuth (true or false) always wins.
    const requireAuth = landing.requireAuth ?? config.mcpAuthMode !== 'none';
    // Source URL resolution order: per-definition override → repo-convention derivation.
    const toolList = tools.map((def) => {
        const d = def;
        const name = d.name ?? '';
        const inputSchema = safeToJsonSchema(d.input);
        const outputSchema = safeToJsonSchema(d.output);
        const requiredFields = extractRequiredFields(d.input);
        const sourceUrl = d.sourceUrl ?? deriveSourceUrl(repoRoot, 'tools', name);
        const disabled = getDisabledMetadata(def);
        return {
            name,
            title: d.title ?? d.annotations?.title ?? deriveTitleFromName(name),
            description: d.description ?? '',
            ...(d.annotations && { annotations: d.annotations }),
            isTask: d.task === true,
            isApp: isMcpAppTool(def),
            ...(d.auth && d.auth.length > 0 && { auth: d.auth }),
            ...(inputSchema !== undefined && { inputSchema }),
            ...(outputSchema !== undefined && { outputSchema }),
            requiredFields,
            ...(sourceUrl && { sourceUrl }),
            ...(disabled && { disabled }),
        };
    });
    const resourceList = resources.map((def) => {
        const name = def.name ?? def.uriTemplate ?? '';
        const sourceUrl = def.sourceUrl ?? deriveSourceUrl(repoRoot, 'resources', name);
        return {
            name,
            title: def.title ?? deriveTitleFromName(name),
            description: def.description ?? '',
            uriTemplate: def.uriTemplate ?? '',
            ...(def.mimeType && { mimeType: def.mimeType }),
            ...(def.annotations && { annotations: def.annotations }),
            ...(def.auth && def.auth.length > 0 && { auth: def.auth }),
            ...(sourceUrl && { sourceUrl }),
        };
    });
    const promptList = prompts.map((def) => {
        const name = def.name ?? '';
        const sourceUrl = def.sourceUrl ?? deriveSourceUrl(repoRoot, 'prompts', name);
        const args = extractPromptArgs(def.args);
        return {
            name,
            title: deriveTitleFromName(name),
            description: def.description ?? '',
            args,
            ...(sourceUrl && { sourceUrl }),
        };
    });
    return {
        server: {
            name: config.mcpServerName,
            version: config.mcpServerVersion,
            // Explicit option wins; fall back to env/package.json config value.
            ...(description
                ? { description }
                : config.mcpServerDescription
                    ? { description: config.mcpServerDescription }
                    : {}),
            ...(config.mcpServerKeywords?.length && { keywords: config.mcpServerKeywords }),
            ...(config.mcpServerHomepage && { homepage: config.mcpServerHomepage }),
            ...(title && { title }),
            ...(websiteUrl && { websiteUrl }),
            ...(icons && { icons }),
            environment: config.environment,
        },
        transport: {
            type: config.mcpTransportType,
            endpointPath: config.mcpHttpEndpointPath,
            sessionMode: config.mcpSessionMode,
            ...(config.mcpPublicUrl && { publicUrl: config.mcpPublicUrl }),
        },
        protocol: {
            supportedVersions: protocolVersions,
            latestVersion: latestProtocol,
        },
        definitionCounts: {
            tools: tools.length,
            resources: resources.length,
            prompts: prompts.length,
        },
        capabilities: {
            logging: true,
            tools: tools.length > 0,
            resources: resources.length > 0,
            prompts: prompts.length > 0,
        },
        ...(extensions && { extensions }),
        framework: {
            name: FRAMEWORK_NAME,
            version: FRAMEWORK_VERSION,
            homepage: 'https://github.com/cyanheads/mcp-ts-core',
        },
        auth: {
            mode: config.mcpAuthMode,
            ...(config.oauthIssuerUrl && { oauthIssuer: config.oauthIssuerUrl }),
            ...(config.oauthAudience && { oauthAudience: config.oauthAudience }),
            ...(config.oauthJwksUri && { jwksUri: config.oauthJwksUri }),
            ...(config.mcpServerResourceIdentifier && {
                resourceIdentifier: config.mcpServerResourceIdentifier,
            }),
        },
        definitions: {
            tools: toolList,
            resources: resourceList,
            prompts: promptList,
        },
        landing: buildManifestLanding({
            landing,
            accent,
            repoRoot,
            requireAuth,
            attribution,
            // mcpServerName is derived from the consumer's package.json > MCP_SERVER_NAME > options.name.
            // detectNpmPackage filters to scoped/slashed names, so unscoped server names yield no link.
            packageName: config.mcpServerName,
            version: config.mcpServerVersion,
        }),
        builtAt: new Date().toISOString(),
    };
}
/** Build the derived `landing` branch of the manifest, respecting exactOptionalPropertyTypes. */
function buildManifestLanding(input) {
    const { landing, accent, repoRoot, requireAuth, attribution, packageName, version } = input;
    const npmPackage = detectNpmPackage(packageName);
    const changelogUrl = landing.changelogUrl ?? (repoRoot ? `${repoRoot.url}/blob/main/CHANGELOG.md` : undefined);
    return {
        enabled: landing.enabled !== false,
        ...(landing.tagline && { tagline: landing.tagline }),
        ...(landing.logo && { logo: landing.logo }),
        links: normalizeLinks(landing.links),
        envExample: normalizeEnvExample(landing.envExample),
        connectSnippets: normalizeConnectSnippets(landing.connectSnippets),
        theme: { accent },
        ...(repoRoot && { repoRoot }),
        ...(changelogUrl && { changelogUrl }),
        requireAuth,
        attribution,
        ...(npmPackage && { npmPackage }),
        preRelease: classifyPreRelease(version),
    };
}
/**
 * Filter operator-supplied connect snippet overrides to known tab IDs with
 * string values. Unknown keys and non-string values are dropped silently —
 * the linter surfaces structural issues separately.
 */
function normalizeConnectSnippets(overrides) {
    if (!overrides || typeof overrides !== 'object')
        return {};
    const out = {};
    for (const tab of CONNECT_SNIPPET_TABS) {
        const value = overrides[tab];
        if (typeof value === 'string' && value.length > 0)
            out[tab] = value;
    }
    return out;
}
/** Extract prompt args (name/description/required) from a Zod object. */
function extractPromptArgs(schema) {
    if (!schema || typeof schema !== 'object')
        return [];
    try {
        const asObj = schema;
        if (!asObj.shape)
            return [];
        const args = [];
        for (const [name, field] of Object.entries(asObj.shape)) {
            const required = typeof field?.isOptional === 'function' ? !field.isOptional() : true;
            const description = field?.description ?? field?._def?.description;
            args.push({ name, required, ...(description && { description }) });
        }
        return args;
    }
    catch {
        return [];
    }
}
//# sourceMappingURL=serverManifest.js.map