01 / My articles / Published
I scored my own MCP server
Everyone audits other people's servers. I measured mine, and a good score did not mean a closed door.
Written on
In May 2026, the NSA published a guidance sheet on the security of MCP servers. In July, the Cloud Security Alliance noted that the main development environments automatically launch the MCP servers declared in a project, with the developer's rights and without process isolation.
My first reflex was to wonder which of the servers I use were affected. It was the wrong reflex. I write one myself, and it runs in production on my server.
The first measurement
systemd-analyze security gives a service an exposure score, from zero to ten,
by reading the confinement directives it declares. The lower the score, the
better. My MCP server, the one that queries my training data, scores
9.2.
Overall exposure level: 9.2 UNSAFE
No hardening directive at all. It runs under my agent's main account, with full access to the file system, no memory limit, no system call filter. I had written it carefully on the code side, and launched it without any precaution on the system side.
For comparison, I measured a service I had written that same week while paying attention, with a dynamic user, a protected file system and capped memory. It scores 8.1.
Overall exposure level: 8.1 EXPOSED
Eight out of ten while applying good practice. The score is harsh, and it is right to be: a service that keeps network access, no system call filter and all the default capabilities is still a wide surface, however clean it is otherwise.
The second measurement, which wiped the smile off my face
An exposure score reads what a service declares. It never checks what a process can reach. I wanted to close that gap with a probe: a small program that tries to open a connection to the internet, read my agent's API keys, and write outside its scope. I ran it twice: without confinement, then inside a transient service declaring everything it should, private network, home directory read-only, protected file system, isolated temporary space.
Without confinement:
RESEAU ATTEINT
CONFIG AGENT LUE
ECRITURE /tmp REUSSIE
With confinement:
RESEAU ATTEINT
CONFIG AGENT LUE
ECRITURE /tmp REUSSIE
Strictly identical. The confinement had confined nothing. (The probe prints in French: network reached, agent config read, write to /tmp succeeded.)
What the log said, and that nobody reads
The explanation had been there from the start, in the system log, on a line I had no reason to look for:
PrivateNetwork=yes is configured, but the kernel does not support
or we lack privileges for network namespace, proceeding without.
"Proceeding without". It warns, then carries on without the protection. The command succeeded, the service started, the exit code was zero, and the requested protection did not exist.
It is a question of privileges: a user session manager cannot create a network namespace. The same directives applied by the administrator work, and the probe then reports an unreachable network. So the confinement is correct; the rights were what was missing.
I tried the other route, the one that in theory requires no privilege:
bubblewrap, the isolation tool behind Flatpak. Immediate refusal:
bwrap: setting up uid map: Permission denied
Recent versions of Ubuntu restrict unprivileged user namespaces, through a setting that is on by default. With it lifted, bubblewrap does the job perfectly: network cut, keys outright nonexistent since the home directory is replaced by an empty file system. But lifting that setting requires the administrator, and it protects against real attacks.
What it means
On a recent machine, an unprivileged user cannot confine a process without the administrator stepping in first. Neither through systemd, nor through bubblewrap.
Yet that is exactly the situation the Cloud Security Alliance describes: a developer clones a repository, the repository ships an MCP configuration, and their environment launches the declared servers with the developer's own rights. They are not the administrator of their work machine, and the tool that promised them a sandbox without privileges would hand them a sandbox that does not exist.
So the worst case is not the absence of confinement. It is confinement that is declared and silently dropped, because it produces exactly the same configuration file, the same absence of error, and a confidence with no foundation.
What it looks like once fixed
On this server, I am the administrator, so the privilege problem does not exist: it is enough to make it a properly hardened system service. I wrote an override file rather than rewriting the unit, and the paths open for writing are not guessed: they are taken from the file descriptors the process actually had open.
The score goes from 9.2 to 1.5.
avant 9.2 UNSAFE
apres 1.5 OK
Ingestion still works, the Garmin sync resumed without a single error, and the
proof does not rest on the score. It rests on what the running process shows of
itself: no capabilities, no_new_privs set, a mount namespace separate from the
system's, a system call filter in place.
The detour that teaches the most
My first version scored 1.6 and looked finished to me. Then I ran my probe again inside that confinement:
LU <configuration de l'agent, avec ses clés>
LU <un fichier de mémoire de l'agent>
refuse ecriture hors perimetre
An excellent score, and the MCP server could still read my agent's API keys and its memory. I had made the home directory read-only, which prevents writing, not reading. And to steal a key, reading is enough.
The fix is to mask the home directory with an empty file system and re-expose only the launcher, then to make the agent's memory folders explicitly inaccessible: they are none of a sports ingestion service's business. Logs go to the system journal, which removes the last need to write there. Same probe, afterwards:
refuse <configuration de l'agent> (FileNotFoundError)
refuse <fichier de mémoire> (PermissionError)
ECRIT sa base, normal
RESEAU garmin joignable, normal
The configuration file does not return a permission error: it simply no longer exists for this process.
The score gain between 1.6 and 1.5 is negligible. The real gain is the difference between a server that can read my keys and a server that cannot. A good score is not a closed door.
And this time, the failure is loud
Along the way, I left an inconsistency: a path allowed for writing that no longer existed once the home directory was masked. The service refused to start.
Failed to set up mount namespacing: No such file or directory
Main process exited, code=exited, status=226/NAMESPACE
It is the exact opposite of the unprivileged behaviour described above. At the system level, systemd fails closed: no protection, no start. At the user level, it starts without the protection and writes a line in a log. The same request, two opposite philosophies, and only the first one is defensible.
What I am writing next
A tool for the part that bothered me most: checking what an MCP server can actually reach, by launching a probe under the same startup conditions rather than rereading the declared directives. Without ever running the server itself, so that auditing a hostile server does not mean handing it control.
It is published: See the code of the tool that measures what a server can reach. Two of its tests are negative controls, and they are what it stands on: a probe launched under real confinement must report the network as unreachable, and without confinement must report it reachable. A tool that gave the same answer everywhere would pass its whole suite while being useless.
The lesson, beyond MCP
A protection you have never seen refuse something does not exist.
I had written the right directives. I had never seen them block anything. Between the two lies precisely the difference between a configuration and a guarantee.
What I could not verify
Whether the behaviour is the same on other distributions: my measurements cover only one, in a recent version. Ubuntu's namespace setting is specific to it, even though systemd's privilege limitation is not.
Nor have I audited the MCP servers I use without having written them. That is next on my list.
Sources
- NSA guidance on MCP security, "Model Context Protocol (MCP): Security Design Considerations for AI-Driven Automation", published in May 2026.
- Cloud Security Alliance note on automatic execution, research note of July 1, 2026, on automatic execution by development environments.
The measurements on my own server are mine, reproducible with the commands quoted. Service names and paths have been removed from the outputs shown.
Scope of the measurements
9.2 out of 10. Exposure score of my MCP server before hardening, as reported by systemd's analysis tool. The lower, the better.
8.1 out of 10. Exposure score of another of my services, written with the usual good practice, recorded at the same time.
1.6 out of 10. Score of the first hardened version, which could still read the agent's keys.
1.5 out of 10. Score of the final version, where the agent's keys and memory are out of reach.