{"record":{"id":"a9b2b3cc0c67a433","repo":"oraios/serena","slug":"tool-req-tool-name-is-not-read-only-and-cannot","errorCode":null,"errorMessage":"Tool '{req.tool_name}' is not read-only and cannot be executed via the query_project route","messagePattern":"Tool '(.+?)' is not read-only and cannot be executed via the query_project route","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/serena/project_server.py","lineNumber":133,"sourceCode":"                project.create_language_server_manager()\n\n            with self._loaded_projects_lock:\n                self._loaded_projects_by_root[key] = project\n            return project\n\n    def _query_project(self, req: QueryProjectRequest) -> str:\n        \"\"\"Handle a /query_project request by invoking the agent on the specified project and tool.\n\n        The active project is process-wide state, whereas ``apply_ex`` runs the tool on the\n        agent's task executor thread. Without the lock, a second request entering\n        ``active_project_context`` while the first request's tool is still executing would\n        redirect that tool to the wrong project (and restore the wrong project afterwards).\n        \"\"\"\n        project = self._get_project(req.project_name)\n        with self._active_project_lock, self._agent.active_project_context(project):\n            tool = self._agent.get_tool_by_name(req.tool_name)\n            if not tool.is_readonly():\n                raise ValueError(f\"Tool '{req.tool_name}' is not read-only and cannot be executed via the query_project route\")\n            params = json.loads(req.tool_params_json)\n            return tool.apply_ex(**params)\n\n    def run(self) -> None:\n        \"\"\"\n        Run the server on the given host and port.\n        \"\"\"\n        from flask import cli\n\n        # suppress the default Flask startup banner\n        # ty cannot model reassigning a third-party module's function attribute (it rejects any\n        # replacement, even one with an identical signature), so the monkeypatch is suppressed here\n        cli.show_server_banner = lambda *args, **kwargs: None  # ty: ignore[invalid-assignment]\n\n        self._app.run(host=self._host, port=self._port, debug=False, use_reloader=False, threaded=True)\n\n\nclass ProjectServerClient:","sourceCodeStart":115,"sourceCodeEnd":151,"githubUrl":"https://github.com/oraios/serena/blob/7fcbca7e62555ec2287ddb2f083caee805848ea6/src/serena/project_server.py#L115-L151","documentation":"ProjectServer's query_project route is restricted to read-only tools. Before applying the requested tool it checks tool.is_readonly(), and mutating tools (editors, shell executors, etc.) are rejected to keep the server side-effect free for concurrent queries.","triggerScenarios":"Calling query_project with tool_name set to a write tool such as replace_symbol_body, insert_after_symbol, delete_symbol, or execute_shell_command; passing a tool whose readonly flag is misconfigured.","commonSituations":"Reusing client code that worked against the full agent toolset but now targets the read-only ProjectServer; attempting quick fixes through the query endpoint instead of the interactive agent.","solutions":["Use a read-only tool for query_project (find_symbol, find_referencing_symbols, etc.)","Apply edits through the interactive Serena agent/session instead of the ProjectServer query route","If you own the tool, ensure is_readonly() correctly reflects its behavior rather than weakening this check"],"exampleFix":"// before\nserver.query_project(\"my-repo\", \"replace_symbol_body\", params)\n// after\nserver.query_project(\"my-repo\", \"find_symbol\", params)  # read-only only","handlingStrategy":"validation","validationCode":"tool = agent.get_tool_by_name(tool_name)\nif not tool.is_readonly():\n    raise ValueError(f\"{tool_name} must be run via the interactive agent, not query_project\")","typeGuard":"def is_readonly_tool(agent, tool_name: str) -> bool:\n    return agent.get_tool_by_name(tool_name).is_readonly()","tryCatchPattern":"try:\n    return server.query_project(project, tool_name, params)\nexcept ValueError as e:\n    if \"not read-only\" in str(e):\n        return run_via_interactive_agent(project, tool_name, params)\n    raise","preventionTips":["Keep a whitelist of read-only tool names for query_project clients","Route all mutating operations through the interactive agent/session","Don't override is_readonly() on custom tools unless they truly have no side effects"],"tags":["serena","read-only","tool-permissions"],"backgroundTag":"tool-not-allowed","analyzedSha":"7fcbca7e62555ec2287ddb2f083caee805848ea6","analyzedAt":"2026-08-29T00:04:09.619Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}