# Files changed · PR \#10559 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/10559/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[feat\(mcp\): expose preview list and close](https://git.cafe/pingdotgg/t3code/pull/10559.md)

Version: 4

Head commit: bcb7b210e0d6d4f2dd2bb3f698fcb36442b536ff

Base commit: e3e71c75fa04a4bdfc277c8eea3266686a56aa72

Comparison base: e3e71c75fa04a4bdfc277c8eea3266686a56aa72

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/server/src/mcp/McpHttpServer.ts

Status: modified; +7; −0

```diff
@@ -12,6 +12,8 @@
 import packageJson from "../../package.json" with { type: "json" };
 import * as McpInvocationContext from "./McpInvocationContext.ts";
 import * as OrchestratorMcpService from "./OrchestratorMcpService.ts";
+import { PreviewControlsToolkit } from "./toolkits/previewControls/tools.ts";
+import { PreviewControlsHandlersLive } from "./toolkits/previewControls/handlers.ts";
 import { ThreadToolkit } from "./toolkits/thread/tools.ts";
 import { ThreadToolkitHandlersLive } from "./toolkits/thread/handlers.ts";
 import * as ThreadMetadataMcpService from "./ThreadMetadataMcpService.ts";
@@ -239,6 +241,10 @@
   Layer.provide(WorktreeMcpService.layer),
 );
 
+export const PreviewControlsRegistrationLive = McpServer.toolkit(PreviewControlsToolkit).pipe(
+  Layer.provide(PreviewControlsHandlersLive),
+);
+
 const McpTransportLive = McpServer.layerHttp({
   name: "T3 Code",
   version: packageJson.version,
@@ -250,5 +256,6 @@
   PreviewToolkitRegistrationLive,
   OrchestratorToolkitRegistrationLive,
   ThreadToolkitRegistrationLive,
+  PreviewControlsRegistrationLive,
   WorktreeToolkitRegistrationLive,
 ).pipe(Layer.provideMerge(McpTransportLive));
```

## apps/server/src/mcp/toolkits/core.test.ts

Status: modified; +8; −1

```diff
@@ -11,12 +11,19 @@
 import { McpInvocationContext, type McpInvocationScope } from "../McpInvocationContext.ts";
 import { OrchestratorToolkit } from "./orchestrator/tools.ts";
 import { PreviewToolkit } from "./preview/tools.ts";
+import { PreviewControlsToolkit } from "./previewControls/tools.ts";
 import { ThreadToolkit } from "./thread/tools.ts";
 import { WorktreeToolkit } from "./worktree/tools.ts";
 
 it("publishes unique tool names with object-root inputs", () => {
   const names = new Set<string>();
+  for (const toolkit of [
+    OrchestratorToolkit,
+    PreviewToolkit,
+    WorktreeToolkit,
-  for (const toolkit of [OrchestratorToolkit, PreviewToolkit, WorktreeToolkit, ThreadToolkit]) {
+    ThreadToolkit,
+    PreviewControlsToolkit,
+  ]) {
     for (const tool of Object.values(toolkit.tools)) {
       expect(names.has(tool.name)).toBe(false);
       names.add(tool.name);
```

## apps/server/src/mcp/toolkits/previewControls/handlers.ts

Status: added; +41; −0

```diff
@@ -1,0 +1,41 @@
+import { OrchestratorMcpFailure } from "@t3tools/contracts";
+import * as Effect from "effect/Effect";
+import * as Preview from "../../../preview/Manager.ts";
+import * as ServerSettings from "../../../serverSettings.ts";
+import { requireMcpCapability } from "../../McpInvocationContext.ts";
+import { unavailable } from "../../threadAccess.ts";
+import { PreviewControlsToolkit } from "./tools.ts";
+
+const access = Effect.gen(function* () {
+  const scope = yield* requireMcpCapability("preview");
+  const settings = yield* ServerSettings.ServerSettingsService;
+  const current = yield* settings.getSettings.pipe(Effect.mapError(unavailable));
+  if (!current.enableAgentBrowserAccess)
+    return yield* new OrchestratorMcpFailure({
+      code: "capability_denied",
+      message: "Agent browser access is disabled.",
+    });
+  return { scope, manager: yield* Preview.PreviewManager };
+});
+export const PreviewControlsHandlersLive = PreviewControlsToolkit.toLayer({
+  t3_preview_list: (input) =>
+    Effect.gen(function* () {
+      const { scope, manager } = yield* access;
+      const result = yield* manager.list({ threadId: scope.threadId });
+      const start = input.cursor ?? 0;
+      const end = start + (input.limit ?? 20);
+      return {
+        ...result,
+        sessions: result.sessions.slice(start, end),
+        nextCursor: end < result.sessions.length ? end : null,
+      };
+    }),
+  t3_preview_close: (input) =>
+    Effect.gen(function* () {
+      const { scope, manager } = yield* access;
+      yield* manager
+        .close({ threadId: scope.threadId, tabId: input.tabId })
+        .pipe(Effect.mapError(unavailable));
+      return {};
+    }),
+});
```

## apps/server/src/mcp/toolkits/previewControls/tools.ts

Status: added; +41; −0

```diff
@@ -1,0 +1,41 @@
+import {
+  NonNegativeInt,
+  OrchestratorMcpFailure,
+  PreviewAutomationUnavailableError,
+  PreviewListResult,
+  PreviewTabId,
+} from "@t3tools/contracts";
+import * as Schema from "effect/Schema";
+import { Tool, Toolkit } from "effect/unstable/ai";
+import { PreviewManager } from "../../../preview/Manager.ts";
+import { ServerSettingsService } from "../../../serverSettings.ts";
+import { McpInvocationContext } from "../../McpInvocationContext.ts";
+
+const shared = {
+  failure: Schema.Union([OrchestratorMcpFailure, PreviewAutomationUnavailableError]),
+  failureMode: "return" as const,
+  dependencies: [McpInvocationContext, PreviewManager, ServerSettingsService],
+};
+export const PreviewListTool = Tool.make("t3_preview_list", {
+  ...shared,
+  description:
+    "List this thread's preview tabs. Pages reflect the current server state and may shift as tabs change.",
+  parameters: Schema.Struct({
+    cursor: Schema.optional(NonNegativeInt),
+    limit: Schema.optional(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 50 }))),
+  }),
+  success: Schema.Struct({
+    ...PreviewListResult.fields,
+    nextCursor: Schema.NullOr(NonNegativeInt),
+  }),
+})
+  .annotate(Tool.Readonly, true)
+  .annotate(Tool.Destructive, false);
+export const PreviewCloseTool = Tool.make("t3_preview_close", {
+  ...shared,
+  description:
+    "Close one preview tab owned by this thread through the normal server/host tab lifecycle. This does not wait for renderer cleanup.",
+  parameters: Schema.Struct({ tabId: PreviewTabId }),
+  success: Schema.Struct({}),
+}).annotate(Tool.Destructive, true);
+export const PreviewControlsToolkit = Toolkit.make(PreviewListTool, PreviewCloseTool);
```

## apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts

Status: modified; +2; −0

```diff
@@ -45,6 +45,7 @@
 
 import { attachmentRelativePath } from "../../attachmentStore.ts";
 import * as McpProviderSession from "../../mcp/McpProviderSession.ts";
+import { PreviewControlsToolkit } from "../../mcp/toolkits/previewControls/tools.ts";
 import { ThreadToolkit } from "../../mcp/toolkits/thread/tools.ts";
 import { OrchestratorToolkit } from "../../mcp/toolkits/orchestrator/tools.ts";
 import type { EventNdjsonLogger } from "../../provider/Layers/EventNdjsonLogger.ts";
@@ -584,6 +585,7 @@
     const readOnlyToolNames = [
       ...Object.values(OrchestratorToolkit.tools),
       ...Object.values(ThreadToolkit.tools),
+      ...Object.values(PreviewControlsToolkit.tools),
     ]
       .filter((tool) => Context.get(tool.annotations, Tool.Readonly))
       .map((tool) => `mcp__t3-code__${tool.name}`)
```

## apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts

Status: modified; +1; −0

```diff
@@ -812,6 +812,7 @@
   "mcp__t3-code__t3_pending_request_read",
   "mcp__t3-code__t3_thread_configuration",
   "mcp__t3-code__t3_thread_transfers",
+  "mcp__t3-code__t3_preview_list",
   "mcp__t3-code__t3_queue_list",
   "mcp__t3-code__t3_queue_read",
 ];
```

## packages/shared/src/t3McpToolPresentation.ts

Status: modified; +2; −0

```diff
@@ -69,6 +69,8 @@
   t3_thread_interrupt: { displayName: "Interrupt a T3 thread", summaryAction: "thread-interrupt" },
   t3_worktree_handoff: { displayName: "Hand off thread to a git worktree" },
   t3_worktree_status: { displayName: "Get thread worktree status" },
+  t3_preview_list: { displayName: "List preview tabs" },
+  t3_preview_close: { displayName: "Close a preview tab" },
   preview_status: { displayName: "Get preview browser status" },
   preview_open: { displayName: "Open a page in the preview browser" },
   preview_navigate: { displayName: "Navigate the preview browser" },
```
