Operations #
The two deployment shapes #
stdio, launched by an editor. The normal case. Your client spawns the process, speaks newline-delimited JSON over the pipes, and kills it when the session ends. There is nothing to deploy: install the package and point a client at it.
HTTP, in a container. For a shared or remote deployment. Adds authentication, an origin check and health probes, and needs a reverse proxy for TLS.
Running it over stdio #
{
"mcpServers": {
"devserver": {
"command": "uvx",
"args": ["--from", "mcp-devserver", "mcp-devserver", "serve", "--transport", "stdio"],
"env": {
"MCP_SANDBOX__WORKSPACE": "/absolute/path/to/your/repository",
"MCP_OBSERVABILITY__LOG_LEVEL": "warning"
}
}
}
}
Before wiring it up, check what it would serve:
MCP_SANDBOX__WORKSPACE=/absolute/path mcp-devserver doctor
Then check it speaks the protocol:
MCP_SANDBOX__WORKSPACE=/absolute/path \
mcp-devserver conform --stdio "mcp-devserver serve --transport stdio" --strict
Running it over HTTP #
export MCP_WORKSPACE=/absolute/path/to/your/repository
export MCP_HTTP__BEARER_TOKEN="$(openssl rand -hex 32)"
docker compose up --build -d
curl -s localhost:8080/readyz | python -m json.tool
Or directly:
docker run --rm -p 127.0.0.1:8080:8080 \
-v "$PWD:/workspace:ro" \
-e MCP_HTTP__BEARER_TOKEN="$TOKEN" \
ghcr.io/kogunlowo123/mcp-developer-server:latest
The image serves HTTP by default, binds 0.0.0.0 internally — a loopback bind
inside a container is unreachable from a published port — and reads
/workspace. What limits exposure is the port mapping, not the bind address.
Other subcommands still work, because the entry point is the binary and the command is the argument:
docker run --rm -v "$PWD:/workspace:ro" mcp-devserver:local doctor
docker run --rm -v "$PWD:/workspace:ro" mcp-devserver:local tools
Probes #
| Endpoint | Meaning | Unauthenticated |
|---|---|---|
GET /healthz |
The process is up and can serialise a response | Yes |
GET /readyz |
The workspace still resolves; returns 503 if not |
Yes |
Both answer without a token deliberately: a liveness probe that needs a credential reports the wrong thing when the credential is wrong.
{
"status": "ready",
"workspace_present": true,
"tools": 7,
"protocol_versions": ["2026-07-28"]
}
Logs #
JSON on stderr. Never stdout — on stdio, stdout is the protocol wire, and a log line written there corrupts the stream for a reason the client cannot diagnose. Third-party records are routed through the same chain, so a library warning arrives as JSON on stderr rather than as prose on stdout.
Events worth alerting on:
| Event | Meaning |
|---|---|
rpc.error with code: -32022 |
A client is speaking a revision this server does not serve |
rpc.error with code: -32602 |
Malformed requests; a burst suggests a broken client |
rpc.unhandled |
An unexpected failure. The traceback is here and never in the response |
tool.hard_timeout |
A handler blocked past its own deadline check |
http.origin_rejected |
A browser origin was refused — worth reading, this is the rebinding defence firing |
MCP_OBSERVABILITY__LOG_TOOL_ARGUMENTS is off by default. Paths are not secrets
but they are the developer's private directory structure, and a log attached to
a bug report should not carry it.
What to watch #
Denial rate. A steady trickle of outside_workspace is a client with a bad
path model. A sudden burst is worth looking at.
Redaction counts. structuredContent.redaction.count non-zero means a
credential is sitting in the workspace. The redaction did its job; the file
still needs fixing.
High-risk content assessments. content_assessment.level == "high" means a
file in the repository contains text written to steer a model. That is a finding
about the repository, not about this server.
Timeouts. Frequent timed_out results mean the workspace is larger than the
budget. Raise MCP_SANDBOX__TOOL_TIMEOUT_SECONDS, or narrow the workspace.
Upgrading a protocol revision #
src/mcp_devserver/protocol/spec.py is a transcription of the specification and
nothing else, so a revision is a diff against one file plus whatever the
conformance suite then reports. The order to work in:
- Update
PROTOCOL_VERSIONandSUPPORTED_VERSIONS. - Run
mcp-devserver conform --strict. Failures name the requirements that moved. - Add or amend checks in
conformance/checks.pyfor anything new, and a negative control for each intests/conformance/. - Only then change handlers.
Backup and state #
There is none. The server holds no state between requests — that is a requirement of the protocol revision and a property of the code. Restarting it loses nothing.
Common problems #
"the container never became ready" — the workspace mount is empty or absent.
Check docker logs; readyz will report workspace_present: false.
Every result is empty and total_files is 0 — the bind mount silently
produced an empty directory. On Docker Desktop this happens when the source path
is not one of the shared paths; use a path inside your home directory or the
project tree.
The client reports a JSON parse error — something is writing to stdout.
Check for a print in a handler, or a library configuring its own logging.
tests/e2e asserts the exact line count on stdout for exactly this reason.
-32602 on every request — the client is not sending _meta. This revision
has no handshake; every request carries its own version and capabilities.
A legitimate regular expression is refused — the ReDoS guard is a heuristic.
Rewrite without a repetition inside a repeated group, or set regex: false.