MCP config security for repository changes
MCP config security starts with pull request review. Learn how command paths, package runners, arguments, cwd, and environment entries change risk.

A repository MCP configuration is executable deployment material wearing the clothes of a small JSON file. It can select a binary, fetch a package, choose where that process wakes up, pass it data, and give it a view of the checkout. That makes MCP config security a pull request review problem before it becomes an agent safety problem.
I have watched teams spend an hour debating whether an agent should be allowed to call a tool, then merge a launcher that can install whatever a public package name resolves to that afternoon. The tool list is the visible part. The process that creates the tool list has already crossed a boundary.
The configuration is an instruction to start a process
A .mcp.json change deserves the same attention as a change to a build script because a client must turn its fields into a local process. The exact schema differs by host, but the common shape is familiar: a command, an argument array, optional environment entries, and sometimes a working directory or transport setting.
That process starts with the permissions of the developer or agent host that launches it. It can read files that user can read, create files where that user can write, inherit selected environment values, and make network requests unless the operating system or another control blocks it. The fact that its eventual role is "MCP server" does not reduce those ordinary process permissions.
Reviewers often infer safety from the name of the package or the narrowness of the advertised tools. Neither inference holds. A server named issue-reader may expose only read operations after startup, while its launcher invokes a shell wrapper that reads a token from a home directory. A server with a broad sounding name may be a checked in binary that does exactly what the repository documents. Start with evidence, not names.
The dangerous line is often visually boring:
{
"mcpServers": {
"docs": {
"command": "npx",
"args": ["-y", "@example/docs-mcp"]
}
}
}
This does not mean npx or a registry package is automatically unacceptable. It means the pull request has delegated part of the executable supply chain to a later moment, on each machine that launches the server. The reviewer must know which package version will run, where it comes from, what it installs, and what it does on first start.
A checked in configuration is also social pressure. Once it sits in the repository, a new contributor sees a server described as project setup and assumes it has already received scrutiny. That assumption is exactly why this file class needs an owner and a review habit.
MCP messages and repository launchers are different layers
The Model Context Protocol specification describes the conversation between an MCP client and server, including initialization, tool discovery, tool calls, and transport behavior. It does not create one universal .mcp.json format or make a client configuration harmless. Each client decides where it reads configuration and how it launches a local stdio server.
That distinction sounds academic until a review goes wrong. The protocol may limit a server to a declared set of methods after the connection comes up. The host configuration decides which program gets the opportunity to announce those methods in the first place. If you inspect only the protocol-facing behavior, you start your review after the launcher has already acted.
Keep two questions separate:
- What actions can the connected server request or perform through its tools?
- What does the local launcher do before the client has a server connection?
The first question belongs to agent permissions, approval flow, and tool design. The second belongs to repository trust, process execution, dependency provenance, and filesystem scope. A good review answers both, but never lets an answer to one substitute for an answer to the other.
The protocol documentation encourages clients and servers to negotiate capabilities. That is useful for compatibility, not for establishing trust in a repository command. Capability negotiation can tell the client that a server supports tools. It cannot prove that the command path, package content, or startup hook deserves the developer's access.
This also explains why a familiar server can still be risky in a new configuration. The server code may be unchanged, yet a modified argument points it at a different endpoint, a different credential source, or a different project directory. Launch context is behavior.
Read the launched command before the advertised tools
Review command resolution as a chain, not as the first string in the JSON. Ask what program the host will actually execute after it resolves the command through the current environment. python, node, uvx, npx, bunx, sh, and a relative path all mean that another resolver decides the final executable.
A direct absolute path is easiest to inspect, although it still needs a provenance story. A repository relative path can be acceptable when the repository contains the file and normal code review covers it. A bare command name requires you to inspect the developer's PATH behavior. A shell command needs the highest level of suspicion because quoting, expansion, pipes, redirects, and command substitution hide more execution than a plain argument array.
Arguments deserve their own pass. Reviewers commonly read the first two arguments, recognize a package name, and move on. Read every element. An argument can select a configuration file outside the checkout, choose an output directory, enable a plugin loader, point at an alternate registry, or turn a harmless command into a script evaluator.
Consider the difference between these two entries:
{
"command": "node",
"args": ["tools/mcp-server.js", "--root", "."]
}
{
"command": "node",
"args": ["tools/mcp-server.js", "--config", "../../shared/runtime.json"]
}
The second entry may be legitimate. It also creates a dependency on a file outside the checkout boundary that the pull request might not show. If the server reads instructions or credentials from that file, reviewers cannot evaluate the change from the diff alone. Ask the author to bring the referenced configuration into review, replace it with an explicit repository path, or explain why the external file is necessary.
Do not accept a vague answer such as "the package handles that." The package may handle it correctly today, but the change still assigns it authority. A useful answer names the program, its version, its inputs, and its expected files. If the author has not run the exact command from a fresh checkout, they are describing intent rather than behavior.
Shell wrappers deserve a blunt default: reject them unless the repository has a concrete need that an argument array cannot express. A wrapper may seem convenient because it sets an environment value and starts two helpers. It also makes review depend on shell parsing rules and whatever commands the wrapper reaches. Put setup in a reviewed script with a clear filename if it must exist, then review that script like application code.
Package runners turn startup into a supply chain event
A package runner can download and execute code during the first server launch, so a package name alone is not a complete dependency review. This is why npx -y package, uvx package, and similar forms need more scrutiny than a command that invokes a checked in file.
The popular argument for an unpinned runner is convenience. Contributors do not have to install anything manually, and the project appears to stay current. The cost is that the repository no longer states the exact code it asked each contributor to run. A newly published package version, a changed dependency, or a registry routing change can alter behavior without a new pull request.
At minimum, make the version explicit and make the source understandable:
{
"mcpServers": {
"schema-checker": {
"command": "npx",
"args": ["-y", "@acme/[email protected]"],
"cwd": "${workspaceFolder}"
}
}
}
Version pinning does not certify the package. It makes the review target stable. A reviewer can inspect that release, compare it with the previous one, and require an intentional pull request for an update. Use the package manager's lockfile, integrity data, or a checked in artifact where the surrounding toolchain supports it. A version string without any way to verify the downloaded content leaves a gap, but it is still better than an unbounded name.
Do not confuse a package manager cache with review. A cache only changes where the bytes came from on one machine. It does not tell a reviewer which bytes other contributors will receive, whether install hooks run, or whether a clean machine behaves the same way.
Ask one awkward question that catches many weak changes: what happens if the package is missing? If the answer is "it installs itself," the configuration has a network and code execution path. If the answer is "the launch fails with an instruction to install a documented dependency," the team has chosen a slower but more visible setup path. Neither answer is inherently right. Pretending the difference does not exist is wrong.
Working directories and environment entries set the real scope
The cwd field tells a process where to begin looking for relative files, and that choice often controls more than people expect. Many tools discover project configuration, language runtimes, ignore files, credentials, and plugins by walking upward from the working directory. A server that begins at the repository root can see a different world than one that begins in a dedicated fixture directory.
Set the working directory deliberately. If a server only needs generated API specifications under tools/specs, do not start it from a parent directory that contains deployment material and private development notes. If it must inspect the whole checkout, say so in the pull request. The goal is not to make every path tiny. The goal is to make the access the reviewer approves match the access the server receives.
Environment entries carry similar weight. Some are harmless operational settings such as a locale or a port. Others select a package registry, extend a module search path, activate a debug mode that writes request bodies, or carry credentials. A configuration that uses ${TOKEN} hides the token value from the file, but it still passes that secret into a child process.
Avoid credentials in repository MCP configuration whenever the server can operate through a separate action boundary. If a server truly needs a secret, document the source, the purpose, and whether the process can pass it to children. Do not put a sample production token in a comment or a copied shell transcript. Developers will copy the example that is easiest to make work.
Inherited environment values deserve attention too. A process can inherit far more than the JSON lists. The host decides what it passes, but the repository author can choose a command that reads standard runtime locations. That is another reason to favor direct, small launchers over general shells and package runners.
A pull request diff can reveal the execution graph
You can review most configuration changes without running untrusted code by expanding the diff into the processes and inputs it implies. Start with a focused diff rather than the rendered file view so removed arguments and changed paths remain visible:
git diff --check
git diff -- .mcp.json
The first command reports whitespace errors when it finds them. The second should show every added, removed, and altered line in the configuration path. If the repository stores the file elsewhere or generates it, adjust the path and ask for the generator change too. A generated configuration without its source is an incomplete review.
Take this small change:
"mcpServers": {
"release-notes": {
- "command": "node",
- "args": ["tools/release-notes-server.js"],
- "cwd": "${workspaceFolder}"
+ "command": "npx",
+ "args": ["-y", "release-notes-mcp"],
+ "cwd": ".."
}
}
The visible story says the team replaced a local helper with a published server. The execution graph says more. The client resolves npx through the developer's PATH. npx may fetch release-notes-mcp and its dependencies. The package starts outside the repository root because cwd now points at the parent directory. The server may discover configuration there, write cache files there, or read sibling projects. Each arrow needs an answer before merge.
A disciplined review asks for evidence in this order:
- Identify the final executable and the exact package or repository file that supplies it.
- List every file path, URL, registry, and environment value that can change startup behavior.
- State the process working directory and the directories the server reads or writes during normal use.
- Confirm what happens on a clean machine before any package cache exists.
- Compare the claimed tool purpose with the permissions the launcher actually needs.
That sequence is a concrete artifact, not ceremony. It prevents the usual failure where a reviewer approves a tool description while an installer, runtime selector, or parent directory slipped past in a one line change.
If you must execute a configuration to validate it, do it in a disposable account or isolated environment with no production credentials and an empty package cache. Record the resolved executable, package version, outbound hosts, and files written. A screenshot of successful tool discovery is weak evidence because it omits the part that caused the most risk.
Tool descriptions cannot compensate for a broad launcher
A narrow tool schema does not erase what happened before the MCP handshake. Teams regularly treat a server's tools/list output as a permission inventory, then decide it looks safe because it offers search_docs and read_status. That inventory describes the interface after initialization. It says little about package installation, configuration discovery, telemetry, child processes, or files read at startup.
This distinction cuts both ways. A server with a write capable tool may be acceptable when the launcher is pinned, local, documented, and confined to the intended project. A read only server can be unacceptable when a repository config silently runs a mutable package from the network with access to a developer's broader workspace.
Ask the author to state the lifecycle in plain language: what starts, what it reads before connecting, what it downloads if anything, and what the server can touch once connected. The request should fit in a pull request description. If it requires a long investigation to answer, the configuration is too indirect for routine review.
Do not let "we trust this vendor" close the discussion. Trust is relevant, but it does not pin a version, narrow a working directory, or show whether the command uses an alternate registry. Those are separate controls with separate failure modes.
Repository ownership needs a runtime backstop
Repository review stops unsafe launch instructions from becoming normal project setup. Runtime controls still matter because an approved configuration can be abused by a compromised dependency, a malicious prompt, or a user who asks an agent to act too broadly.
Keep the boundary clear. The repository controls what it proposes to launch. The human operating the machine controls whether a particular agent session may act. Sensitive actions need records that let the operator reconstruct what the agent did and revoke the session when the run goes wrong.
Sallyport keeps API and SSH credentials out of the agent process, while its session authorization and activity record give an operator a separate place to approve and inspect actions. That does not make a risky repository command harmless, which is why the configuration review still comes first.
For teams that use another boundary, apply the same test. Can a developer see which agent process asked for access? Can they stop that process without killing unrelated work? Can they distinguish an attempted action from a successful one? If the answer is no, a bad configuration has more time to cause trouble after merge.
Treat these files as owned execution code
A repository should assign code owners or an equivalent review rule to MCP configuration, its wrapper scripts, and any package manifests that determine what the launcher installs. The owner does not need to be a security specialist. They need enough context to ask why a change starts this program with this access.
Keep configurations small. One server per purpose is easier to review than a universal helper that reaches every system a developer uses. Put unusual requirements beside the configuration in ordinary prose: expected command, package version policy, working directory, required network access, and intended inputs. That note turns tribal knowledge into something the next reviewer can challenge.
The practical test is simple. Hand the diff to an experienced developer who does not know the feature and ask them to explain what will execute on a clean machine, where it will execute, and what it can see. If they cannot answer from the pull request and its referenced code, the change needs more work before it joins the repository.
FAQ
Is .mcp.json part of the MCP specification?
No. The protocol defines how a client and server exchange messages; a repository file that starts a server is a convention of the client or development tool. Treat its command, arguments, environment, and working directory as local execution instructions.
Can an MCP configuration run code when I open a repository?
It can, especially when it uses a package runner, a shell, an interpreter, or a relative executable. A server may expose harmless looking tools after its launcher has already read files, contacted a registry, or installed code.
Should I allow npx in an MCP config?
Ask for a pinned package version or a checked in executable with a recorded checksum. A bare package name leaves the actual program dependent on registry state when a developer starts the server.
Why does cwd matter in an MCP server configuration?
Only when the repository has a documented reason and the team expects that directory to be readable or writable by the server. A working directory is an access boundary in practice, even though it looks like a convenience setting.
What should I review in a .mcp.json pull request?
Read the executable, every argument, the environment entries, the working directory, and how the host decides to start it. Tool names and descriptions tell you far less about what happens before the server connects.
How do I review a package installer in an MCP configuration?
Ask for the exact resolved command, the package version, the registry source, and the permissions the process needs. If the author cannot explain what first launch downloads or writes, the change is not ready to merge.
Does an MCP server start automatically from a repository config?
It depends on the host. Some clients wait for an explicit action, while others can discover or offer configured servers as part of opening a workspace. Review the file as though a developer will run it with the repository open and sensitive credentials available.
Are environment variables safe for MCP credentials?
Secrets in environment variables often reach child processes, diagnostic output, crash reports, and package install hooks. Keeping credentials outside the agent is good, but the launcher still deserves review because it may read other material from the developer’s machine.
How can teams make repository MCP configs safer?
Keep the diff small, identify every launched binary, pin mutable dependencies, and document the intended access boundary. Then make someone who did not write the change reproduce the launch in a disposable checkout.
Does runtime approval replace configuration review?
No. Approval can stop an unexpected action at the boundary, but it does not tell you whether the repository asked to launch the intended program. Configuration review prevents one class of mistake; runtime approval contains another.