Quick answer: In a hosted MCP deployment, secrets belong in the platform's environment variable store or a dedicated secret manager, never in the tool configuration, the JSON the server returns, or the repository. Inject the credential at the gateway so the MCP server and the agent never see it, rotate it without a redeploy, and scope it to the minimum permissions the upstream needs. Hardcoding a token is the one option that guarantees a future leak.

Where the secret actually travels

A typical MCP server calls an upstream API on the user's behalf, and that upstream requires a credential. The important design question is where that credential is held. If it is embedded in the MCP configuration or returned in a tool result, it becomes visible to the agent and to anyone who can read the config, and it gets copied into logs and exports. The gateway pattern keeps the boundary clean: the client authenticates to the gateway, and the gateway attaches the upstream token to the forwarded request, so the secret never crosses into the MCP layer.

Storage choiceVisibilityRotationRisk
Hardcoded in config or codeBroad, often committedRequires edit and deployHigh
Environment variableScoped to the processRe-inject and restartModerate
Dedicated secret managerAccess-controlled, auditedAutomated, no deployLow
Gateway-held tokenNever exposed to the agentManaged at the edgeLowest

Injecting through the environment

The baseline approach is to set the credential as an environment variable that the platform keeps encrypted at rest and injects only into the running process. The code reads it at startup and never writes it back out. This keeps the secret out of the repository and lets you change it without touching the source.

# Configured in the hosting platform, not committed to the repo
UPSTREAM_TOKEN=secret-value-here
UPSTREAM_BASE_URL=https://api.example.com
const token = process.env.UPSTREAM_TOKEN;
if (!token) {
  throw new Error("UPSTREAM_TOKEN is not configured");
}

async function callUpstream(path, init) {
  const res = await fetch(`${process.env.UPSTREAM_BASE_URL}${path}`, {
    ...init,
    headers: { ...init.headers, Authorization: `Bearer ${token}` }
  });
  return res;
}

Using a secret manager for rotation and audit

When you need rotation and an access trail, back the environment variable with a secret manager rather than a static value. The process fetches the credential at startup or on a short refresh, and access is logged, so a key can be rotated on a schedule or revoked immediately without a code change. This matters most for long-lived deployments where a static token tends to outlive the person who created it and quietly becomes a permanent credential.

Keeping the secret out of logs and errors

A token that is correctly stored can still leak through observability. Strip authorization headers before logging, avoid serializing the full request object, and make sure an upstream error message does not echo the header back into a stored response. When you report a failure, name the operation and the status code rather than the request that carried the credential, and treat any log line that contains a token as an incident to clean up rather than ordinary output.

Scoping and least privilege

Where the secret is stored is only half of the problem; what it can do is the other. Issue the upstream credential with the narrow scope the MCP operations actually need, and separate read and write credentials when the platform supports it. A broad admin token behind a single read-only tool concentrates risk, while a scoped credential limits the blast radius if the deployment is ever compromised. Per-role keys also let you revoke one team's access without rotating the whole deployment.

How an agent mishandles it

An agent that is asked to wire a hosted server often pastes a working token into a config file to make the call succeed, or it prints the full request while debugging and leaves the credential in a log. Ask it to read the token from the environment, show where it is injected, and confirm no code path writes it back. If the agent proposes committing a .env file for convenience, reject that; commit an example with blank values and keep the real secret in the platform's encrypted store.

Verifying the deployment's secret handling

A hosted configuration deserves a concrete check, not just a confident assumption. Confirm that the environment variable is set in the platform, that the process fails clearly when it is missing rather than running with an empty token, and that a test request to the upstream succeeds with the injected credential. A separate scan of the repository and the exported configuration confirms no secret was committed or written into a tool result. These checks are quick and they turn the security claim into something you can actually demonstrate.

# Fail fast if the required variable is absent
node -e 'if (!process.env.UPSTREAM_TOKEN) process.exit(1)' && echo "token configured"

# Confirm the repo history does not contain a known secret pattern
grep -rE "UPSTREAM_TOKEN=.+" --include="*.json" --include="*.env" . || echo "no static token"

Re-run the same verification after a rotation, since that is the moment a misconfigured injection would surface. The invariant you are protecting is simple: the credential exists only in the encrypted store and the running process, never in source, config exports, or logs.

Frequently asked questions

Can I just put the token in the MCP server configuration?

No. Config can be exported, logged, or committed, and it may be visible to the agent. Use an environment variable or secret manager and inject the credential at the gateway.

How do I rotate a secret without downtime?

Back it with a secret manager and refresh the value on a short interval, or re-inject the environment variable and restart the process. The key is that rotation never requires a code change.

Should the agent ever see the upstream token?

It should not. The gateway attaches the token to the forwarded request, so the agent and the MCP server interact only with the operation, not the credential.

How do I keep secrets out of logs?

Strip authorization headers before logging, never serialize the full request object, and verify that upstream error messages do not echo the credential back into stored output.

This is part of the Powerduck vs Smithery: Generating and Hosting MCP Servers — start there for the full comparison of hosting and securing MCP deployments.