fix: apply scope filter to getSystemMcpServerNames and fix async native skill description refresh

This commit is contained in:
YeonGyu-Kim
2026-03-31 17:05:55 -07:00
parent 9f2c4500e8
commit 8fba90766d
4 changed files with 126 additions and 9 deletions
@@ -1,3 +1,5 @@
/// <reference types="bun-types" />
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test"
import { mkdirSync, writeFileSync, rmSync } from "fs"
import { join } from "path"
@@ -198,10 +200,10 @@ describe("getSystemMcpServerNames", () => {
}
})
it("reads both ~/.claude.json and ~/.claude/.mcp.json for user scope", async () => {
// given
const claudeDir = join(TEST_HOME, ".claude")
mkdirSync(claudeDir, { recursive: true })
it("reads both ~/.claude.json and ~/.claude/.mcp.json for user scope", async () => {
// given
const claudeDir = join(TEST_HOME, ".claude")
mkdirSync(claudeDir, { recursive: true })
writeFileSync(join(TEST_HOME, ".claude.json"), JSON.stringify({
mcpServers: {
@@ -226,10 +228,55 @@ describe("getSystemMcpServerNames", () => {
// then
expect(names.has("server-from-claude-json")).toBe(true)
expect(names.has("server-from-mcp-json")).toBe(true)
} finally {
process.chdir(originalCwd)
}
})
it("ignores local-scope user MCP entries for other projects", async () => {
//#given
const otherProjectDir = join(TEST_DIR, "project-a")
const currentProjectDir = join(TEST_DIR, "project-b")
mkdirSync(otherProjectDir, { recursive: true })
mkdirSync(currentProjectDir, { recursive: true })
writeFileSync(join(TEST_HOME, ".claude.json"), JSON.stringify({
mcpServers: {
playwright: {
command: "npx",
args: ["@playwright/mcp@latest"],
scope: "local",
projectPath: otherProjectDir,
},
sqlite: {
command: "uvx",
args: ["mcp-server-sqlite"],
scope: "local",
projectPath: currentProjectDir,
},
memory: {
command: "npx",
args: ["memory-mcp"],
},
},
}))
const originalCwd = process.cwd()
process.chdir(currentProjectDir)
try {
//#when
const { getSystemMcpServerNames } = await import("./loader")
const names = getSystemMcpServerNames()
//#then
expect(names.has("playwright")).toBe(false)
expect(names.has("sqlite")).toBe(true)
expect(names.has("memory")).toBe(true)
} finally {
process.chdir(originalCwd)
}
})
})
})
describe("loadMcpConfigs", () => {
@@ -334,4 +381,3 @@ describe("loadMcpConfigs", () => {
}
})
})
@@ -48,6 +48,7 @@ async function loadMcpConfigFile(
export function getSystemMcpServerNames(): Set<string> {
const names = new Set<string>()
const paths = getMcpConfigPaths()
const cwd = process.cwd()
for (const { path } of paths) {
if (!existsSync(path)) continue
@@ -59,6 +60,7 @@ export function getSystemMcpServerNames(): Set<string> {
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
if (serverConfig.disabled) continue
if (!shouldLoadMcpServer(serverConfig, cwd)) continue
names.add(name)
}
} catch {
@@ -0,0 +1,69 @@
/// <reference types="bun-types" />
import { describe, expect, it } from "bun:test"
import { createSkillTool } from "./tools"
import type { LoadedSkill } from "../../features/opencode-skill-loader/types"
function createMockSkill(name: string): LoadedSkill {
return {
name,
path: `/test/skills/${name}/SKILL.md`,
resolvedPath: `/test/skills/${name}`,
definition: {
name,
description: `Test skill ${name}`,
template: `Test skill template for ${name}`,
},
scope: "opencode-project",
}
}
async function waitForRefresh(predicate: () => boolean): Promise<void> {
for (let attempt = 0; attempt < 20; attempt += 1) {
if (predicate()) {
return
}
await new Promise<void>((resolve) => setTimeout(resolve, 0))
}
}
describe("skill tool - async native skill description refresh", () => {
it("updates description after async native skills resolve", async () => {
//#given
let allCallCount = 0
const tool = createSkillTool({
skills: [createMockSkill("seeded-skill")],
commands: [],
nativeSkills: {
async all() {
allCallCount += 1
return [{
name: "async-native-skill",
description: "Async native skill from plugin input",
location: "/external/skills/async-native-skill/SKILL.md",
content: "Async native skill body",
}]
},
async get() {
return undefined
},
async dirs() {
return []
},
},
})
expect(tool.description).toContain("seeded-skill")
expect(tool.description).not.toContain("async-native-skill")
//#when
await waitForRefresh(() => allCallCount === 2)
//#then
expect(allCallCount).toBe(2)
expect(tool.description).toContain("seeded-skill")
expect(tool.description).toContain("async-native-skill")
})
})
+3 -3
View File
@@ -265,8 +265,8 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition
})
}
const buildDescription = async (): Promise<string> => {
if (cachedDescription) return cachedDescription
const buildDescription = async (force = false): Promise<string> => {
if (!force && cachedDescription) return cachedDescription
const skills = await getSkills()
const commands = getCommands()
const skillInfos = skills.map(loadedSkillToInfo)
@@ -294,7 +294,7 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition
cachedDescription = formatCombinedDescription(skillInfos, commandsForDescription)
if (needsAsyncRefresh) {
void buildDescription()
void buildDescription(true)
}
} else if (options.commands !== undefined) {
cachedDescription = formatCombinedDescription([], options.commands)