1import { OrchestratorMcpFailure } from "@t3tools/contracts";
2import * as Effect from "effect/Effect";
3
4import * as HtmlRender from "../../../htmlRender/HtmlRender.ts";
5import * as McpInvocationContext from "../../McpInvocationContext.ts";
6import * as McpToolAccess from "../../McpToolAccess.ts";
7import { HtmlPreviewToolkit, HtmlRenderToolkit, type HtmlToolkit } from "./tools.ts";
8
9const INVALID_PAGE_ERRORS = new Set([
10 "HtmlRenderImagesNotFoundError",
11 "HtmlRenderImageTooLargeError",
12 "HtmlRenderPageTooLargeError",
13]);
14
15// Every HTML render error message is built on the server and tells the agent what to do next.
16const toFailure = (error: { readonly _tag: string; readonly message: string }) =>
17 new OrchestratorMcpFailure({
18 code: INVALID_PAGE_ERRORS.has(error._tag) ? "invalid_request" : "orchestration_error",
19 message: error.message,
20 });
21
22const handlers = {
23 // The headless browser runs on the host and can open local files, so only
24 // agents T3 launched, which already work on this machine, get it.
25 html_preview: McpToolAccess.readsAsCaller((input) =>
26 Effect.gen(function* () {
27 const htmlRender = yield* HtmlRender.HtmlRender;
28 const { png, ...preview } = yield* htmlRender.preview(input).pipe(Effect.mapError(toFailure));
29 return {
30 ...preview,
31 screenshot: {
32 mimeType: "image/png" as const,
33 data: png,
34 width: preview.width,
35 height: preview.capturedHeight,
36 },
37 };
38 }),
39 ),
40 // The page is stored in the calling thread, so it needs that thread's live run.
41 html_render: McpToolAccess.actsAsCaller((input) =>
42 Effect.gen(function* () {
43 const scope = yield* McpInvocationContext.McpInvocationContext;
44 const { thread } = yield* McpInvocationContext.requireThreadScope(scope, "html_render");
45 const htmlRender = yield* HtmlRender.HtmlRender;
46 const reference = yield* htmlRender
47 .publish({ threadId: thread.threadId, ...input })
48 .pipe(Effect.mapError(toFailure));
49 return {
50 htmlRender: reference,
51 message:
52 "Shown to the reader above your reply. Don't mention or describe the page; reply with only what it doesn't already say.",
53 };
54 }),
55 ),
56} satisfies McpToolAccess.Handlers<typeof HtmlToolkit.tools>;
57
58export const layerPreview = McpToolAccess.toLayer(HtmlPreviewToolkit, {
59 html_preview: handlers.html_preview,
60});
61
62export const layerRender = McpToolAccess.toLayer(HtmlRenderToolkit, {
63 html_render: handlers.html_render,
64});