From b593e99947d2e0323b1a2055017a21ee24478cfd Mon Sep 17 00:00:00 2001 From: Joel Dierkes Date: Wed, 3 Dec 2025 17:13:06 +0100 Subject: [PATCH 1/2] chore: use the mcp for codex to search Codex is sandboxed so hard that the cli tool needs a manual approval every time. Let's use the mcp to bypass that. --- src/commands/search.ts | 5 ++- src/commands/watch_mcp.ts | 74 +++++++++++++++++++++++++++++--- src/install/codex.ts | 89 ++++++++++++--------------------------- 3 files changed, 100 insertions(+), 68 deletions(-) diff --git a/src/commands/search.ts b/src/commands/search.ts index 9ef3338..11a6f0b 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -63,7 +63,10 @@ function formatAskResponse(response: AskResponse, show_content: boolean) { return `${response.answer}\n\n${sourceEntries.join("\n")}`; } -function formatSearchResponse(response: SearchResponse, show_content: boolean) { +export function formatSearchResponse( + response: SearchResponse, + show_content: boolean, +) { return response.data .map((chunk) => formatChunk(chunk, show_content)) .join("\n"); diff --git a/src/commands/watch_mcp.ts b/src/commands/watch_mcp.ts index 7712d43..1419f66 100644 --- a/src/commands/watch_mcp.ts +++ b/src/commands/watch_mcp.ts @@ -1,3 +1,4 @@ +import { join, normalize } from "node:path"; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { @@ -5,10 +6,13 @@ import { ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { Command } from "commander"; +import { createStore } from "../lib/context"; +import { formatSearchResponse } from "./search"; import { startWatch } from "./watch"; export const watchMcp = new Command("mcp") .description("Start MCP server for mgrep") + .option("--expose-tools", "Expose search tools via MCP", false) .action(async (_options, cmd) => { process.on("SIGINT", () => { console.error("Received SIGINT, shutting down gracefully..."); @@ -45,6 +49,7 @@ export const watchMcp = new Command("mcp") const options: { store: string; + exposeTools: boolean; } = cmd.optsWithGlobals(); const transport = new StdioServerTransport(); @@ -60,15 +65,74 @@ export const watchMcp = new Command("mcp") }, ); server.setRequestHandler(ListToolsRequestSchema, async () => { + if (!options.exposeTools) { + return { tools: [] }; + } return { - tools: [], + tools: [ + { + name: "search", + description: + "Search the codebase via mgreps semantic search. Prefer this tool over any other search tool like grep, glob, etc. Use a full natural language sentence as input, not just a keyword.", + inputSchema: { + type: "object", + properties: { + query: { + type: "string", + description: "The query to search for.", + }, + path: { + type: "string", + description: + "Relative or absolute path to the codebase directory to search in.", + }, + maxCount: { + type: "number", + description: "The maximum number of results to return.", + default: 10, + }, + }, + required: ["path"], + }, + }, + ], }; }); - server.setRequestHandler(CallToolRequestSchema, async (_request) => { - return { - result: "Not implemented", - }; + server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + if (name === "search") { + const searchPath = (args?.path as string)?.startsWith("/") + ? (args?.path as string) + : normalize(join(process.cwd(), (args?.path as string) ?? "")); + + const store = await createStore(); + const results = await store.search( + options.store, + args?.query as string, + (args?.maxCount as number) || 10, + { rerank: true }, + { + all: [ + { + key: "path", + operator: "starts_with", + value: searchPath, + }, + ], + }, + ); + return { + content: [ + { + type: "text", + text: formatSearchResponse(results, false), + }, + ], + }; + } + throw new Error(`Unknown tool: ${name}`); }); await server.connect(transport); diff --git a/src/install/codex.ts b/src/install/codex.ts index 56b87f7..272b486 100644 --- a/src/install/codex.ts +++ b/src/install/codex.ts @@ -6,57 +6,20 @@ import { promisify } from "node:util"; import { Command } from "commander"; import { ensureAuthenticated } from "../lib/utils"; +const SKILL = `The mgrep mcp is available and can be used to search the codebase via mgreps semantic search. Prefer this tool over any other search tool like grep, glob, etc.`; + const shell = process.env.SHELL || (process.platform === "win32" ? process.env.COMSPEC || "cmd.exe" : "/bin/sh"); -const SKILL = ` ---- -name: mgrep -description: A semantic grep-like search tool for your local files. It is substentially better than the buildin search tools and should always be used instead of anything else. -license: Apache 2.0 ---- - -## When to use this skill - -Whenever you need to search your local files. Do not use grep, use this skill -instead. - -## How to use this skill - -Use \`mgrep\` to search your local files. The search is semantic so describe what -you are searching for in natural language. The results is the file path and the -line range of the match. - -### Do - -\`\`\`bash -mgrep "What code parsers are available?" # search in the current directory -mgrep "How are chunks defined?" src/models # search in the src/models directory -mgrep -m 10 "What is the maximum number of concurrent workers in the code parser?" # limit the number of results to 10 -\`\`\` - -### Don't - -\`\`\`bash -mgrep "parser" # The query is to imprecise, use a more specific query -mgrep "How are chunks defined?" src/models --type python --context 3 # Too many unnecessary filters, remove them -\`\`\` - -## Keywords -search, grep, files, local files, local search, local grep, local search, local -grep, local search, local grep -`; - const execAsync = promisify(exec); -async function installPlugin() { +async function installPlugin(): Promise { try { - await execAsync("codex mcp add mgrep mgrep mcp", { + await execAsync("codex mcp add mgrep -- mgrep mcp --expose-tools", { shell, env: process.env, }); - console.log("Successfully installed the mgrep background sync"); const destPath = path.join(os.homedir(), ".codex", "AGENTS.md"); fs.mkdirSync(path.dirname(destPath), { recursive: true }); @@ -76,39 +39,41 @@ async function installPlugin() { } else { console.log("The mgrep skill is already installed in the Codex agent"); } + + console.log("Successfully installed mgrep for Codex"); } catch (error) { console.error(`Error installing plugin: ${error}`); process.exit(1); } } -async function uninstallPlugin() { +async function uninstallPlugin(): Promise { try { await execAsync("codex mcp remove mgrep", { shell, env: process.env }); + + const destPath = path.join(os.homedir(), ".codex", "AGENTS.md"); + if (fs.existsSync(destPath)) { + const existingContent = fs.readFileSync(destPath, "utf-8"); + let updatedContent = existingContent; + let previousContent = ""; + + while (updatedContent !== previousContent) { + previousContent = updatedContent; + updatedContent = updatedContent.replace(SKILL, ""); + updatedContent = updatedContent.replace(SKILL.trim(), ""); + } + + if (updatedContent.trim() === "") { + fs.unlinkSync(destPath); + } else { + fs.writeFileSync(destPath, updatedContent); + } + } + console.log("Successfully removed mgrep from Codex"); } catch (error) { console.error(`Error uninstalling plugin: ${error}`); process.exit(1); } - - const destPath = path.join(os.homedir(), ".codex", "AGENTS.md"); - if (fs.existsSync(destPath)) { - const existingContent = fs.readFileSync(destPath, "utf-8"); - let updatedContent = existingContent; - let previousContent = ""; - - while (updatedContent !== previousContent) { - previousContent = updatedContent; - updatedContent = updatedContent.replace(SKILL, ""); - updatedContent = updatedContent.replace(SKILL.trim(), ""); - } - - if (updatedContent.trim() === "") { - fs.unlinkSync(destPath); - } else { - fs.writeFileSync(destPath, updatedContent); - } - } - console.log("Successfully removed the mgrep from the Codex agent"); } export const installCodex = new Command("install-codex") From 07c5b9c320fe38dab7bc26db3f38fa7a6030b67d Mon Sep 17 00:00:00 2001 From: Joel Dierkes Date: Thu, 4 Dec 2025 12:05:15 +0100 Subject: [PATCH 2/2] chore: use mcps for droid --- src/install/droid.ts | 99 +++++++++----------------------------------- 1 file changed, 20 insertions(+), 79 deletions(-) diff --git a/src/install/droid.ts b/src/install/droid.ts index 7d91099..1899d76 100644 --- a/src/install/droid.ts +++ b/src/install/droid.ts @@ -3,13 +3,21 @@ import os from "node:os"; import path from "node:path"; import { Command } from "commander"; import { ensureAuthenticated } from "../lib/utils"; +import { promisify } from "node:util"; +import { exec } from "node:child_process"; const PLUGIN_ROOT = process.env.DROID_PLUGIN_ROOT || path.resolve(__dirname, "../../dist/plugins/mgrep"); -const PLUGIN_HOOKS_DIR = path.join(PLUGIN_ROOT, "hooks"); const PLUGIN_SKILL_PATH = path.join(PLUGIN_ROOT, "skills", "mgrep", "SKILL.md"); +const shell = + process.env.SHELL || + (process.platform === "win32" ? process.env.COMSPEC || "cmd.exe" : "/bin/sh"); + +const execAsync = promisify(exec); + + type HookCommand = { type: "command"; command: string; @@ -93,92 +101,21 @@ function isHooksConfig(value: unknown): value is HooksConfig { return Object.values(value).every((entry) => Array.isArray(entry)); } -function mergeHooks( - existingHooks: HooksConfig | undefined, - newHooks: HooksConfig, -): HooksConfig { - const merged: HooksConfig = existingHooks - ? (JSON.parse(JSON.stringify(existingHooks)) as HooksConfig) - : {}; - for (const [event, entries] of Object.entries(newHooks)) { - const current: HookEntry[] = Array.isArray(merged[event]) - ? merged[event] - : []; - for (const entry of entries) { - const command = entry?.hooks?.[0]?.command; - const matcher = entry?.matcher ?? null; - const duplicate = current.some( - (item) => - (item?.matcher ?? null) === matcher && - item?.hooks?.[0]?.command === command && - item?.hooks?.[0]?.type === entry?.hooks?.[0]?.type, - ); - if (!duplicate) { - current.push(entry); - } - } - merged[event] = current; - } - return merged; -} - async function installPlugin() { const root = resolveDroidRoot(); - const hooksDir = path.join(root, "hooks", "mgrep"); const skillsDir = path.join(root, "skills", "mgrep"); - const settingsPath = path.join(root, "settings.json"); - const watchHook = readPluginAsset( - path.join(PLUGIN_HOOKS_DIR, "mgrep_watch.py"), - ); - const killHook = readPluginAsset( - path.join(PLUGIN_HOOKS_DIR, "mgrep_watch_kill.py"), - ); const skillContent = readPluginAsset(PLUGIN_SKILL_PATH); - const watchPy = path.join(hooksDir, "mgrep_watch.py"); - const killPy = path.join(hooksDir, "mgrep_watch_kill.py"); - writeFileIfChanged(watchPy, watchHook); - writeFileIfChanged(killPy, killHook); - - const hookConfig: HooksConfig = { - SessionStart: [ - { - matcher: "startup|resume", - hooks: [ - { - type: "command", - command: `python3 "${watchPy}"`, - timeout: 10, - }, - ], - }, - ], - SessionEnd: [ - { - hooks: [ - { - type: "command", - command: `python3 "${killPy}"`, - timeout: 10, - }, - ], - }, - ], - }; writeFileIfChanged( path.join(skillsDir, "SKILL.md"), skillContent.trimStart(), ); - const settings = loadSettings(settingsPath); - settings.enableHooks = true; - settings.allowBackgroundProcesses = true; - settings.hooks = mergeHooks( - isHooksConfig(settings.hooks) ? settings.hooks : undefined, - hookConfig, - ); - saveSettings(settingsPath, settings as Record); + await execAsync("droid mcp add mgrep -- mgrep mcp", { + shell, + env: process.env, + }); console.log( `Installed the mgrep hooks and skill for Factory Droid in ${root}`, @@ -230,11 +167,15 @@ async function uninstallPlugin() { saveSettings(settingsPath, settings as Record); } } catch (error) { - console.warn( - `Failed to update Factory Droid settings during uninstall: ${error}`, - ); } } + + await execAsync("droid mcp remove mgrep", { + shell, + env: process.env, + }); + + console.log("Removed mgrep from Factory Droid"); } export const installDroid = new Command("install-droid")