-
Notifications
You must be signed in to change notification settings - Fork 168
chore: use the mcp for codex to search #66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,18 @@ | ||
| import { join, normalize } from "node:path"; | ||
| import { Server } from "@modelcontextprotocol/sdk/server/index.js"; | ||
| import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; | ||
| import { | ||
| CallToolRequestSchema, | ||
| 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(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Interactive prompts corrupt MCP server communication streamWhen |
||
| 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); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<string, unknown>); | ||
| await execAsync("droid mcp add mgrep -- mgrep mcp", { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Droid installation missing
|
||
| 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<string, unknown>); | ||
| } | ||
| } 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") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Missing required query parameter in search tool schema
The MCP search tool schema marks only
pathas required but notquery. When clients call the search tool without providing aqueryparameter,args?.query as stringevaluates toundefinedand gets passed tostore.search(), resulting in a search with an undefined query. Thequeryfield is the essential search term and must be included in therequiredarray.