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