1import type {
2 ToolActivityIcon,
3 ToolActivityNativeAppReference,
4 ToolActivitySource,
5} from "@t3tools/contracts";
6import * as Crypto from "effect/Crypto";
7import * as Effect from "effect/Effect";
8import * as Hex from "effect/encoding/Hex";
9import type * as EffectCodexSchema from "effect-codex-app-server/schema";
10
11import {
12 mcpToolPresentation as integrationToolPresentation,
13 normalizeMcpHttpUrl as normalizedHttpUrl,
14 normalizeMcpText as normalizedDisplayName,
15} from "@t3tools/provider-core/server/mcpToolPresentation";
16
17type CodexLifecycleItem = EffectCodexSchema.V2ItemCompletedNotification["item"];
18
19function asUnknownRecord(value: unknown): Record<string, unknown> | undefined {
20 return value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;
21}
22
23function normalizedImageUrl(value: unknown): string | undefined {
24 if (typeof value !== "string" || value.length > 4096) return undefined;
25 try {
26 const url = new URL(value);
27 return url.protocol === "http:" || url.protocol === "https:" || url.protocol === "data:"
28 ? url.href
29 : undefined;
30 } catch {
31 return undefined;
32 }
33}
34
35function normalizedAppId(value: unknown): string | undefined {
36 if (typeof value !== "string") return undefined;
37 const appId = value.trim();
38 return appId.length > 0 && appId.length <= 512 && /^[A-Za-z0-9._-]+$/u.test(appId)
39 ? appId
40 : undefined;
41}
42
43function normalizedSourceKeyPart(value: string): string {
44 return value.trim().toLowerCase();
45}
46
47const nativeAppSourceKey = Effect.fn("CodexToolPresentation.nativeAppSourceKey")(function* (
48 appId: string,
49) {
50 const key = `native-app:${appId.toLowerCase()}`;
51 if (key.length <= 512) return key;
52 const crypto = yield* Crypto.Crypto;
53 const digest = Hex.encode(
54 yield* crypto.digest("SHA-256", new TextEncoder().encode(key)).pipe(Effect.orDie),
55 );
56 return `${key.slice(0, 512 - digest.length - 1)}:${digest}`;
57});
58
59function browserDisplayName(value: unknown): string | undefined {
60 const normalized = normalizedDisplayName(value)?.toLowerCase();
61 if (!normalized) return undefined;
62 if (normalized.includes("chrome") || normalized === "chromium") return "Chrome";
63 if (normalized.includes("edge")) return "Microsoft Edge";
64 if (normalized.includes("firefox")) return "Firefox";
65 if (normalized.includes("safari")) return "Safari";
66 if (normalized.includes("arc")) return "Arc";
67 if (normalized === "iab" || normalized.includes("in-app")) return "Browser";
68 return normalizedDisplayName(value);
69}
70
71function browserNativeAppReference(name: string): ToolActivityNativeAppReference | undefined {
72 switch (name) {
73 case "Chrome":
74 return { _tag: "display-name", displayName: "Google Chrome" };
75 case "Microsoft Edge":
76 case "Firefox":
77 case "Safari":
78 case "Arc":
79 return { _tag: "display-name", displayName: name };
80 default:
81 return undefined;
82 }
83}
84
85function appDisplayNameFromId(appId: string): string | undefined {
86 const knownNames: Readonly<Record<string, string>> = {
87 "com.apple.finder": "Finder",
88 "com.apple.safari": "Safari",
89 "com.google.chrome": "Chrome",
90 "com.microsoft.edgemac": "Microsoft Edge",
91 "org.mozilla.firefox": "Firefox",
92 "company.thebrowser.browser": "Arc",
93 };
94 return knownNames[appId.toLowerCase()];
95}
96
97function nativeAppReference(value: unknown): ToolActivityNativeAppReference | undefined {
98 const app = asUnknownRecord(value);
99 if (app?.kind === "appId") {
100 const appId = normalizedAppId(app.appId);
101 return appId ? { _tag: "app-id", appId } : undefined;
102 }
103 if (app?.kind === "displayName") {
104 const displayName = normalizedDisplayName(app.displayName);
105 return displayName ? { _tag: "display-name", displayName } : undefined;
106 }
107 return undefined;
108}
109
110function themedLogoIcon(
111 ...records: ReadonlyArray<Record<string, unknown> | undefined>
112): ToolActivityIcon | undefined {
113 for (const record of records) {
114 const logoUrl = normalizedImageUrl(record?.logoUrl);
115 if (!logoUrl) continue;
116 const logoUrlDark = normalizedImageUrl(record?.logoUrlDark ?? record?.logoDarkUrl);
117 return {
118 _tag: "themed-logo",
119 logoUrl,
120 ...(logoUrlDark ? { logoUrlDark } : {}),
121 };
122 }
123 return undefined;
124}
125
126export interface McpToolPresentation {
127 readonly title?: string;
128 readonly toolSurface?: "browser" | "computer";
129 readonly toolIcon?: ToolActivityIcon;
130 readonly toolSource?: ToolActivitySource;
131}
132
133export const mcpToolPresentation = Effect.fn("CodexToolPresentation.mcpToolPresentation")(
134 function* (
135 item: Extract<CodexLifecycleItem, { readonly type: "mcpToolCall" }>,
136 ): Effect.fn.Return<McpToolPresentation, never, Crypto.Crypto> {
137 const result = asUnknownRecord(item.result);
138 const metadata = asUnknownRecord(result?._meta);
139 const surface = asUnknownRecord(metadata?.["codex/toolSurface"]);
140 const sourceMetadata = asUnknownRecord(metadata?.source);
141 const appContext = asUnknownRecord(item.appContext);
142 const sourceLogo = themedLogoIcon(surface, sourceMetadata, appContext);
143 if (surface?.kind === "browserUse") {
144 const screenshot = asUnknownRecord(surface.screenshot);
145 const browserUse = asUnknownRecord(metadata?.browser_use);
146 const openTabs = Array.isArray(surface.openTabs) ? surface.openTabs : [];
147 const latestOpenTab = openTabs
148 .toReversed()
149 .map(asUnknownRecord)
150 .find((tab) => normalizedHttpUrl(tab?.url) !== undefined);
151 const selectedPage = [
152 { record: screenshot, url: screenshot?.pageUrl },
153 { record: browserUse, url: browserUse?.url },
154 { record: latestOpenTab, url: latestOpenTab?.url },
155 ]
156 .map((candidate) => ({ ...candidate, pageUrl: normalizedHttpUrl(candidate.url) }))
157 .find((candidate) => candidate.pageUrl !== undefined);
158 const pageUrl = selectedPage?.pageUrl;
159 const faviconUrl = normalizedImageUrl(
160 selectedPage?.record?.faviconUrl ?? selectedPage?.record?.favIconUrl,
161 );
162 const faviconUrlDark = normalizedImageUrl(
163 selectedPage?.record?.faviconUrlDark ?? selectedPage?.record?.favIconUrlDark,
164 );
165 const name =
166 browserDisplayName(appContext?.appName) ??
167 browserDisplayName(surface.browserFamily) ??
168 browserDisplayName(surface.backend) ??
169 "Browser";
170 const nativeBrowserIcon = browserNativeAppReference(name);
171 const sourceIcon =
172 sourceLogo ??
173 (nativeBrowserIcon ? ({ _tag: "native-app", app: nativeBrowserIcon } as const) : undefined);
174 const sourceKeyPart = normalizedSourceKeyPart(name) || "browser";
175 return {
176 toolSurface: "browser",
177 ...(pageUrl
178 ? {
179 toolIcon: {
180 _tag: "website",
181 pageUrl,
182 ...(faviconUrl ? { faviconUrl } : {}),
183 ...(faviconUrlDark ? { faviconUrlDark } : {}),
184 } as const,
185 }
186 : {}),
187 toolSource: {
188 key: `browser-use:${sourceKeyPart}`,
189 name,
190 kind: name === "Browser" ? "browser" : "integration",
191 ...(sourceIcon ? { icon: sourceIcon } : {}),
192 },
193 };
194 }
195 if (surface?.kind === "computerUse") {
196 const app = nativeAppReference(surface.app);
197 const args = asUnknownRecord(item.arguments);
198 const argumentAppName =
199 normalizedDisplayName(args?.appName) ??
200 normalizedDisplayName(args?.application) ??
201 normalizedDisplayName(typeof args?.app === "string" ? args.app : undefined);
202 const name =
203 normalizedDisplayName(appContext?.appName) ??
204 argumentAppName ??
205 (app?._tag === "display-name" ? app.displayName : undefined) ??
206 (app?._tag === "app-id" ? appDisplayNameFromId(app.appId) : undefined) ??
207 "Computer Use";
208 const sourceIcon = sourceLogo ?? (app ? ({ _tag: "native-app", app } as const) : undefined);
209 const sourceKey = app
210 ? app._tag === "app-id"
211 ? yield* nativeAppSourceKey(app.appId)
212 : `native-app-name:${normalizedSourceKeyPart(app.displayName)}`
213 : "computer-use";
214 return {
215 toolSurface: "computer",
216 ...(app ? { toolIcon: { _tag: "native-app", app } as const } : {}),
217 toolSource: {
218 key: sourceKey,
219 name,
220 kind: "computer",
221 ...(sourceIcon ? { icon: sourceIcon } : {}),
222 },
223 };
224 }
225
226 return integrationToolPresentation({
227 serverName: appContext?.connectorId ?? item.server,
228 toolName: item.tool,
229 title: appContext?.actionName,
230 serverDisplayName: appContext?.appName,
231 source: sourceMetadata,
232 iconUrl: sourceLogo?._tag === "themed-logo" ? sourceLogo.logoUrl : undefined,
233 iconUrlDark: sourceLogo?._tag === "themed-logo" ? sourceLogo.logoUrlDark : undefined,
234 });
235 },
236);