Home / Articles

Serena MCP Server: Symbol-Aware Code Editing for AI Assistants

Set up Serena with Docker and SSE so Claude Code, VS Code and OpenCode get LSP-backed symbol search, safe renames and persistent memory with fewer tokens.

Publisher: REACTAPP.TOP9 min read2020 words

This article is published in English.

Sources: Serena MCP: Giving Your AI Coding Tools an IDE Brain

Serena is an open-source MCP server that gives AI coding assistants an IDE-style understanding of a codebase. Tried on a TypeScript monorepo, the effect is noticeable: the assistant gets smarter about the code and spends fewer tokens doing so.

Where a typical assistant sees files full of text, Serena plugs into a Language Server Protocol backend, the engine behind Go to Definition, Find All References and safe renames in an editor, and publishes those features as MCP tools for any compatible client. Everything runs on the local machine, 30-plus languages are covered, and the supported clients include Claude Code, VS Code, Cursor, OpenCode and Claude Desktop.

Ask an assistant to find every function that handles authentication and the usual routine is:

  1. Run a text search with grep or ripgrep for the word "authentication".
  2. Read the matching files, sometimes whole, pouring thousands of lines into context.
  3. Return every line containing the string, comments, string literals and unrelated hits included.

That's fine for a small project and breaks down as the codebase grows. Four everyday tasks show the gap:

+----------------------------+-------------------------------------+--------------------------------------------+
| Task                       | Without Serena                      | With Serena                                |
+============================+=====================================+============================================+
| **Semantic search**        | Text match on "auth" - returns      | Returns `authenticateUser()`,              |
| "find auth functions"      | false positives, misses functions   | `login()`, `verifyCredentials()`           |
|                            | named `verifyCredentials`           | with file locations and line numbers       |
+----------------------------+-------------------------------------+--------------------------------------------+
| **Go to definition**       | Searches files for "User" and       | Jumps directly to the `User`               |
| "show me the User schema"  | "schema" - returns every reference  | class/interface definition with            |
|                            |                                     | full import tree                           |
+----------------------------+-------------------------------------+--------------------------------------------+
| **Find references**        | Text search for "PaymentProcessor"  | Returns all usages with context:           |
| "where is                  | - misses dynamic usages             | imports, instantiations, method calls      |
| PaymentProcessor used?"    |                                     |                                            |
+----------------------------+-------------------------------------+--------------------------------------------+
| **Cross-file refactoring** | Text search and replace - misses    | Semantic rename via LSP - updates          |
| "rename UserService        | string interpolations or aliased    | every reference correctly across           |
| to AccountService"         | imports, breaks things              | the entire codebase                        |
+----------------------------+-------------------------------------+--------------------------------------------+

Token cost compounds the problem. Without semantic tools, the assistant reads entire files to understand structure and then carries that text in context. With Serena, a single find_symbol("UserService") call returns just what's needed.

Working at the symbol level

Serena indexes a project through a real language server and builds a relational map: which functions call which, where types are defined and what imports what. When the assistant must understand or change code, it reaches for these tools in place of raw file reads and writes:

  • find_symbol locates a symbol by name and can return its full body.
  • get_symbols_overview maps the symbols in a file or directory without reading the source.
  • find_referencing_symbols lists every call site and usage across the codebase.
  • rename_symbol renames through the LSP, updating each reference.
  • replace_symbol_body swaps a function or class body and leaves surrounding code alone.
  • insert_before_symbol and insert_after_symbol add code at a precise semantic position.

The LLM doesn't edit line by line and risk losing a reference somewhere. It hands the edit to Serena, which works from indexed data and the language server, so edits are reference-aware. A symbol used in 20 places gets updated in all 20. The project docs list every language-server tool.

Memory and dashboard

Each project also gets a persistent memory store. As the assistant learns conventions, gotchas and architecture decisions, it can save them as notes. They survive conversation resets and are available next session, so work doesn't begin from zero.

A web dashboard at http://localhost:24282/dashboard/index.html is on by default and shows what Serena is doing:

  • Session overview and active projects
  • A tool-call log with statistics on which tools ran, how often and for how long
  • Live server logs
  • A config editor, so settings change without a restart
  • A memory manager for viewing and editing accumulated project notes
  • Server controls

On macOS and Windows there's also a native system-tray wrapper, while Linux is browser-only.

Contexts decide the tool list

Predefined contexts control which tools get exposed. The reasoning is that a client may already handle some things natively, such as file reading or shell execution, so Serena skips duplicating them and keeps the tool list tidy.

+---------------------+------------------------------+------------------------------------------------+
| Context             | Designed for                 | What it does                                   |
+=====================+==============================+================================================+
| `desktop-app`       | Claude Desktop, general use  | **Full toolset** - everything Serena offers.   |
|                     |                              | Use this when the client has no built-in       |
|                     |                              | coding capabilities. This is also the right    |
|                     |                              | choice for a shared Docker instance serving    |
|                     |                              | multiple different clients.                    |
+---------------------+------------------------------+------------------------------------------------+
| `claude-code`       | Claude Code                  | Disables tools that overlap with Claude        |
|                     |                              | Code's built-in capabilities (file edits,      |
|                     |                              | shell commands, etc.) to avoid conflicts.      |
|                     |                              | Single-project context.                        |
+---------------------+------------------------------+------------------------------------------------+
| `ide`               | VS Code, Cursor, Cline, Kilo | Generic IDE augmentation - focuses on          |
|                     |                              | semantic tools, assumes the IDE already        |
|                     |                              | handles basic file operations.                 |
|                     |                              | Single-project context.                        |
+---------------------+------------------------------+------------------------------------------------+
| `agent`             | Agno, autonomous agents      | Broader autonomy for agents that drive the     |
|                     |                              | full workflow independently.                   |
+---------------------+------------------------------+------------------------------------------------+
| `codex`             | OpenAI Codex                 | Optimized for Codex's tool calling format.     |
+---------------------+------------------------------+------------------------------------------------+
| NOTE: The `claude-code` and `ide` contexts are **single-project**: when you pass a project          |
| path at startup, those contexts lock down to only the tools relevant to that project and            |
| disable the project-switching tool entirely (since you won't need it).                              |
+---------------------+------------------------------+------------------------------------------------+

For one Docker container serving Claude Code, VS Code and OpenCode together, use desktop-app, the superset. When Serena runs locally per client, pick the context matching that tool.

Installing

Use uv. Serena's own documentation warns against MCP marketplaces, which tend to ship outdated commands:

uv tool install -p 3.13 serena-agent@latest --prerelease=allow
serena init

That installs the serena CLI globally and starts the setup wizard. Clients are configured next.

Claude Code, global (recommended):

claude mcp add --scope user serena -- serena start-mcp-server \
  --context claude-code --project-from-cwdlaude mcp add serena -- serena start-mcp-server --context claude-code --project "$(pwd)"

Claude Code, per project:

claude mcp add serena -- serena start-mcp-server --context claude-code --project "$(pwd)"

VS Code, in .vscode/mcp.json at the project root:

{
  "servers": {
    "serena": {
      "type": "stdio",
      "command": "serena",
      "args": [
        "start-mcp-server",
        "--context", "ide",
        "--project", "${workspaceFolder}"
      ]
    }
  }
}

Claude Desktop, in ~/.config/claude_desktop_config.json on Linux or ~/Library/Application Support/Claude/claude_desktop_config.json on macOS:

{
  "mcpServers": {
    "serena": {
      "command": "serena",
      "args": ["start-mcp-server", "--context", "desktop-app"]
    }
  }
}

Running in Docker

Docker is the approach Serena's docs favor for isolation, especially when an agent is allowed to make edits. In a container it can only see the directories mounted into it. A compose.yml entry:

services:
  serena:
    image: ghcr.io/oraios/serena:latest
    container_name: myproject-serena
    restart: unless-stopped
    environment:
      - SERENA_DOCKER=1
    ports:
      - "10121:9121"   # SSE endpoint
      - "34282:24282"  # Web dashboard
    volumes:
      - .:/workspace/myproject
    command: >
      serena start-mcp-server
        --transport sse
        --port 9121
        --host 0.0.0.0
        --context desktop-app
        --project /workspace/myproject

Notes on that configuration:

  • --transport sse has to be set inside Docker, because stdio can only work when the client launches Serena itself as a child process.
  • --context desktop-app exposes the largest tool set and suits Claude Code, VS Code extensions and OpenCode alike. Narrower contexts like claude-code switch off tools that other clients rely on.
  • --project /workspace/myproject has to be the container-internal path where the code is mounted. Avoid --project-from-cwd here, because the container's working directory is Serena's install location and not the project.
  • Unusual host ports (10121 and 34282) let several projects each run their own Serena without collisions.

With SERENA_DOCKER=1 set, Serena knows it's inside a container. The global ~/.serena/serena_config.yml, mounted in or baked into the setup, should carry container-friendly settings:

gui_log_window: false
web_dashboard_listen_address: "0.0.0.0"
web_dashboard_open_on_launch: false

To mount the host config, add a volume mapping ~/.serena:/root/.serena. Start the service:

docker compose up -d serena

Connecting clients to the container

Every client talks to the same SSE endpoint, and only the config format differs. Claude Code can register it globally:

claude mcp add serena --transport sse --url http://localhost:10121/sse

Or the project can carry a .mcp.json:

{
  "mcpServers": {
    "serena": {
      "type": "sse",
      "url": "http://localhost:10121/sse"
    }
  }
}

VS Code takes a .vscode/mcp.json:

{
  "servers": {
    "serena": {
      "type": "sse",
      "url": "http://localhost:10121/sse"
    }
  }
}

OpenCode uses opencode.jsonc:

{
  "mcp": {
    "serena": {
      "type": "remote",
      "url": "http://localhost:10121/sse",
      "enabled": true
    }
  }
}

Codex, Kilo Code and other MCP-compatible clients work likewise.

Project configuration

Serena reads .serena/project.yml in the project root. A practical TypeScript setup starts with the name:

project_name: "myproject"

Then languages and ignore rules:

languages:
  - typescript   # uses typescript-language-serverencoding: "utf-8"
ignore_all_files_in_gitignore: trueignored_paths:
  - "node_modules"
  - "dist"
  - "build"
  - "coverage"
  - ".next"
  - "out"
  - ".cache"

The ignored_paths list is a safety net on top of .gitignore. node_modules alone can hold over 80,000 files, and indexing them makes startup painfully slow and clutters symbol search with third-party internals.

What to commit and what to ignore:

.serena/project.yml      ← commit this (shared config)
.serena/memories/        ← commit this (AI-generated project notes, useful for everyone)
.serena/cache/           ← gitignore (rebuilt per machine)
.serena/project.local.yml ← gitignore (per-developer overrides)

What changes in practice

On a Deno 2 and TypeScript monorepo with a frontend, an API and shared packages, four things stand out:

  • Cross-package resolution. With the whole monorepo mounted as one workspace, Serena traces a type from packages/ into its usages under apps/api/ in one step, where the assistant used to read file after file and stitch things together.
  • Safer refactors. A task touching around 20 files, renaming a shared interface, went through rename_symbol and missed nothing. Text search-and-replace always leaves one dynamic usage hiding inside a template string.
  • Natural-language questions. Asking to explain the auth flow returns the relevant symbols with context in place of the assistant wandering through files. The answer arrives faster, more accurately and with less context used.
  • Token savings. Hard to measure exactly but real. Finding a definition means calling find_symbol and getting the body directly, and edits stay out of context since Serena does the writing.

Makefile shortcuts make the workflow a habit:

make serena-up      # Start the Serena container
make serena-stop    # Stop it
make serena-logs    # Tail logs
make serena-index   # Force re-index after big changes
make serena-health  # Health check the workspace

Verdict

Reasoning and code generation are the strengths of AI coding tools. Moving around a large codebase is a weaker spot. A language server made for exactly that job lifts both answer quality and cost. Teams that spend lots of context on file reads and still get sloppy refactors across big repositories will find an afternoon of setup worthwhile. Docker keeps it neat: Serena runs next to the rest of the stack, retains its index and offers one SSE endpoint to every client on the team.