safe-dev-tools
safe-dev-mcp
A minimal MCP server that gives an editor LLM a fixed set of abilities:
lint, test, git status, git diff, git log, git show.
Requirements
Python 3.11 or newer (the config parser is the standard library tomllib).
On macOS the built-in python3 is 3.9, which is too old for both this server
and the mcp package. If pip install mcp reports "No matching distribution
found", that is the cause. Install a current Python with Homebrew:
brew install python@3.12One-time setup
Create a virtualenv inside this clone from the newer interpreter and install the MCP SDK:
cd /path/to/safe-dev-mcp
/opt/homebrew/bin/python3.12 -m venv .venv
.venv/bin/pip install mcpRegister the server once at Claude Code user scope. The Claude agent in Zed is Claude Code running over the Agent Client Protocol, so it reads this config:
claude mcp add --scope user safe-dev-tools -- \
/path/to/safe-dev-mcp/.venv/bin/python \
/path/to/safe-dev-mcp/server.pyIf you prefer to configure it in Zed itself, add the same command to
~/.config/zed/settings.json. Zed forwards these to external agents:
{
"context_servers": {
"safe-dev-tools": {
"command": "/path/to/safe-dev-mcp/.venv/bin/python",
"args": ["/path/to/safe-dev-mcp/server.py"]
}
}
}Then, in the agent's permissions, allow this server's tools and leave general terminal access denied.
Per-repo configuration
In each repo where you want lint or test tools:
cp /path/to/safe-dev-mcp/safe-dev.example.toml .safe-dev.toml
echo .safe-dev.toml >> .git/info/excludeEdit .safe-dev.toml for that repo. Commands are grouped into targets. A
target is a part of the repo with its own lint and test commands. Every key
is optional, and a tool is only offered when at least one target defines it.
Single-surface repos
Top-level lint and test define one target that owns every path:
lint = ["ruff", "check", "."]
test = ["pytest", "-q"]Monorepos
Use one [[targets]] table per surface instead:
[[targets]]
name = "frontend"
path_prefix = "apps/frontend"
cwd = "apps/frontend"
lint = ["yarn", "lint"]
test = ["yarn", "test"]
[[targets]]
name = "backend"
path_prefix = "apps/backend"
cwd = "apps/backend"
test = ["yarn", "test"]
[[targets]]
name = "e2e"
path_prefix = "e2e"
test = ["yarn", "test:e2e"]
timeout_seconds = 600Key | Where | Effect |
| target | Label used in results. Required |
| target | Repo-relative directory this target owns. Omit to own every path |
| target | Repo-relative directory the commands run from. Default is the repo root |
| top level or target | Command for |
| top level or target | Command for |
| top level or target | Per-command timeout. Default 60. Target value overrides top level |
| top level or target | Output truncation limit. Default 20000. Target value overrides top level |
Top-level keys must appear before the first [[targets]] table, which is a
TOML rule.
How the tools behave with targets:
run_tests(path)runs only the target whosepath_prefixowns that path. When several match, the longest prefix wins. If none match, the tool returns an error naming the configured targets rather than guessing.The scoped path is rewritten relative to the target's
cwdbefore it is appended, and dropped entirely when it is thecwditself. This matters for workspace scripts:yarn workspace backend testruns Vitest insideapps/backend, so a repo-relative path would be doubled. Setcwdto the workspace and call the script directly, as in the example above.run_tests()with no path runs every target that has atestcommand and returns a summary line followed by a section per target.run_lint()runs every target that has alintcommand, skips the rest, and reports per target the same way.The tool descriptions shown to the model list the target names and prefixes, so it knows which paths route where.
Repos that only need the read-only git tools need no config file at all.
The tool list is fixed when the server starts, so restart the agent thread after editing the config.
How the server finds the repo
First match wins:
SAFE_DEV_REPO_ROOTenvironment variable, for manual testing.CLAUDE_PROJECT_DIR, which Claude Code sets for every MCP server it launches. User-scope servers run with~/.claudeas their working directory, so this is what makes a global registration work per repo.Walk up from the current working directory until a directory containing
.gitor.safe-dev.tomlis found.The current working directory.
Testing it standalone
The MCP Inspector lets you see the tool list and call tools by hand. Launch it from inside the repo you want to test so the server resolves that repo as its root:
cd /path/to/some/repo
npx @modelcontextprotocol/inspector \
/path/to/safe-dev-mcp/.venv/bin/python \
/path/to/safe-dev-mcp/server.pyThe SDK's mcp dev shortcut is not used here because it always launches the
server through uv, which fails if uv is not installed.
Troubleshooting
On startup the server writes two lines to stderr: the repo root it resolved and the tools it registered. Run it directly from inside a repo to see them, then press Ctrl-C:
cd /path/to/some/repo
/path/to/safe-dev-mcp/.venv/bin/python /path/to/safe-dev-mcp/server.pyA malformed lint or test value (for example a plain string instead of a
list) is reported on stderr and that tool is left out. The server still
starts.
Extending it
To add another safe capability (git blame, a formatter check, a type
checker), add a plain function following the same pattern and register it in
_register_tools, conditionally if it depends on config. Fixed command list,
validated and bounded parameters, no shell string concatenation. Resist the
temptation to add a generic "run this git subcommand" tool. Enumerate the ones
you actually want.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/Neighborkid01/safe-dev-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server