An MCP server for your local development environment
Giving an assistant shell access to your machine is easy and reckless. The Model Context Protocol offers something narrower: a defined set of operations, with a boundary you control.
The problem with "just give it a terminal"
The fastest way to let an assistant manage your dev environment is to hand it a shell. It works immediately and it is the wrong shape for the job.
A shell has no vocabulary. The assistant has to know your exact layout, guess at config file paths, and parse human-readable output that changes between versions. Every operation is free-form text, so there is no way to say "you may read but not write" — the same command that lists databases can drop one.
And the results are invisible to your tools. If the assistant edits a config file directly, the application managing that file has no idea; its in-memory state and the file on disk quietly disagree.
What MCP actually is
Model Context Protocol is a JSON-RPC interface between an assistant and a program that exposes capabilities. The program publishes a list of tools — each with a name, a description and a typed schema for its arguments — and the assistant calls them by name.
Three properties follow from that, and they are the entire reason to prefer it over a shell:
- The vocabulary is explicit.
list_domainseither exists or it does not. There is nothing to discover by trial and error. - Arguments are typed and validated before anything runs, so a malformed call fails at the boundary rather than halfway through.
- The surface is finite. Whatever is not a tool cannot be done — not "should not", cannot.
Why the server belongs inside the app
An MCP server for a dev environment can be a standalone process that talks to the same services. That is the obvious design and it reintroduces the problem it was meant to solve: two programs managing the same configuration, disagreeing whenever one changes something.
Running the server inside the application that already owns that state avoids it. Tools call the same code paths the buttons call. There is no separate implementation to drift, and the window updates the moment a tool runs, because it is the same object graph.
The cost is a dependency. If the app is closed there is no endpoint. That is worth stating plainly rather than papering over — the fix is to have the app start with your session, not to move the server out of it.
Scoped permissions matter more than tool count
"23 tools" tells you nothing about safety. What matters is whether you can decide, per area, what the assistant is allowed to do — and whether that decision is enforced in more than one place.
A workable model groups tools into scopes and gives each scope three levels:
| Scope | No access | Read | Read + write |
|---|---|---|---|
| Domains | invisible | list | create, update, enable, start apps |
| Services | invisible | status, health | start, stop, restart |
| Databases | invisible | list, read-only SQL | create, dump, restore, write SQL |
| Logs | invisible | read | — |
Two details make this real rather than decorative.
Disallowed tools are never sent. Filtering the tools/list response means the assistant does not know the capability exists, so it cannot decide to try it, and cannot mention it to you as an option.
The call is refused as well. List filtering alone is not a security boundary — a client that hardcoded a tool name would sail past it. The permission has to be checked again when the call arrives.
Read-only by default, especially for SQL
The most dangerous tool in a dev environment is arbitrary SQL, and the naive implementation checks the first word. That fails immediately:
-- Starts with WITH, deletes everything
WITH doomed AS (DELETE FROM users RETURNING *) SELECT * FROM doomed;
-- Starts with SELECT, reads a file off disk
SELECT LOAD_FILE('/etc/passwd');
A prefix check passes both. The statement body has to be inspected for data-modifying commands and for functions that reach the filesystem — LOAD_FILE, pg_read_file, lo_import, INTO OUTFILE.
Getting it strict without breaking honest queries is the harder half. These are all legitimate reads and must still pass:
SELECT 'delete me' AS note; -- dangerous word, inside a string
SHOW CHARACTER SET; -- contains SET
SELECT updated_at FROM orders; -- contains UPDATE as a substring
Writing has to stay possible — but behind an explicit flag and the write permission, so it takes two deliberate acts rather than one careless one.
Keep it on loopback, off by default
A local dev server has no reason to accept connections from the network. Binding to 127.0.0.1 rather than 0.0.0.0 is the difference between a tool your assistant uses and a service anyone on the café wifi can reach.
Off by default matters for the same reason: a capability nobody enabled cannot be abused. Checking the Origin header on incoming requests closes the remaining gap, where a web page you visit tries to reach the port from your own browser.
Tell the assistant how to use it
A tool list is not documentation. Names and schemas say what exists, not when to reach for it or what order operations belong in. That gap is what skill files fill: prose the client loads alongside the tools, describing workflows and constraints.
The useful content is the part a schema cannot express — take a backup before importing a dump; confirm destructive SQL with the user first; do not guess a database name, list them; if a tool is missing it is a permission setting, not a reason to fall back to the shell.
How BRAMPP does it
BRAMPP publishes an MCP server from inside the running app on 127.0.0.1, off until you enable it. It exposes 23 tools across the five scopes above, with the two-layer permission model and the SQL rules described here, and ships two skill files so the assistant knows how to use them.
Free and open source under the MIT license — the server, the permission checks and the SQL filter are all in the repository if you want to see exactly where the boundaries are.
← Back to BRAMPP