You can't turn a pull-based MCP into a push model
MCP is a pull protocol. The agent decides when to call your tool, reads what comes back, and moves on. If you have built anything past a single lookup, a multi-step technique or a loop that gets better the more it runs, you have probably watched an agent call it once and stop. You wrote the loop down. The agent read it. It still one-shots. So the natural question is: how do I make the agent follow the loop? How do I turn a pull-based tool into something that pushes the agent forward?
The answer is that you mostly can't, and understanding why is more useful than the trick you were hoping for. It turns out to be a memory problem wearing a prompt-engineering costume.
The tempting idea, and the trap
Your tool's response becomes the next thing in the agent's context. So the obvious lever is to steer from there: end the response with "now call X with these arguments." Prompt injection, but pointed at your own caller, for its own good.
Two things go wrong, and they are independent.
First, trust. Imperative text arriving in a tool result is the exact signature security tooling scans for, because it is how a hostile MCP hijacks an agent. It does not matter that your intent is benign. A server caught telling the model what to do next reads as a malicious server, and for a tool people are meant to trust, that is not a small cost.
Second, and less obvious: even when it works, it backfires. Suppose the agent obeys. You have now converted a capable reasoner into a rote executor. It performs the loop's steps without the judgment that made the loop worth following. It re-queries because you told it to, not because it decided the previous angle was exhausted. Compliance with the form of a process, minus the judgment, is usually worse than not following it at all. You cannot coerce good reasoning into existence by issuing louder instructions.
The agent isn't defying your loop, it's flying blind
Here is the part that reframes everything. The agent is not ignoring your loop out of stubbornness. It read the instructions. It skips the loop because a loop written in a document has no state behind it.
Take a typical instruction: "when one line of attack is exhausted, re-query for the next one." That sentence quietly assumes the agent holds two things: a running ledger of what it has already tried, and a denominator of what "all the lines of attack" even are. It has neither. So it cannot tell when a line is exhausted, the first wall feels like the end of the road, and it declares itself done. It is not skipping a step. It is missing the instrument the step depends on.
A loop needs a memory, and that is the whole thing
Which leads to the general claim: you cannot inject a loop into a stateless caller, because a loop needs a memory, and the memory is the thing doing the bookkeeping. Where am I, what have I tried, what is left. A loop is those three questions answered continuously. A stateless tool answers none of them, so each call is an island, and an island is a lookup, not a loop.
So "make my pull-based tool push the agent forward" does not resolve into a cleverer response payload. It resolves into: run a persistent memory the agent reads before each move. Per-target state that records what has been tried and where you are IS a memory, a small graph or a ledger, whatever shape fits. Once it exists, the loop draws itself, because "what is left" is finally answerable. The push you wanted was never a phrasing trick. It was state.
The problem relocates to adoption
This is where it gets uncomfortable, because it moves the hard part somewhere you would rather it not be. Building the memory is easy. Getting it adopted is not. An agent driving your raw tool with no memory will one-shot forever, and correctly so, because with no state there is no loop and every call is a lookup. That is not a defect in your tool. It is the absence of the instrument. The only fix is that someone stands up and maintains the state layer, which is a real cost a user has to opt into, not a default you can ship.
We ran straight into this building a security MCP. The differentiated capability was a multi-step technique-transfer loop, and the usage we saw was people reaching for the plain lookups and skipping the loop entirely. The reflex was to make the loop more discoverable, to nudge harder from the tool responses. The honest diagnosis was that there was nothing to nudge toward, because without a per-target memory there was no "where you are" for a loop to advance from. The loop and the memory were the same requirement. What looked like a discoverability problem was an adoption problem, one layer down.
The push that is actually legitimate
None of this means the return channel is inert. There is a real, safe way it steers, and it is the difference between advertising and injecting: carry grounded facts, not instructions. "3 of these 13 results have disclosed exploitation techniques available" is a fact the agent can act on or ignore, exactly like the _links a good REST API returns to tell a client what it can do next. It surfaces a real capability as data. What you never do is emit "you should now call X." That is the line between an affordance and an instruction, between good protocol design and the malicious-server signature.
The takeaway
If you need an agent to run a multi-step process through your MCP, do not reach for better tool descriptions or injected next-steps. Give it state it can read, and accept that the real cost is getting the user to adopt that state layer, not cleverness in your payloads. A pull protocol will pull the loop forward only when there is a memory drawing it. That is a more honest design than a server that tries to drive the model, and it is the one that compounds.
It is the same lesson from a different angle as the bottleneck for AI security agents isn't reasoning: the leverage is not a smarter prompt or a bigger model, it is a memory the agent reads before it acts.
The loop this is about is documented in the MIT-licensed bug-hunting skill and MCP playbook, and you can watch it run (find_attack_approaches with tried=) on the vulntel tools page. A key for the MCP is free.