diff --git a/src/features/claude-code-mcp-loader/loader.test.ts b/src/features/claude-code-mcp-loader/loader.test.ts
index bd9e206d8..48ab1a288 100644
--- a/src/features/claude-code-mcp-loader/loader.test.ts
+++ b/src/features/claude-code-mcp-loader/loader.test.ts
@@ -1,3 +1,5 @@
+///
+
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", () => {
}
})
})
-
diff --git a/src/features/claude-code-mcp-loader/loader.ts b/src/features/claude-code-mcp-loader/loader.ts
index 6ccf08b42..49c56ca2f 100644
--- a/src/features/claude-code-mcp-loader/loader.ts
+++ b/src/features/claude-code-mcp-loader/loader.ts
@@ -48,6 +48,7 @@ async function loadMcpConfigFile(
export function getSystemMcpServerNames(): Set {
const names = new Set()
const paths = getMcpConfigPaths()
+ const cwd = process.cwd()
for (const { path } of paths) {
if (!existsSync(path)) continue
@@ -59,6 +60,7 @@ export function getSystemMcpServerNames(): Set {
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
if (serverConfig.disabled) continue
+ if (!shouldLoadMcpServer(serverConfig, cwd)) continue
names.add(name)
}
} catch {
diff --git a/src/tools/skill/async-description-refresh.test.ts b/src/tools/skill/async-description-refresh.test.ts
new file mode 100644
index 000000000..27931597d
--- /dev/null
+++ b/src/tools/skill/async-description-refresh.test.ts
@@ -0,0 +1,69 @@
+///
+
+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 {
+ for (let attempt = 0; attempt < 20; attempt += 1) {
+ if (predicate()) {
+ return
+ }
+
+ await new Promise((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")
+ })
+})
diff --git a/src/tools/skill/tools.ts b/src/tools/skill/tools.ts
index 448b3752c..70d2016e3 100644
--- a/src/tools/skill/tools.ts
+++ b/src/tools/skill/tools.ts
@@ -265,8 +265,8 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition
})
}
- const buildDescription = async (): Promise => {
- if (cachedDescription) return cachedDescription
+ const buildDescription = async (force = false): Promise => {
+ 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)