fix: apply scope filter to getSystemMcpServerNames and fix async native skill description refresh
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
/// <reference types="bun-types" />
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test"
|
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test"
|
||||||
import { mkdirSync, writeFileSync, rmSync } from "fs"
|
import { mkdirSync, writeFileSync, rmSync } from "fs"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
@@ -198,10 +200,10 @@ describe("getSystemMcpServerNames", () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it("reads both ~/.claude.json and ~/.claude/.mcp.json for user scope", async () => {
|
it("reads both ~/.claude.json and ~/.claude/.mcp.json for user scope", async () => {
|
||||||
// given
|
// given
|
||||||
const claudeDir = join(TEST_HOME, ".claude")
|
const claudeDir = join(TEST_HOME, ".claude")
|
||||||
mkdirSync(claudeDir, { recursive: true })
|
mkdirSync(claudeDir, { recursive: true })
|
||||||
|
|
||||||
writeFileSync(join(TEST_HOME, ".claude.json"), JSON.stringify({
|
writeFileSync(join(TEST_HOME, ".claude.json"), JSON.stringify({
|
||||||
mcpServers: {
|
mcpServers: {
|
||||||
@@ -226,10 +228,55 @@ describe("getSystemMcpServerNames", () => {
|
|||||||
// then
|
// then
|
||||||
expect(names.has("server-from-claude-json")).toBe(true)
|
expect(names.has("server-from-claude-json")).toBe(true)
|
||||||
expect(names.has("server-from-mcp-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 {
|
} finally {
|
||||||
process.chdir(originalCwd)
|
process.chdir(originalCwd)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("loadMcpConfigs", () => {
|
describe("loadMcpConfigs", () => {
|
||||||
@@ -334,4 +381,3 @@ describe("loadMcpConfigs", () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ async function loadMcpConfigFile(
|
|||||||
export function getSystemMcpServerNames(): Set<string> {
|
export function getSystemMcpServerNames(): Set<string> {
|
||||||
const names = new Set<string>()
|
const names = new Set<string>()
|
||||||
const paths = getMcpConfigPaths()
|
const paths = getMcpConfigPaths()
|
||||||
|
const cwd = process.cwd()
|
||||||
|
|
||||||
for (const { path } of paths) {
|
for (const { path } of paths) {
|
||||||
if (!existsSync(path)) continue
|
if (!existsSync(path)) continue
|
||||||
@@ -59,6 +60,7 @@ export function getSystemMcpServerNames(): Set<string> {
|
|||||||
|
|
||||||
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
||||||
if (serverConfig.disabled) continue
|
if (serverConfig.disabled) continue
|
||||||
|
if (!shouldLoadMcpServer(serverConfig, cwd)) continue
|
||||||
names.add(name)
|
names.add(name)
|
||||||
}
|
}
|
||||||
} catch {
|
} 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")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -265,8 +265,8 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildDescription = async (): Promise<string> => {
|
const buildDescription = async (force = false): Promise<string> => {
|
||||||
if (cachedDescription) return cachedDescription
|
if (!force && cachedDescription) return cachedDescription
|
||||||
const skills = await getSkills()
|
const skills = await getSkills()
|
||||||
const commands = getCommands()
|
const commands = getCommands()
|
||||||
const skillInfos = skills.map(loadedSkillToInfo)
|
const skillInfos = skills.map(loadedSkillToInfo)
|
||||||
@@ -294,7 +294,7 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition
|
|||||||
|
|
||||||
cachedDescription = formatCombinedDescription(skillInfos, commandsForDescription)
|
cachedDescription = formatCombinedDescription(skillInfos, commandsForDescription)
|
||||||
if (needsAsyncRefresh) {
|
if (needsAsyncRefresh) {
|
||||||
void buildDescription()
|
void buildDescription(true)
|
||||||
}
|
}
|
||||||
} else if (options.commands !== undefined) {
|
} else if (options.commands !== undefined) {
|
||||||
cachedDescription = formatCombinedDescription([], options.commands)
|
cachedDescription = formatCombinedDescription([], options.commands)
|
||||||
|
|||||||
Reference in New Issue
Block a user