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...@@ -41,6 +41,7 @@ it("isolates Claude capability probes without dropping workspace setting sources
41 FORCE_CODE_TERMINAL: "1",41 FORCE_CODE_TERMINAL: "1",
42 },42 },
43 cwd: "/workspace/project",43 cwd: "/workspace/project",
44 managedMcpConfig: false,
44 });45 });
4546
46 assert.deepEqual(options.mcpServers, {});47 assert.deepEqual(options.mcpServers, {});
...@@ -59,6 +60,21 @@ it("isolates Claude capability probes without dropping workspace setting sources...@@ -59,6 +60,21 @@ it("isolates Claude capability probes without dropping workspace setting sources
59 assert.equal(options.env?.CLAUDE_CODE_IDE_SKIP_AUTO_INSTALL, "1");60 assert.equal(options.env?.CLAUDE_CODE_IDE_SKIP_AUTO_INSTALL, "1");
60});61});
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
62it.layer(NodeServices.layer)("Claude capability probe SDK boundary", (it) => {78it.layer(NodeServices.layer)("Claude capability probe SDK boundary", (it) => {
63 it.effect(79 it.effect(
64 "discovers commands and skills separately for each cwd without replacing machine metadata",80 "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 {...@@ -35,7 +35,7 @@ import {
35 type ServerProviderDraft,35 type ServerProviderDraft,
36} from "@t3tools/provider-core/server/snapshotProbe";36} from "@t3tools/provider-core/server/snapshotProbe";
37import { resolveClaudeSdkExecutablePath } from "./Drivers/ClaudeExecutable.ts";37import { resolveClaudeSdkExecutablePath } from "./Drivers/ClaudeExecutable.ts";
38import { makeClaudeEnvironment } from "./Drivers/ClaudeHome.ts";38import { hasClaudeManagedMcpConfig, makeClaudeEnvironment } from "./Drivers/ClaudeHome.ts";
39import { discoverClaudeSkills } from "./Drivers/ClaudeSkills.ts";39import { discoverClaudeSkills } from "./Drivers/ClaudeSkills.ts";
40import type { ProviderWorkspaceSnapshot } from "@t3tools/provider-core/server/driver";40import type { ProviderWorkspaceSnapshot } from "@t3tools/provider-core/server/driver";
41import { makeUnavailableUsageLimits } from "@t3tools/provider-core/server/usageLimits";41import { makeUnavailableUsageLimits } from "@t3tools/provider-core/server/usageLimits";
...@@ -223,6 +223,8 @@ export function buildClaudeCapabilitiesProbeQueryOptions(input: {...@@ -223,6 +223,8 @@ export function buildClaudeCapabilitiesProbeQueryOptions(input: {
223 readonly abortController: AbortController;223 readonly abortController: AbortController;
224 readonly environment: NodeJS.ProcessEnv;224 readonly environment: NodeJS.ProcessEnv;
225 readonly cwd: string | undefined;225 readonly cwd: string | undefined;
226 /** An administrator's `managed-mcp.json` is installed; see `hasClaudeManagedMcpConfig`. */
227 readonly managedMcpConfig: boolean;
226}): ClaudeQueryOptions {228}): ClaudeQueryOptions {
227 return {229 return {
228 persistSession: false,230 persistSession: false,
...@@ -235,9 +237,10 @@ export function buildClaudeCapabilitiesProbeQueryOptions(input: {...@@ -235,9 +237,10 @@ export function buildClaudeCapabilitiesProbeQueryOptions(input: {
235 settings: { disableAllHooks: true },237 settings: { disableAllHooks: true },
236 allowedTools: [],238 allowedTools: [],
237 // Ignore MCP definitions from every filesystem setting source above. The239 // 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.
239 mcpServers: {},242 mcpServers: {},
240 strictMcpConfig: true,243 ...(input.managedMcpConfig ? {} : { strictMcpConfig: true }),
241 env: {244 env: {
242 ...input.environment,245 ...input.environment,
243 // Connected claude.ai MCP servers are discovered outside filesystem246 // Connected claude.ai MCP servers are discovered outside filesystem
...@@ -384,6 +387,7 @@ const probeClaudeCapabilities = (...@@ -384,6 +387,7 @@ const probeClaudeCapabilities = (
384 claudeSettings.binaryPath,387 claudeSettings.binaryPath,
385 claudeEnvironment,388 claudeEnvironment,
386 );389 );
390 const managedMcpConfig = yield* hasClaudeManagedMcpConfig();
387 return yield* Effect.tryPromise(async () => {391 return yield* Effect.tryPromise(async () => {
388 const q = claudeQuery({392 const q = claudeQuery({
389 // Never yield — we only need initialization data, not a conversation.393 // Never yield — we only need initialization data, not a conversation.
...@@ -397,6 +401,7 @@ const probeClaudeCapabilities = (...@@ -397,6 +401,7 @@ const probeClaudeCapabilities = (
397 abortController: abort,401 abortController: abort,
398 environment: claudeEnvironment,402 environment: claudeEnvironment,
399 cwd,403 cwd,
404 managedMcpConfig,
400 }),405 }),
401 });406 });
402 const init = await q.initializationResult();407 const init = await q.initializationResult();
apps/server/src/provider/Drivers/ClaudeHome.ts+20
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import type { ClaudeSettings } from "@t3tools/contracts";1import type { ClaudeSettings } from "@t3tools/contracts";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
3import * as Path from "effect/Path";4import * as Path from "effect/Path";
4import * as Schema from "effect/Schema";5import * as Schema from "effect/Schema";
56
...@@ -53,6 +54,25 @@ export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function...@@ -53,6 +54,25 @@ export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function
53 };54 };
54});55});
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
56export const makeClaudeContinuationGroupKey = Effect.fn("makeClaudeContinuationGroupKey")(76export const makeClaudeContinuationGroupKey = Effect.fn("makeClaudeContinuationGroupKey")(
57 function* (77 function* (
58 config: Pick<ClaudeSettings, "homePath">,78 config: Pick<ClaudeSettings, "homePath">,
apps/server/src/textGeneration/ClaudeTextGeneration.ts+8-2
...@@ -37,7 +37,10 @@ import {...@@ -37,7 +37,10 @@ import {
37 resolveClaudeModelSlug,37 resolveClaudeModelSlug,
38 scopeClaudeModelCatalog,38 scopeClaudeModelCatalog,
39} from "../provider/ClaudeModelCatalog.ts";39} from "../provider/ClaudeModelCatalog.ts";
40import { makeClaudeEnvironment } from "../provider/Drivers/ClaudeHome.ts";40import {
41 hasClaudeManagedMcpConfig,
42 makeClaudeEnvironment,
43} from "../provider/Drivers/ClaudeHome.ts";
4144
42const CLAUDE_TIMEOUT_MS = 180_000;45const CLAUDE_TIMEOUT_MS = 180_000;
43/** The tools an investigation can see. `--tools` takes bare names; patterns go in the allow list. */46/** 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...@@ -209,6 +212,7 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
209 ),212 ),
210 )213 )
211 : cwd;214 : cwd;
215 const managedMcpConfig = yield* hasClaudeManagedMcpConfig();
212 const spawnCommand = yield* resolveSpawnCommand(216 const spawnCommand = yield* resolveSpawnCommand(
213 claudeSettings.binaryPath || "claude",217 claudeSettings.binaryPath || "claude",
214 [218 [
...@@ -236,7 +240,9 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu...@@ -236,7 +240,9 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
236 ]240 ]
237 : ["--tools", ""]),241 : ["--tools", ""]),
238 "--disable-slash-commands",242 "--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"]),
240 "--permission-mode",246 "--permission-mode",
241 "dontAsk",247 "dontAsk",
242 ],248 ],