Dayward AI
Week 1 · D5About 6 hours

Writing an MCP Client: Discovering and Calling Tools Inside Your Own Agent Loop, Multi-Server Aggregation and Name Collisions

Switch to the client side: hand-write a client that discovers tools, translates them into a shape the model can understand, and feeds results back into the loop, then handle name collisions and failures when aggregating multiple servers.

Today's goals 0/3

Sign in to tick these off and save your progress.

Today's Goals

  1. Hand-write a stdio client that completes discovery, invocation, and error handling
  2. Translate MCP tool definitions into a model API's tool parameters, and point out where the two sides' names differ
  3. Design a naming-prefix and degradation strategy for aggregating multiple servers, so one server going down doesn't take the whole loop with it

For four days you wrote servers: registering tools, exposing resources, moving to the public internet, adding authorization. Today you move to the other side of the table. That order is deliberate — only after implementing everything a server can return do you know how many situations a client must catch. Come back and tick off the three goals above.

Plain-Language Walkthrough

What a client actually does

First, dispel a misconception: many people think an MCP client is a forwarder, handing the model's words to the server verbatim and the server's words back verbatim. Write one and you find the forwarding part is a few dozen lines and everything else is boundaries.

Back to the universal adapter metaphor. The server is the appliance, the protocol is the plug standard, and the client is the protected socket panel on your wall: it recognizes plugs, it limits current, and when one appliance starts smoking it trips only its own circuit rather than blacking out the building.

Concretely, three boundaries.

The first is the boundary of shape. An MCP tool definition and a model API's tool parameters are not the same thing. The field names differ, the naming rules differ, and what they can express differs. A translation has to sit in between, and a translation is always lossy — you need to know where.

The second is the boundary of naming. One host connected to five servers at once is normal, and the spec guarantees tool name uniqueness only within a single server. Two servers each having a search is inevitable, not an accident. Who disambiguates? Only the client can.

The third is the boundary of failure. A server runs in somebody else's process on somebody else's machine. It will time out, it will crash, a user will delete it by accident. Those failures must be held outside the agent loop — one server going down should at worst mean a few fewer tools, not a failed conversational turn.

Those three boundaries are exactly the three pieces of code to write today.

Discovery and translation

Discovery has two steps: server/discover to ask which version you support and what capabilities you have, then tools/list to fetch all the tools.

The first step is optional in this revision. The spec requires every server to implement server/discover, but a client need not call it — because the version, identity, and capabilities are in every request's _meta, so you can send tools/list straight away, and a server that does not support your version returns -32022 with the versions it supports for you to pick and resend. The benefit of calling server/discover is asking everything at once and saving a round of trial and error; in an implementation I recommend calling it, because the return also carries instructions and caching hints.

The second step has a pothole: tools/list is paginated. Page size is the server's call, and while your local server sends everything in one page, the one the user installed may take ten. The criterion is one line — only a missing nextCursor is the end. Do not guess from this page having fewer than a page size, since the spec does not guarantee a server fills a page; and certainly do not parse the cursor, which is opaque to the client.

With the tools in hand comes translation. MCP calls it inputSchema while the model API may call it input_schema or parameters; MCP has title, annotations, and outputSchema while the model API usually has none of them.

discover-and-translate.ts
// One: page through everything. The only criterion is that a missing nextCursor is the end
async function listAllTools(client: StdioClient): Promise<McpToolDef[]> {
  const tools: McpToolDef[] = []
  let cursor: string | undefined
  for (let page = 0; page < 50; page++) {
    // The cursor is opaque to the client: return it verbatim, never parse or construct one
    const result = await client.request('tools/list', cursor === undefined ? {} : { cursor })
    tools.push(...(result.tools as McpToolDef[]))
    if (typeof result.nextCursor !== 'string') break // an empty array is not the end; a missing nextCursor is
    cursor = result.nextCursor
  }
  return tools
}
 
// Two: translate. More is lost than it looks; see the table below
function toModelTool(alias: string, def: McpToolDef) {
  return {
    name: modelToolName(alias, def.name),
    description: `[from ${alias}] ${def.description ?? def.name}`,
    input_schema: def.inputSchema,
  }
}

Three things get lost in translation, each with consequences:

What is lostConsequenceWhat the client should do
annotationsThe model does not know which tool is destructiveThe client decides whether to raise a confirmation from the annotations itself
outputSchemaDownstream code can only parse proseValidate structured returns yourself rather than handing them to the model to parse again
titleNo human-readable name in the interfaceUse title in the interface and still give the model the description

The annotations row needs saying separately. The spec is hard: a client must treat tool annotations as untrusted input unless they come from a trusted server. That is, readOnlyHint: true is not proof that a tool is safe; it is only the server's statement about itself. Treating it as a permission is letting the callee issue its own pass. Why that matters so much is tomorrow's whole subject.

Wiring calls back into the agent loop

With translation done, what remains is the loop from the 30-day course, unchanged.

question messages + tool table I want to call mcp__notes__search tools/call name=search content + isError put the result back as a message final answer answer User Agent loop Model MCP server
Mermaid source
mermaidmermaid
sequenceDiagram
  participant U as User
  participant L as Agent loop
  participant M as Model
  participant S as MCP server
  U->>L: question
  L->>M: messages + tool table
  M-->>L: I want to call mcp__notes__search
  L->>S: tools/call name=search
  S-->>L: content + isError
  L->>M: put the result back as a message
  M-->>L: final answer
  L-->>U: answer

Three things are worth watching.

One tool call takes two steps in the loop. The model's output is one step, and asking the model again after feeding the result back is another. So the loop must have a hard maximum step count: a model can perfectly well ping-pong between two tools, and without that gate you get an infinite loop and a bill.

Feeding back distinguishes two error classes. That is day 2's division, which on the client side becomes two ways of writing: a protocol error (the tool does not exist, the arguments do not fit the structure, the version is unsupported) is a bug in the program the model cannot fix, so throw it; a tool execution error (a successful response with isError true) is written for the model, so feed it back verbatim and let it try different arguments. The spec's wording is precise: a client may show a protocol error to the model and should show a tool execution error to it.

The most common bug I have seen is this one inverted — somebody takes the shortcut of throwing on isError too, so the model never receives "your date format is wrong, today is such-and-such" and can only watch a self-healable call turn into a failure.

One more class of failure must be swallowed. A nonexistent tool name, a server already offline, a call that timed out — none should crash the loop. The right approach is translating them into an isError tool result so the model can see it and has the chance to change tack.

Multi-server aggregation: which layer the prefix belongs to

Now wire in two servers: a note library and a ticketing system. Each has a tool called search.

The spec's position is very clear: tool name uniqueness holds only within a single server; a client or proxy aggregating several servers may meet collisions and should implement a disambiguation strategy, such as prefixing the tool name with a server identifier. Then comes a crucial restriction: a server's self-reported serverInfo.name is not guaranteed unique across servers and should not be used to disambiguate.

That sentence nails the answer: the prefix must come from the client's own configuration. The user gives each server a local alias in a configuration file, and a duplicate alias is an error at startup — that is a configuration error and must blow up the first time the user runs it rather than waiting for the model to call the wrong tool.

How the name is assembled depends on the model API's rules too. MCP's allowed character set for a tool name is letters, digits, underscores, hyphens, and dots, with 128 characters recommended as the limit; a model API is usually stricter, allowing only alphanumerics, underscores, and hyphens up to 64 characters. Take the intersection, and this course settles on the shape mcp__alias__toolname. Over-long names get truncated with a short hash appended — truncation itself manufactures new collisions, and the hash is what restores uniqueness.

Then the most easily mis-written step: what goes back to the server on a call must be the original name. The prefixed name circulates only between the client and the model, and the server does not recognize it at all.

registry.ts
// Reverse lookup goes only through this table. Never split the string back into alias and original name:
// original tool names may legitimately contain underscores (read_note, close_ticket), and truncated ones cannot be split at all
const entries = new Map<string, Entry>()
 
async function call(modelName: string, args: Record<string, unknown>) {
  const entry = entries.get(modelName)
  if (!entry) {
    // Do not throw: translate into an isError result so the model can see it and change tack
    return { isError: true, content: [{ type: 'text', text: `there is no tool named ${modelName}` }] }
  }
  if (down.has(entry.alias)) {
    return { isError: true, content: [{ type: 'text', text: `the server ${entry.alias} is currently unavailable` }] }
  }
  try {
    // Note: what goes to the server is entry.toolName (the original), not the prefixed modelName
    return await clients[entry.alias].request('tools/call', { name: entry.toolName, arguments: args })
  } catch (error) {
    return { isError: true, content: [{ type: 'text', text: `the call failed: ${String(error)}` }] }
  }
}

One small practical trick: write the provenance into the description. The name prefix is for the program, and the description is the model's only basis for picking a tool. Prefixing the description with "from the ticketing system" gets the model to the right tool more reliably than burying a prefix in the name.

Failure isolation

The aggregation layer's most worthwhile investment is not naming but failure.

The principle in one line: try/catch per server during discovery, and never throw during invocation.

During discovery, wrap each server's server/discover plus tools/list in one catch. On failure do two things: record the reason in an offline table, then continue to the next. Record the reason rather than only a boolean — afterwards you have to be able to answer what is missing and why, and you can put that sentence into the system prompt so the model knows the ticketing system is unavailable right now.

During invocation, as covered above: translate every failure into an isError result.

One more thing must be done and is often forgotten: every request needs a timeout. Over stdio, a server that does not answer never answers, that await hangs forever, and the whole agent is stuck. There is no standard answer for the timeout value, but having no timeout is always wrong.

When there are too many tools

Finally, the question you will inevitably meet: seven or eight servers connected, the tool table swollen to over a hundred, all of it in the context, eating most of the window before the user has said anything.

At MCP's layer there is only one thing to do: attach on demand. The official client best practices call it extending progressive discovery from the tool level to the server level — the host keeps a catalog of which servers are available and only actually connects to a server once the model judges it necessary, disconnecting afterwards to give the context back. That works especially well for a general-purpose agent, where what the user wants is unknown in advance.

Beyond that it is no longer MCP's business: whether tool definitions get injected on demand, how over-long results are pruned, when to switch to having the model write code that calls tools — that is context engineering's home turf, so see managing context for tool results and retrieval. This course is responsible only for covering MCP's own layer of interfaces cleanly.

One cross-boundary pothole is worth noting now: adding and removing tools dynamically destroys the prompt cache. Most model services cache a prompt prefix, the tool table is in that prefix, and adding a tool midway invalidates the whole cache — the definition tokens you saved do not cover the loss. So either append newly discovered definitions after the cache breakpoint, or expose only one stable forwarding tool so the table never changes.

Source Reading

Hands-On Lab

🧪 D5 lab: a hand-written client and agent loop aggregating several MCP servers

Code location: labs/mcp-7days/day-05-agent-mcp-client

Acceptance criteria:

  1. All 4 exercise points in starter are completed, and MOCK=1 pnpm start shows 9 of 9 self-checks green with exit code 0
  2. Item 3 shows all 3 of the note library's tools, proving you detect the end by nextCursor disappearing
  3. Item 5 shows both search tools still reverse-resolving to their original names, with the original name sent to the server on a call
  4. Item 8 shows the ticketing server in the offline table with the note library still usable, and calling an offline server's tool returns isError rather than throwing
  5. pnpm typecheck prints nothing

The lab directory's servers/ holds two ready-made servers, dependency-free and offline, and they are not exercises but what you connect to. They were deliberately broken in two ways: the note library's tools/list returns only 2 tools per page, and the ticketing system has a search colliding with the note library's. Run as-is, starter shows 4 green and 5 red, and your job is turning those 5 green.

  1. Read the solution's stdio-client.ts first, finding the start, request, and close phases, and especially how onLine matches responses back to requests by id.
  2. Add the error branch to the starter's onLine, run the self-test, and watch item 2 go from red to green — before that fix it waits until a timeout to report, which is the hardest kind of slowness to diagnose.
  3. Change listAllTools to loop through pages, then change modelToolName to prefix with the alias and truncate over-long names with a hash, turning items 3 and 5 green.
  4. Add try/catch around refresh's per-server loop, record the failure reason in the offline table, and watch item 8 go green.
  5. Finally, run a full agent loop with the QUESTION environment variable and watch the model select the prefixed name while the original name goes to the server.

Interview Questions

Today's 3 questions are in the question bank below, weighted toward name collisions in multi-server aggregation, handling a single server's failure, and the information lost when translating tool definitions. Expand each one and read the analysis before the key points — practicing the derivation beats memorizing bullets. The high-frequency labels for the domestic and overseas markets are there so you can pick by target market.

Checklist and Tomorrow

  • Hand-write a stdio client that completes discovery, invocation, and error handling
  • Translate MCP tool definitions into a model API's tool parameters, and point out where the two sides' names differ
  • Design a naming-prefix and degradation strategy for aggregating multiple servers, so one server going down doesn't take the whole loop with it
  • Explain why a server's self-reported name cannot be the prefix, and why reverse lookup must use a table rather than splitting a string
  • All 5 acceptance criteria of the lab pass
  • Answer at least 2 of the 3 interview questions without looking at the key points

Tomorrow (D6) covers security and governance. You already met one sentence today: a client must treat tool annotations as untrusted input. Tomorrow pushes that to its conclusion — tool descriptions, tool returns, state handles, tokens, and local servers, five entry points in all, what each can produce and how each is plugged. The client you wrote today becomes tomorrow's target: you will watch, with your own eyes, an instruction hidden inside a tool description travel untouched into the model's context.

Interview questions

  • Your client is connected to five MCP servers and two of them expose a tool called search. How do you merge them into one tool list for the model, and why can't you just prefix with the server's name?你的客户端同时连了五个 MCP 服务端,其中两个都有一个叫 search 的工具。合并成一张工具表给模型时,重名该怎么处理?为什么不能直接拿服务端名做前缀?
    Common in ChinaCommon overseasIntermediate#client#tool-naming

    How to reason about it · think before answering

    1. The screen is whether you have actually aggregated multiple servers. 'Add a prefix' is half the answer; the real question is the second half, why the server's own name will not do.
    2. Set the premise straight: the spec guarantees tool-name uniqueness only within a single server, and explicitly says clients or proxies that aggregate multiple servers may hit collisions and should implement a disambiguation strategy. Collisions are permitted by design, and disambiguation is the client's job.
    3. Now the second half: the name a server reports in serverInfo is not guaranteed to be unique across servers, and the spec says it should not be relied upon for disambiguation. The server fills it in itself, two unrelated servers may both call themselves github, and worse, it is untrusted input, so a malicious server can impersonate another. The prefix must come from the client's own configuration, a local alias the user assigns per server, with duplicate aliases rejected at startup as a configuration error.
    4. Then the naming mechanics. MCP allows letters, digits, underscore, hyphen and dot with a suggested 128-character limit; model APIs are usually stricter, often letters, digits, underscore and hyphen with a 64-character cap. Take the intersection, and on overflow truncate plus append a short hash — say why: truncation itself creates new collisions, and the hash restores uniqueness.
    5. The conclusion, and the most likely follow-up: keep a reverse map from the prefixed name back to server plus original tool name. The call sent to the server must carry the original name, since the server has never heard of the prefixed one. Never recover it by string splitting, because original names may legitimately contain underscores and truncated names cannot be split back at all.
    6. Likely follow-ups: is the prefix enough? No, the model chooses by description, so put the source in the description too. And what about list changes? Servers declaring listChanged send a notification, on which the client refetches and rebuilds the map, keeping in mind that churning the tool list invalidates prompt caching because the tool array sits in the cached prefix.

    分析过程 · 先想清楚再作答

    1. 这题在筛「有没有真的聚合过多个服务端」。只答「加个前缀」能拿一半分,题眼在后半句——为什么不能用服务端自报的那个名字。
    2. 先把前提摆正:规范只保证工具名在**单个服务端内**唯一,并且明确说聚合多个服务端的客户端或代理可能遇到重名,应当实现一套消歧策略。也就是说重名不是异常情况,是设计上就允许的,消歧责任在客户端这一层,服务端管不着。
    3. 再答后半句:服务端在 serverInfo 里自报的 name **不保证跨服务端唯一**,规范明说不应当拿它来消歧。它由服务端自己填,两个不相干的服务端都叫 github 完全合法;更糟的是它是不可信输入,一个恶意服务端可以故意把自己报成别人的名字,让模型把请求发到错的地方。所以前缀必须来自客户端自己的配置——用户在配置文件里给每个服务端起的本地别名,别名重复时在启动阶段直接报错,因为那是配置错误。
    4. 接着说名字怎么拼。MCP 允许字母数字下划线连字符和点、长度建议 128 以内;模型 API 那边通常更严,比如只允许字母数字下划线连字符、最长 64。取交集,超长就截断并缀一段短哈希——要主动说出为什么加哈希:截断本身会制造新的重名,哈希是把唯一性补回来的。
    5. 结论也是最容易被追问的一条:客户端必须留一张反查表,从带前缀的名字映射回「哪个服务端 + 原来的工具名」。调用时发给服务端的必须是**原名**,服务端根本不认识带前缀的那个。绝不能靠切字符串反推,因为工具原名里本来就允许有下划线,截断过的名字更是拆不回来。
    6. 可预期的追问一:光靠名字前缀够不够?答不够,模型选工具看的是描述,所以还应当把来源写进描述里。追问二:工具列表变了怎么办?服务端支持 listChanged 时会发通知,客户端收到就重新拉取并重建反查表;同时注意频繁增删工具会打掉提示缓存,因为工具表在缓存前缀里。

    Key points

    • Uniqueness holds only within one server; collisions are expected on aggregation and the client owns disambiguation
    • The prefix must come from a client-configured local alias, since the server-reported name is neither unique nor trustworthy
    • Build names from the intersection of MCP and model-API charset and length limits; truncate plus a short hash on overflow
    • Keep a reverse map and send the original name on calls; never split the prefixed string, as original names contain underscores

    答题要点

    • 唯一性只在单个服务端内成立,聚合时重名是设计允许的,消歧责任在客户端
    • 前缀必须来自客户端配置的本地别名,服务端自报的 name 不保证唯一且是不可信输入
    • 名字取 MCP 与模型 API 的字符集与长度交集,超长截断并缀短哈希补回唯一性
    • 留一张反查表,调用时发原名;不能靠切字符串反推,工具原名里本来就有下划线
  • One of your MCP servers times out in production. How should your agent loop react?线上一个 MCP 服务端超时了。你的 Agent 循环应该怎么反应?
    Common in ChinaCommon overseasIntermediate#client#reliability

    How to reason about it · think before answering

    1. This probes engineering instinct: can you separate 'one dependency is down' from 'this turn fails'. Answering 'retry three times' just moves the problem, and the interviewer will ask what the user is staring at meanwhile.
    2. Split by phase first. A timeout happens at two very different moments: discovery (server/discover or tools/list) and invocation (tools/call). The correct reaction differs, and blurring them shows you have not built this.
    3. Discovery: wrap each server in its own try/catch, record the failure with its reason in a down list, and continue to the next server. The tool table loses a few entries but the loop still starts. Record the reason, not a boolean, because afterwards you must be able to say what is missing and why.
    4. Invocation: translate the failure into a tool result with isError true and feed it back to the model rather than throwing. MCP already uses isError for 'the tool failed but the protocol succeeded', so the model can switch tools or arguments; throwing kills the turn and leaves the user with no explanation.
    5. Then three supporting points. Every request needs a timeout, because on stdio a silent server is silent forever. Idempotency decides whether a retry is safe, and the idempotentHint annotation is a hint, not a guarantee, so writes need a client-side dedup key. And outages must be visible, surfaced in the UI or in the system prompt, or the model will behave as if the capability never existed and confidently report nothing found.
    6. Conclusion: the worst outcome of one server timing out should be a few missing tools plus an explicit note, never a failed turn.
    7. Likely follow-ups: should you add a circuit breaker? Yes, after consecutive failures mark the server unusable for a while so you stop paying a timeout every turn, with recovery by health check or reconnect on the next session. And how do you set the timeout? Per tool rather than per server, since a thirty-second analysis tool and a cache lookup should not share a threshold.

    分析过程 · 先想清楚再作答

    1. 这题看的是工程直觉:能不能把「一个依赖挂了」和「这一轮对话失败」分开。答「重试三次」是把问题往后推了一步,面试官会立刻追问重试期间用户在等什么。
    2. 先分阶段。超时发生在两个完全不同的时刻:发现阶段(server/discover 或 tools/list)和调用阶段(tools/call)。两个阶段的正确反应不一样,混着答就会露怯。
    3. 发现阶段:逐个服务端 try/catch,失败的记进一张掉线表并继续下一个。整张工具表少几个工具,但循环照常起得来。记的必须是原因而不是一个布尔值,因为事后你要能回答少了什么、为什么少。
    4. 调用阶段:把失败翻译成一条 isError 为真的工具结果喂回模型,不要抛。理由是 MCP 本来就用 isError 表达「工具执行失败但协议是成功的」,模型看得见这句话就有机会换个工具或换个参数;抛出去只会把整轮对话打断,而且用户什么解释都得不到。
    5. 接着补三件配套的事。一是**每条请求都必须有超时**,stdio 上服务端不回你就永远不回;二是**幂等性决定能不能重试**,工具注解里的 idempotentHint 是提示不是保证,写操作的重试要靠客户端自己的去重键;三是**掉线要让用户看得见**,把掉线的服务端标在界面上或写进系统提示,否则模型会表现得像那个能力从来不存在,一本正经地说查不到。
    6. 结论:一个服务端超时,最坏的后果应该是少几个工具加一条明确的说明,而不是这一轮对话失败。
    7. 可预期的追问:要不要熔断?连续失败到阈值就把这个服务端标记为不可用一段时间,避免每一轮都白等一次超时;恢复用探活或下一次会话重连。再追问会问到超时值怎么定——按工具而不是按服务端定,一个跑三十秒的分析工具和一个查缓存的工具不该共用一个阈值。

    Key points

    • Split by phase: per-server try/catch during discovery with a recorded reason, and never throw during invocation
    • Translate call failures into isError tool results so the model can switch tools or arguments
    • Every request needs a timeout; retry safety depends on idempotency, and the annotation is a hint, not a guarantee
    • Outages must be visible to user and model, otherwise silent degradation makes the model deny the capability ever existed

    答题要点

    • 分阶段:发现阶段逐个服务端 try/catch 记进掉线表并继续,调用阶段一律不抛
    • 调用失败翻译成 isError 为真的工具结果喂回模型,让它换工具或换参数
    • 每条请求必须设超时;能不能重试取决于幂等性,注解只是提示不是保证
    • 掉线必须对用户和模型可见,否则会变成静默降级,模型会假装那个能力不存在
  • When translating an MCP tool definition into a model API's tool parameters, what is most easily lost, and what goes wrong when it is?把 MCP 的工具定义翻译成模型 API 的工具参数时,最容易丢掉的是什么?丢了会怎样?
    Common in ChinaCommon overseasDeep dive#client#tool-schema

    How to reason about it · think before answering

    1. This checks whether you know the step is lossy. Anyone answering 'just map the field names' has probably not written a client, because several parts of an MCP tool definition have no home on the model-API side.
    2. List first, then consequences. Three things go missing: annotations (readOnlyHint, destructiveHint, idempotentHint), outputSchema, and title. A fourth, often overlooked, is pagination — taking only the first page of tools/list silently drops whole batches of tools.
    3. Consequences one by one. Without annotations the model cannot tell which tool is destructive and the client has nothing to base a confirmation prompt on, so confirmation logic must live in the client and read the annotations directly. Without outputSchema, downstream code parses natural language, and code-mode generation cannot produce accurate return types. Without title, the UI can only show a prefixed machine name.
    4. Volunteer the most important caveat: annotations are untrusted input. The spec requires clients to treat tool annotations as untrusted unless they come from trusted servers. readOnlyHint being true is not proof of safety, only the server's own claim, so annotations may drive whether you ask the user, never whether the caller is authorized.
    5. Conclusion: the right posture is to know exactly what you dropped and compensate in the client — confirmation driven by annotations, structured results validated by you, title for the UI and description for the model, and pagination followed until nextCursor disappears.
    6. Likely follow-ups: may you rewrite the description? Light augmentation is fine, such as prefixing the source to help disambiguate collisions, but do not rewrite the meaning, since the description is what the server author tuned and their only lever on model choice. And what if outputSchema is absent? The official guidance is to accept a generic type and move on, or extract a typed result with a fast model outside loops and validate it.

    分析过程 · 先想清楚再作答

    1. 这题在考「你知不知道这一步是有损的」。答「字段名对一下就行」的人多半没写过客户端,因为 MCP 的工具定义里有好几样东西在模型 API 那边根本没有对应位置。
    2. 先列清单再讲后果。丢的主要是三样:annotations(readOnlyHint、destructiveHint、idempotentHint 这类行为提示)、outputSchema(结构化返回的形状)、title(给人看的名字)。另外还有一样常被忽略的是分页——只取 tools/list 第一页等于把后面的工具整批丢掉。
    3. 逐条讲后果。annotations 丢了,模型不知道哪个工具是破坏性的,客户端也就没法自动决定要不要弹确认框——所以确认逻辑必须由客户端按注解自己做,不能指望模型自觉。outputSchema 丢了,下游只能靠解析自然语言拿数据,而且做代码模式(让模型写代码调工具)时生成不出准确的返回类型。title 丢了,界面上只能显示一串带前缀的机器名。
    4. 这里必须主动补一句最重要的:**注解本身是不可信输入**。规范要求客户端把工具注解当成不可信的,除非来自可信服务端。readOnlyHint 为真不是「这个工具安全」的证明,它只是服务端的自我声明。所以注解可以用来决定 UI 上要不要多问一句,但不能拿它当权限判据。
    5. 结论:翻译这一步的正确心态是「知道自己丢了什么,并在客户端补回来」。补法是——确认与拦截由客户端按注解做、结构化返回自己校验、界面用 title 而给模型用 description、分页翻到 nextCursor 消失为止。
    6. 可预期的追问:description 要不要改写?可以适度加工,比如在前面缀一句来源说明帮助模型在重名时选对,但不要重写语义——描述是服务端作者调过的,也是他们唯一能影响模型选择的地方。再追问可能是「outputSchema 缺失怎么办」,官方建议先用泛型接住往下游传,真需要类型时用一个小模型做一次抽取并校验,别在循环里做。

    Key points

    • Annotations, outputSchema and title are lost, plus every tool past the first page if pagination is ignored
    • Without annotations there is nothing to drive a confirmation prompt, so that logic must live in the client
    • Annotations are untrusted input: they may drive UI prompts, never authorization decisions
    • Losing outputSchema forces downstream natural-language parsing; you may prefix the description but must not rewrite its meaning

    答题要点

    • 丢的是 annotations、outputSchema、title,外加只取第一页时整批丢掉的工具
    • annotations 丢了就没法决定要不要弹确认框,确认逻辑必须由客户端按注解自己做
    • 注解是不可信输入,只能驱动 UI 提示,不能当权限判据
    • outputSchema 丢了下游只能解析自然语言;description 可以缀来源但不要重写语义

Comments