1import type { ToolActivityIcon, ToolActivitySource } from "@t3tools/contracts";
2import { resolveT3McpToolDefinition } from "@t3tools/shared/t3McpToolPresentation";
3
4export function normalizeMcpText(value: unknown, maxLength = 160): string | undefined {
5 if (typeof value !== "string") return undefined;
6 const text = value.trim().replace(/\s+/gu, " ");
7 return text.length > 0 && text.length <= maxLength ? text : undefined;
8}
9
10export function normalizeMcpHttpUrl(value: unknown): string | undefined {
11 if (typeof value !== "string" || value.length > 4096) return undefined;
12 try {
13 const url = new URL(value);
14 return (url.protocol === "https:" || url.protocol === "http:") && url.href.length <= 4096
15 ? url.href
16 : undefined;
17 } catch {
18 return undefined;
19 }
20}
21
22export function mcpToolPresentation(input: {
23 readonly toolName?: unknown;
24 readonly serverName?: unknown;
25 readonly title?: unknown;
26 readonly serverDisplayName?: unknown;
27 readonly iconUrl?: unknown;
28 readonly iconUrlDark?: unknown;
29 readonly source?: unknown;
30}): {
31 readonly title?: string;
32 readonly toolIcon?: ToolActivityIcon;
33 readonly toolSource?: ToolActivitySource;
34} {
35 const source =
36 typeof input.source === "object" && input.source !== null ? input.source : undefined;
37 const qualified =
38 typeof input.toolName === "string" ? /^mcp__(.+?)__(.+)$/i.exec(input.toolName) : null;
39 const server = normalizeMcpText(input.serverName ?? qualified?.[1] ?? input.serverDisplayName);
40 const tool = normalizeMcpText(qualified?.[2] ?? input.toolName);
41 if (server && tool && resolveT3McpToolDefinition(`${server}.${tool}`)) return {};
42 const title =
43 normalizeMcpText(input.title) ??
44 (server && tool ? normalizeMcpText(tool.replace(/[_-]+/gu, " ")) : undefined);
45 if (!server) return title ? { title } : {};
46 const name =
47 normalizeMcpText(input.serverDisplayName) ??
48 normalizeMcpText(source && Reflect.get(source, "name")) ??
49 normalizeMcpText(server.replace(/[_-]+/gu, " ")) ??
50 server;
51 const logoUrl =
52 normalizeMcpHttpUrl(input.iconUrl) ??
53 normalizeMcpHttpUrl(source && Reflect.get(source, "logoUrl"));
54 const logoUrlDark =
55 normalizeMcpHttpUrl(input.iconUrlDark) ??
56 normalizeMcpHttpUrl(source && Reflect.get(source, "logoUrlDark"));
57 const icon = logoUrl
58 ? { _tag: "themed-logo" as const, logoUrl, ...(logoUrlDark ? { logoUrlDark } : {}) }
59 : undefined;
60 return {
61 ...(title ? { title } : {}),
62 ...(icon ? { toolIcon: icon } : {}),
63 toolSource: {
64 key: `mcp:${server.toLowerCase()}`,
65 name,
66 kind: "integration",
67 ...(icon ? { icon } : {}),
68 },
69 };
70}