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
7 changes: 7 additions & 0 deletions apps/server/src/mcp/McpHttpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstab
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";
Expand Down Expand Up @@ -239,6 +241,10 @@ export const WorktreeToolkitRegistrationLive = McpServer.toolkit(WorktreeToolkit
Layer.provide(WorktreeMcpService.layer),
);

export const PreviewControlsRegistrationLive = McpServer.toolkit(PreviewControlsToolkit).pipe(
Layer.provide(PreviewControlsHandlersLive),
);

const McpTransportLive = McpServer.layerHttp({
name: "T3 Code",
version: packageJson.version,
Expand All @@ -250,5 +256,6 @@ export const layer = Layer.mergeAll(
PreviewToolkitRegistrationLive,
OrchestratorToolkitRegistrationLive,
ThreadToolkitRegistrationLive,
PreviewControlsRegistrationLive,
WorktreeToolkitRegistrationLive,
).pipe(Layer.provideMerge(McpTransportLive));
9 changes: 8 additions & 1 deletion apps/server/src/mcp/toolkits/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,19 @@ import * as McpHttpServer from "../McpHttpServer.ts";
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, ThreadToolkit]) {
for (const toolkit of [
OrchestratorToolkit,
PreviewToolkit,
WorktreeToolkit,
ThreadToolkit,
PreviewControlsToolkit,
]) {
for (const tool of Object.values(toolkit.tools)) {
expect(names.has(tool.name)).toBe(false);
names.add(tool.name);
Expand Down
41 changes: 41 additions & 0 deletions apps/server/src/mcp/toolkits/previewControls/handlers.ts
Original file line number Diff line number Diff line change
@@ -0,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,
};
Comment thread
juliusmarminge marked this conversation as resolved.
}),
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 {};
}),
});
41 changes: 41 additions & 0 deletions apps/server/src/mcp/toolkits/previewControls/tools.ts
Original file line number Diff line number Diff line change
@@ -0,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);
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { formatClaudeResumeCompactionQuestion } from "@t3tools/shared/claudeComp

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";
Expand Down Expand Up @@ -584,6 +585,7 @@ describe("ClaudeAdapterV2 MCP query overrides", () => {
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}`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,7 @@ export const CLAUDE_READ_ONLY_T3_MCP_ALLOWED_TOOLS: ReadonlyArray<string> = [
"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",
];
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/t3McpToolPresentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ const T3_MCP_TOOLS: Record<
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" },
Expand Down
Loading