1import * as Orchestrator from "../../../orchestration-v2/Orchestrator.ts";
2import * as ProjectionStore from "../../../orchestration-v2/ProjectionStore.ts";
3import * as DeviceService from "../../../device/DeviceService.ts";
4import * as ServerConfig from "../../../config.ts";
5import { expect, it } from "@effect/vitest";
6import { NodeHttpServer } from "@effect/platform-node";
7import * as NodeServices from "@effect/platform-node/NodeServices";
8import { ProviderInstanceId, ThreadId } from "@t3tools/contracts";
9import * as Effect from "effect/Effect";
10import * as Layer from "effect/Layer";
11import * as Schema from "effect/Schema";
12import { HttpBody, HttpClient, HttpRouter } from "effect/http";
13
14import * as ServerEnvironment from "../../../environment/ServerEnvironment.ts";
15import * as GitWorkflowService from "../../../git/GitWorkflowService.ts";
16import * as ProviderAdapterRegistry from "../../../orchestration-v2/ProviderAdapterRegistry.ts";
17import * as ThreadManagementService from "../../../orchestration-v2/ThreadManagementService.ts";
18import * as ProjectService from "../../../project/ProjectService.ts";
19import * as PullRequestReviewGuideService from "../../../pullRequest/PullRequestReviewGuideService.ts";
20import * as ProjectSetupScriptRunner from "../../../project/ProjectSetupScriptRunner.ts";
21import * as ProviderRegistry from "../../../provider/ProviderRegistry.ts";
22import * as ScheduledTaskService from "../../../scheduledTasks/ScheduledTaskService.ts";
23import * as SecretRequests from "../../../secrets/SecretRequests.ts";
24import * as ServerSettings from "../../../serverSettings.ts";
25import * as VcsStatusBroadcaster from "../../../vcs/VcsStatusBroadcaster.ts";
26import * as ServerSecretStore from "../../../auth/ServerSecretStore.ts";
27import * as ManagedProjectFolders from "../../../project/ManagedProjectFolders.ts";
28import * as ThreadLaunchService from "../../../orchestration-v2/ThreadLaunchService.ts";
29import * as ThreadSearch from "../../../orchestration-v2/ThreadSearch.ts";
30import * as PreviewManager from "../../../preview/Manager.ts";
31import * as SourceControlRepositoryService from "../../../sourceControl/SourceControlRepositoryService.ts";
32import * as GitVcsDriver from "../../../vcs/GitVcsDriver.ts";
33import * as McpHttpServer from "../../McpHttpServer.ts";
34import * as McpSessionRegistry from "../../McpSessionRegistry.ts";
35import * as PreviewAutomationBroker from "../../PreviewAutomationBroker.ts";
36import * as PreviewBrowser from "../../../preview/PreviewBrowser.ts";
37
38const layerStubServices = Layer.mergeAll(
39 Layer.mock(Orchestrator.OrchestratorV2)({}),
40 Layer.mock(ProjectionStore.ProjectionStoreV2)({}),
41 Layer.mock(DeviceService.DeviceService)({}),
42 Layer.mock(ThreadManagementService.ThreadManagementService)({}),
43 Layer.mock(ProviderRegistry.ProviderRegistry)({}),
44 Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({}),
45 Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
46 Layer.mock(SecretRequests.SecretRequests)({}),
47 Layer.mock(ProjectService.ProjectService)({}),
48 Layer.mock(PullRequestReviewGuideService.PullRequestReviewGuideService)({}),
49 ServerSettings.layerTest({}),
50 Layer.mock(GitWorkflowService.GitWorkflowService)({}),
51 Layer.mock(ProjectSetupScriptRunner.ProjectSetupScriptRunner)({}),
52 Layer.mock(VcsStatusBroadcaster.VcsStatusBroadcaster)({}),
53 Layer.mock(GitVcsDriver.GitVcsDriver)({}),
54 Layer.mock(ManagedProjectFolders.ManagedProjectFolders)({ namedProjectsRoot: "/unused" }),
55 Layer.mock(PreviewManager.PreviewManager)({}),
56 Layer.mock(ServerSecretStore.ServerSecretStore)({}),
57 Layer.mock(SourceControlRepositoryService.SourceControlRepositoryService)({}),
58 Layer.mock(ThreadLaunchService.ThreadLaunchService)({}),
59 Layer.mock(ThreadSearch.ThreadSearch)({}),
60);
61
62const ToolsListPayload = Schema.fromJsonString(
63 Schema.Struct({
64 result: Schema.Struct({
65 tools: Schema.Array(
66 Schema.Struct({
67 name: Schema.String,
68 inputSchema: Schema.Struct({ type: Schema.optional(Schema.String) }),
69 annotations: Schema.optional(
70 Schema.Struct({
71 readOnlyHint: Schema.optional(Schema.Boolean),
72 destructiveHint: Schema.optional(Schema.Boolean),
73 openWorldHint: Schema.optional(Schema.Boolean),
74 }),
75 ),
76 }),
77 ),
78 }),
79 }),
80);
81const decodeToolsListPayload = Schema.decodeUnknownEffect(ToolsListPayload);
82
83it.effect("production mcp layer lists worktree tools over http", () =>
84 Effect.scoped(
85 Effect.gen(function* () {
86 const layerRoutes = McpHttpServer.layer.pipe(Layer.provide(McpSessionRegistry.layer));
87 yield* HttpRouter.serve(layerRoutes, {
88 disableListenLog: true,
89 disableLogger: true,
90 }).pipe(
91 Layer.provide(
92 Layer.mock(ServerEnvironment.ServerEnvironment)({
93 getEnvironmentId: Effect.succeed("environment-scratch" as never),
94 }),
95 ),
96 Layer.provide(PreviewAutomationBroker.layer),
97 Layer.provide(PreviewBrowser.layer),
98 Layer.provide(layerStubServices),
99 Layer.build,
100 );
101
102 const registry = McpSessionRegistry.issueActiveMcpCredential({
103 threadId: ThreadId.make("thread-scratch"),
104 providerInstanceId: ProviderInstanceId.make("claudeAgent"),
105 });
106 const credential = yield* registry;
107 expect(credential).toBeDefined();
108
109 const httpClient = yield* HttpClient.HttpClient;
110 const auth = credential!.config.authorizationHeader;
111 const initResponse = yield* httpClient.post("/mcp", {
112 headers: {
113 accept: "application/json, text/event-stream",
114 authorization: auth,
115 },
116 body: HttpBody.text(
117 `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"scratch","version":"1.0.0"}}}`,
118 "application/json",
119 ),
120 });
121 expect(initResponse.status).toBe(200);
122 const sessionId = initResponse.headers["mcp-session-id"];
123
124 const listResponse = yield* httpClient.post("/mcp", {
125 headers: {
126 accept: "application/json, text/event-stream",
127 authorization: auth,
128 "mcp-protocol-version": "2025-06-18",
129 ...(sessionId ? { "mcp-session-id": sessionId } : {}),
130 },
131 body: HttpBody.text(
132 `{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}`,
133 "application/json",
134 ),
135 });
136 const bodyText = yield* listResponse.text;
137 const payload = yield* decodeToolsListPayload(bodyText.match(/\{.*\}/s)![0]);
138 const tools = payload.result.tools;
139 const toolNames = tools.map((tool) => tool.name);
140 expect(toolNames).toContain("t3_worktree_handoff");
141 expect(toolNames).toContain("t3_worktree_status");
142 // The worktree registration merges alongside the other toolkits rather
143 // than replacing them.
144 expect(toolNames).toContain("preview_status");
145 expect(toolNames).toContain("delegate_task");
146
147 // The handoff tool mutates thread state, reaches the network (origin
148 // fetch), and runs project setup scripts, so its MCP hints must not
149 // promise a read-only, closed-world, non-destructive tool.
150 const handoff = tools.find((tool) => tool.name === "t3_worktree_handoff");
151 expect(handoff?.annotations?.readOnlyHint).toBe(false);
152 expect(handoff?.annotations?.destructiveHint).toBe(true);
153 expect(handoff?.annotations?.openWorldHint).toBe(true);
154 const status = tools.find((tool) => tool.name === "t3_worktree_status");
155 expect(status?.annotations?.readOnlyHint).toBe(true);
156 expect(status?.annotations?.destructiveHint).toBe(false);
157
158 // MCP requires every tool input schema to be a top-level object schema.
159 // A non-object schema (e.g. the anyOf produced by an empty
160 // Schema.Struct({})) makes clients reject the entire server.
161 for (const tool of tools) {
162 expect(tool.inputSchema.type, `inputSchema.type of ${tool.name}`).toBe("object");
163 }
164 }),
165 ).pipe(
166 Effect.provide(
167 Layer.mergeAll(
168 NodeHttpServer.layerTest,
169 ServerConfig.layerTest(process.cwd(), { prefix: "t3-worktree-mcp-" }).pipe(
170 Layer.provide(NodeServices.layer),
171 ),
172 NodeServices.layer,
173 ),
174 ),
175 ),
176);