Skip to content
Closed
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
16 changes: 15 additions & 1 deletion apps/server/src/mcp/EnvironmentMcpService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import * as Layer from "effect/Layer";
import * as Schema from "effect/Schema";

import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
import { layer as threadCommandExecutorLayer } from "../orchestration-v2/ThreadCommandExecutor.ts";
import { ThreadManagementService } from "../orchestration-v2/ThreadManagementService.ts";
import * as ProviderRegistryModule from "../provider/Services/ProviderRegistry.ts";
import { makeProviderRegistryLayer } from "../provider/testUtils/providerRegistryMock.ts";
import * as ServerSettings from "../serverSettings.ts";
Expand All @@ -24,6 +26,10 @@ import type { McpInvocationScope } from "./McpInvocationContext.ts";
const encodeEnvironmentMcpReadResult = Schema.encodeUnknownEffect(EnvironmentMcpReadResult);
const encodeUnknownJsonString = Schema.encodeUnknownSync(Schema.fromJsonString(Schema.Unknown));
const environmentId = EnvironmentId.make("environment-test");
const mutationDependencies = Layer.merge(
Layer.mock(ThreadManagementService)({}),
threadCommandExecutorLayer,
);
const scope: McpInvocationScope = {
environmentId,
threadId: ThreadId.make("thread-test"),
Expand Down Expand Up @@ -121,6 +127,7 @@ const serviceLayer = (input: {
followChangeRequestTemplates: true,
},
}),
mutationDependencies,
),
),
);
Expand Down Expand Up @@ -211,6 +218,7 @@ describe("EnvironmentMcpService", () => {
environmentLayer("Test", "1.0.0"),
providerLayer,
ServerSettings.layerTest({}),
mutationDependencies,
),
),
),
Expand All @@ -235,6 +243,7 @@ describe("EnvironmentMcpService", () => {
environmentLayer("Test", "1.0.0"),
unavailableRegistry,
ServerSettings.layerTest(DEFAULT_SERVER_SETTINGS),
mutationDependencies,
),
),
),
Expand Down Expand Up @@ -270,7 +279,12 @@ describe("EnvironmentMcpService", () => {
Effect.provide(
EnvironmentMcp.layer.pipe(
Layer.provide(
Layer.mergeAll(environmentLayer("Test", "1.0.0"), providerLayer, settingsLayer),
Layer.mergeAll(
environmentLayer("Test", "1.0.0"),
providerLayer,
settingsLayer,
mutationDependencies,
),
),
),
),
Expand Down
93 changes: 89 additions & 4 deletions apps/server/src/mcp/EnvironmentMcpService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,22 @@ import {
ENVIRONMENT_MCP_MAX_WRITING_INSTRUCTIONS,
EnvironmentMcpFailure,
type EnvironmentMcpPreferences,
type EnvironmentMcpPreferencesUpdateInput,
type EnvironmentMcpPreferencesUpdateResult,
type EnvironmentMcpReadInput,
type EnvironmentMcpReadResult,
type ServerProvider,
type ServerSettings,
type ServerSettingsPatch,
} from "@t3tools/contracts";
import * as Cause from "effect/Cause";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";

import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
import { ThreadCommandExecutor } from "../orchestration-v2/ThreadCommandExecutor.ts";
import { ThreadManagementService } from "../orchestration-v2/ThreadManagementService.ts";
import { ProviderRegistry } from "../provider/Services/ProviderRegistry.ts";
import * as ServerSettingsModule from "../serverSettings.ts";
import type { McpInvocationScope } from "./McpInvocationContext.ts";
Expand All @@ -26,6 +31,10 @@ export class EnvironmentMcpService extends Context.Service<
scope: McpInvocationScope,
input: EnvironmentMcpReadInput,
) => Effect.Effect<EnvironmentMcpReadResult, EnvironmentMcpFailure>;
readonly updatePreferences: (
scope: McpInvocationScope,
input: EnvironmentMcpPreferencesUpdateInput,
) => Effect.Effect<EnvironmentMcpPreferencesUpdateResult, EnvironmentMcpFailure>;
}
>()("t3/mcp/EnvironmentMcpService") {}

Expand Down Expand Up @@ -109,17 +118,73 @@ const providerHealth = (providers: ReadonlyArray<ServerProvider>) => {
} as const;
};

const make = Effect.gen(function* () {
export const make = Effect.gen(function* () {
const environment = yield* ServerEnvironment.ServerEnvironment;
const providerRegistry = yield* ProviderRegistry;
const settingsService = yield* ServerSettingsModule.ServerSettingsService;
const threads = yield* ThreadManagementService;
const threadDispatch = yield* ThreadCommandExecutor;

const requireCapability = (scope: McpInvocationScope) =>
scope.capabilities.has("orchestration")
? Effect.void
: Effect.fail(new EnvironmentMcpFailure({ code: "capability_denied" }));

const requireCurrentEnvironment = (scope: McpInvocationScope) =>
unavailable(environment.getEnvironmentId, "environment_unavailable").pipe(
Effect.filterOrFail(
(currentEnvironmentId) => currentEnvironmentId === scope.environmentId,
() => new EnvironmentMcpFailure({ code: "environment_mismatch" }),
),
);

const loadCurrentCaller = (scope: McpInvocationScope) =>
threads.getThreadShell(scope.threadId).pipe(
Effect.mapError(() => new EnvironmentMcpFailure({ code: "operation_failed" })),
Effect.flatMap((shell) =>
shell === null || shell.deletedAt !== null
? Effect.fail(new EnvironmentMcpFailure({ code: "thread_not_found" }))
: Effect.succeed(shell),
),
);

const preferencePatch = (input: EnvironmentMcpPreferencesUpdateInput): ServerSettingsPatch => ({
...(input.defaultThreadEnvMode === undefined
? {}
: { defaultThreadEnvMode: input.defaultThreadEnvMode }),
...(input.newWorktreesStartFromOrigin === undefined
? {}
: { newWorktreesStartFromOrigin: input.newWorktreesStartFromOrigin }),
...(input.enableProviderUpdateChecks === undefined
? {}
: { enableProviderUpdateChecks: input.enableProviderUpdateChecks }),
...(input.backgroundActivity === undefined
? {}
: { backgroundActivity: { profile: input.backgroundActivity.profile } }),
...(input.sourceControlWritingStyle === undefined
? {}
: {
sourceControlWritingStyle: {
...(input.sourceControlWritingStyle.mode === undefined
? {}
: { mode: input.sourceControlWritingStyle.mode }),
...(input.sourceControlWritingStyle.customInstructions === undefined
? {}
: { customInstructions: input.sourceControlWritingStyle.customInstructions }),
...(input.sourceControlWritingStyle.followChangeRequestTemplates === undefined
? {}
: {
followChangeRequestTemplates:
input.sourceControlWritingStyle.followChangeRequestTemplates,
}),
},
}),
});

return EnvironmentMcpService.of({
read: (scope, input) =>
Effect.gen(function* () {
if (!scope.capabilities.has("orchestration")) {
return yield* new EnvironmentMcpFailure({ code: "capability_denied" });
}
yield* requireCapability(scope);
const descriptor = yield* unavailable(environment.getDescriptor, "environment_unavailable");
if (descriptor.environmentId !== scope.environmentId) {
return yield* new EnvironmentMcpFailure({ code: "environment_mismatch" });
Expand Down Expand Up @@ -189,6 +254,26 @@ const make = Effect.gen(function* () {
preferenceScope: "server_owned",
};
}),
updatePreferences: (scope, input) =>
requireCapability(scope).pipe(
Effect.andThen(requireCurrentEnvironment(scope)),
Effect.andThen(
threadDispatch.withLock(
scope.threadId,
Effect.gen(function* () {
const caller = yield* loadCurrentCaller(scope);
if (caller.runtimeMode !== "full-access" || caller.interactionMode !== "default") {
return yield* new EnvironmentMcpFailure({ code: "permission_denied" });
}
const settings = yield* unavailable(
settingsService.updateSettings(preferencePatch(input)),
"settings_unavailable",
);
return { preferences: presentEnvironmentPreferences(settings) };
}),
),
),
),
});
});

Expand Down
Loading
Loading