fix: resolve 3 community-reported bugs (#2915, #2917, #2918)

- background_output: snapshot read cursor before consuming, restore on
  /undo message removal so re-reads return data (fixes #2915)
- MCP loader: preserve oauth field in transformMcpServer, add scope/
  projectPath filtering so local-scoped MCPs only load in matching
  directories (fixes #2917)
- runtime-fallback: add 'reached your usage limit' to retryable error
  patterns so quota exhaustion triggers model fallback (fixes #2918)

Verified: bun test (4606 pass / 0 fail), tsc --noEmit clean
This commit is contained in:
YeonGyu-Kim
2026-03-29 04:53:36 +09:00
parent 9fc56ab544
commit b2497f1327
16 changed files with 575 additions and 6 deletions
@@ -0,0 +1,76 @@
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"
import { mkdirSync, rmSync, writeFileSync } from "fs"
import { tmpdir } from "os"
import { join } from "path"
import type { LoadedPlugin } from "./types"
const TEST_DIR = join(tmpdir(), `plugin-mcp-loader-test-${Date.now()}`)
const PROJECT_DIR = join(TEST_DIR, "project")
const PLUGIN_DIR = join(TEST_DIR, "plugin")
const MCP_CONFIG_PATH = join(PLUGIN_DIR, "mcp.json")
describe("loadPluginMcpServers", () => {
beforeEach(() => {
mkdirSync(PROJECT_DIR, { recursive: true })
mkdirSync(PLUGIN_DIR, { recursive: true })
mock.module("../../shared/logger", () => ({
log: () => {},
}))
})
afterEach(() => {
mock.restore()
rmSync(TEST_DIR, { recursive: true, force: true })
})
describe("#given plugin MCP entries with local scope metadata", () => {
it("#when loading plugin MCP servers #then only entries matching the current cwd are included", async () => {
writeFileSync(
MCP_CONFIG_PATH,
JSON.stringify({
mcpServers: {
globalServer: {
command: "npx",
args: ["global-plugin-server"],
},
matchingLocal: {
command: "npx",
args: ["matching-plugin-local"],
scope: "local",
projectPath: PROJECT_DIR,
},
nonMatchingLocal: {
command: "npx",
args: ["non-matching-plugin-local"],
scope: "local",
projectPath: join(PROJECT_DIR, "other-project"),
},
},
})
)
const plugin: LoadedPlugin = {
name: "demo-plugin",
version: "1.0.0",
scope: "project",
installPath: PLUGIN_DIR,
pluginKey: "demo-plugin@test",
mcpPath: MCP_CONFIG_PATH,
}
const originalCwd = process.cwd()
process.chdir(PROJECT_DIR)
try {
const { loadPluginMcpServers } = await import("./mcp-server-loader")
const servers = await loadPluginMcpServers([plugin])
expect(servers).toHaveProperty("demo-plugin:globalServer")
expect(servers).toHaveProperty("demo-plugin:matchingLocal")
expect(servers).not.toHaveProperty("demo-plugin:nonMatchingLocal")
} finally {
process.chdir(originalCwd)
}
})
})
})
@@ -1,6 +1,7 @@
import { existsSync } from "fs"
import type { McpServerConfig } from "../claude-code-mcp-loader/types"
import { expandEnvVarsInObject } from "../claude-code-mcp-loader/env-expander"
import { shouldLoadMcpServer } from "../claude-code-mcp-loader/scope-filter"
import { transformMcpServer } from "../claude-code-mcp-loader/transformer"
import type { ClaudeCodeMcpConfig } from "../claude-code-mcp-loader/types"
import { log } from "../../shared/logger"
@@ -11,6 +12,7 @@ export async function loadPluginMcpServers(
plugins: LoadedPlugin[],
): Promise<Record<string, McpServerConfig>> {
const servers: Record<string, McpServerConfig> = {}
const cwd = process.cwd()
for (const plugin of plugins) {
if (!plugin.mcpPath || !existsSync(plugin.mcpPath)) continue
@@ -25,6 +27,15 @@ export async function loadPluginMcpServers(
if (!config.mcpServers) continue
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
if (!shouldLoadMcpServer(serverConfig, cwd)) {
log(`Skipping local plugin MCP server "${name}" outside current cwd`, {
path: plugin.mcpPath,
projectPath: serverConfig.projectPath,
cwd,
})
continue
}
if (serverConfig.disabled) {
log(`Skipping disabled MCP server "${name}" from plugin ${plugin.name}`)
continue