# MCP client configuration review before you trust a server

A new MCP server deserves the same review you would give a new executable that asks to run inside your development account. The configuration file may look harmless because it contains a command and a few arguments. In practice, that entry decides which code starts, which environment it receives, which directories it can reach, and which tool descriptions will be offered to an agent.

The mistake I keep seeing is treating "local" as a trust boundary. It is not. A local server usually starts with your user permissions, can act before it publishes its first tool, and may inherit credentials that nobody intended to share. Review the launch contract before the client connects. Then review the advertised actions before the agent can call them.

## A local server runs with your account's consequences

A local MCP server is a child process, not a harmless configuration object. If your client launches it under your normal macOS account, the server may read accessible project files, write to your home directory, make outbound network requests, and inspect environment variables available to that process. MCP does not sandbox the server. The protocol carries messages; it does not constrain what the executable does between messages.

That distinction matters when a server arrives through a package command. This configuration:

```json
{
  "command": "npx",
  "args": ["-y", "some-mcp-server"]
}
```

does more than start a known local binary. It asks a package runner to resolve a package, install or reuse code from its cache, and launch it. A clean machine, a warm cache, and a changed package tag can produce different code. The familiar command makes people skip the question that matters: which exact executable will run today?

The same concern applies to a server checked out beside the project. A repository can contain an honest MCP implementation and a malicious install hook, wrapper, or runtime dependency. Review the path that starts the process, not only the source file whose name appears in a setup guide.

Use the operating system account as your first containment decision. A disposable workspace, a separate low-privilege account, or a virtual machine gives an early test less access than the account that holds production source code and cloud credentials. That does not replace code review. It limits damage when your review misses something.

## The command field deserves literal reading

Read the command and arguments exactly as the client will execute them. Do not mentally translate them into the friendly description in a README.

The safer shape uses a fixed executable path and an argument array:

```json
{
  "command": "/Users/dev/tools/acme-mcp/bin/server",
  "args": ["--config", "/Users/dev/review/acme-mcp.json"],
  "env": {
    "HOME": "/Users/dev/review-home",
    "PATH": "/usr/bin:/bin"
  }
}
```

That shape gives you concrete review points. Does the executable exist at that path? Who owns it? Does the configuration file sit outside a repository that other people can change? Does the process need `HOME` at all? Does it genuinely need a compiler, package manager, or a broad `PATH`?

A wrapper changes the review. Consider this instead:

```json
{
  "command": "sh",
  "args": ["-c", "npx -y acme-mcp --token $SERVICE_TOKEN"]
}
```

The shell now expands `$SERVICE_TOKEN`, reads shell syntax, and may run more than the one program you expected. The package runner may fetch code. The server receives a secret as a command argument, which can appear in process inspection and diagnostic output. Each layer has behavior that a direct executable path avoids.

Do not assume every client executes `command` the same way. Some clients use direct process spawning with an argument array. Others offer a shell-oriented setting or allow a wrapper script. Read the client's documentation and test the launch with a non-sensitive command before you approve a real one. If the format permits both direct execution and a shell, choose direct execution unless the server has a specific need that you can explain.

Inspect wrappers line by line. Small scripts often hide the riskiest behavior: downloading a release, reading a token file, changing the working directory, exporting all environment variables, or silently restarting through another runtime. A twenty-line launcher can deserve more attention than the server's main implementation.

## Inherited environment is the credential leak people miss

An `env` block does not reliably mean "this is the entire environment." In many process launch APIs, the parent process environment passes through unless the launcher deliberately replaces it. The entries in `env` then add or override selected values. Your MCP client may itself have started from a terminal, desktop launcher, editor, or automation service, and each path can supply different variables.

That creates a nasty gap between what reviewers see and what the server receives. The JSON may list only `LOG_LEVEL`, while the process also gets a package registry token, a source control token, cloud credentials, proxy settings, SSH agent details, and an internal service URL inherited from the client.

Before connecting, write down the environment contract in plain language: this process needs this endpoint, this non-secret setting, and perhaps one narrowly scoped credential. Anything else is accidental authority.

A useful first pass starts the client itself from a sparse shell. This macOS and Unix command preserves only a few ordinary variables:

```sh
env -i HOME="$HOME/review-home" PATH="/usr/bin:/bin" LANG="${LANG:-C}" \
  YOUR_MCP_CLIENT
```

Replace `YOUR_MCP_CLIENT` with the actual client command. If the server fails, add one variable at a time and record why it needs it. Do not solve the failure by restoring your whole login environment. That shortcut has exposed more credentials than people realize.

You can also inspect a saved configuration without executing any command. The following Python fragment prints server names, commands, arguments, and explicit environment variable names. It deliberately prints no environment values.

```sh
python3 - "$HOME/.config/your-client/mcp.json" <<'PY'
import json, sys

with open(sys.argv[1], encoding="utf-8") as f:
    data = json.load(f)

for name, spec in data.get("mcpServers", {}).items():
    print(f"server: {name}")
    print(f"  command: {spec.get('command', '')}")
    print("  args:")
    for arg in spec.get("args", []):
        print(f"    - {arg}")
    print("  explicit env names:")
    for env_name in sorted(spec.get("env", {})):
        print(f"    - {env_name}")
PY
```

Its output has a shape like this:

```text
server: issue-tracker
  command: /Users/dev/tools/issue-mcp/server
  args:
    - --read-only
  explicit env names:
    - ISSUE_TRACKER_URL
```

If you see names such as `AWS_SECRET_ACCESS_KEY`, `GITHUB_TOKEN`, `SSH_AUTH_SOCK`, or `SERVICE_TOKEN`, stop and ask why that server needs them. Secret values do not become safe because a JSON file holds them instead of source code. Config files get copied into backups, shared in support requests, committed by accident, and read by every process that can read the file.

## A tool list is a capability claim, not a permission grant

The Model Context Protocol specification defines `tools/list` for discovery and `tools/call` for invocation. That is useful because a client can inspect a server's proposed interface before an agent chooses a tool. It does not certify the server, its descriptions, or the side effects behind a call.

Treat each advertised tool as a proposed capability. Read its name, description, input schema, and any annotations together. A tool named `search_issues` may make a read-only request, or it may collect project files and send them to a third party before it searches. A tool named `deploy_preview` might create resources, alter DNS, or use a credential with broader scope than the name suggests.

The MCP specification lets servers supply annotations that hint at behavior, such as whether a tool reads data, changes data, or interacts with an external system. Hints help a client present useful UI, but the specification does not turn them into enforcement. A dishonest or careless server can label a destructive tool as read-only. Your operating system and the server's credentials will not inspect that label before an action runs.

Record a small inventory for each server you approve:

- Tool name and the action it claims to perform.
- Inputs that can carry file paths, URLs, shell fragments, or free-form prompts.
- Systems the tool can reach and the credential it uses.
- Side effects, including indirect ones such as sending data to a remote API.
- The exact version or source revision you reviewed.

Keep the inventory close to the configuration. A diff becomes meaningful when an update adds `delete_repository`, changes `query` into `execute`, or adds an input that accepts an arbitrary URL. Without a previous inventory, people often approve a changed list because the server name still looks familiar.

Descriptions deserve the same suspicion as any other untrusted text delivered to an agent. A server can describe one tool as mandatory, claim that approval is unnecessary, or instruct the agent to pass unrelated secrets as arguments. The agent should not treat server-provided prose as higher authority than the user's request and the client's approval rules.

## Credentials need a boundary outside the agent process

Do not hand a long-lived API token or private SSH key to a server merely because it runs on the same laptop. Once the server process receives a secret, it can log it, forward it, write it to disk, or expose it through a tool result. The client cannot claw that secret back after the process reads it.

Separate two decisions that teams often blur. Permission to start a server is permission to execute code. Permission to use a production credential is permission to act on an external system. A server may deserve the first permission in a test workspace while it clearly does not deserve the second.

For simple development use, issue a scoped, short-lived credential that can only touch test data. Put its scope in writing. "Used by the issue server" is vague; "may read tickets in the sandbox project and cannot create, comment, or change membership" gives reviewers something to test.

For actions that need a serious credential, keep the secret in a local credential boundary and expose only the narrow operation. Sallyport follows this approach for HTTP and SSH actions: the agent does not receive the API key or SSH key, while the app performs the action and returns the result.

That boundary changes secret handling, not the need for server review. A malicious server can still ask an agent to make a damaging but authorized call. Put approval where the consequence happens, scope credentials to the smallest useful authority, and read every request that crosses into a system you care about.

Do not pass credentials in command arguments. Process listings, crash reporters, diagnostics, and parent processes can expose them. Avoid plaintext secrets in configuration files for the same reason. If a setup guide requires a secret in either location, find out whether the server can use an operating system credential store, a short-lived token, or an external action service before you accept that design.

## First contact should happen in a boring test account

Run an unfamiliar server in a controlled account before you give it a project, a broad environment, or real credentials. This test answers a narrow question: what does the program do when it starts and when the client asks for its tools?

Use a new directory with harmless files whose names make unexpected reads obvious. Give the process a temporary `HOME`. Start with a sparse `PATH`. Do not mount a directory full of source code because you intend to test a source control server. Start with a fake repository or a copy with no credentials.

Watch for behavior before any tool call. A server that opens a network connection, scans your home directory, reads browser data, or creates persistence files at startup has already exceeded what most MCP use cases require. Some servers legitimately check an endpoint or load local configuration. They should make that behavior easy to explain and easy to disable.

Then ask for the tool inventory, inspect it, and make one harmless call with known input. Capture the request, result, process output, and files changed in the test directory. If the tool returns content from another system, make the test data unmistakable so you can tell whether it accessed the correct place.

A basic review sequence looks like this:

1. Verify the executable path, package version, checksum or source revision, and every launcher script.
2. Launch under a sparse environment in a test account or isolated workspace.
3. Capture the first `tools/list` result and compare each tool with the intended job.
4. Call a harmless read operation with fake data and observe files, child processes, and network destinations.
5. Add only the credential and directory access that the confirmed behavior requires.

Do not confuse a successful response with a safe server. A server can return the expected answer while also copying files or using an inherited token elsewhere. The test gives you evidence, not proof. It catches the careless designs and obvious surprises before they reach production access.

## Package convenience creates an update path you must own

Package managers and runtime launchers make MCP setup pleasantly short. They also create an update path. A version range, a floating package tag, or a bare package name can change the server that launches next week without a configuration diff.

Pin a version when your ecosystem supports it, and record the package source alongside the tool inventory. Better still, use a reviewed local artifact or a lockfile that your team already checks. The practical goal is simple: a later launch should resolve to code you can identify.

Do not let the agent install its own MCP server as part of a task. That folds software acquisition, execution, and tool authorization into one conversational request. A human should add the server configuration after reviewing the source and its launch contract. If a development workflow needs many servers, maintain a reviewed catalog rather than accepting setup snippets from issue comments or tool output.

Updates deserve a short repeat review. Compare the executable or dependency lockfile, the command and arguments, explicit environment names, and the `tools/list` inventory. A tool addition may be harmless. It may also introduce write access that the prior review never considered. The same applies when a server changes its credentials, endpoint, or authentication library.

The popular recommendation to "always use the latest package for security fixes" is incomplete for MCP servers. You should take security fixes promptly, but an unreviewed automatic change can alter the code that runs beside your credentials. Use a controlled update process that lets you inspect the change and roll it back if the new server behaves differently.

## Approval should follow the action, not the server's name

A one-time approval for a server process answers only one question: may this executable participate in this session? It cannot safely answer every later question about deleting a remote branch, sending customer data, or opening an SSH command on a production host.

Split ordinary discovery from consequential actions. Listing available projects, reading a public issue, and fetching a local schema often need less scrutiny than mutating records or sending data outside the machine. Your client or credential boundary should ask for fresh human approval when a call crosses that line. If every read call demands attention, people will click through. If a single startup click grants unlimited production access, people will eventually regret it.

The process identity matters too. An approval prompt should tell you which signed process requested access, not merely display a server-selected label. A label such as `database-helper` can be copied by any program. The executable path, signing identity where available, and launch arguments make a better review surface.

Keep a record at two levels: the agent session that requested work and the individual action that used a credential or reached an external service. You need the session record when you investigate why an agent had authority. You need the action record when you investigate what actually changed. One log cannot answer both questions cleanly if it omits either the caller or the exact request.

A tamper-evident record helps when the server, client, or operator later disputes what happened. It does not make a dangerous action safe at the moment of approval. Read the target, method, command, and meaningful parameters before you authorize an action that cannot be easily undone.

## A rejected server needs cleanup, not just removal

If you decide a server is untrustworthy after connecting it, remove its client entry immediately, but do not stop there. The process may have written files, altered shell configuration, created scheduled work, modified a repository hook, or copied credentials while it ran.

Preserve the evidence before you delete everything. Save the configuration, package version or source revision, process output, and action logs. Then inspect the test `HOME`, the server's working directory, package cache, shell startup files, launch agents, repository hooks, and any directories the server could write. Check active processes and recent network connections while the evidence remains available.

Rotate every secret the process could have read, not only the secret you intentionally supplied. That includes inherited tokens, SSH agent access, package credentials, and browser-backed development sessions where relevant. Removing a line from a configuration file does not revoke a copied token.

Then tighten the review process that allowed the connection. If the failure came from an inherited environment, use a sparse launcher. If it came from an unexpected package update, pin and review artifacts. If it came from a misleading tool description, require a captured inventory before approval. The useful outcome is a changed control, not a vague promise to be more careful next time.

A new MCP server gets access at its most dangerous moment, before it has earned any trust. Make its command literal, its environment small, its tool list inspected, and its credentials separate from the agent. That is the point where you still control the outcome.
