feat(mcp): register ast-grep as built-in MCP
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,119 @@
|
|||||||
|
import { afterEach, describe, expect, it } from "bun:test"
|
||||||
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
||||||
|
import { tmpdir } from "node:os"
|
||||||
|
import { join } from "node:path"
|
||||||
|
import { pathToFileURL } from "node:url"
|
||||||
|
import { createAstGrepMcpConfig } from "./ast-grep"
|
||||||
|
|
||||||
|
const temporaryDirectories: string[] = []
|
||||||
|
|
||||||
|
function createTemporaryDirectory(prefix: string): string {
|
||||||
|
const directory = mkdtempSync(join(tmpdir(), prefix))
|
||||||
|
temporaryDirectories.push(directory)
|
||||||
|
return directory
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const directory of temporaryDirectories.splice(0)) {
|
||||||
|
rmSync(directory, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("createAstGrepMcpConfig", () => {
|
||||||
|
it("resolves bundled dist cli from module root when cwd is unrelated", () => {
|
||||||
|
// given
|
||||||
|
const packageRoot = createTemporaryDirectory("omo-ast-grep-package-root-")
|
||||||
|
const unrelatedCwd = createTemporaryDirectory("omo-ast-grep-unrelated-cwd-")
|
||||||
|
const moduleFilePath = join(packageRoot, "dist", "index.js")
|
||||||
|
const cliPath = join(packageRoot, "packages", "ast-grep-mcp", "dist", "cli.js")
|
||||||
|
mkdirSync(join(packageRoot, "dist"), { recursive: true })
|
||||||
|
mkdirSync(join(packageRoot, "packages", "ast-grep-mcp", "dist"), { recursive: true })
|
||||||
|
writeFileSync(cliPath, "#!/usr/bin/env node\n", "utf-8")
|
||||||
|
|
||||||
|
// when
|
||||||
|
const config = createAstGrepMcpConfig({
|
||||||
|
cwd: unrelatedCwd,
|
||||||
|
moduleUrl: pathToFileURL(moduleFilePath).href,
|
||||||
|
})
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(config.command).toEqual(["node", cliPath, "mcp"])
|
||||||
|
expect(config.environment?.OMO_AST_GREP_WORKSPACE).toBe(unrelatedCwd)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("falls back to bun source cli for source checkouts before build", () => {
|
||||||
|
// given
|
||||||
|
const packageRoot = createTemporaryDirectory("omo-ast-grep-source-root-")
|
||||||
|
const moduleFilePath = join(packageRoot, "src", "mcp", "ast-grep.ts")
|
||||||
|
const sourceCliPath = join(packageRoot, "packages", "ast-grep-mcp", "src", "cli.ts")
|
||||||
|
mkdirSync(join(packageRoot, "src", "mcp"), { recursive: true })
|
||||||
|
mkdirSync(join(packageRoot, "packages", "ast-grep-mcp", "src"), { recursive: true })
|
||||||
|
writeFileSync(sourceCliPath, "console.log('mcp')\n", "utf-8")
|
||||||
|
|
||||||
|
// when
|
||||||
|
const config = createAstGrepMcpConfig({
|
||||||
|
cwd: createTemporaryDirectory("omo-ast-grep-source-cwd-"),
|
||||||
|
moduleUrl: pathToFileURL(moduleFilePath).href,
|
||||||
|
})
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(config.command).toEqual(["bun", sourceCliPath, "mcp"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("still returns a built-in MCP config when the cli has not been built yet", () => {
|
||||||
|
// given
|
||||||
|
const packageRoot = createTemporaryDirectory("omo-ast-grep-missing-root-")
|
||||||
|
const moduleFilePath = join(packageRoot, "dist", "index.js")
|
||||||
|
mkdirSync(join(packageRoot, "dist"), { recursive: true })
|
||||||
|
|
||||||
|
// when
|
||||||
|
const config = createAstGrepMcpConfig({
|
||||||
|
cwd: createTemporaryDirectory("omo-ast-grep-missing-cwd-"),
|
||||||
|
moduleUrl: pathToFileURL(moduleFilePath).href,
|
||||||
|
})
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(config.enabled).toBe(true)
|
||||||
|
expect(config.command[0]).toBe("node")
|
||||||
|
expect(config.command[1]).toContain(join("packages", "ast-grep-mcp", "dist", "cli.js"))
|
||||||
|
expect(config.command[2]).toBe("mcp")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not resolve the MCP command from the opened workspace", () => {
|
||||||
|
// given
|
||||||
|
const packageRoot = createTemporaryDirectory("omo-ast-grep-safe-package-root-")
|
||||||
|
const workspaceRoot = createTemporaryDirectory("omo-ast-grep-malicious-workspace-")
|
||||||
|
const moduleFilePath = join(packageRoot, "dist", "index.js")
|
||||||
|
const workspaceCliPath = join(workspaceRoot, "packages", "ast-grep-mcp", "dist", "cli.js")
|
||||||
|
mkdirSync(join(packageRoot, "dist"), { recursive: true })
|
||||||
|
mkdirSync(join(workspaceRoot, "packages", "ast-grep-mcp", "dist"), { recursive: true })
|
||||||
|
writeFileSync(workspaceCliPath, "console.log('malicious')\n", "utf-8")
|
||||||
|
|
||||||
|
// when
|
||||||
|
const config = createAstGrepMcpConfig({
|
||||||
|
cwd: workspaceRoot,
|
||||||
|
moduleUrl: pathToFileURL(moduleFilePath).href,
|
||||||
|
})
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(config.command[1]).not.toBe(workspaceCliPath)
|
||||||
|
expect(config.command[1]).toContain(packageRoot)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("maps disabled ast-grep tool names to MCP subtools", () => {
|
||||||
|
// given
|
||||||
|
const packageRoot = createTemporaryDirectory("omo-ast-grep-disabled-root-")
|
||||||
|
const moduleFilePath = join(packageRoot, "dist", "index.js")
|
||||||
|
mkdirSync(join(packageRoot, "dist"), { recursive: true })
|
||||||
|
|
||||||
|
// when
|
||||||
|
const config = createAstGrepMcpConfig({
|
||||||
|
cwd: createTemporaryDirectory("omo-ast-grep-disabled-cwd-"),
|
||||||
|
disabledTools: ["ast_grep_replace", "glob"],
|
||||||
|
moduleUrl: pathToFileURL(moduleFilePath).href,
|
||||||
|
})
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(config.environment?.OMO_AST_GREP_DISABLED_TOOLS).toBe("replace")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import { dirname, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import type { LocalMcpConfig } from "./lsp";
|
||||||
|
|
||||||
|
const PACKAGE_REL = "packages/ast-grep-mcp";
|
||||||
|
const DIST_CLI_REL = "dist/cli.js";
|
||||||
|
const SOURCE_CLI_REL = "src/cli.ts";
|
||||||
|
const WORKSPACE_ENV = "OMO_AST_GREP_WORKSPACE";
|
||||||
|
const DISABLED_TOOLS_ENV = "OMO_AST_GREP_DISABLED_TOOLS";
|
||||||
|
|
||||||
|
const MCP_TOOL_BY_OPENCODE_TOOL: Readonly<Record<string, string>> = {
|
||||||
|
ast_grep_search: "search",
|
||||||
|
ast_grep_replace: "replace",
|
||||||
|
};
|
||||||
|
|
||||||
|
type AstGrepMcpConfigOptions = {
|
||||||
|
readonly cwd?: string;
|
||||||
|
readonly disabledTools?: readonly string[];
|
||||||
|
readonly moduleUrl?: string;
|
||||||
|
readonly exists?: (path: string) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CommandCandidate = {
|
||||||
|
readonly command: string[];
|
||||||
|
readonly path: string;
|
||||||
|
readonly exists: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function addAncestorCommandCandidates(
|
||||||
|
startDirectory: string,
|
||||||
|
target: CommandCandidate[],
|
||||||
|
seenPaths: Set<string>,
|
||||||
|
pathExists: (path: string) => boolean,
|
||||||
|
): void {
|
||||||
|
let currentDirectory = resolve(startDirectory);
|
||||||
|
while (true) {
|
||||||
|
const distCliPath = resolve(currentDirectory, PACKAGE_REL, DIST_CLI_REL);
|
||||||
|
if (!seenPaths.has(distCliPath)) {
|
||||||
|
seenPaths.add(distCliPath);
|
||||||
|
target.push({ command: ["node", distCliPath, "mcp"], path: distCliPath, exists: pathExists(distCliPath) });
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceCliPath = resolve(currentDirectory, PACKAGE_REL, SOURCE_CLI_REL);
|
||||||
|
if (!seenPaths.has(sourceCliPath)) {
|
||||||
|
seenPaths.add(sourceCliPath);
|
||||||
|
target.push({ command: ["bun", sourceCliPath, "mcp"], path: sourceCliPath, exists: pathExists(sourceCliPath) });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentDirectory = resolve(currentDirectory, "..");
|
||||||
|
if (parentDirectory === currentDirectory) return;
|
||||||
|
currentDirectory = parentDirectory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModuleDirectory(moduleUrl: string): string | null {
|
||||||
|
try {
|
||||||
|
return dirname(fileURLToPath(moduleUrl));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAstGrepCommand(options: AstGrepMcpConfigOptions = {}): string[] {
|
||||||
|
const pathExists = options.exists ?? existsSync;
|
||||||
|
const candidates: CommandCandidate[] = [];
|
||||||
|
const seenPaths = new Set<string>();
|
||||||
|
const moduleDirectory = getModuleDirectory(options.moduleUrl ?? import.meta.url);
|
||||||
|
if (moduleDirectory) addAncestorCommandCandidates(moduleDirectory, candidates, seenPaths, pathExists);
|
||||||
|
|
||||||
|
const distCandidate = candidates.find((candidate) => candidate.path.endsWith(DIST_CLI_REL) && candidate.exists);
|
||||||
|
if (distCandidate) return distCandidate.command;
|
||||||
|
const sourceCandidate = candidates.find((candidate) => candidate.path.endsWith(SOURCE_CLI_REL) && candidate.exists);
|
||||||
|
if (sourceCandidate) return sourceCandidate.command;
|
||||||
|
return candidates[0]?.command ?? ["node", resolve(PACKAGE_REL, DIST_CLI_REL), "mcp"];
|
||||||
|
}
|
||||||
|
|
||||||
|
function astGrepDisabledTools(disabledTools: readonly string[] | undefined): string {
|
||||||
|
if (!disabledTools) return "";
|
||||||
|
return disabledTools
|
||||||
|
.map((toolName) => MCP_TOOL_BY_OPENCODE_TOOL[toolName])
|
||||||
|
.filter((toolName): toolName is string => typeof toolName === "string")
|
||||||
|
.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAstGrepMcpConfig(options: AstGrepMcpConfigOptions = {}): LocalMcpConfig {
|
||||||
|
const workspaceDirectory = options.cwd ?? process.cwd();
|
||||||
|
return {
|
||||||
|
type: "local",
|
||||||
|
command: resolveAstGrepCommand(options),
|
||||||
|
enabled: true,
|
||||||
|
environment: {
|
||||||
|
[WORKSPACE_ENV]: workspaceDirectory,
|
||||||
|
[DISABLED_TOOLS_ENV]: astGrepDisabledTools(options.disabledTools),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
+10
-1
@@ -1,6 +1,7 @@
|
|||||||
import { createWebsearchConfig } from "./websearch"
|
import { createWebsearchConfig } from "./websearch"
|
||||||
import { context7 } from "./context7"
|
import { context7 } from "./context7"
|
||||||
import { grep_app } from "./grep-app"
|
import { grep_app } from "./grep-app"
|
||||||
|
import { createAstGrepMcpConfig } from "./ast-grep"
|
||||||
import { createLspMcpConfig, type LocalMcpConfig } from "./lsp"
|
import { createLspMcpConfig, type LocalMcpConfig } from "./lsp"
|
||||||
import type { OhMyOpenCodeConfig } from "../config/schema"
|
import type { OhMyOpenCodeConfig } from "../config/schema"
|
||||||
|
|
||||||
@@ -16,7 +17,11 @@ type RemoteMcpConfig = {
|
|||||||
|
|
||||||
type BuiltinMcpConfig = RemoteMcpConfig | LocalMcpConfig
|
type BuiltinMcpConfig = RemoteMcpConfig | LocalMcpConfig
|
||||||
|
|
||||||
export function createBuiltinMcps(disabledMcps: string[] = [], config?: OhMyOpenCodeConfig) {
|
type BuiltinMcpOptions = {
|
||||||
|
readonly cwd?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBuiltinMcps(disabledMcps: string[] = [], config?: OhMyOpenCodeConfig, options: BuiltinMcpOptions = {}) {
|
||||||
const mcps: Record<string, BuiltinMcpConfig> = {}
|
const mcps: Record<string, BuiltinMcpConfig> = {}
|
||||||
|
|
||||||
if (!disabledMcps.includes("websearch")) {
|
if (!disabledMcps.includes("websearch")) {
|
||||||
@@ -38,5 +43,9 @@ export function createBuiltinMcps(disabledMcps: string[] = [], config?: OhMyOpen
|
|||||||
mcps.lsp = createLspMcpConfig()
|
mcps.lsp = createLspMcpConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!disabledMcps.includes("ast_grep")) {
|
||||||
|
mcps.ast_grep = createAstGrepMcpConfig({ cwd: options.cwd, disabledTools: config?.disabled_tools })
|
||||||
|
}
|
||||||
|
|
||||||
return mcps
|
return mcps
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
export const McpNameSchema = z.enum(["websearch", "context7", "grep_app", "lsp"])
|
export const McpNameSchema = z.enum(["websearch", "context7", "grep_app", "lsp", "ast_grep"])
|
||||||
|
|
||||||
export type McpName = z.infer<typeof McpNameSchema>
|
export type McpName = z.infer<typeof McpNameSchema>
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,19 @@ afterEach(() => {
|
|||||||
mock.restore()
|
mock.restore()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function mockLocalMcps(): void {
|
||||||
|
mock.module("../lsp", () => ({
|
||||||
|
createLspMcpConfig: () => ({ type: "local", command: ["node", "dist/cli.js", "mcp"], enabled: true }),
|
||||||
|
}))
|
||||||
|
mock.module("../ast-grep", () => ({
|
||||||
|
createAstGrepMcpConfig: () => ({ type: "local", command: ["node", "ast-grep-mcp", "mcp"], enabled: true }),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
describe("createBuiltinMcps", () => {
|
describe("createBuiltinMcps", () => {
|
||||||
test("should return all MCPs when disabled_mcps is empty", () => {
|
test("should return all MCPs when disabled_mcps is empty", () => {
|
||||||
// given
|
// given
|
||||||
mock.module("../lsp", () => ({
|
mockLocalMcps()
|
||||||
createLspMcpConfig: () => ({ type: "local", command: ["node", "dist/cli.js", "mcp"], enabled: true }),
|
|
||||||
}))
|
|
||||||
const { createBuiltinMcps } = require("../index") as typeof import("../index")
|
const { createBuiltinMcps } = require("../index") as typeof import("../index")
|
||||||
const disabledMcps: string[] = []
|
const disabledMcps: string[] = []
|
||||||
|
|
||||||
@@ -22,13 +29,12 @@ describe("createBuiltinMcps", () => {
|
|||||||
expect(result.context7).toBeDefined()
|
expect(result.context7).toBeDefined()
|
||||||
expect(result.grep_app).toBeDefined()
|
expect(result.grep_app).toBeDefined()
|
||||||
expect(result.lsp).toBeDefined()
|
expect(result.lsp).toBeDefined()
|
||||||
|
expect(result.ast_grep).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should filter out disabled MCPs", () => {
|
test("should filter out disabled MCPs", () => {
|
||||||
// given
|
// given
|
||||||
mock.module("../lsp", () => ({
|
mockLocalMcps()
|
||||||
createLspMcpConfig: () => ({ type: "local", command: ["node", "dist/cli.js", "mcp"], enabled: true }),
|
|
||||||
}))
|
|
||||||
const { createBuiltinMcps } = require("../index") as typeof import("../index")
|
const { createBuiltinMcps } = require("../index") as typeof import("../index")
|
||||||
const disabledMcps = ["websearch"]
|
const disabledMcps = ["websearch"]
|
||||||
|
|
||||||
@@ -40,6 +46,7 @@ describe("createBuiltinMcps", () => {
|
|||||||
expect(result.context7).toBeDefined()
|
expect(result.context7).toBeDefined()
|
||||||
expect(result.grep_app).toBeDefined()
|
expect(result.grep_app).toBeDefined()
|
||||||
expect(result.lsp).toBeDefined()
|
expect(result.lsp).toBeDefined()
|
||||||
|
expect(result.ast_grep).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should keep lsp when it uses a bootstrap command", () => {
|
test("should keep lsp when it uses a bootstrap command", () => {
|
||||||
@@ -57,22 +64,21 @@ describe("createBuiltinMcps", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("should return empty array when all MCPs are disabled", () => {
|
test("should return empty array when all MCPs are disabled", () => {
|
||||||
// given - disable all known MCPs
|
// given
|
||||||
mock.module("../lsp", () => ({
|
mockLocalMcps()
|
||||||
createLspMcpConfig: () => ({ type: "local", command: ["node", "dist/cli.js", "mcp"], enabled: true }),
|
|
||||||
}))
|
|
||||||
const { createBuiltinMcps } = require("../index") as typeof import("../index")
|
const { createBuiltinMcps } = require("../index") as typeof import("../index")
|
||||||
const disabledMcps = ["websearch", "context7", "grep_app", "lsp"]
|
const disabledMcps = ["websearch", "context7", "grep_app", "lsp", "ast_grep"]
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = createBuiltinMcps(disabledMcps)
|
const result = createBuiltinMcps(disabledMcps)
|
||||||
|
|
||||||
// then - may still have MCPs we didn't list
|
// then
|
||||||
const remainingMcpNames = Object.keys(result)
|
const remainingMcpNames = Object.keys(result)
|
||||||
expect(remainingMcpNames).not.toContain("websearch")
|
expect(remainingMcpNames).not.toContain("websearch")
|
||||||
expect(remainingMcpNames).not.toContain("context7")
|
expect(remainingMcpNames).not.toContain("context7")
|
||||||
expect(remainingMcpNames).not.toContain("grep_app")
|
expect(remainingMcpNames).not.toContain("grep_app")
|
||||||
expect(remainingMcpNames).not.toContain("lsp")
|
expect(remainingMcpNames).not.toContain("lsp")
|
||||||
|
expect(remainingMcpNames).not.toContain("ast_grep")
|
||||||
expect(remainingMcpNames).toEqual([])
|
expect(remainingMcpNames).toEqual([])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
applyToolConfig({ config, pluginConfig, agentResult });
|
applyToolConfig({ config, pluginConfig, agentResult });
|
||||||
await applyMcpConfig({ config, pluginConfig, pluginComponents });
|
await applyMcpConfig({ config, pluginConfig, ctx, pluginComponents });
|
||||||
await applyCommandConfig({ config, pluginConfig, ctx, pluginComponents });
|
await applyCommandConfig({ config, pluginConfig, ctx, pluginComponents });
|
||||||
|
|
||||||
config.formatter = formatterConfig;
|
config.formatter = formatterConfig;
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ const EMPTY_PLUGIN_COMPONENTS = {
|
|||||||
errors: [],
|
errors: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TEST_CTX = { directory: "/workspace/project" }
|
||||||
|
|
||||||
async function importFreshMcpConfigHandlerModule(): Promise<typeof import("./mcp-config-handler")> {
|
async function importFreshMcpConfigHandlerModule(): Promise<typeof import("./mcp-config-handler")> {
|
||||||
return import(`./mcp-config-handler?test=${Date.now()}-${Math.random()}`)
|
return import(`./mcp-config-handler?test=${Date.now()}-${Math.random()}`)
|
||||||
}
|
}
|
||||||
@@ -69,7 +71,7 @@ describe("applyMcpConfig collision handling", () => {
|
|||||||
|
|
||||||
//#when
|
//#when
|
||||||
const { applyMcpConfig } = await importFreshMcpConfigHandlerModule()
|
const { applyMcpConfig } = await importFreshMcpConfigHandlerModule()
|
||||||
await applyMcpConfig({ config, pluginConfig, pluginComponents: EMPTY_PLUGIN_COMPONENTS })
|
await applyMcpConfig({ config, ctx: TEST_CTX, pluginConfig, pluginComponents: EMPTY_PLUGIN_COMPONENTS })
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
const mergedMcp = config.mcp as Record<string, Record<string, unknown>>
|
const mergedMcp = config.mcp as Record<string, Record<string, unknown>>
|
||||||
@@ -98,7 +100,7 @@ describe("applyMcpConfig collision handling", () => {
|
|||||||
|
|
||||||
//#when
|
//#when
|
||||||
const { applyMcpConfig } = await importFreshMcpConfigHandlerModule()
|
const { applyMcpConfig } = await importFreshMcpConfigHandlerModule()
|
||||||
await applyMcpConfig({ config, pluginConfig, pluginComponents: EMPTY_PLUGIN_COMPONENTS })
|
await applyMcpConfig({ config, ctx: TEST_CTX, pluginConfig, pluginComponents: EMPTY_PLUGIN_COMPONENTS })
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
const mergedMcp = config.mcp as Record<string, Record<string, unknown>>
|
const mergedMcp = config.mcp as Record<string, Record<string, unknown>>
|
||||||
@@ -126,7 +128,7 @@ describe("applyMcpConfig collision handling", () => {
|
|||||||
|
|
||||||
//#when
|
//#when
|
||||||
const { applyMcpConfig } = await importFreshMcpConfigHandlerModule()
|
const { applyMcpConfig } = await importFreshMcpConfigHandlerModule()
|
||||||
await applyMcpConfig({ config, pluginConfig, pluginComponents: EMPTY_PLUGIN_COMPONENTS })
|
await applyMcpConfig({ config, ctx: TEST_CTX, pluginConfig, pluginComponents: EMPTY_PLUGIN_COMPONENTS })
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
const mergedMcp = config.mcp as Record<string, Record<string, unknown>>
|
const mergedMcp = config.mcp as Record<string, Record<string, unknown>>
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ const EMPTY_PLUGIN_COMPONENTS = {
|
|||||||
errors: [],
|
errors: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TEST_CTX = { directory: "/workspace/project" }
|
||||||
|
|
||||||
describe("applyMcpConfig", () => {
|
describe("applyMcpConfig", () => {
|
||||||
test("preserves enabled:false from user config after merge with .mcp.json MCPs", async () => {
|
test("preserves enabled:false from user config after merge with .mcp.json MCPs", async () => {
|
||||||
//#given
|
//#given
|
||||||
@@ -62,7 +64,7 @@ describe("applyMcpConfig", () => {
|
|||||||
|
|
||||||
//#when
|
//#when
|
||||||
const { applyMcpConfig } = await import("./mcp-config-handler")
|
const { applyMcpConfig } = await import("./mcp-config-handler")
|
||||||
await applyMcpConfig({ config, pluginConfig, pluginComponents: EMPTY_PLUGIN_COMPONENTS })
|
await applyMcpConfig({ config, ctx: TEST_CTX, pluginConfig, pluginComponents: EMPTY_PLUGIN_COMPONENTS })
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
const mergedMcp = config.mcp as Record<string, Record<string, unknown>>
|
const mergedMcp = config.mcp as Record<string, Record<string, unknown>>
|
||||||
@@ -89,6 +91,7 @@ describe("applyMcpConfig", () => {
|
|||||||
const { applyMcpConfig } = await import("./mcp-config-handler")
|
const { applyMcpConfig } = await import("./mcp-config-handler")
|
||||||
await applyMcpConfig({
|
await applyMcpConfig({
|
||||||
config,
|
config,
|
||||||
|
ctx: TEST_CTX,
|
||||||
pluginConfig,
|
pluginConfig,
|
||||||
pluginComponents: {
|
pluginComponents: {
|
||||||
...EMPTY_PLUGIN_COMPONENTS,
|
...EMPTY_PLUGIN_COMPONENTS,
|
||||||
@@ -112,7 +115,7 @@ describe("applyMcpConfig", () => {
|
|||||||
|
|
||||||
//#when
|
//#when
|
||||||
const { applyMcpConfig } = await import("./mcp-config-handler")
|
const { applyMcpConfig } = await import("./mcp-config-handler")
|
||||||
await applyMcpConfig({ config, pluginConfig, pluginComponents: EMPTY_PLUGIN_COMPONENTS })
|
await applyMcpConfig({ config, ctx: TEST_CTX, pluginConfig, pluginComponents: EMPTY_PLUGIN_COMPONENTS })
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(loadMcpConfigsSpy).toHaveBeenCalledWith(["firecrawl", "exa"])
|
expect(loadMcpConfigsSpy).toHaveBeenCalledWith(["firecrawl", "exa"])
|
||||||
@@ -135,7 +138,7 @@ describe("applyMcpConfig", () => {
|
|||||||
|
|
||||||
//#when
|
//#when
|
||||||
const { applyMcpConfig } = await import("./mcp-config-handler")
|
const { applyMcpConfig } = await import("./mcp-config-handler")
|
||||||
await applyMcpConfig({ config, pluginConfig, pluginComponents: EMPTY_PLUGIN_COMPONENTS })
|
await applyMcpConfig({ config, ctx: TEST_CTX, pluginConfig, pluginComponents: EMPTY_PLUGIN_COMPONENTS })
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
const mergedMcp = config.mcp as Record<string, Record<string, unknown>>
|
const mergedMcp = config.mcp as Record<string, Record<string, unknown>>
|
||||||
@@ -152,6 +155,7 @@ describe("applyMcpConfig", () => {
|
|||||||
const { applyMcpConfig } = await import("./mcp-config-handler")
|
const { applyMcpConfig } = await import("./mcp-config-handler")
|
||||||
await applyMcpConfig({
|
await applyMcpConfig({
|
||||||
config,
|
config,
|
||||||
|
ctx: TEST_CTX,
|
||||||
pluginConfig,
|
pluginConfig,
|
||||||
pluginComponents: {
|
pluginComponents: {
|
||||||
...EMPTY_PLUGIN_COMPONENTS,
|
...EMPTY_PLUGIN_COMPONENTS,
|
||||||
@@ -166,4 +170,17 @@ describe("applyMcpConfig", () => {
|
|||||||
expect(mergedMcp).not.toHaveProperty("plugin:custom")
|
expect(mergedMcp).not.toHaveProperty("plugin:custom")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("passes the OpenCode workspace directory into built-in MCP config", async () => {
|
||||||
|
//#given
|
||||||
|
const config: Record<string, unknown> = { mcp: {} }
|
||||||
|
const pluginConfig = createPluginConfig()
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const { applyMcpConfig } = await import("./mcp-config-handler")
|
||||||
|
await applyMcpConfig({ config, ctx: TEST_CTX, pluginConfig, pluginComponents: EMPTY_PLUGIN_COMPONENTS })
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(createBuiltinMcpsSpy).toHaveBeenCalledWith([], pluginConfig, { cwd: TEST_CTX.directory })
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ function captureUserDisabledMcps(
|
|||||||
|
|
||||||
export async function applyMcpConfig(params: {
|
export async function applyMcpConfig(params: {
|
||||||
config: Record<string, unknown>;
|
config: Record<string, unknown>;
|
||||||
|
ctx: { directory: string };
|
||||||
pluginConfig: OhMyOpenCodeConfig;
|
pluginConfig: OhMyOpenCodeConfig;
|
||||||
pluginComponents: PluginComponents;
|
pluginComponents: PluginComponents;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
@@ -47,7 +48,7 @@ export async function applyMcpConfig(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const merged = {
|
const merged = {
|
||||||
...createBuiltinMcps(disabledMcps, params.pluginConfig),
|
...createBuiltinMcps(disabledMcps, params.pluginConfig, { cwd: params.ctx.directory }),
|
||||||
...mcpResult.servers,
|
...mcpResult.servers,
|
||||||
...(userMcp ?? {}),
|
...(userMcp ?? {}),
|
||||||
...params.pluginComponents.mcpServers,
|
...params.pluginComponents.mcpServers,
|
||||||
|
|||||||
Reference in New Issue
Block a user