Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/server/src/mcp/McpDeviceToolkit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as ServerConfig from "../config.ts";
import * as DeviceService from "../device/DeviceService.ts";
import * as McpHttpServer from "./McpHttpServer.ts";
import * as McpInvocationContext from "./McpInvocationContext.ts";
import * as McpToolAccessTestkit from "./McpToolAccess.testkit.ts";

const environmentId = EnvironmentId.make("environment-device-test");
const threadId = ThreadId.make("thread-device-test");
Expand Down Expand Up @@ -102,6 +103,7 @@ const layerDeviceServiceMock = Layer.mock(DeviceService.DeviceService)({

const layerTest = McpHttpServer.layerDeviceToolkit.pipe(
Layer.provideMerge(McpServer.McpServer.layer),
Layer.provideMerge(McpToolAccessTestkit.liveThreadsLayer),
Layer.provideMerge(layerDeviceServiceMock),
Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-mcp-device-toolkit-test-" })),
Layer.provide(NodeServices.layer),
Expand Down Expand Up @@ -172,6 +174,7 @@ it.effect("rejects unavailable agent access before booting or opening a device",
Effect.provide(
McpHttpServer.layerDeviceToolkit.pipe(
Layer.provideMerge(McpServer.McpServer.layer),
Layer.provideMerge(McpToolAccessTestkit.liveThreadsLayer),
Layer.provide(layerUnavailable),
Layer.provide(NodeServices.layer),
),
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/mcp/McpHttpServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { HttpBody, HttpClient, HttpRouter, HttpServerResponse } from "effect/htt
import * as ProjectService from "../project/ProjectService.ts";
import * as ServerConfig from "../config.ts";
import * as McpHttpServer from "./McpHttpServer.ts";
import * as McpToolAccessTestkit from "./McpToolAccess.testkit.ts";
import * as McpInvocationContext from "./McpInvocationContext.ts";
import * as PreviewAutomationBroker from "./PreviewAutomationBroker.ts";

Expand Down Expand Up @@ -51,12 +52,14 @@ const client = McpSchema.McpServerClient.of({
});
const layerTest = McpHttpServer.layerPreviewToolkit.pipe(
Layer.provideMerge(McpServer.McpServer.layer),
Layer.provideMerge(McpToolAccessTestkit.liveThreadsLayer),
Layer.provideMerge(PreviewAutomationBroker.layer),
Layer.provideMerge(ServerConfig.layerTest(process.cwd(), { prefix: "t3-mcp-http-server-test-" })),
Layer.provideMerge(NodeServices.layer),
);
const layerPullRequestsTest = McpHttpServer.layerPullRequestsToolkit.pipe(
Layer.provideMerge(McpServer.McpServer.layer),
Layer.provideMerge(McpToolAccessTestkit.liveThreadsLayer),
Layer.provide(
Layer.mergeAll(
Layer.mock(ProjectService.ProjectService)({}),
Expand Down
104 changes: 68 additions & 36 deletions apps/server/src/mcp/McpHttpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@ import * as Schema from "effect/Schema";
import * as Sink from "effect/Sink";
import * as Stream from "effect/Stream";
import type * as Types from "effect/Types";
import { AiError, McpProtocol, McpSchema, McpServer, Tool } from "effect/ai";
import { AiError, McpProtocol, McpSchema, McpServer, Tool, type Toolkit } from "effect/ai";
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
import { OrchestratorMcpFailure, PreviewAutomationError } from "@t3tools/contracts";

import packageJson from "../../package.json" with { type: "json" };
import * as ServerConfig from "../config.ts";
import * as DeviceService from "../device/DeviceService.ts";
import * as HtmlRender from "../htmlRender/HtmlRender.ts";
import * as ThreadManagementService from "../orchestration-v2/ThreadManagementService.ts";
import * as McpInvocationContext from "./McpInvocationContext.ts";
import * as McpToolAccess from "./McpToolAccess.ts";
import * as OrchestratorMcpService from "./OrchestratorMcpService.ts";
import { PreviewControlsToolkit } from "./toolkits/previewControls/tools.ts";
import * as PreviewControlsHandlers from "./toolkits/previewControls/handlers.ts";
Expand Down Expand Up @@ -396,6 +398,7 @@ const previewSnapshotFailure = <E>(cause: Cause.Cause<E>) => {
const registerPreviewSnapshot = Effect.fn("McpHttpServer.registerPreviewSnapshot")(function* () {
const server = yield* McpServer.McpServer;
const broker = yield* PreviewAutomationBroker.PreviewAutomationBroker;
const threads = yield* ThreadManagementService.ThreadManagementService;
// The MCP tool runner only supplies the client, so hand the save path its services here.
const saveServices = yield* Effect.context<
ServerConfig.ServerConfig | FileSystem.FileSystem | Path.Path | Crypto.Crypto
Expand Down Expand Up @@ -430,6 +433,7 @@ const registerPreviewSnapshot = Effect.fn("McpHttpServer.registerPreviewSnapshot
Stream.run(Sink.last()),
Effect.flatMap(Effect.fromOption),
Effect.provideService(PreviewAutomationBroker.PreviewAutomationBroker, broker),
Effect.provideService(ThreadManagementService.ThreadManagementService, threads),
Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),
Effect.flatMap(({ encodedResult }) =>
Effect.gen(function* () {
Expand Down Expand Up @@ -655,14 +659,19 @@ const registerImageTool = <T extends Tool.Any, E, R>(

const registerDeviceScreenshot = Effect.fn("McpHttpServer.registerDeviceScreenshot")(function* () {
const devices = yield* DeviceService.DeviceService;
const threads = yield* ThreadManagementService.ThreadManagementService;
const built = yield* DeviceScreenshotToolkit;
yield* registerImageTool(
DeviceScreenshotTool,
(payload) =>
built
.handle("device_screenshot", payload)
.pipe(Stream.unwrap, Stream.run(Sink.last()), Effect.flatMap(Effect.fromOption)),
(effect) => effect.pipe(Effect.provideService(DeviceService.DeviceService, devices)),
(effect) =>
effect.pipe(
Effect.provideService(DeviceService.DeviceService, devices),
Effect.provideService(ThreadManagementService.ThreadManagementService, threads),
),
"screenshot",
"Device screenshot failed.",
);
Expand All @@ -672,14 +681,19 @@ const isOrchestratorMcpFailure = Schema.is(OrchestratorMcpFailure);

const registerHtmlPreview = Effect.fn("McpHttpServer.registerHtmlPreview")(function* () {
const htmlRender = yield* HtmlRender.HtmlRender;
const threads = yield* ThreadManagementService.ThreadManagementService;
const built = yield* HtmlPreviewToolkit;
yield* registerImageTool(
HtmlPreviewTool,
(payload) =>
built
.handle("html_preview", payload)
.pipe(Stream.unwrap, Stream.run(Sink.last()), Effect.flatMap(Effect.fromOption)),
(effect) => effect.pipe(Effect.provideService(HtmlRender.HtmlRender, htmlRender)),
(effect) =>
effect.pipe(
Effect.provideService(HtmlRender.HtmlRender, htmlRender),
Effect.provideService(ThreadManagementService.ThreadManagementService, threads),
),
"preview",
// Parameter errors and HTML render errors are both written by the server for the agent.
(error) =>
Expand All @@ -689,65 +703,83 @@ const registerHtmlPreview = Effect.fn("McpHttpServer.registerHtmlPreview")(funct
);
});

/**
* `McpServer.toolkit` for handlers that declared their access (see
* `McpToolAccess`). Every toolkit on `/mcp` registers through this.
*/
export const toolkitRegistration = <Tools extends Record<string, Tool.Any>, EX, RX>(
toolkit: Toolkit.Toolkit<Tools>,
handlers: McpToolAccess.HandlersLayer<Tools, EX, RX>,
) => McpServer.toolkit(toolkit).pipe(Layer.provide(McpToolAccess.HandlersLayer.layer(handlers)));

/** A hand-registered tool, also only with handlers that declared their access. */
const imageToolRegistration = <Tools extends Record<string, Tool.Any>, A, E, R, EX, RX>(
register: Effect.Effect<A, E, R>,
handlers: McpToolAccess.HandlersLayer<Tools, EX, RX>,
) => Layer.effectDiscard(register).pipe(Layer.provide(McpToolAccess.HandlersLayer.layer(handlers)));

export const layerHtmlToolkit = Layer.mergeAll(
McpServer.toolkit(HtmlRenderToolkit).pipe(Layer.provide(HtmlHandlers.layerRender)),
Layer.effectDiscard(registerHtmlPreview()).pipe(Layer.provide(HtmlHandlers.layerPreview)),
toolkitRegistration(HtmlRenderToolkit, HtmlHandlers.layerRender),
imageToolRegistration(registerHtmlPreview(), HtmlHandlers.layerPreview),
).pipe(Layer.provide(HtmlRender.layer));

const layerPreviewStandardToolkitRegistration = McpServer.toolkit(PreviewStandardToolkit).pipe(
Layer.provide(PreviewHandlers.layerStandard),
const layerPreviewStandardToolkitRegistration = toolkitRegistration(
PreviewStandardToolkit,
PreviewHandlers.layerStandard,
);

const layerPreviewSnapshotRegistration = Layer.effectDiscard(registerPreviewSnapshot()).pipe(
Layer.provide(PreviewHandlers.layerSnapshot),
const layerPreviewSnapshotRegistration = imageToolRegistration(
registerPreviewSnapshot(),
PreviewHandlers.layerSnapshot,
);

export const layerPreviewToolkit = Layer.mergeAll(
layerPreviewStandardToolkitRegistration,
layerPreviewSnapshotRegistration,
);

export const layerOrchestratorToolkit = McpServer.toolkit(OrchestratorToolkit).pipe(
Layer.provide(OrchestratorHandlers.layer),
Layer.provide(OrchestratorMcpService.layer),
Layer.provide(ThreadMetadataMcpService.layer),
);
export const layerOrchestratorToolkit = toolkitRegistration(
OrchestratorToolkit,
OrchestratorHandlers.layer,
).pipe(Layer.provide(OrchestratorMcpService.layer), Layer.provide(ThreadMetadataMcpService.layer));

export const layerThreadToolkit = McpServer.toolkit(ThreadToolkit).pipe(
Layer.provide(ThreadHandlers.layer),
);
export const layerThreadToolkit = toolkitRegistration(ThreadToolkit, ThreadHandlers.layer);

const layerWorktreeToolkitRegistration = McpServer.toolkit(WorktreeToolkit).pipe(
Layer.provide(WorktreeHandlers.layer),
Layer.provide(WorktreeMcpService.layer),
);
const layerWorktreeToolkitRegistration = toolkitRegistration(
WorktreeToolkit,
WorktreeHandlers.layer,
).pipe(Layer.provide(WorktreeMcpService.layer));

const layerPreviewControlsRegistration = McpServer.toolkit(PreviewControlsToolkit).pipe(
Layer.provide(PreviewControlsHandlers.layer),
const layerPreviewControlsRegistration = toolkitRegistration(
PreviewControlsToolkit,
PreviewControlsHandlers.layer,
);

const layerEnvironmentRegistration = McpServer.toolkit(EnvironmentToolkit).pipe(
Layer.provide(EnvironmentHandlers.layer),
const layerEnvironmentRegistration = toolkitRegistration(
EnvironmentToolkit,
EnvironmentHandlers.layer,
);

const layerProjectRegistration = McpServer.toolkit(ProjectToolkit).pipe(
Layer.provide(ProjectHandlers.layer),
);
const layerProjectRegistration = toolkitRegistration(ProjectToolkit, ProjectHandlers.layer);

const layerAttachmentRegistration = McpServer.toolkit(AttachmentToolkit).pipe(
Layer.provide(AttachmentHandlers.layer),
const layerAttachmentRegistration = toolkitRegistration(
AttachmentToolkit,
AttachmentHandlers.layer,
);

export const layerPullRequestsToolkit = McpServer.toolkit(PullRequestsToolkit).pipe(
Layer.provide(PullRequestsHandlers.layer),
export const layerPullRequestsToolkit = toolkitRegistration(
PullRequestsToolkit,
PullRequestsHandlers.layer,
);

const layerDeviceStandardToolkitRegistration = McpServer.toolkit(DeviceStandardToolkit).pipe(
Layer.provide(DeviceHandlers.layerStandard),
const layerDeviceStandardToolkitRegistration = toolkitRegistration(
DeviceStandardToolkit,
DeviceHandlers.layerStandard,
);

const layerDeviceScreenshotRegistration = Layer.effectDiscard(registerDeviceScreenshot()).pipe(
Layer.provide(DeviceHandlers.layerScreenshot),
const layerDeviceScreenshotRegistration = imageToolRegistration(
registerDeviceScreenshot(),
DeviceHandlers.layerScreenshot,
);

export const layerDeviceToolkit = Layer.mergeAll(
Expand Down
Loading
Loading