1import { OrchestratorMcpFailure, type ServerSettings } from "@t3tools/contracts";
2import * as Effect from "effect/Effect";
3import * as Environment from "../../../environment/ServerEnvironment.ts";
4import * as ThreadCommandExecutor from "../../../orchestration-v2/ThreadCommandExecutor.ts";
5import * as Settings from "../../../serverSettings.ts";
6import * as McpInvocationContext from "../../McpInvocationContext.ts";
7import * as McpToolAccess from "../../McpToolAccess.ts";
8import { readCaller, unavailable } from "../../threadAccess.ts";
9import { EnvironmentToolkit } from "./tools.ts";
10
11export function preferences(settings: ServerSettings) {
12 const {
13 defaultThreadEnvMode,
14 newWorktreesStartFromOrigin,
15 enableProviderUpdateChecks,
16 backgroundActivity,
17 sourceControlWritingStyle,
18 } = settings;
19 const characters = Array.from(sourceControlWritingStyle.customInstructions);
20 return {
21 defaultThreadEnvMode,
22 newWorktreesStartFromOrigin,
23 enableProviderUpdateChecks,
24 backgroundActivity: { profile: backgroundActivity.profile },
25 sourceControlWritingStyle: {
26 ...sourceControlWritingStyle,
27 customInstructions: characters.slice(0, 4000).join(""),
28 truncated: characters.length > 4000,
29 },
30 };
31}
32const access = Effect.gen(function* () {
33 const context = yield* readCaller();
34 const environment = yield* Environment.ServerEnvironment;
35 const descriptor = yield* environment.getDescriptor;
36 if (descriptor.environmentId !== context.scope.environmentId)
37 return yield* new OrchestratorMcpFailure({
38 code: "capability_denied",
39 message: "This credential belongs to another environment.",
40 });
41 return { ...context, descriptor, settings: yield* Settings.ServerSettingsService };
42});
43export const layer = McpToolAccess.toLayer(EnvironmentToolkit, {
44 t3_environment_read: McpToolAccess.reads(() =>
45 Effect.gen(function* () {
46 const { descriptor, settings } = yield* access;
47 const current = yield* settings.getSettings.pipe(Effect.mapError(unavailable));
48 return {
49 environmentId: descriptor.environmentId,
50 label: descriptor.label,
51 serverVersion: descriptor.serverVersion,
52 platform: descriptor.platform,
53 preferences: preferences(current),
54 };
55 }),
56 ),
57 t3_environment_preferences_update: McpToolAccess.writesEnvironment((patch, check) =>
58 Effect.gen(function* () {
59 const scope = yield* McpInvocationContext.McpInvocationContext;
60 const executor = yield* ThreadCommandExecutor.ThreadCommandExecutor;
61 const update = Effect.gen(function* () {
62 // The turn may have ended, or the thread's modes changed, while this waited for the lock.
63 yield* check;
64 const { settings } = yield* access;
65 return preferences(
66 yield* settings.updateSettings(patch).pipe(Effect.mapError(unavailable)),
67 );
68 });
69 // A thread caller serializes with its own turn; a client has no thread to lock.
70 return yield* scope.thread === undefined
71 ? update
72 : executor.withLock(scope.thread.threadId, update);
73 }),
74 ),
75});