Building a news agent
Connecting an agent to the endpoint takes two minutes. Making it good takes a system prompt and two budget decisions.
1. Connect
import { query } from "@anthropic-ai/claude-agent-sdk";
const options = {
mcpServers: {
unzoi: {
type: "http" as const,
url: "https://api.unzoi.com/mcp",
headers: { "x-api-key": process.env.UNZOI_KEY! },
},
},
}; Any framework works the same way — LangChain, LlamaIndex, the OpenAI Agents SDK. What follows is about the agent, not the plumbing.
2. The system prompt
The tool descriptions already tell the model what each tool does. What they cannot know is your application's priorities. Four lines cover almost everything:
You have access to a global news index through the unzoi tools.
Prefer list_stories over search_news. One row per event, with the outlets that
covered it, is almost always more useful than forty near-identical articles —
and the outlets count tells the user how much attention something got.
Always pass a time range. "from" defaults to your plan's window, which may be
narrower than you expect; if a result comes back with from_clamped true, say so
rather than reporting that there is no coverage.
Cite outlets by name and link the url from the result. Never state a fact from
these tools without attributing it to a source in the results. Each paragraph is fixing a specific failure:
-
Without the first, the agent uses
search_newsby default, fills its context with duplicates, and loses the attention signal entirely. - Without the second, it reports "I found no coverage" when it actually hit a plan boundary.
- Without the third, it summarises the news into ungrounded assertions — the failure that makes a news agent untrustworthy rather than merely wrong.
3. Restrict the tools
Every tool you leave enabled is a tool the agent can spend quota on.
allowedTools: [
"mcp__unzoi__list_stories",
"mcp__unzoi__search_news",
"mcp__unzoi__get_article",
"mcp__unzoi__find_related",
],
// Omitted: top_headlines, which a question-answering agent rarely needs, and
// account_status, which is for your code rather than for the model.
Tool names are namespaced mcp__<server>__<tool> by most SDKs; a bare name in an allow-list
silently matches nothing.
4. Cap the loop
const result = query({
prompt: userQuestion,
options: { ...options, maxTurns: 8 },
}); The shape that works
The strongest pattern is narrow-then-deepen, and the agent will follow it if the tools are available:
list_storieswith a time range — what happened.search_newsfiltered bystory_id— how one of those was covered.get_article— the full record for a specific piece, including quotations.find_related— what else is adjacent, without inventing a new query.
Four calls, each narrowing. The alternative — repeated broad searches with slightly different wording — costs more and returns overlapping results, because it is asking the same question in synonyms.
5. Handle the refusals
Two failures arrive as tool errors on an open session rather than as transport errors: a suspended account and an exhausted quota. Your agent will see them as tool output. Surface them:
If a tool returns an error containing "quota_exceeded" or "suspended", stop
searching and tell the user the account needs attention — do not retry, and do
not answer from memory as though you had searched. The last clause matters more than it looks. A model that cannot search will otherwise answer from training data and present it with the confidence of a fresh search.
6. Check it actually works
Ask it three questions with known-different shapes and watch which tools it reaches for:
| Question | Should use |
|---|---|
| "What happened with X this week?" | list_stories |
| "How did European outlets frame X?" | search_news with publisher_country |
| "What is happening right now in Y?" | top_headlines, if you enabled it |
If it reaches for search_news every time, the first paragraph of your system prompt is not landing.