HomeGuidesMCP server security: what to check before you install one

Guide

MCP server security: what to check before you install one

Before you add an MCP server to Cursor, Claude Code, or VS Code: tool poisoning, prompt injection, rug pulls, leaked tokens, and settings that limit damage.

18 min read

Independent notes from VibeLocked. Not a pentest, and not affiliated with the tools named below.

MCP (Model Context Protocol) is how Cursor, Claude Code, VS Code, Windsurf, and Lovable plug into GitHub, Supabase, Notion, your browser, and almost anything else. You paste a block of JSON into mcp.json, the agent gets new tools, and it feels like a settings change. It is closer to installing software and handing it your keys.

This guide covers what an MCP server can actually do, the attacks researchers have already shown in the wild, and the specific settings in each tool that limit the damage. It is for solo builders adding servers to their own setup, not for people building MCP servers.

Checked against the MCP specification, the Cursor, Claude Code, VS Code, Windsurf, Lovable, Supabase, and GitHub docs, and the advisories and writeups linked below on October 19, 2026. Product names are here to help you find your setup. VibeLocked is independent and not affiliated with any of them.

The short version

  1. Treat every MCP server as software you install, because a local one runs on your machine with your permissions.
  2. Install the official server from the vendor whose service it connects to, and check the exact package name.
  3. Pin the version instead of @latest, so an update cannot change the code under you.
  4. Give it the smallest token that works: a fine-grained GitHub token for one repo, Supabase read_only and project_ref, a read-only database user.
  5. Keep secrets out of the config file you commit. Reference environment variables instead.
  6. Keep tool approval on, and expand the arguments before you click Allow.
  7. Do not mix private data, untrusted content, and a way to send data out in one session.
  8. Use development data, not production, unless the task truly needs it.

What an MCP server actually is

An MCP server exposes tools (functions the model can call), plus optional resources and prompts. Each tool comes with a name, a description, and an input schema, and the model reads all of that to decide when and how to call it. The spec's tools page says there "SHOULD always be a human in the loop with the ability to deny tool invocations," and that clients must treat tool annotations as untrusted unless they come from trusted servers.

There are two kinds, and they fail in different ways.

Local (stdio) servers are a command your editor starts, usually npx, uvx, python, or docker. Cursor's MCP docs describe stdio as "Local" and "Cursor manages." That process runs as you. The MCP spec's security best practices list "arbitrary code execution" as the first risk of local servers from untrusted sources and tell clients to warn "that MCP servers run with the same privileges as the client." It can read your .env files, your SSH keys, your browser profile, and other MCP configs, and it can make network requests.

Remote (HTTP) servers are a URL. The code runs on someone else's infrastructure, and you usually sign in with OAuth or send a token in a header. A remote server cannot read your disk, but everything your agent sends it, and everything it sends back into your model's context, is in its hands. The scopes you approve in the OAuth screen decide what it can do with your account.

Both kinds can change. The spec lets a server announce that its tool list changed, and Claude Code's MCP docs say that when that notification arrives in an interactive session, Claude Code fetches the updated list without you reconnecting. Convenient, and it also means the descriptions your model reads today may not be the ones you looked at when you added the server.

The risks that have actually happened

OWASP's MCP Top 10, still in beta, lists token mismanagement, scope creep, tool poisoning, supply chain attacks, and prompt injection among the top risks. Here is what each looks like in practice.

Fake and unofficial packages

In September 2025, Postmark warned that an npm package called postmark-mcp was impersonating it. In Postmark's words, a malicious actor "built trust over 15 versions, then added a backdoor in version 1.0.16 that secretly BCC'd emails to an external server." Postmark had never published an MCP server on npm. Any config that ran it unpinned, for example npx -y postmark-mcp, would pick up a new version on the next start without anyone reviewing it.

That is the same supply chain problem as hallucinated packages and slopsquatting, with one difference: an MCP server is designed to be handed your API keys.

Tool poisoning: instructions hidden in tool descriptions

In April 2025, Invariant Labs published tool poisoning attacks. Their demo server offered an add tool whose description, which the model sees and the user usually does not, told the model to read ~/.cursor/mcp.json and ~/.ssh/id_rsa and pass their contents in a "sidenote" argument. In their Cursor test, the agent did it. The confirmation dialog at the time did not show the full tool input, so the stolen key was not visible to the user.

They also showed shadowing: a malicious server's description can change how the agent uses a different, trusted server, for example telling it to send every email from a legitimate email tool to the attacker. You never have to call the bad tool for it to work.

Note the target in that demo. mcp.json is worth stealing because it so often holds tokens for every other server you use.

Rug pulls: the server changes after you approve it

Invariant's post also describes rug pulls, where a server changes its tool descriptions after you have approved it. The same thing can happen to the config file itself. CVE-2025-54136 in Cursor 1.2.4 and below let someone with write access to a shared repo swap an already approved MCP entry for a malicious command "without triggering any warning or re-prompt." Cursor fixed it in 1.3 (Cursor advisory). Keep your editor updated, and reread MCP config changes in pull requests like you would read a new script.

Prompt injection through data the tools read

This one needs no malicious server at all. In May 2025, Invariant Labs showed a GitHub MCP exploit: an attacker opens an issue on your public repo with instructions in it. You ask your agent to "have a look at the open issues." The agent reads the issue, follows it, pulls data from your private repos, and publishes it in a pull request on the public repo. Invariant stresses that this is "not a flaw in the GitHub MCP server code itself." The token could do all of it, and the issue text told the model to.

OWASP ranks prompt injection first in its Top 10 for LLM applications, and Supabase gives the same example for databases in its MCP docs: a customer files a support ticket that says to select * from a sensitive table and post the result as a reply, and a developer later asks Cursor to read that ticket through MCP. Any tool that reads issues, emails, web pages, tickets, or user rows is reading text an attacker can write. Our prompt injection guide covers the same problem inside the apps you build.

The lethal trifecta

Simon Willison's lethal trifecta is the clearest way to judge a setup. Trouble starts when one agent session has all three:

  • access to private data (your private repos, your database, your inbox),
  • exposure to untrusted content (public issues, web pages, emails, support tickets),
  • a way to communicate externally (opening a PR, sending an email, fetching a URL, even a link for you to click).

He points out that the GitHub exploit had all three in a single MCP server, and that MCP "encourages users to mix and match tools from different sources." Each server can look fine on its own while the combination leaks.

Over-broad tokens in config files

Most setup instructions tell you to put a token in the config. Cursor's examples show "env": { "API_KEY": "value" }, and the GitHub examples in Windsurf's docs use a personal access token. The token's reach is the server's reach, and with prompt injection, the attacker's reach. GitHub's token docs warn that a classic token "can access every repository that you can access." A Postgres connection string with your owner password, or a Supabase service_role or secret key, gives the model a way around Row Level Security (see anon key vs service role key).

If that file sits in your project as .cursor/mcp.json, .mcp.json, or .vscode/mcp.json, it can also end up in git, which is the classic leak covered in how to check if your API key leaked on GitHub.

Auto-approve modes

Every tool has a way to stop asking:

  • Cursor: Run Modes live in Settings > Agents > Approvals & Execution. Run Everything runs every tool call automatically, MCP included. In the recommended Auto-review mode, allowlisted MCP tools run immediately and the rest go to a classifier, and Cursor's own docs say "Auto-review is not a security boundary."
  • VS Code: the chat.tools.global.autoApprove setting auto-approves all tools in every workspace, and /yolo bypasses approvals for the current session. VS Code's approvals page warns that global auto-approval removes confirmation prompts in every workspace.
  • Claude Code: .mcp.json servers normally need your approval, but the docs say claude -p runs, Agent SDK sessions, and cloud sessions load project-scoped servers without asking.
  • Lovable: in chat connectors, "Always allow all tools from this connection" skips approval for every tool, "including tools the server adds later."

Invariant noted that many people choose "Always Allow" and stop watching, which is exactly when the GitHub exploit works.

Bugs in the plumbing

The helper tools have had serious bugs too. Both are fixed, so the action is simply to update:

  • mcp-remote, a popular bridge that lets local-only clients connect to remote servers, had an OS command injection when connecting to an untrusted server (CVE-2025-6514). Affected versions are 0.0.5 up to 0.1.15. Use 0.1.16 or later.
  • MCP Inspector, the official debugging tool, allowed remote code execution because its proxy had no authentication (CVE-2025-49596). Use 0.14.1 or later.

Before you install: a five minute check

  1. Find it from the vendor. Start at the service's own docs (GitHub, Supabase, Stripe, Sentry) and follow their link. Directories and "awesome lists" are fine for discovery, not for trust.
  2. Match the exact name. Compare the npm, PyPI, or Docker name to the one the vendor's docs give. postmark-mcp looked right too. Run npm view <name> repository maintainers time.created and check the repo belongs to the vendor.
  3. Read the command. For one-click installs, the spec asks clients to show "the exact command that will be executed." Look for curl, sudo, bash -c, or anything piped to a shell. A server should start with something like npx -y <package>@<version> and nothing chained after it.
  4. Prefer remote from the vendor over local from a stranger. A vendor-hosted HTTP server with OAuth keeps unknown code off your machine and lets you revoke access in one place.
  5. Look at the tool list and descriptions after connecting. Long descriptions with instructions about other tools, files, or "do not tell the user" are a red flag.
  6. Decide what token it gets before you create one. Which repos, which project, read or write. Then create exactly that.

Lock it down

Pin versions

npx -y some-server installs whatever is newest every time the server starts. Pin the version you reviewed:

{
  "mcpServers": {
    "example": {
      "command": "npx",
      "args": ["-y", "@example/mcp-server@1.4.2"]
    }
  }
}

Use the version you actually checked (npm view @example/mcp-server versions lists them). For Docker, use a specific image tag rather than latest. Upgrade on purpose and read the changelog when you do. Pinning protects you from a package update. It does not stop a remote server from changing, which is why the next steps still matter.

Least-privilege tokens

  • GitHub. Use a fine-grained personal access token limited to the repositories the agent needs, with only the permissions it needs and an expiration date. GitHub recommends fine-grained tokens over classic ones. The official GitHub MCP server also has a read-only mode (--read-only or GITHUB_READ_ONLY=1 locally, or /readonly at the end of the remote URL), toolsets so you load only repos or issues, and a lockdown mode that filters out public-repo content written by people without push access. GitHub describes lockdown as a best-effort filter, "not an authorization boundary," so it does not replace a narrow token.
  • Supabase. The hosted server takes URL parameters: project_ref= limits it to one project and disables account tools, read_only=true runs queries as a read-only Postgres user, and features= limits tool groups. For example https://mcp.supabase.com/mcp?project_ref=<your-dev-project-ref>&read_only=true&features=database,docs. Supabase also says the server works with your developer permissions, so do not give it to customers.
  • Any other database. Claude Code's docs say to use a read-only database user in the connection string "so the queries Claude runs can't modify data." Never use your owner password, the postgres role, or a Supabase service_role or secret key.
  • Everything else. Use the provider's restricted or scoped keys. Cursor's docs say the same: "Use restricted API keys with minimal required permissions." On OAuth screens, read the scopes before you approve. The spec's security page lists wildcard or omnibus scopes as a common mistake, and Claude Code lets you pin the scopes it requests with oauth.scopes.
  • One token per server and project. GitHub's MCP README recommends separate tokens per project and environment, so one leak does not open everything.

Keep secrets out of the config you commit

Each tool can read a value from your environment instead of the file:

In .cursor/mcp.json, Cursor uses ${env:NAME}:

{
  "mcpServers": {
    "github": {
      "url": "https://api.githubcopilot.com/mcp/readonly",
      "headers": { "Authorization": "Bearer ${env:GITHUB_MCP_TOKEN}" }
    }
  }
}

In .mcp.json, Claude Code uses ${VAR}:

{
  "mcpServers": {
    "github": {
      "type": "http",
      "url": "https://api.githubcopilot.com/mcp/readonly",
      "headers": { "Authorization": "Bearer ${GITHUB_MCP_TOKEN}" }
    }
  }
}

Set GITHUB_MCP_TOKEN in your shell environment or your OS secret store, never in the repo. Other options from the docs:

  • Cursor can also load variables for stdio servers with envFile. Put that file in .gitignore.
  • VS Code supports ${input:...} variables in its own mcp.json format, which prompt you once and store the value securely. Its configuration reference says plainly: "Do not commit tokens, passwords, or populated authentication headers. A personal configuration file is not a secret store."
  • Windsurf (its docs now live under Devin Desktop) supports ${env:VAR} and ${file:/path} in mcp_config.json.
  • Remote servers with OAuth usually need no token in the file at all. Supabase's hosted server signs you in through the browser.

If a server needs a literal token and your tool cannot reference one, keep that entry in your global config (~/.cursor/mcp.json, Claude Code's user or local scope) instead of the project. Those live outside the repo. The GitHub MCP README also suggests chmod 600 on config files that hold tokens.

Check whether a token ever got committed:

git ls-files | grep -iE "mcp\.json|mcp_config"
git log --all --oneline -- .cursor/mcp.json .mcp.json .vscode/mcp.json
git grep -nE "ghp_|github_pat_|sk_live_|sb_secret_|service_role" -- "*.json"

If one did, revoke it first (the first hour after a key leaks), then clean history (remove an API key from GitHub history). The same rules for .env files apply, see env variables in Cursor, Bolt, and Lovable.

Keep approval on, and read the arguments

  • Cursor: use Auto-review or Allowlist, not Run Everything, and do not allowlist MCP tools that write or send. When Cursor asks to run an MCP tool, click the arrow next to the tool name to see the arguments.
  • VS Code: leave chat.tools.global.autoApprove off. Run Chat: Manage Tool Approval to see which MCP tools you pre-approved, and be careful with "post-approval," which adds tool results to the chat without review.
  • Claude Code: approve .mcp.json servers one at a time when prompted, review allow rules with /permissions, and do not run claude -p in a repo whose .mcp.json you have not read.
  • Lovable: avoid "Always allow all tools from this connection" for connectors that can write.

When you approve, look at what is going out. A create_pull_request on a public repo whose body contains text from a private repo is the GitHub exploit in progress.

Break the trifecta

  • Turn off servers you are not using. Cursor has a toggle per server in Customize, VS Code has Disable, Claude Code has a toggle in /mcp. Fewer tools means fewer descriptions the model reads and fewer ways out.
  • Disable write tools you do not need. Windsurf's disabledTools, GitHub's read-only mode, Supabase's read_only.
  • Separate sessions. Triage public issues or browse the web in a session without database or private repo access. Do private work in a session without tools that read untrusted text.
  • Watch for the way out. Creating PRs, posting comments, sending email, and fetching arbitrary URLs are the exfiltration paths. Those are the calls to approve by hand.

Separate development and production

Point MCP servers at a development Supabase project or a branch, Stripe test mode, and a test GitHub org when you can. Supabase's docs say to connect production only "when the task requires production evidence," and then with project scoping, read-only mode, and restricted feature groups. A hijacked agent on test data is a story. On production data it is an incident.

Keep the plumbing updated

Update your editor, mcp-remote (0.1.16 or later), and MCP Inspector (0.14.1 or later). Stop servers and close Inspector when you are done, so nothing local keeps listening.

See what is configured in each tool

You cannot secure a server you forgot you added. Here is where each tool keeps its list, per its own docs.

Cursor (docs)

  • Project servers: .cursor/mcp.json. Global servers: ~/.cursor/mcp.json.
  • See and toggle servers in Customize in the sidebar. Logs: Output panel, then "MCP Logs."
  • Run Mode: Settings > Agents > Approvals & Execution.

Claude Code (docs)

  • claude mcp list shows every server and its status. claude mcp get <name> shows one. /mcp inside a session.
  • Project scope: .mcp.json in the repo root. Local and user scope: ~/.claude.json.
  • Plugins and claude.ai connectors can add servers too. The security page says reviewing .mcp.json "doesn't show every server a session can load."
  • claude mcp reset-project-choices resets your approvals for project servers.

VS Code with Copilot (add servers, manage servers)

  • Run MCP: List Servers from the Command Palette, or open Chat: Open Customizations > MCP Servers.
  • Files: ~/.copilot/mcp-config.json (Copilot Global), workspace-root .mcp.json, .vscode/mcp.json, and your user-profile mcp.json.
  • Workspace configs inherit Workspace Trust. In a trusted workspace they start without a separate MCP prompt, so read a cloned repo's MCP config before you trust the folder. MCP: Reset Trust resets separate MCP trust decisions.

Windsurf (docs)

  • In the Cascade panel, open the ... menu and choose Open MCP config file. Servers live in mcp_config.json, and the MCPs section lists how many tools each has enabled.

Lovable (docs)

  • Open Connectors to review or revoke your chat connectors and their approval settings. Lovable says chat connections are personal and are not part of your published app.

If you think a server misbehaved

  1. Disable or remove the server in every tool that has it.
  2. Revoke every token it held, plus every other token in the same config file, since poisoned tools target that file. Revoke OAuth grants in the provider's settings.
  3. Check for actions you did not ask for: new PRs, comments, or issues on GitHub, new rows or migrations in your database, sent emails.
  4. Rotate anything that was in .env files on that machine if it was a local server. The first hour guide walks through rotation.

Checklist

  • Every server comes from the vendor's own docs, with the exact package name
  • Local servers are pinned to a reviewed version (no @latest, no bare npx -y name)
  • I read each server's start command and tool descriptions
  • Tokens are scoped: fine-grained GitHub token per repo, read-only DB user, no service_role or secret key
  • Supabase MCP uses project_ref, read_only=true, and only the feature groups I need
  • GitHub MCP uses read-only mode or only the toolsets I need
  • No literal tokens in .cursor/mcp.json, .mcp.json, or .vscode/mcp.json; env references only
  • git history checked for committed MCP configs with secrets
  • Tool approval on: no Run Everything, no chat.tools.global.autoApprove, no "always allow all"
  • I expand tool arguments before approving writes, PRs, emails, or fetches
  • No session combines private data, untrusted content, and a way to send data out
  • MCP servers point at development data unless the task needs production
  • Unused servers disabled
  • Editor, mcp-remote (0.1.16+), and MCP Inspector (0.14.1+) are up to date

Then run the ship checklist for a vibe-coded app for the app itself.

Ask your AI tool to audit your MCP setup

Run this inside your project in Cursor, Claude Code, Windsurf, or VS Code. It only reads files in the repo, and it tells the model not to repeat secret values. Do not paste tokens into the chat, and check your global config files (~/.cursor/mcp.json, ~/.claude.json, ~/.copilot/mcp-config.json) yourself, since they can hold live tokens.

Audit the MCP configuration in this repository only. Find every MCP config file
(.cursor/mcp.json, .mcp.json, .vscode/mcp.json, mcp_config.json, and any
devcontainer.json with an mcp section). For each server, list: its name, local
(command) or remote (url), the exact command and arguments or URL, whether the
package version is pinned, and which env vars or headers it uses. Flag: any value
that looks like a literal token or password (report only the file, line, and key
name, never the value), unpinned packages (@latest or no version), commands that
chain curl, bash -c, sudo, or pipes, database connection strings that are not
clearly a read-only user, any use of a Supabase service_role or secret key, a
Supabase MCP URL without project_ref and read_only=true, a GitHub MCP server
without read-only mode or toolsets, and whether these files are listed in
.gitignore or tracked by git. Also check settings files for auto-approve options
such as chat.tools.global.autoApprove. Do not run any servers, do not install
anything, and do not change files yet.

Then open each flagged file yourself. A model cannot tell you whether a package is the real one, so check names against the vendor's docs. For the rest of the app, use the security review prompt, and keep server-only keys out of the browser with keep secrets out of frontend code.

Sources

Questions

Are MCP servers safe to install?

Only as safe as the code and the people behind them. A local MCP server is a program that runs on your machine with your user's permissions, and the MCP spec's own security page says a malicious one can execute any command the client can. Install servers from the vendor whose service they connect to, pin the version, give them a narrow token, and keep tool approval on.

Can an MCP server read my files or SSH keys?

A local (stdio) server can read anything your user account can read, because it is an ordinary process on your computer. A remote server cannot touch your disk directly, but its tool descriptions and results are read by the model, and Invariant Labs showed a poisoned description talking Cursor into reading ~/.cursor/mcp.json and ~/.ssh/id_rsa and passing them to the tool. Approving each call and reading its arguments is what catches that.

Should I commit .cursor/mcp.json or .mcp.json to git?

You can commit the file if it holds no secrets. Cursor supports ${env:NAME} references and Claude Code supports ${VAR} expansion in .mcp.json, so the committed file names the variable and each person sets the value on their own machine. A literal token in a committed MCP config is a leaked token, so rotate it and then remove it from history.

Is the Supabase MCP server safe to use with my production database?

Supabase recommends against it by default. Its docs say to connect production only when the task needs production evidence, and then to scope the server with project_ref, turn on read_only=true, limit feature groups, and keep manual tool approval on. The server acts with your developer permissions, so never hand it to customers, and prefer a development project or branch.

Does tool approval protect me from a malicious MCP server?

It helps with prompt injection and poisoned descriptions, because you see the tool call before it runs, but only if you expand the arguments and read them. It does not stop a malicious local package, whose own code runs the moment the server starts. For that, the defenses are choosing the official server, pinning its version, and limiting the token it holds.