authorgravatar for 76637177+SunkenInTime@users.noreply.github.comDara Adedeji <76637177+SunkenInTime@users.noreply.github.com> 2026-10-14 16:47:55-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-14 17:47:55-03:00
loge47bfe4617f6fdca2c6513bd0afe0f6b8d24688a
treed04de5b87d9afbf63bd8f5e1b42c3d6f7bd68cfd
parentbf8ffe81a63c82d3de5fbaa9991fd5f30034a452
signature Signed by PGP key B5690EEEBB952194

fix(server): Claude works next to an enterprise managed-mcp.json (#18706)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

4 files changed, 52 insertions(+), 5 deletions(-)

apps/server/src/provider/ClaudeCapabilitiesProbe.test.ts+16
......@@ -41,6 +41,7 @@ it("isolates Claude capability probes without dropping workspace setting sources
4141 FORCE_CODE_TERMINAL: "1",
4242 },
4343 cwd: "/workspace/project",
44 managedMcpConfig: false,
4445 });
4546
4647 assert.deepEqual(options.mcpServers, {});
......@@ -59,6 +60,21 @@ it("isolates Claude capability probes without dropping workspace setting sources
5960 assert.equal(options.env?.CLAUDE_CODE_IDE_SKIP_AUTO_INSTALL, "1");
6061});
6162
63it("drops strict MCP mode, and only that, next to an administrator's managed-mcp.json", () => {
64 const options = buildClaudeCapabilitiesProbeQueryOptions({
65 executablePath: "/usr/bin/claude",
66 abortController: new AbortController(),
67 environment: { HOME: "/home/user" },
68 cwd: undefined,
69 managedMcpConfig: true,
70 });
71
72 // Claude exits before initializing when handed --strict-mcp-config here.
73 assert.equal(options.strictMcpConfig, undefined);
74 assert.deepEqual(options.mcpServers, {});
75 assert.equal(options.env?.ENABLE_CLAUDEAI_MCP_SERVERS, "false");
76});
77
6278it.layer(NodeServices.layer)("Claude capability probe SDK boundary", (it) => {
6379 it.effect(
6480 "discovers commands and skills separately for each cwd without replacing machine metadata",
apps/server/src/provider/ClaudeProvider.ts+8-3
......@@ -35,7 +35,7 @@ import {
3535 type ServerProviderDraft,
3636} from "@t3tools/provider-core/server/snapshotProbe";
3737import { resolveClaudeSdkExecutablePath } from "./Drivers/ClaudeExecutable.ts";
38import { makeClaudeEnvironment } from "./Drivers/ClaudeHome.ts";
38import { hasClaudeManagedMcpConfig, makeClaudeEnvironment } from "./Drivers/ClaudeHome.ts";
3939import { discoverClaudeSkills } from "./Drivers/ClaudeSkills.ts";
4040import type { ProviderWorkspaceSnapshot } from "@t3tools/provider-core/server/driver";
4141import { makeUnavailableUsageLimits } from "@t3tools/provider-core/server/usageLimits";
......@@ -223,6 +223,8 @@ export function buildClaudeCapabilitiesProbeQueryOptions(input: {
223223 readonly abortController: AbortController;
224224 readonly environment: NodeJS.ProcessEnv;
225225 readonly cwd: string | undefined;
226 /** An administrator's `managed-mcp.json` is installed; see `hasClaudeManagedMcpConfig`. */
227 readonly managedMcpConfig: boolean;
226228}): ClaudeQueryOptions {
227229 return {
228230 persistSession: false,
......@@ -235,9 +237,10 @@ export function buildClaudeCapabilitiesProbeQueryOptions(input: {
235237 settings: { disableAllHooks: true },
236238 allowedTools: [],
237239 // Ignore MCP definitions from every filesystem setting source above. The
238 // SDK combines this empty explicit map with --strict-mcp-config.
240 // SDK combines this empty explicit map with --strict-mcp-config, which
241 // Claude refuses next to a managed-mcp.json that already excludes them.
239242 mcpServers: {},
240 strictMcpConfig: true,
243 ...(input.managedMcpConfig ? {} : { strictMcpConfig: true }),
241244 env: {
242245 ...input.environment,
243246 // Connected claude.ai MCP servers are discovered outside filesystem
......@@ -384,6 +387,7 @@ const probeClaudeCapabilities = (
384387 claudeSettings.binaryPath,
385388 claudeEnvironment,
386389 );
390 const managedMcpConfig = yield* hasClaudeManagedMcpConfig();
387391 return yield* Effect.tryPromise(async () => {
388392 const q = claudeQuery({
389393 // Never yield — we only need initialization data, not a conversation.
......@@ -397,6 +401,7 @@ const probeClaudeCapabilities = (
397401 abortController: abort,
398402 environment: claudeEnvironment,
399403 cwd,
404 managedMcpConfig,
400405 }),
401406 });
402407 const init = await q.initializationResult();
apps/server/src/provider/Drivers/ClaudeHome.ts+20
......@@ -1,5 +1,6 @@
11import type { ClaudeSettings } from "@t3tools/contracts";
22import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
34import * as Path from "effect/Path";
45import * as Schema from "effect/Schema";
56
......@@ -53,6 +54,25 @@ export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function
5354 };
5455});
5556
57/**
58 * Whether an administrator installed Claude Code's `managed-mcp.json`. While it
59 * exists Claude refuses `--strict-mcp-config` and exits before initializing,
60 * but it already ignores user, project and `--mcp-config` servers on its own,
61 * so callers that pass the flag for isolation can drop it. Paths match the
62 * ones Claude Code checks.
63 */
64export const hasClaudeManagedMcpConfig = Effect.fn("hasClaudeManagedMcpConfig")(function* () {
65 const fileSystem = yield* FileSystem.FileSystem;
66 const platform = yield* HostProcess.Platform;
67 const managedMcpConfigPath =
68 platform === "darwin"
69 ? "/Library/Application Support/ClaudeCode/managed-mcp.json"
70 : platform === "win32"
71 ? "C:\\Program Files\\ClaudeCode\\managed-mcp.json"
72 : "/etc/claude-code/managed-mcp.json";
73 return yield* fileSystem.exists(managedMcpConfigPath).pipe(Effect.orElseSucceed(() => false));
74});
75
5676export const makeClaudeContinuationGroupKey = Effect.fn("makeClaudeContinuationGroupKey")(
5777 function* (
5878 config: Pick<ClaudeSettings, "homePath">,
apps/server/src/textGeneration/ClaudeTextGeneration.ts+8-2
......@@ -37,7 +37,10 @@ import {
3737 resolveClaudeModelSlug,
3838 scopeClaudeModelCatalog,
3939} from "../provider/ClaudeModelCatalog.ts";
40import { makeClaudeEnvironment } from "../provider/Drivers/ClaudeHome.ts";
40import {
41 hasClaudeManagedMcpConfig,
42 makeClaudeEnvironment,
43} from "../provider/Drivers/ClaudeHome.ts";
4144
4245const CLAUDE_TIMEOUT_MS = 180_000;
4346/** The tools an investigation can see. `--tools` takes bare names; patterns go in the allow list. */
......@@ -209,6 +212,7 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
209212 ),
210213 )
211214 : cwd;
215 const managedMcpConfig = yield* hasClaudeManagedMcpConfig();
212216 const spawnCommand = yield* resolveSpawnCommand(
213217 claudeSettings.binaryPath || "claude",
214218 [
......@@ -236,7 +240,9 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
236240 ]
237241 : ["--tools", ""]),
238242 "--disable-slash-commands",
239 "--strict-mcp-config",
243 // Claude refuses this flag next to a managed-mcp.json, which already keeps the
244 // user's own MCP servers out.
245 ...(managedMcpConfig ? [] : ["--strict-mcp-config"]),
240246 "--permission-mode",
241247 "dontAsk",
242248 ],