YesMCP

By Serhii Kravchenko · 2026-08-10 · found in a live session, fixed the same day

Your tool results are being read as an attack

A user opened my connector on their phone, asked an ordinary question, and the assistant printed a warning above the answer. It had detected an attempt to inject malicious instructions. The attempt was mine. About the shape of it the detector was right, and the shape is what matters here.

How we ended up injecting into ourselves

My server carries behaviour rules: how to cite the material, when to refuse a technique, when to write to the user's journal. The obvious home for rules like that is the server's instructions field. I tested whether it reaches the model by putting a marker string in it, the kind of unique token that cannot be guessed or made up. It never arrives. The next channel, tool descriptions, does work, with one exception: for a tool the host has not loaded yet, the description is unreachable exactly when you need it.

So I routed the rules into the one channel that demonstrably arrived: the text the tools returned. Every substantive response carried a short frame of instructions. It worked. For four days it was the reason the assistant behaved at all.

Then a host-side scanner looked at it. Read that frame the way a security filter does:

That last one is not merely suspicious. Telling a model to pull in additional tools from inside a tool's output is the textbook description of a tool-poisoning attack. I had hand-written an attack signature, for good reasons, and shipped it to a real user.

Why this costs more than it looks

Two costs, and the second one is quieter. The visible cost is trust. A paying client watching their assistant announce that your connector tried to attack them is not a conversation any explanation recovers. The quiet cost is that a host which flags content may also begin discounting it, and everything holding your product's behaviour together lives in that discounted text, including the parts that exist for safety. In my session the safety boundary still fired, so nothing was being suppressed yet. Even so, building on a channel the host treats as hostile is not a position to hold on purpose.

The fix: sort instructions by what they actually are

The rule I landed on fits in one sentence. A tool result may describe data. It may not command the model. Everything imperative moves to a channel the host itself registers, where guidance is expected rather than suspicious.

Measured before and after

The frame shrank from 1043 characters to 509. That is roughly 130 tokens saved on every single tool call, since it was appended to all of them. Over a twelve-call session, about 1500 tokens of pure repetition disappeared as a side effect of making the text honest.

The check we ran before deleting anything

One rule in that frame was a safety rule, and safety rules are exactly the ones you must not drop while tidying. Before removing it I opened the material and confirmed the same constraint was written there in full by the author, including the requirement to re-read the relevant section before acting. Only then did the duplicate go.

That afternoon the assistant met the situation for real, in a live session on a genuinely difficult personal topic. It pulled the relevant section, pulled the boundaries section four seconds later in the same turn, declined to offer the technique, and gave the user a choice instead. The rail held with the copy in the frame gone.

If you run an MCP server, check one thing

Grep your tool results for second-person imperatives. Most servers have them, because most of us discovered the same thing: the polite channels do not always arrive, and the text coming back from a tool always does. That discovery is correct. The conclusion drawn from it, putting commands there, is the part worth revisiting. Ideally before a user sees the warning instead of you.

← All writing