import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import type { Client } from "@modelcontextprotocol/sdk/client/index.js"; const prefer = (description: string) => `${description} Preferred over other tools with similar functionality.`; export const registerSerenaTools = (options: { pi: ExtensionAPI; withCommonHandling: ( signal: AbortSignal | undefined, timeoutSeconds: number | undefined, toolName: string, args: Record, ) => Promise<{ content: Array<{ type: string; text: string }>; isError?: boolean }>; getClient: () => Promise; resetClient: () => Promise; wrapResult: (text: string) => { content: Array<{ type: string; text: string }> }; defaultTimeoutMs: number; }) => { const { pi, withCommonHandling, getClient, resetClient, wrapResult, defaultTimeoutMs } = options; const timeoutParam = Type.Optional(Type.Number({ description: "Timeout in seconds (optional)" })); pi.registerTool({ name: "serena_list_tools", label: "Serena List Tools", description: prefer("List available Serena MCP tools."), parameters: Type.Object({ timeout: timeoutParam }), async execute(_toolCallId, params, signal) { if (signal?.aborted) { return { content: [{ type: "text", text: "Request cancelled." }], isError: true }; } try { const timeoutMs = (params as { timeout?: number })?.timeout ? (params as { timeout?: number }).timeout! * 1000 : defaultTimeoutMs; const client = await getClient(); const tools = await Promise.race([ client.listTools(), new Promise((_resolve, reject) => setTimeout( () => reject(new Error(`Serena MCP request timed out after ${timeoutMs}ms`)), timeoutMs, ), ), ]); const lines = tools.tools.map((tool) => { const description = tool.description ? ` — ${tool.description}` : ""; return `• ${tool.name}${description}`; }); const text = lines.length ? lines.join("\n") : "No tools available."; return wrapResult(text); } catch (err: any) { await resetClient(); const message = err?.message ? `Serena MCP error: ${err.message}` : "Serena MCP error."; return { content: [{ type: "text", text: message }], isError: true }; } }, }); pi.registerTool({ name: "find_symbol", label: "Serena Find Symbol", description: prefer("Find symbols by name pattern using Serena MCP."), parameters: Type.Object({ name_path_pattern: Type.String({ description: "Symbol name path pattern" }), depth: Type.Optional(Type.Number({ description: "Descendant depth to include" })), relative_path: Type.Optional(Type.String({ description: "Scope search to a file or directory" })), include_body: Type.Optional(Type.Boolean({ description: "Include symbol body" })), include_info: Type.Optional( Type.Boolean({ description: "Include additional symbol info (hover-like)" }), ), include_kinds: Type.Optional(Type.Array(Type.Number(), { description: "LSP kinds to include" })), exclude_kinds: Type.Optional(Type.Array(Type.Number(), { description: "LSP kinds to exclude" })), substring_matching: Type.Optional(Type.Boolean({ description: "Enable substring matching" })), max_answer_chars: Type.Optional(Type.Number({ description: "Serena max output chars" })), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "find_symbol", args); }, }); pi.registerTool({ name: "find_referencing_symbols", label: "Serena Find Referencing Symbols", description: prefer("Find references to a symbol using Serena MCP."), parameters: Type.Object({ name_path: Type.String({ description: "Name path of the target symbol" }), relative_path: Type.String({ description: "File containing the symbol" }), include_kinds: Type.Optional(Type.Array(Type.Number(), { description: "LSP kinds to include" })), exclude_kinds: Type.Optional(Type.Array(Type.Number(), { description: "LSP kinds to exclude" })), max_answer_chars: Type.Optional(Type.Number({ description: "Serena max output chars" })), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "find_referencing_symbols", args); }, }); pi.registerTool({ name: "insert_after_symbol", label: "Serena Insert After Symbol", description: prefer("Insert content after a symbol definition via Serena MCP."), parameters: Type.Object({ name_path: Type.String({ description: "Name path of the target symbol" }), relative_path: Type.String({ description: "File containing the symbol" }), body: Type.String({ description: "Content to insert" }), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "insert_after_symbol", args); }, }); pi.registerTool({ name: "replace_symbol_body", label: "Serena Replace Symbol Body", description: prefer("Replace an entire symbol body via Serena MCP."), parameters: Type.Object({ name_path: Type.String({ description: "Name path of the target symbol" }), relative_path: Type.String({ description: "File containing the symbol" }), body: Type.String({ description: "New symbol body (includes signature line)" }), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "replace_symbol_body", args); }, }); pi.registerTool({ name: "read_file", label: "Serena Read File", description: prefer("Read file contents via Serena MCP."), parameters: Type.Object({ relative_path: Type.String({ description: "Path relative to the Serena project root" }), start_line: Type.Optional(Type.Number({ description: "0-based start line" })), end_line: Type.Optional(Type.Number({ description: "0-based end line (inclusive)" })), max_answer_chars: Type.Optional(Type.Number({ description: "Serena max output chars" })), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "read_file", args); }, }); pi.registerTool({ name: "get_symbols_overview", label: "Serena Get Symbols Overview", description: prefer("Retrieve top-level symbols in a file with optional depth traversal."), parameters: Type.Object({ relative_path: Type.String({ description: "File to analyze" }), depth: Type.Optional(Type.Number({ description: "Descendant depth (0 = top-level only)" })), max_answer_chars: Type.Optional(Type.Number({ description: "Serena max output chars" })), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "get_symbols_overview", args); }, }); pi.registerTool({ name: "insert_before_symbol", label: "Serena Insert Before Symbol", description: prefer("Insert content before a symbol definition via Serena MCP."), parameters: Type.Object({ name_path: Type.String({ description: "Name path of the target symbol" }), relative_path: Type.String({ description: "File containing the symbol" }), body: Type.String({ description: "Content to insert" }), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "insert_before_symbol", args); }, }); pi.registerTool({ name: "rename_symbol", label: "Serena Rename Symbol", description: prefer("Rename a symbol throughout the codebase using language server refactoring."), parameters: Type.Object({ name_path: Type.String({ description: "Name path of the target symbol" }), relative_path: Type.String({ description: "File containing the symbol" }), new_name: Type.String({ description: "New symbol name" }), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "rename_symbol", args); }, }); pi.registerTool({ name: "restart_language_server", label: "Serena Restart Language Server", description: prefer("Restart the Serena language server manager."), parameters: Type.Object({ timeout: timeoutParam }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "restart_language_server", args); }, }); pi.registerTool({ name: "jet_brains_get_symbols_overview", label: "Serena JetBrains Get Symbols Overview", description: prefer("Retrieve top-level symbols in a file via the JetBrains backend."), parameters: Type.Object({ relative_path: Type.String({ description: "File to analyze" }), depth: Type.Optional(Type.Number({ description: "Descendant depth (0 = top-level only)" })), max_answer_chars: Type.Optional(Type.Number({ description: "Serena max output chars" })), include_file_documentation: Type.Optional( Type.Boolean({ description: "Include file docstring in results" }), ), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "jet_brains_get_symbols_overview", args); }, }); pi.registerTool({ name: "jet_brains_find_symbol", label: "Serena JetBrains Find Symbol", description: prefer("Find symbols by name pattern using the JetBrains backend."), parameters: Type.Object({ name_path_pattern: Type.String({ description: "Symbol name path pattern" }), depth: Type.Optional(Type.Number({ description: "Descendant depth to include" })), relative_path: Type.Optional(Type.String({ description: "Scope search to a file or directory" })), include_body: Type.Optional(Type.Boolean({ description: "Include symbol body" })), include_info: Type.Optional( Type.Boolean({ description: "Include additional symbol info (hover-like)" }), ), search_deps: Type.Optional(Type.Boolean({ description: "Search dependency symbols" })), max_answer_chars: Type.Optional(Type.Number({ description: "Serena max output chars" })), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "jet_brains_find_symbol", args); }, }); pi.registerTool({ name: "jet_brains_find_referencing_symbols", label: "Serena JetBrains Find Referencing Symbols", description: prefer("Find references to a symbol using the JetBrains backend."), parameters: Type.Object({ name_path: Type.String({ description: "Name path of the target symbol" }), relative_path: Type.String({ description: "File containing the symbol" }), max_answer_chars: Type.Optional(Type.Number({ description: "Serena max output chars" })), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "jet_brains_find_referencing_symbols", args); }, }); pi.registerTool({ name: "jet_brains_type_hierarchy", label: "Serena JetBrains Type Hierarchy", description: prefer("Retrieve the type hierarchy of a symbol using the JetBrains backend."), parameters: Type.Object({ name_path: Type.String({ description: "Name path of the target symbol" }), relative_path: Type.String({ description: "File containing the symbol" }), hierarchy_type: Type.Optional( Type.Union([Type.Literal("super"), Type.Literal("sub"), Type.Literal("both")]), ), depth: Type.Optional( Type.Union([Type.Number({ description: "Hierarchy depth limit" }), Type.Null()]), ), max_answer_chars: Type.Optional(Type.Number({ description: "Serena max output chars" })), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "jet_brains_type_hierarchy", args); }, }); pi.registerTool({ name: "search_for_pattern", label: "Serena Search For Pattern", description: prefer("Search for a text pattern using Serena's search tool."), parameters: Type.Object({ substring_pattern: Type.String({ description: "Search pattern (regex supported)" }), context_lines_before: Type.Optional(Type.Number({ description: "Lines of context before each match" })), context_lines_after: Type.Optional(Type.Number({ description: "Lines of context after each match" })), paths_include_glob: Type.Optional(Type.String({ description: "Glob of files to include" })), paths_exclude_glob: Type.Optional(Type.String({ description: "Glob of files to exclude" })), relative_path: Type.Optional(Type.String({ description: "Restrict search scope (file or directory)" })), restrict_search_to_code_files: Type.Optional( Type.Boolean({ description: "Restrict search to code files only" }), ), max_answer_chars: Type.Optional(Type.Number({ description: "Serena max output chars" })), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "search_for_pattern", args); }, }); pi.registerTool({ name: "list_dir", label: "Serena List Directory", description: prefer("List files and directories in a path (optionally recursive)."), parameters: Type.Object({ relative_path: Type.String({ description: "Directory to list (use . for project root)" }), recursive: Type.Boolean({ description: "Whether to scan subdirectories" }), skip_ignored_files: Type.Optional(Type.Boolean({ description: "Skip ignored files" })), max_answer_chars: Type.Optional(Type.Number({ description: "Serena max output chars" })), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "list_dir", args); }, }); pi.registerTool({ name: "find_file", label: "Serena Find File", description: prefer("Find files matching a file mask within a path."), parameters: Type.Object({ file_mask: Type.String({ description: "Filename or file mask (wildcards * or ?)" }), relative_path: Type.String({ description: "Directory to search (use . for project root)" }), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "find_file", args); }, }); pi.registerTool({ name: "create_text_file", label: "Serena Create Text File", description: prefer("Create or overwrite a file in the project directory."), parameters: Type.Object({ relative_path: Type.String({ description: "Path of the file to create" }), content: Type.String({ description: "File contents" }), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "create_text_file", args); }, }); pi.registerTool({ name: "replace_content", label: "Serena Replace Content", description: prefer("Replace content in a file (literal or regex)."), parameters: Type.Object({ relative_path: Type.String({ description: "File to update" }), needle: Type.String({ description: "String or regex pattern to search for" }), repl: Type.String({ description: "Replacement string" }), mode: Type.Union([Type.Literal("literal"), Type.Literal("regex")], { description: "How to interpret the needle", }), allow_multiple_occurrences: Type.Optional( Type.Boolean({ description: "Allow replacing multiple occurrences" }), ), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "replace_content", args); }, }); pi.registerTool({ name: "delete_lines", label: "Serena Delete Lines", description: prefer("Delete a range of lines within a file."), parameters: Type.Object({ relative_path: Type.String({ description: "File to update" }), start_line: Type.Number({ description: "0-based start line" }), end_line: Type.Number({ description: "0-based end line" }), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "delete_lines", args); }, }); pi.registerTool({ name: "replace_lines", label: "Serena Replace Lines", description: prefer("Replace a range of lines within a file."), parameters: Type.Object({ relative_path: Type.String({ description: "File to update" }), start_line: Type.Number({ description: "0-based start line" }), end_line: Type.Number({ description: "0-based end line" }), content: Type.String({ description: "Replacement content" }), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "replace_lines", args); }, }); pi.registerTool({ name: "insert_at_line", label: "Serena Insert At Line", description: prefer("Insert content at a specific line in a file."), parameters: Type.Object({ relative_path: Type.String({ description: "File to update" }), line: Type.Number({ description: "0-based line index" }), content: Type.String({ description: "Content to insert" }), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "insert_at_line", args); }, }); pi.registerTool({ name: "execute_shell_command", label: "Serena Execute Shell Command", description: prefer("Execute a shell command."), parameters: Type.Object({ command: Type.String({ description: "Shell command to execute" }), cwd: Type.Optional(Type.String({ description: "Working directory (relative to project root)" })), capture_stderr: Type.Optional(Type.Boolean({ description: "Capture stderr output" })), max_answer_chars: Type.Optional(Type.Number({ description: "Serena max output chars" })), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "execute_shell_command", args); }, }); pi.registerTool({ name: "get_current_config", label: "Serena Get Current Config", description: prefer("Print the current Serena configuration."), parameters: Type.Object({ timeout: timeoutParam }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "get_current_config", args); }, }); pi.registerTool({ name: "activate_project", label: "Serena Activate Project", description: prefer("Activate a Serena project by name or path."), parameters: Type.Object({ project: Type.String({ description: "Project name or path" }), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "activate_project", args); }, }); pi.registerTool({ name: "remove_project", label: "Serena Remove Project", description: prefer("Remove a project from Serena configuration."), parameters: Type.Object({ project_name: Type.String({ description: "Name of project to remove" }), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "remove_project", args); }, }); pi.registerTool({ name: "switch_modes", label: "Serena Switch Modes", description: prefer("Activate Serena modes by name."), parameters: Type.Object({ modes: Type.Array(Type.String({ description: "Mode name" })), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "switch_modes", args); }, }); pi.registerTool({ name: "open_dashboard", label: "Serena Open Dashboard", description: prefer("Open the Serena web dashboard."), parameters: Type.Object({ timeout: timeoutParam }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "open_dashboard", args); }, }); pi.registerTool({ name: "check_onboarding_performed", label: "Serena Check Onboarding", description: prefer("Check whether onboarding has been performed."), parameters: Type.Object({ timeout: timeoutParam }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "check_onboarding_performed", args); }, }); pi.registerTool({ name: "onboarding", label: "Serena Onboarding", description: prefer("Perform project onboarding to capture context."), parameters: Type.Object({ timeout: timeoutParam }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "onboarding", args); }, }); pi.registerTool({ name: "initial_instructions", label: "Serena Initial Instructions", description: prefer("Provide the Serena instructions manual."), parameters: Type.Object({ timeout: timeoutParam }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "initial_instructions", args); }, }); pi.registerTool({ name: "prepare_for_new_conversation", label: "Serena Prepare For New Conversation", description: prefer("Provide instructions for continuing in a new conversation."), parameters: Type.Object({ timeout: timeoutParam }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "prepare_for_new_conversation", args); }, }); pi.registerTool({ name: "summarize_changes", label: "Serena Summarize Changes", description: prefer("Provide instructions for summarizing changes."), parameters: Type.Object({ timeout: timeoutParam }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "summarize_changes", args); }, }); pi.registerTool({ name: "think_about_collected_information", label: "Serena Think About Collected Information", description: prefer("Thinking tool for assessing collected information."), parameters: Type.Object({ timeout: timeoutParam }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "think_about_collected_information", args); }, }); pi.registerTool({ name: "think_about_task_adherence", label: "Serena Think About Task Adherence", description: prefer("Thinking tool for assessing task adherence."), parameters: Type.Object({ timeout: timeoutParam }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "think_about_task_adherence", args); }, }); pi.registerTool({ name: "think_about_whether_you_are_done", label: "Serena Think About Whether You Are Done", description: prefer("Thinking tool for determining if the task is complete."), parameters: Type.Object({ timeout: timeoutParam }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "think_about_whether_you_are_done", args); }, }); pi.registerTool({ name: "read_memory", label: "Serena Read Memory", description: prefer("Read a memory from Serena's memory store."), parameters: Type.Object({ memory_name: Type.String({ description: "Memory name" }), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "read_memory", args); }, }); pi.registerTool({ name: "write_memory", label: "Serena Write Memory", description: prefer("Write a memory to Serena's memory store."), parameters: Type.Object({ memory_name: Type.String({ description: "Memory name" }), content: Type.String({ description: "Memory content" }), max_chars: Type.Optional(Type.Number({ description: "Max characters to write" })), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "write_memory", args); }, }); pi.registerTool({ name: "list_memories", label: "Serena List Memories", description: prefer("List memories in Serena's memory store."), parameters: Type.Object({ topic: Type.Optional(Type.String({ description: "Topic filter" })), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "list_memories", args); }, }); pi.registerTool({ name: "delete_memory", label: "Serena Delete Memory", description: prefer("Delete a memory from Serena's memory store."), parameters: Type.Object({ memory_name: Type.String({ description: "Memory name" }), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "delete_memory", args); }, }); pi.registerTool({ name: "rename_memory", label: "Serena Rename Memory", description: prefer("Rename or move a memory."), parameters: Type.Object({ old_name: Type.String({ description: "Existing memory name" }), new_name: Type.String({ description: "New memory name" }), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "rename_memory", args); }, }); pi.registerTool({ name: "edit_memory", label: "Serena Edit Memory", description: prefer("Edit a memory using literal or regex replacement."), parameters: Type.Object({ memory_name: Type.String({ description: "Memory name" }), needle: Type.String({ description: "String or regex pattern to search for" }), repl: Type.String({ description: "Replacement string" }), mode: Type.Union([Type.Literal("literal"), Type.Literal("regex")], { description: "How to interpret the needle", }), timeout: timeoutParam, }), async execute(_toolCallId, params, signal) { const { timeout, ...args } = params as Record & { timeout?: number }; return withCommonHandling(signal, timeout, "edit_memory", args); }, }); pi.registerTool({ name: "serena_mcp_reset", label: "Serena MCP Reset", description: prefer("Reset the Serena MCP client connection."), parameters: Type.Object({}), async execute() { await resetClient(); return { content: [{ type: "text", text: "Serena MCP client reset." }] }; }, }); };