Securing a Local MCP Server: What Actually Needs Protecting
For a local, stdio-based MCP server like open-context's, the network isn't the attack surface —
the filesystem is. What actually needs protecting is the connection string sitting in
~/.opencontext/config.json, the context store it points to, and, if you run the
bundled HTTP server, the port that server listens on. Lock down those three things and a local
MCP server carries roughly the same risk as any other CLI tool running under your own account.
Key takeaways
- MCP over stdio is a local subprocess launched by Claude Desktop or Claude Code — there's no network listener and nothing to authenticate across, because there's no network boundary in the first place.
- The one real secret is your database connection string. open-context writes it to
~/.opencontext/config.jsonwith 0600 permissions and redacts it everywhere else viaredactDsn(). - Switching the context store from the default JSON file to a remote database changes the threat model from "who has shell on this machine" to "who can reach this network endpoint."
- open-context's Express HTTP server (the REST API and web UI) has no authentication of its own — fine on
localhost, not fine bound to a shared or public network. - The MCP protocol doesn't mandate authentication for local transports by design; the security guidance that matters mostly targets remote, HTTP-based MCP deployments, a different shape of server entirely.
Why "it runs locally" isn't the same as "it's already secure"
It's easy to hear "local MCP server" and file it under solved problems — no public endpoint, no internet exposure, nothing for an attacker to scan. That's mostly true, and it's exactly why MCP's stdio transport doesn't build in authentication: Claude Desktop or Claude Code spawns the server as a child process and talks to it over stdin/stdout, the same trust boundary as any other program you run. There's no socket for a stranger to connect to.
But "local" narrows the threat model, it doesn't erase it. Two questions are still worth asking for any MCP server you actually run: what does it write to disk, and does anything about it ever touch a network — even one you control?
What open-context actually stores, and where
By default, everything lives under ~/.opencontext/ (or /root/.opencontext/
inside the Docker image, on a mounted volume):
| File | Contents |
|---|---|
contexts.json | The default JSON context store — every memory Claude has saved via MCP. |
preferences.json / .md | Your structured and generated Claude preferences. |
memory.md | Generated factual context about you, for pasting into Claude's memory. |
config.json | The saved database connection string, if you've switched off the default JSON store. |
Of these, contexts.json and the preference files are only as sensitive as the
conversations you've imported — worth protecting, but not fundamentally different from any other
personal document on your machine. config.json is the one that's structurally
different, because it can contain a database password.
The connection string is the one secret that matters
open-context is BYODB — it can store context in a plain JSON
file or in any of fifteen supported databases, addressed by a connection string like
postgres://user:pass@host:5432/opencontext. That string is the single piece of data
in the whole system that's genuinely a credential, and the codebase treats it that way in three
specific ways:
- Restrictive file permissions.
writeDatabaseUrl()writesconfig.jsonwith mode0600— owner read/write only — and the containing directory with0700, so no other local account can read it. - Redaction everywhere it's displayed.
redactDsn()masks the password in the userinfo segment and any secret-looking query parameter (token,apiKey,authToken, and similar) before a connection string reaches a log line, an API response, or the settings UI. The raw string is documented in the code as "never log this" and is only ever handed to the actual database driver. - A canonical form drivers must use. Internally, every adapter is passed
dsn.canonicalrather thandsn.raw, which matters more than it sounds: aliases likerediss://ormongodb+srv://carry TLS and DNS-resolution meaning that a careless normalization step could silently strip, quietly turning off encryption a user thought was on.
There's a smaller example of the same instinct in the SQL Server driver: TLS is on by default, and
TLS forbids a bare IP address as the SNI server name — so mssql://…@10.0.0.5:1433/db
fails loudly instead of connecting unencrypted. The fix is to address the server by hostname, or
opt out explicitly with ?encrypt=false on a trusted local network. The failure is
annoying in the moment; it's also the secure default doing its job.
Local file store vs. a real database: different threat models
Staying on the default JSON file (or moving to SQLite) keeps the entire threat model on one machine: whoever can read your user account's files can read your context store, and that's it. There's no separate credential to leak, because there's no network hop.
Pointing OPENCONTEXT_DB_URL at Postgres, MySQL, MongoDB, or any of the other remote
backends changes that. Now a network endpoint exists, and the questions that come with any
database deployment apply: is the connection encrypted (?sslmode=require for
Postgres, rediss:// instead of redis://), is the database user scoped to
only the opencontext schema rather than a shared admin account, and is the port
reachable from anywhere it doesn't need to be. None of this is opencontext-specific — it's the
same checklist you'd run for any application database — but switching backends is the moment to
actually run it, not skip it because "it's just my memory store."
The HTTP server has no authentication of its own
This is the part worth being explicit about: open-context's Express server, which serves the
React UI and the REST API on port 3000, ships with no login, no API key, and no CORS restriction
in front of it. That's a reasonable default for a tool meant to run on localhost or
behind a Docker port mapping you control — but some of its endpoints are meaningfully powerful.
PUT /api/db/config changes which database the entire store reads and writes from;
GET /api/contexts returns every memory Claude has saved. Neither checks who's asking.
If you run the Docker image on a box other people can reach — a home server on a LAN with other
devices, a shared VM, anything multi-tenant — don't publish port 3000 directly. Bind it to
127.0.0.1 and tunnel in, or put a reverse proxy with its own authentication in front
of it. The MCP stdio channel Claude actually talks to doesn't have this problem; the web dashboard
does, and it's worth treating differently. See our
guide to running the MCP server in Docker
for the full container and volume layout.
A minimal checklist
- Leave
~/.opencontext/at its default location and permissions — don't relax the0600/0700modes onconfig.jsonor its directory. - If you switch to a remote database, use a connection string with TLS enabled and a database user that can only touch the opencontext schema.
- Never paste a raw connection string into a shared log, ticket, or chat — use the redacted form the UI and CLI already show you.
- Bind the HTTP/REST server to
localhostunless you've put real authentication in front of it. - Treat the Docker named volume (
opencontext-data) like any other volume holding credentials — back it up carefully, don't mount it into containers that don't need it.
Run your own MCP memory server the way it's meant to run — local, self-hosted, and yours.
Get started with open-context →FAQ
Does a local MCP server need its own authentication?
No. A stdio-based MCP server like open-context's is launched as a subprocess by Claude Desktop or Claude Code and talks to it over stdin/stdout — there is no network socket to authenticate against. The protocol only requires authentication for remote, HTTP-based MCP servers, which are a different deployment shape entirely.
Is it safe to put a database password in an MCP connection string?
It's as safe as any other credential on your machine, provided it's handled the way open-context handles it: written to ~/.opencontext/config.json with 0600 (owner-only) permissions, and stripped out of every log line, API response, and UI field by redactDsn() before it's ever displayed.
Should I expose open-context's HTTP server beyond localhost?
Not without putting something in front of it. The Express REST API ships with no built-in authentication, and endpoints like PUT /api/db/config can change which database the server talks to. Bind it to localhost, or put it behind a reverse proxy that adds its own auth, before exposing it on a shared network.
Does moving from the JSON store to Postgres change what I need to secure?
Yes. A local JSON or SQLite file is only as exposed as your machine's filesystem permissions. A remote database adds a network endpoint, so TLS, firewall rules, and a database user scoped to just the opencontext schema all start to matter in a way they didn't before.
Are there known vulnerabilities specific to MCP servers?
Most publicly reported MCP security issues are about remote, HTTP-based servers missing authentication or over-trusting tool input from the model — not about local stdio servers, which have a much smaller attack surface by construction. See the official MCP security best practices documentation for the current guidance.