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

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

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

Visibility: public

[feat\(mcp\): expose selected environment preferences](https://git.cafe/pingdotgg/t3code/pull/10560.md)

Version: 4

Head commit: d971e17ad8998b8147cac026bf213707dfc0f571

Base commit: bcb7b210e0d6d4f2dd2bb3f698fcb36442b536ff

Comparison base: bcb7b210e0d6d4f2dd2bb3f698fcb36442b536ff

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
@@ -14,6 +14,8 @@
 import * as OrchestratorMcpService from "./OrchestratorMcpService.ts";
 import { PreviewControlsToolkit } from "./toolkits/previewControls/tools.ts";
 import { PreviewControlsHandlersLive } from "./toolkits/previewControls/handlers.ts";
+import { EnvironmentToolkit } from "./toolkits/environment/tools.ts";
+import { EnvironmentHandlersLive } from "./toolkits/environment/handlers.ts";
 import { ThreadToolkit } from "./toolkits/thread/tools.ts";
 import { ThreadToolkitHandlersLive } from "./toolkits/thread/handlers.ts";
 import * as ThreadMetadataMcpService from "./ThreadMetadataMcpService.ts";
@@ -245,6 +247,10 @@
   Layer.provide(PreviewControlsHandlersLive),
 );
 
+export const EnvironmentRegistrationLive = McpServer.toolkit(EnvironmentToolkit).pipe(
+  Layer.provide(EnvironmentHandlersLive),
+);
+
 const McpTransportLive = McpServer.layerHttp({
   name: "T3 Code",
   version: packageJson.version,
@@ -256,6 +262,7 @@
   PreviewToolkitRegistrationLive,
   OrchestratorToolkitRegistrationLive,
   ThreadToolkitRegistrationLive,
+  EnvironmentRegistrationLive,
   PreviewControlsRegistrationLive,
   WorktreeToolkitRegistrationLive,
 ).pipe(Layer.provideMerge(McpTransportLive));
```

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

Status: modified; +27; −1

```diff
@@ -1,6 +1,11 @@
 import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
 import { expect, it } from "@effect/vitest";
+import {
+  DEFAULT_SERVER_SETTINGS,
+  EnvironmentId,
+  ProviderInstanceId,
+  ThreadId,
+} from "@t3tools/contracts";
-import { EnvironmentId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";
 import * as Effect from "effect/Effect";
 import * as Layer from "effect/Layer";
 import { McpSchema, McpServer, Tool } from "effect/unstable/ai";
@@ -12,6 +17,8 @@
 import { OrchestratorToolkit } from "./orchestrator/tools.ts";
 import { PreviewToolkit } from "./preview/tools.ts";
 import { PreviewControlsToolkit } from "./previewControls/tools.ts";
+import { EnvironmentToolkit } from "./environment/tools.ts";
+import * as EnvironmentHandlers from "./environment/handlers.ts";
 import { ThreadToolkit } from "./thread/tools.ts";
 import { WorktreeToolkit } from "./worktree/tools.ts";
 
@@ -22,6 +29,7 @@
     PreviewToolkit,
     WorktreeToolkit,
     ThreadToolkit,
+    EnvironmentToolkit,
     PreviewControlsToolkit,
   ]) {
     for (const tool of Object.values(toolkit.tools)) {
@@ -108,3 +116,21 @@
     ),
   ),
 );
+
+it("keeps MCP preference output allowlisted and Unicode-bounded", () => {
+  const settings = {
+    ...DEFAULT_SERVER_SETTINGS,
+    privateCredential: "must-not-escape",
+    sourceControlWritingStyle: {
+      ...DEFAULT_SERVER_SETTINGS.sourceControlWritingStyle,
+      customInstructions: "🙂".repeat(4001),
+    },
+  };
+  const result = EnvironmentHandlers.preferences(settings);
+  expect(result).not.toHaveProperty("privateCredential");
+  expect(result).not.toHaveProperty("providers");
+  expect(result.sourceControlWritingStyle).toMatchObject({
+    customInstructions: "🙂".repeat(4000),
+    truncated: true,
+  });
+});
```

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

Status: added; +79; −0

```diff
@@ -1,0 +1,79 @@
+import { OrchestratorMcpFailure, type ServerSettings } from "@t3tools/contracts";
+import * as Effect from "effect/Effect";
+import * as Environment from "../../../environment/ServerEnvironment.ts";
+import * as ThreadCommandExecutor from "../../../orchestration-v2/ThreadCommandExecutor.ts";
+import * as Settings from "../../../serverSettings.ts";
+import { McpInvocationContext } from "../../McpInvocationContext.ts";
+import { readCaller, readMutationCaller, unavailable } from "../../threadAccess.ts";
+import { EnvironmentToolkit } from "./tools.ts";
+
+export function preferences(settings: ServerSettings) {
+  const {
+    defaultThreadEnvMode,
+    newWorktreesStartFromOrigin,
+    enableProviderUpdateChecks,
+    backgroundActivity,
+    sourceControlWritingStyle,
+  } = settings;
+  const characters = Array.from(sourceControlWritingStyle.customInstructions);
+  return {
+    defaultThreadEnvMode,
+    newWorktreesStartFromOrigin,
+    enableProviderUpdateChecks,
+    backgroundActivity: { profile: backgroundActivity.profile },
+    sourceControlWritingStyle: {
+      ...sourceControlWritingStyle,
+      customInstructions: characters.slice(0, 4000).join(""),
+      truncated: characters.length > 4000,
+    },
+  };
+}
+const access = (writable = false) =>
+  Effect.gen(function* () {
+    const context = yield* writable ? readMutationCaller() : readCaller();
+    const environment = yield* Environment.ServerEnvironment;
+    const descriptor = yield* environment.getDescriptor;
+    if (descriptor.environmentId !== context.scope.environmentId)
+      return yield* new OrchestratorMcpFailure({
+        code: "capability_denied",
+        message: "This credential belongs to another environment.",
+      });
+    return { ...context, descriptor, settings: yield* Settings.ServerSettingsService };
+  });
+export const EnvironmentHandlersLive = EnvironmentToolkit.toLayer({
+  t3_environment_read: () =>
+    Effect.gen(function* () {
+      const { descriptor, settings } = yield* access();
+      const current = yield* settings.getSettings.pipe(Effect.mapError(unavailable));
+      return {
+        environmentId: descriptor.environmentId,
+        label: descriptor.label,
+        serverVersion: descriptor.serverVersion,
+        platform: descriptor.platform,
+        preferences: preferences(current),
+      };
+    }),
+  t3_environment_preferences_update: (patch) =>
+    Effect.gen(function* () {
+      const scope = yield* McpInvocationContext;
+      const executor = yield* ThreadCommandExecutor.ThreadCommandExecutor;
+      return yield* executor.withLock(
+        scope.threadId,
+        Effect.gen(function* () {
+          const { caller, settings } = yield* access(true);
+          if (
+            caller.archivedAt !== null ||
+            caller.runtimeMode !== "full-access" ||
+            caller.interactionMode !== "default"
+          )
+            return yield* new OrchestratorMcpFailure({
+              code: "capability_denied",
+              message: "Preference updates require a live full-access/default thread.",
+            });
+          return preferences(
+            yield* settings.updateSettings(patch).pipe(Effect.mapError(unavailable)),
+          );
+        }),
+      );
+    }),
+});
```

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

Status: added; +67; −0

```diff
@@ -1,0 +1,67 @@
+import {
+  BackgroundActivityProfile,
+  BackgroundActivityProfileSelection,
+  ExecutionEnvironmentDescriptor,
+  OrchestratorMcpFailure,
+  ServerSettings,
+  ServerSettingsPatch,
+} from "@t3tools/contracts";
+import * as Schema from "effect/Schema";
+import { Tool, Toolkit } from "effect/unstable/ai";
+import { ServerEnvironment } from "../../../environment/ServerEnvironment.ts";
+import { ThreadCommandExecutor } from "../../../orchestration-v2/ThreadCommandExecutor.ts";
+import { ThreadManagementService } from "../../../orchestration-v2/ThreadManagementService.ts";
+import { ServerSettingsService } from "../../../serverSettings.ts";
+import { McpInvocationContext } from "../../McpInvocationContext.ts";
+
+export const PreferenceFields = {
+  defaultThreadEnvMode: ServerSettings.fields.defaultThreadEnvMode,
+  newWorktreesStartFromOrigin: ServerSettings.fields.newWorktreesStartFromOrigin,
+  enableProviderUpdateChecks: ServerSettings.fields.enableProviderUpdateChecks,
+  backgroundActivity: Schema.Struct({ profile: BackgroundActivityProfileSelection }),
+  sourceControlWritingStyle: Schema.Struct({
+    mode: Schema.String,
+    followChangeRequestTemplates: Schema.Boolean,
+    customInstructions: Schema.String,
+    truncated: Schema.Boolean,
+  }),
+};
+const shared = {
+  failure: OrchestratorMcpFailure,
+  failureMode: "return" as const,
+  dependencies: [
+    McpInvocationContext,
+    ThreadManagementService,
+    ServerEnvironment,
+    ServerSettingsService,
+    ThreadCommandExecutor,
+  ],
+};
+export const EnvironmentReadTool = Tool.make("t3_environment_read", {
+  ...shared,
+  description:
+    "Read this server's identity and selected environment preferences. Provider/model availability is exposed by orchestrator_capabilities. Writing instructions are limited to 4,000 characters.",
+  success: Schema.Struct({
+    environmentId: ExecutionEnvironmentDescriptor.fields.environmentId,
+    label: Schema.String,
+    serverVersion: Schema.String,
+    platform: ExecutionEnvironmentDescriptor.fields.platform,
+    preferences: Schema.Struct(PreferenceFields),
+  }),
+})
+  .annotate(Tool.Readonly, true)
+  .annotate(Tool.Destructive, false);
+export const EnvironmentPreferencesTool = Tool.make("t3_environment_preferences_update", {
+  ...shared,
+  description:
+    "Update selected environment-wide preferences through normal settings persistence and notifications. Requires a live full-access/default calling thread. Omitted fields are preserved; empty customInstructions clears them.",
+  parameters: Schema.Struct({
+    defaultThreadEnvMode: ServerSettingsPatch.fields.defaultThreadEnvMode,
+    newWorktreesStartFromOrigin: ServerSettingsPatch.fields.newWorktreesStartFromOrigin,
+    enableProviderUpdateChecks: ServerSettingsPatch.fields.enableProviderUpdateChecks,
+    backgroundActivity: Schema.optionalKey(Schema.Struct({ profile: BackgroundActivityProfile })),
+    sourceControlWritingStyle: ServerSettingsPatch.fields.sourceControlWritingStyle,
+  }),
+  success: Schema.Struct(PreferenceFields),
+}).annotate(Tool.Destructive, true);
+export const EnvironmentToolkit = Toolkit.make(EnvironmentReadTool, EnvironmentPreferencesTool);
```

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

Status: modified; +2; −0

```diff
@@ -46,6 +46,7 @@
 import { attachmentRelativePath } from "../../attachmentStore.ts";
 import * as McpProviderSession from "../../mcp/McpProviderSession.ts";
 import { PreviewControlsToolkit } from "../../mcp/toolkits/previewControls/tools.ts";
+import { EnvironmentToolkit } from "../../mcp/toolkits/environment/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";
@@ -585,6 +586,7 @@
     const readOnlyToolNames = [
       ...Object.values(OrchestratorToolkit.tools),
       ...Object.values(ThreadToolkit.tools),
+      ...Object.values(EnvironmentToolkit.tools),
       ...Object.values(PreviewControlsToolkit.tools),
     ]
       .filter((tool) => Context.get(tool.annotations, Tool.Readonly))
```

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

Status: modified; +1; −0

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

## packages/shared/src/t3McpToolPresentation.ts

Status: modified; +2; −0

```diff
@@ -71,6 +71,8 @@
   t3_worktree_status: { displayName: "Get thread worktree status" },
   t3_preview_list: { displayName: "List preview tabs" },
   t3_preview_close: { displayName: "Close a preview tab" },
+  t3_environment_read: { displayName: "Read environment preferences" },
+  t3_environment_preferences_update: { displayName: "Update environment preferences" },
   preview_status: { displayName: "Get preview browser status" },
   preview_open: { displayName: "Open a page in the preview browser" },
   preview_navigate: { displayName: "Navigate the preview browser" },
```
