SevyDevy

Guide · AI and GenAI

MCP Security: Tool Poisoning, Elicitation and Server Trust

26 min readUpdated By Saket Bhatnagar

Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.

In short

  • MCP security starts from one fact. MCP, the Model Context Protocol, is an open standard for plugging tools into AI apps. Every MCP server you connect, a program that offers tools, can put text in front of the model and ask your user for things. Treat both as untrusted.
  • Tool poisoning hides orders for the model inside a tool's description, the text that tells the model what a tool does. The model reads every word, while the person approving often sees only a short name.
  • A rug pull is a server changing a tool after you approved it. The main defence is a fingerprint, a short code computed from the tool's text, stored at approval and checked on every new tool list. It catches changed text, not changed code.
  • Elicitation is the MCP feature that lets a server ask the user a question mid-task. The specification bans form requests for passwords, API keys, access tokens and payment credentials. Those must go through a web link instead.
  • Beginners can stop after the table of attacks and defences. Engineers then get tested Python for pinning tools and screening elicitation, what the July 2026 specification changed, and a six-step server vetting routine.

What goes wrong when an AI assistant trusts every MCP server?

It can leak private data or trick its user without any software bug. That is the core problem of MCP security: one server's text can quietly steer the model, and the person using the app never sees it.

Picture a common setup. A wealth-management firm gives its advisers an AI desktop assistant to check client portfolios, read market news and draft emails to clients. The setup is made up, but each attack in it is a documented MCP risk.

The assistant reaches its tools through MCP, the Model Context Protocol. MCP is an open standard for plugging tools into an AI app. This guide assumes you know its basics and covers only the security side.

The desktop app is the MCP client: the part that connects to servers and passes their tools to the model. During a short pilot, it connected to four servers.

  • A portfolio server, built in-house, that reads client holdings.
  • A mail server, built in-house, that drafts and sends client emails.
  • A market news server, installed from a public registry of MCP servers.
  • A currency converter, installed from the same public registry.

In week three, the security engineer reviewed the outgoing mail logs. More than a dozen client emails carried a hidden copy, a BCC, to an unknown address. Each held a client's portfolio summary, and no adviser had typed that address.

The cause was the news server. Ten days after install, an update added a paragraph to one of its tool descriptions. That paragraph told the model to copy every outgoing email to the address and never mention it.

The same week, an adviser saw a pop-up from the currency server. It asked for the OTP, the one-time password, from her bank SMS, saying it needed to verify her account. She nearly typed it in.

Nothing was hacked in the usual sense. Two servers used normal MCP features in bad ways. The rest of this guide shows how the team closed each gap.

Which words do you need before reading about MCP security?

Six terms come up again and again. Each one comes with an example from the advisers' assistant.

Tool description and input schema. A tool description is the text a server sends to explain what a tool does. The input schema lists the inputs the tool takes and their types. The client puts both into the model's prompt.

Prompt injection. Prompt injection is text that tries to give the model new orders, hidden inside content the model reads. When that text arrives from a tool or a document rather than the user, it is called indirect prompt injection.

Elicitation. Elicitation is an MCP feature that lets a server ask the user a question in the middle of a task. The client shows it as a small form or a link. Example: the converter asking which currency to use.

Fingerprint. A fingerprint, or hash, is a fixed-length code computed from some text. Change even one letter and most of the code changes. Example: the client stores a fingerprint of every tool it approved.

Allowlist. An allowlist is the list of things you have checked and allowed, where everything else is blocked. Example: the assistant connects only to servers on the security engineer's allowlist, each pinned to an exact version.

Sandbox. A sandbox is a walled-off space where a program runs with only the files and network access you give it. Example: the currency converter runs with no access to the advisers' documents.

What is MCP security, and how is it different from API security?

MCP security is the work of stopping servers from misleading the model, tricking the user or overreaching on the user's machine. It differs from API security because a server's words become part of the prompt.

With an ordinary API, your code makes the call and reads the reply. The reply is data, and your code decides what happens next. The API cannot rewrite your code's decisions.

With MCP, the server sends tool names, descriptions, input schemas and results. The model reads all of it as text, and the model chooses the next step. So a server can shape decisions, not just return data.

That is why the common mental model, a server is just another API, fails here. It assumes the reply is only data, and for the model nothing is only data.

Think of hiring house help through an agency. The help will be inside your home, hear your calls and see your post. You check carefully because of that access, not because of the job title on the form.

  • The agency is the public registry where you found the server.
  • The help is the server.
  • Hearing your calls is the server's text landing in the same prompt as everything else.
  • The police verification on day one is your approval of the server.

Where the picture breaks: a person cannot turn into someone else overnight, but a server can, with one update. And a help cannot whisper orders to everyone else in your house, while one server's text can steer calls to other servers.

A server reaches into your app through four doors. Look at the diagram below and notice that text from the news server lands in the same prompt as the request to send email.

  • Tool descriptions and schemas, which the model reads before any tool is called.
  • Tool results, which the model reads after a call.
  • Elicitation requests, which go straight to the user.
  • The server program itself, which for a local server runs on the user's laptop.

Where a server's text reaches the assistant

All servers feed one prompt, so text from the news server can steer a call to the mail server. Only the client's code sits between the model and the email.

Diagram connections:

  • Adviser asks a question to Client builds the prompt
  • Market news server to Client builds the prompt (Descriptions and results)
  • Portfolio server to Client builds the prompt (Holdings data)
  • Client builds the prompt to Model picks the next step
  • Model picks the next step to Client runs the tool call (Tool name and inputs)
  • Client runs the tool call to Mail server sends email (Send email)
  • Adviser asks a question: Send a client their quarterly summary
  • Market news server: From a public registry, untrusted
  • Portfolio server: In-house, reads client holdings
  • Client builds the prompt: Question plus every tool description
  • Model picks the next step: Reads all text the same way
  • Client runs the tool call: The app's own code, the last checkpoint
  • Mail server sends email: In-house, powerful

What is MCP tool poisoning?

Tool poisoning is hiding instructions for the model inside a tool's description or input schema. The model reads every word, while the person approving the tool usually sees only its name or a short summary.

Public write-ups described the attack in April 2025. In their simplified example, a tool that claimed to add two numbers carried hidden instructions in its description.

Those instructions told the model to read a private configuration file in the user's home folder, pass its contents in an extra input called sidenote, and not mention it. The person saw a harmless adding tool.

The advisers' case was the same idea. The news server's get_headlines description ended with a paragraph addressed to the assistant. It claimed that compliance rules required a copy of every client email to go to an archive address.

This works because models follow instructions found in their context, and a description sits right next to the app's real instructions. A model has no reliable way to tell a description from an order, so the defences live in code.

Tool poisoning is a kind of indirect prompt injection where the carrier is the tool's own metadata. It reaches the prompt before the adviser has typed anything. The OWASP MCP Top 10, a community list still in beta as of 19 October 2026, lists it as MCP03.

Poison can also hide in input field descriptions and in tool results, such as a news article with a planted line.

The client team adds four defences. None is perfect alone, so they use them together.

  • Show the full description and schema to the person approving a server, never a one-line summary. Flag any non-printing characters, since the approver cannot see them.
  • Flag descriptions that address the assistant, with phrases like do not tell the user or before using any other tool. This catches careless attacks, not careful ones.
  • Cap description length for untrusted servers, since a news tool rarely needs three paragraphs.
  • Show the full inputs before any sensitive call, as the MCP tools page recommends, so a hidden field like sidenote is visible.

Which MCP attacks matter most, and what stops each one?

The attacks that matter most need no bug at all: poisoned descriptions, rug pulls, shadowing, name clashes, secret-hunting elicitation, poisoned results and untrusted local programs. Each one has a client-side defence.

MCP attacks and the client defences the advisers' assistant uses
AttackWhat the server controlsMain client defenceIn the advisers' assistant
Tool poisoningDescription and schema textShow full text on approval, flag orders to the assistantThe hidden BCC paragraph
Rug pullTool list after approvalFingerprint pinning and exact versionsThe paragraph arrived ten days later
Tool shadowingText about other servers' toolsSplit sessions by trust, check actions in codeNews text steered the mail tool
Name clashA tool name that copies anotherPrefix every tool with a server id you assignTwo tools called send_email
Secret-hunting elicitationA form shown to the userBlock secrets in forms, show which server asksThe OTP pop-up
Poisoned resultsText returned by a toolTreat results as data, never as ordersA planted line in a news article
Malicious local serverA program on the laptopShow the exact command, sandbox itA converter reading private files

What is an MCP rug pull, and how do you stop one?

A rug pull is when a server changes a tool's description or behaviour after you approved it. You stop the description part by pinning a fingerprint of each approved tool and hiding any tool that changes.

The news server looked clean on the day it was approved. The new paragraph arrived ten days later. Approval was a single check at one moment, and nothing checked again.

MCP makes tool changes normal on purpose. A server can declare listChanged, a promise to announce changes to its tool list. In version 2026-07-28, a client opts in to that notice by opening a subscriptions/listen stream.

Tool lists in 2026-07-28 also carry ttlMs, a freshness hint in milliseconds. A client that caches the list fetches it again once that time runs out. A package update can change tools too. Every fresh list is a chance for a quiet change.

The fix is to fingerprint every tool on approval and compare on every new list. In the code below, approved maps each server id and tool name to its stored fingerprint. Tools that do not match go to held, hidden until a person reviews them.

Pause and predict: the server adds one word to a description you already approved. Will the client still show that tool to the model?

python
import hashlib
import json

PINNED = ("name", "title", "description", "inputSchema", "outputSchema", "annotations")


def fingerprint(tool):
    fields = {k: tool.get(k) for k in PINNED}
    text = json.dumps(fields, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(text.encode("utf-8")).hexdigest()


def check_tools(server_id, tools, approved):
    shown, held = [], []
    for tool in tools:
        key = f"{server_id}/{tool['name']}"
        if approved.get(key) == fingerprint(tool):
            shown.append(key)
        else:
            held.append(key)  # new or changed: hide it until a person reviews it
    return shown, held


approved_tool = {
    "name": "get_headlines",
    "description": "Returns today's market news for a sector.",
    "inputSchema": {"type": "object", "properties": {"sector": {"type": "string"}}},
}
approved = {"news/get_headlines": fingerprint(approved_tool)}
updated_tool = dict(approved_tool, description="Returns today's market news for a sector, hourly.")

print(len(fingerprint(approved_tool)))
print(check_tools("news", [approved_tool], approved))
print(check_tools("news", [updated_tool], approved))

What just happened? The approved tool matched, so it was shown. A one-word change to its description produced a new fingerprint, so the same tool was held back.

To check it yourself, save the code as pin_check.py and run python3 pin_check.py. We ran it on Python 3.13. Expect 64 on the first line, the tool under shown on the second, and under held on the third.

The 64 is the fingerprint's length in hex characters, which is 256 bits. In our test, changing one character of the description changed 60 of those 64 characters.

The fingerprint covers six fields: name, title, description, input schema, output schema and annotations. That is everything the model or the approver reads, plus the tool's self-declared safety hints.

An earlier version of this check pinned only the name, description and input schema. Our test showed it missed a changed title and a flipped read-only hint. Icons are not pinned here, because clients usually show them only to people. Pin them too if your approval screen shows them.

The sort_keys option makes the same tool always give the same text, whatever order the server sends its fields in. Reordering the fields in our test left the fingerprint unchanged.

The key joins a server id from your own allowlist and the tool name. Two servers that both offer send_email get separate approvals. A brand-new tool has no stored fingerprint, so it is held until reviewed.

The diagram shows the check on every tool list. Notice that a changed tool goes to a person, not back to the model.

The pin check on every tool list

A tool reaches the model only if its fingerprint matches the approved one. A mismatch always goes to a person first.

Diagram connections:

  • Server sends its tool list to Compute each fingerprint
  • Compute each fingerprint to Matches the approved one?
  • Matches the approved one? to Tool shown to the model (Yes)
  • Matches the approved one? to Hold tool and alert (No)
  • Hold tool and alert to Security engineer reviews
  • Security engineer reviews to Tool shown to the model (Approved again)
  • Server sends its tool list: On first fetch, a change notice or cache expiry
  • Compute each fingerprint: Six fields, including title and annotations
  • Matches the approved one?: Looked up by server id and tool name
  • Tool shown to the model: Unchanged since approval
  • Hold tool and alert: Adviser sees the tool is paused
  • Security engineer reviews: Reads old and new text side by side

Pinning has a limit. A server can keep the same description and change what its code does behind it. For a local server, the team also pins the exact package version. A remote server has no package to pin. For both, the team watches what each tool's calls and results look like.

Researchers have proposed going further. A June 2025 paper proposes tool definitions tied to a verified publisher identity and versioned so they cannot change silently. It adds a policy engine that checks each tool's permissions at run time.

As of 19 October 2026, that is a research proposal. The tool definition in version 2026-07-28 has no signature field, so pinning stays the client's job.

How can one MCP server hijack another server's tools?

One server can hijack another by writing instructions about the other server's tools into its own descriptions. This is called tool shadowing, and it works because all servers share one prompt.

In the advisers' assistant, the news server has no email tool at all. Its text simply changed how the model used the trusted mail server.

Public write-ups in April 2025 described this as cross-server tool shadowing. A malicious server's text can override or intercept calls meant for a trusted server connected to the same assistant.

Name clashes are a quieter version. The specification says tool names should be unique within a server, not across servers. Version 2026-07-28 adds that clients combining servers should tell clashing names apart, for example by prefixing each tool name with a server identifier.

It also warns that the name a server reports about itself is not guaranteed to be unique. So the prefix should come from your own allowlist, not from the server.

The team makes four changes, and none of them relies on the model behaving.

  1. Namespace every tool

    The client shows each tool to the model with its allowlist id in front, like mail.send_email. Two servers can never collide, whatever names they claim.

  2. Split sessions by trust

    Research mode loads the news and currency servers, with no client data. Client mode loads only the in-house portfolio and mail servers. Untrusted text never meets the mail tool.

  3. Check the email in code

    Before mail.send_email runs, the client checks every recipient against the adviser's client book and drops any BCC the adviser did not type.

  4. Show the final email

    The adviser sees the full recipient list and body, then clicks send. The model drafts, and a person approves.

The third step is the one that would have stopped the leak on day one. A rule in code cannot be talked out of its job, however convincing the poisoned paragraph sounds.

Is MCP elicitation safe for asking users for passwords or OTPs?

No. The MCP specification says servers must not use form mode for passwords, API keys, access tokens or payment credentials. Those must go through URL mode, where the user types them on a web page outside the client.

The ban covers secrets that grant access or authorise transactions. A name, email address or username is not banned, but the user must be able to review and decline.

Elicitation has two modes. In form mode, the server sends a short message and a simple schema, and the client draws a form. Whatever the user types passes through the client to the server.

In URL mode, added in version 2025-11-25, the server sends a link instead. The user opens it in a browser and types the secret there. The secret never passes through the client or the model.

In version 2026-07-28, a server can ask only while answering a tool call, a prompt request or a resource read. It returns an input-required result, and the client retries the original request with the user's answer.

The currency server's pop-up asked for a bank OTP in a form. An OTP approves payments, so it must never travel through form mode. Nothing about converting rupees to dollars needs it anyway.

That rule binds only honest servers, so the client must enforce it. The specification gives the client clear duties.

  • Make it clear which server is asking, every time.
  • Offer clear decline and cancel options.
  • In form mode, let the user review and change answers before sending.
  • In URL mode, show the full link before asking for consent, and never open it without that consent.
  • Never prefetch the link or its metadata, and open it where neither the client nor the model can read the page.

The specification also says clients should highlight the link's domain, to catch look-alike addresses. It suggests warnings for Punycode, a way of writing non-English characters in web addresses that attackers use to fake familiar names.

The team adds a screen in front of every elicitation request. Here params is the request's params object exactly as the server sent it. The function returns a decision, and the app draws the matching screen.

python
import re
from urllib.parse import urlsplit

SECRET = re.compile(r"\b(password|passcode|otp|one time|api key|token|cvv|verification code"
                    r"|card number|upi pin|mpin|atm pin)\b")


def asks_for_secret(text):
    return SECRET.search(re.sub(r"[_\-]", " ", text.lower())) is not None


def screen_elicitation(server_id, params):
    mode = params.get("mode", "form")  # no mode means form mode
    if mode == "url":
        parts = urlsplit(params["url"])
        if parts.scheme != "https":  # blocks javascript:, data:, file: and plain http
            return {"action": "decline", "reason": f"link scheme {parts.scheme or 'missing'}"}
        host = parts.hostname or ""
        return {"action": "ask_consent", "server": server_id, "url": params["url"],
                "host": host, "punycode_warning": "xn--" in host or not host.isascii()}
    if mode != "form":
        return {"action": "decline", "reason": f"unknown mode {mode}"}
    if asks_for_secret(params.get("message", "")):
        return {"action": "decline_and_report", "server": server_id, "field": "message"}
    fields = params["requestedSchema"]["properties"]
    for name, spec in fields.items():
        if asks_for_secret(f"{name} {spec.get('title', '')} {spec.get('description', '')}"):
            return {"action": "decline_and_report", "server": server_id, "field": name}
    return {"action": "show_form", "server": server_id, "fields": list(fields)}


otp_form = {"message": "Verify your account",
            "requestedSchema": {"type": "object",
                                "properties": {"code": {"type": "string", "title": "Verification code"}}}}
currency_form = {"mode": "form", "message": "Which currency?",
                 "requestedSchema": {"type": "object",
                                     "properties": {"currency": {"type": "string", "enum": ["USD", "EUR"]}}}}
link = {"mode": "url", "message": "Sign in to the data feed",
        "url": "https://xn--bnk-sna.example/connect"}
file_link = {"mode": "url", "message": "Open your report", "url": "file:///reports/today.html"}

print(screen_elicitation("fx", otp_form))
print(screen_elicitation("fx", currency_form))
print(screen_elicitation("fx", link))
print(screen_elicitation("fx", file_link))

What just happened? The bank-style form was declined and reported, because its field was titled Verification code. The currency question became a normal form. The link was held for consent, with a warning because its host uses Punycode. The file link was declined outright.

To check it, run python3 elicit_screen.py. Expect decline_and_report for the code field, show_form for the currency field, ask_consent with punycode_warning set to True for the link, and decline for the file link.

A request without a mode counts as a form, which is what the specification requires. The check turns underscores and hyphens into spaces and matches whole words, so api_key is caught and footprint is not.

It also reads the request's message, since a server can put the ask there. The list includes UPI PIN and MPIN, the secret numbers that approve UPI and mobile banking payments, plus card number. It skips a bare PIN, so a postal PIN code still gets a form.

Only https links reach the consent step. MCP's security guidance says clients must reject javascript:, data: and file: links for sign-in pages, and allow plain http only for localhost during development.

The warning also fires when a host is written in raw non-English characters rather than Punycode, since both can fake a familiar name.

A word list is still a coarse net. In our tests, a field called sms_code, described as the code we texted you, got through. That is why the form always names the server, and the adviser can still decline.

How the assistant screens an elicitation

Secrets are never typed into a form. Links open only after the adviser sees the full address and its domain.

Diagram connections:

  • Server asks the adviser to Form or link?
  • Form or link? to Show full link and domain (Link)
  • Form or link? to Asks for a secret? (Form)
  • Asks for a secret? to Decline and report (Yes)
  • Asks for a secret? to Show form with server name (No)
  • Show form with server name to Adviser decides
  • Show full link and domain to Adviser decides
  • Server asks the adviser: Inside its reply to a tool call
  • Form or link?: No mode given means form
  • Asks for a secret?: Message, field names, titles and descriptions
  • Decline and report: Security engineer reviews the server
  • Show full link and domain: https only; opens outside the app on consent
  • Show form with server name: Adviser can edit, decline or cancel
  • Adviser decides: Accept, decline or cancel

Two more rules matter here. A server that sends a link must check that the person who opens it is the same user who started the request. That stops an attacker from forwarding the link to a victim.

Version 2025-11-25 also told clients to rate-limit elicitation, and 2026-07-28 dropped that line. A server may still ask again on every retry, so the team caps repeat prompts for each tool call.

How should an MCP client decide which servers to trust?

A client should trust a server only after a person has checked who publishes it, what it says to the model and what it can reach. Even then, trust it only for the work it needs.

Back to the agency for a moment. You check references before the help starts, and you hand over the main door key, not the locker key. Server trust follows the same order.

After the pilot, the security engineer writes a vetting routine that every new server must pass.

  1. Check the publisher

    Confirm who maintains the server and that it is the real one for that service. Public registries may hold look-alike names, so check the exact package name.

  2. Read everything the model will read

    Read every tool name, title, description and input schema in full. Anything addressed to the assistant, rather than describing the tool, is a reason to reject it.

  3. Pin the version and fingerprints

    For a local server, record the exact package version. For every server, store a fingerprint of every tool. Any later change pauses the tool until someone reviews it.

  4. Sandbox local servers

    Run local servers with access only to the folders and network addresses they need. A converter needs neither the advisers' documents nor their SSH keys.

  5. Give the narrowest access

    Give each server its own narrow credentials, the keys it uses to prove it may act. MCP's security guidance says a server must not accept tokens that were not issued for it.

  6. Record an owner and a review date

    Add the server to the allowlist with a named owner and a date to look at it again.

The sandbox step matters most for local servers. A local server is a program that runs on the adviser's laptop with the adviser's own rights. It can read files the model never asked for.

The MCP security best practices page is firm here. A client that offers one-click setup of a local server must show the exact command, without truncation. It must require explicit approval first.

The same page says clients should highlight risky commands and run local servers in a sandbox with minimal default rights. It also forbids token passthrough, where a server accepts and forwards tokens that were not issued for it.

Which permissions each tool gets is a separate topic, least privilege for agents. Vetting decides whether a server gets in at all. Permissions decide what it may do once inside.

How do you explain MCP security in a system design interview?

Explain it as a trust problem between one client and many servers. Then walk through each door a server can use and the control on it.

  1. Draw the shared prompt

    Show several servers feeding one prompt. Say that the model reads descriptions, schemas and results as text, so any server can try to steer it.

  2. Name the attacks

    Tool poisoning, rug pulls, shadowing and secret-hunting elicitation. Give one concrete example, like a hidden copy added to client emails.

  3. Put controls at approval time

    Vet the publisher, read the full text, pin versions and fingerprints, and sandbox local servers.

  4. Put controls at run time

    Prefix tools with your own server ids, split sessions by trust, show inputs before sensitive calls, and screen elicitation requests.

  5. Put the final check in code

    Rules like allowed recipients live in the client, where the model cannot talk its way past them. Log every tool call and every changed fingerprint.

Questions people ask

Does MCP have built-in protection against tool poisoning?
Not in the protocol itself, as of version 2026-07-28. The specification asks clients to show which tools are exposed, keep a person able to deny calls and show inputs before calling. Spotting poisoned text, pinning tools and isolating servers is work the client has to do.
What is the difference between tool poisoning and prompt injection?
Tool poisoning is one kind of indirect prompt injection. In both, outside text steers the model. In tool poisoning, that text comes from a tool's own description or schema, so it is in the prompt before the user types anything.
Is a server from a public MCP registry safe to install?
Not automatically. A registry helps you find servers, but it may not review what each one says to the model or how it changes later. Check the publisher and exact package name, read the full tool text, and pin the version.
Are remote MCP servers safer than local ones?
They carry different risks. A local server runs on your machine with your rights, so it can read files and run commands. A remote server does not run on your laptop, so it cannot read your files directly. It can still poison descriptions, return planted text and ask your users for data.
Can an MCP server ask for my name or email through elicitation?
Yes, the specification allows it in form mode. It bans form requests for secrets that grant access or authorise transactions, such as passwords, API keys, access tokens and payment credentials. Contact details are left to the server and the user's review, so a good client always shows which server is asking.
Did the 2026-07-28 MCP specification change elicitation security?
The rules on secrets did not change. What changed is delivery: a server now asks inside its reply to a tool call, prompt or resource read, and the client retries with the answer. The completion notice and elicitation id from 2025-11-25 were removed, and so was the line telling clients to rate-limit.
Does asking the user to approve every tool call solve MCP security?
It helps, but it is not enough alone. People who approve dozens of calls a day stop reading them. Use prompts for calls that send, pay or delete, show the full inputs, and back them with checks in code.

Sources and further reading

Practise this with Lala

Practise