Why a Protocol: the Host/Client/Server Triangle, JSON-RPC Messages, and Three Primitives
Start with the specific duplicated work MCP actually removes, then map the host, client, and server roles onto the tool, resource, and prompt primitives, and finish by reading a real JSON-RPC message field by field.
Today's Goals
- State in one sentence that MCP solves the M-times-N wiring problem, and name one situation where you should not use MCP
- Draw the relationship between host, client, and server, and explain why one client connects to exactly one server
- Read a tools/call request and response field by field, and point out where the protocol version and client capabilities live
This course assumes you have hand-written an agent loop once and know how a model returning a tool call, a program executing it, and the result being fed back all fit together. If not, no problem — day 2 of the 30-day course has the whole hand-written process and is worth filling in first: hand-writing an agent loop. Today we write no loop and answer one question: what makes those tools usable by somebody else's program too. Come back and tick off the three goals above.
Plain-Language Walkthrough
The universal power adapter: why every tool means rewriting the glue code
You have a lamp, a computer, and a rice cooker at home. Their functions are entirely different and their plugs are the same. Today that seems self-evident, but without a plug standard, every appliance you bought would need an electrician to wire it in — and manufacturers would have to ship a version per style of wall wiring.
Writing tools for an agent is that era before plug standards. You wired three tools into your own agent — order lookup, send email, run SQL — and the code holds three functions each doing its own thing: how parameters are defined, how errors come back, how timeouts are handled, all according to your mood at the time. Then a colleague's agent also needs order lookup, and they cannot use your function directly — yours is welded into your loop, so they have to copy the logic out again.
Scale it up and it becomes a multiplication problem. Suppose the team has M agent applications (a support bot, a coding assistant, a data analysis assistant) needing N data sources and tools (the order database, a calendar, the code repository, monitoring). Without a protocol, the number of adapters you write is M times N: every application writes wiring code for every tool. With M and N at 5 each, that is 25 pieces of glue to maintain; the order database renames one field and 5 of those 25 have to change, and nobody remembers which 5.
With a protocol, the multiplication becomes addition: M plus N. Each tool implements the protocol once, each application implements it once, and the protocol joins them in the middle. That is the only thing MCP (the Model Context Protocol) does — it is the plug standard, not the appliance and not the power station.
First, what things look like without a protocol. Below is the typical way you wire a weather tool into your own agent:
// Without a protocol: tool definition, execution, and error handling all welded into this one loop
const tools = [
{
name: 'get_weather',
description: 'Look up the weather for a city',
parameters: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] },
},
]
async function runTool(name, args) {
if (name === 'get_weather') {
const res = await fetch(`https://example.com/weather?city=${args.city}`)
if (!res.ok) throw new Error(`the weather service returned ${res.status}`)
return await res.text()
}
throw new Error(`unknown tool ${name}`)
}
// A colleague wanting this tool can only copy both blocks wholesale and maintain their own copy.# Without a protocol: tool definition, execution, and error handling all welded into this one loop
tools = [
{
"name": "get_weather",
"description": "Look up the weather for a city",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
}
]
async def run_tool(name: str, args: dict) -> str:
if name == "get_weather":
res = await client.get("https://example.com/weather", params={"city": args["city"]})
res.raise_for_status()
return res.text
raise ValueError(f"unknown tool {name}")
# A colleague wanting this tool can only copy both blocks wholesale and maintain their own copy.Not one line of that code is wrong. The problem is that it cannot be used by a second program: the tool's definition, its execution, and its error conventions are bound inside one process. What MCP does is pull those three out of the process and put them into a general language any program can speak.
The engineering cost has to be stated up front too: one more protocol layer means one more process, one more serialization step, and one more place to debug. A tool only you will ever use, never to be called by a second program, is a pure loss as an MCP server — and the end of this section is devoted to that boundary.
Host, client, and server: what each role is responsible for
The protocol defines three roles whose names are a little confusing while the mapping is quite direct.
The host is the application in front of you: a chat client, an IDE, an automation platform. It owns the model, the conversation history, and the user's authorization decisions. The server is the capability being wired in: a process that reads files, a service that queries a database, a gateway wrapping a third-party API. The client sits between them, one connection the host opens per server.
In the adapter analogy: the host is your home, servers are the appliances, and clients are the sockets in the wall. As many appliances as you own, that many sockets you need — the spec states explicitly that one client communicates with exactly one server, strictly one to one.
Mermaid source
graph LR
subgraph Host[Host application process]
H[Host]
C1[Client 1]
C2[Client 2]
H --> C1
H --> C2
end
C1 --> S1[Server A: local files]
C2 --> S2[Server B: remote database]One to one looks wasteful and is in fact the most important safety design in this architecture. The spec puts a principle bluntly: a server should not be able to read the whole conversation, nor see other servers. The full conversation history stays with the host, and each server receives only the few parameters it genuinely needs this time.
The benefit of that design is felt when something goes wrong. Suppose you wire in a third-party weather server whose author has bad intentions. Under one-to-one isolation, all it can see is the city name you passed; without that isolation, with every server sharing one channel, it could read the traffic between you and your company's internal database server — a data breach.
The engineering cost is connection count: wire in ten servers and the host process holds ten connections and ten lifecycles to manage. That is also why client implementations tend to be more complex than expected — we write one on day 5, and you will see the real work is all in connection management and aggregation.
Three server primitives: who decides whether to use them
A server can expose three things, which the spec calls primitives. The difference between them is not what they can do but who decides whether to use one this time. That is the most easily muddled point, and remembering the controlling party keeps you right.
| Primitive | Who decides | In one line |
|---|---|---|
| Tool | The model | The model picks it from its description and executes, with side effects |
| Resource | The host application | Read-only data that can be pulled into the context, chosen by the app or the user |
| Prompt | The user | A preset phrasing the user selects explicitly, often as a slash command |
For example, the same code repository can ship in all three forms. As a tool it is search_code: the model calls it when it judges a code search is needed. As a resource it is file:///project/README.md: the app pushes it into the context, or lets the user pick it in a file chooser. As a prompt it is "review this code": the user types a slash command to bring it up.
That division is not for elegance. Where the decision sits decides who is responsible when it goes wrong. The model picking the wrong tool is a badly written tool description, the user picking the wrong prompt is unclear naming, and the app pushing the wrong resource is a product design problem. Days 2 and 3 write tools and resources respectively, and you will see their code forms differ widely too.
Dissecting a message: JSON-RPC plus MCP's own part
MCP's transport layer is no mystery: every message is JSON-RPC 2.0. A request carries id and method, a response carries the same id plus result or error, and a notification carries no id and needs no reply. That part is an off-the-shelf standard, learned once and useful everywhere.
What MCP adds of its own sits in a field called _meta. In the 2026-07-28 revision of the spec, every client request must carry the protocol version and the client capabilities, written as the two keys io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities. Here is a real call request:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": { "location": "New York" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}The three keys divide the work. protocolVersion is required, and a server that does not recognize the version must return error -32022 with the list of versions it supports. clientCapabilities is required too, telling the server what this side can do — whether it can pop a form for the user to fill in, say. A server must not rely on a capability the client did not declare, and when it needs one that was not declared it returns -32021 listing what is missing. clientInfo is an optional self-introduction, and the spec specifically warns that it is not validated in any way, so it may be used only for display and logging, never for a security decision.
On the response side there is a new required field in this revision, resultType:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"resultType": "complete",
"content": [{ "type": "text", "text": "New York is currently 22 degrees and cloudy" }],
"isError": false,
"_meta": { "io.modelcontextprotocol/serverInfo": { "name": "ExampleServer", "version": "1.0.0" } }
}
}resultType has only two core values. complete means this call is finished; input_required means the server still lacks something and needs the client to supply it and retry — day 4 covers that pattern in detail. An older server will not send this field, and the spec requires the client to treat its absence as complete, which is the door left open for compatibility.
Note also that isError and JSON-RPC's error are two different things. The former means the tool ran and failed on business grounds, and its content is fed back to the model so it can correct itself; the latter means the request itself was wrong, such as a tool name that does not exist. Day 2 expands on that line.
Assembling this _meta yourself is only a few lines, and it is worth hand-writing once so you know what the SDK is filling in for you later:
const PROTOCOL_VERSION = '2026-07-28'
function buildRequest(id, method, params = {}) {
return {
jsonrpc: '2.0',
id,
method,
params: {
...params,
_meta: {
'io.modelcontextprotocol/protocolVersion': PROTOCOL_VERSION,
'io.modelcontextprotocol/clientInfo': { name: 'my-client', version: '0.1.0' },
// An empty object means: I support no client capabilities such as elicitation
'io.modelcontextprotocol/clientCapabilities': {},
},
},
}
}
// The stdio transport is one JSON object per line with a trailing newline; no bare newlines inside a message
process.stdout.write(JSON.stringify(buildRequest(1, 'tools/list')) + '\n')import json
import sys
PROTOCOL_VERSION = "2026-07-28"
def build_request(request_id: int, method: str, params: dict | None = None) -> dict:
meta = {
"io.modelcontextprotocol/protocolVersion": PROTOCOL_VERSION,
"io.modelcontextprotocol/clientInfo": {"name": "my-client", "version": "0.1.0"},
# An empty dict means: I support no client capabilities such as elicitation
"io.modelcontextprotocol/clientCapabilities": {},
}
return {"jsonrpc": "2.0", "id": request_id, "method": method, "params": {**(params or {}), "_meta": meta}}
# The stdio transport is one JSON object per line with a trailing newline; no bare newlines inside a message
sys.stdout.write(json.dumps(build_request(1, "tools/list")) + "\n")Statelessness is this revision's foundation
If you read MCP's older documentation before, this part has to be learned again. The 2026-07-28 revision made a change at the level of a rewrite: the protocol became stateless, and the initialize handshake and the notifications/initialized notification were both removed.
The old flow was to shake hands, negotiate version and capabilities, and then omit them from every later message. The new one inverts that: there is no handshake, and every request carries all its information. The spec's wording is that everything needed to handle a request must be contained in the request itself, and a server must not rely on earlier requests on the same connection to establish context.
Several things disappeared alongside, each worth noting: the HTTP session identifier is gone, the practice of opening a separate long-lived GET connection is gone, and resuming by event id after a broken stream is gone. In their place is a new method, server/discover, which a server must implement and a client may call once before any other request to learn the other side's supported versions, capabilities, and identity.
The cost is real of course. Most directly, messages got fatter: every request repeats the version and capability blocks. In exchange come three things worth a great deal in production: a request can be handled by any replica, so scaling out needs no sticky routing; entirely unrelated requests can be interleaved on one connection; and a process that crashes and restarts only needs in-flight requests resent, because the other side never remembered anything.
So what about cases that genuinely need state across calls, a shopping basket or a database transaction? The spec's answer is an explicit handle: the server returns an id on creation, and later calls pass it back as an ordinary tool argument.
// The one correct way to keep state under a stateless protocol: the server mints the handle and the model carries it
const baskets = new Map() // in a real implementation, Redis or a database
function createBasket(userId) {
const id = `bsk_${crypto.randomUUID()}` // must be random enough; a handle must not be guessable
baskets.set(id, { userId, items: [], expiresAt: Date.now() + 86_400_000 })
return { basket_id: id }
}
function addItem({ basket_id, sku }, callerUserId) {
const basket = baskets.get(basket_id)
// The crucial step: a handle is a name, not a credential — re-check the caller's permission every time
if (!basket || basket.userId !== callerUserId) throw new Error('the basket does not exist or has expired')
basket.items.push(sku)
return { count: basket.items.length }
}import time
import uuid
# The one correct way to keep state under a stateless protocol: the server mints the handle and the model carries it
baskets: dict[str, dict] = {} # in a real implementation, Redis or a database
def create_basket(user_id: str) -> dict:
basket_id = f"bsk_{uuid.uuid4()}" # must be random enough; a handle must not be guessable
baskets[basket_id] = {"user_id": user_id, "items": [], "expires_at": time.time() + 86400}
return {"basket_id": basket_id}
def add_item(basket_id: str, sku: str, caller_user_id: str) -> dict:
basket = baskets.get(basket_id)
# The crucial step: a handle is a name, not a credential — re-check the caller's permission every time
if basket is None or basket["user_id"] != caller_user_id:
raise ValueError("the basket does not exist or has expired")
basket["items"].append(sku)
return {"count": len(basket["items"])}Note that comment about a handle being a name rather than a credential. That is exactly one class of attack covered on day 6: get somebody else's handle and you can operate their basket. The spec's requirement here is hard — a server must not treat possession of a handle as authentication.
MCP is not a replacement for function calling
Finally, a boundary, because this is where interviews most easily expose a gap.
A model's function calling and MCP are not the same layer, and they do not even conflict. Function calling is a model API capability: you put tool definitions in the request and the model replies that it wants to call one. MCP is the question of where tools come from: a tool's definition and its execution live in another process and arrive over a protocol. An MCP client still has to translate tools into its model API's tool parameters and still goes through function calling — you will hand-write that translation on day 5. The full three-way comparison (function calling, MCP, skills) lives in the Agent Skills course: the division of labor between function calling, MCP, and Skills.
So when should you not use MCP? Three situations are worth skipping outright.
First, the tool is used by only this one program and no second one is foreseeable. The process, serialization, and debugging costs the protocol brings are all a net loss, and a local function is faster.
Second, calls are extremely frequent and latency-sensitive. Every tool call crosses a serialization step and a process boundary; local stdio overhead is modest, but with the server across the public internet a round trip is tens to hundreds of milliseconds, and five calls in one conversational turn are perceptibly sluggish.
Third, the model does not need to decide this at all. If your product logic is "the user clicked the button, so look up the order," then look it up; do not detour through having the model pick a tool. Handing a deterministic flow to the model to decide is paying money for uncertainty.
Conversely, the moment any one of these appears — this capability has to serve several applications, this data source should be reachable by somebody else's assistant, I want to swap the host and keep the tools — it is MCP's turn.
Source Reading
Hands-On Lab
Today's lab writes no code and produces a log. It looks tedious and it is the foundation of the next six days: once you can write the skeleton of a tools/call request from memory, every debugging session from day 2 through day 5 goes twice as fast. When stuck, go back to the message dissection section and read along.
- Follow the README to start an existing MCP server locally, watch its standard input and output, and confirm each message occupies its own line in the output.
- Copy a full session's messages into the starter's log: server/discover first, then tools/list, then tools/call, keeping both requests and responses.
- Annotate field by field, using two markers to distinguish what JSON-RPC 2.0 mandates from what MCP added; once done you will find MCP adds very little.
- Fill the table's three blanks with the locations of the protocol version, the client capabilities, and resultType, and state which error code the server returns when each is missing.
- Find one place on the spec site that differs from your impression (the handshake, sessions, and subscriptions are high-frequency areas) and write down the page name and your conclusion.
Interview Questions
Today's 3 questions are in the question bank below, weighted toward the boundary between MCP and function calling, the isolation design of the triangle architecture, and the trade-offs of a stateless protocol. 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
- State in one sentence that MCP solves the M-times-N wiring problem, and name one situation where you should not use MCP
- Draw the relationship between host, client, and server, and explain why one client connects to exactly one server
- Read a tools/call request and response field by field, and point out where the protocol version and client capabilities live
- Say who decides whether each of a tool, a resource, and a prompt gets used
- All 4 acceptance criteria of the lab pass
- Answer at least 2 of the 3 interview questions without looking at the key points
Tomorrow (D2) we write the first server. The order is deliberate: read the messages before writing code and you will know what those few lines of SDK API actually put on the wire. Plenty of people skip today and copy an SDK example straight away, and then the first time a tool is not selected by the model they have no idea where to look — because they never saw what is genuinely transmitted. Tomorrow also brings this course's first pothole: the official JavaScript SDK currently implements the previous protocol revision, and we handle that head-on.
Interview questions
How is MCP actually different from a model's built-in function calling, and when should you not use MCP?MCP 和模型自带的函数调用到底差在哪?什么情况下你不该用 MCP?
Common in ChinaCommon overseasBasic#mcp-basics#architectureHow to reason about it · think before answering
- The screen is whether you have actually wired tools yourself. Calling MCP an upgraded function call fails, because the two sit at different layers.
- Separate the layers first: function calling is a model API feature — you pass tool definitions in the request and the model replies with which one to invoke. MCP governs where that definition and its executor live and how they are exchanged.
- They compose rather than compete: an MCP client still translates tools/list output into the model API's tool parameters, so the final hop is ordinary function calling.
- Conclusion: MCP turns an M-applications-by-N-tools wiring problem into M plus N, at the cost of an extra process, an extra serialization boundary, and an extra place to debug.
- Skip MCP when the tool has exactly one consumer, when calls are hot and latency-sensitive (a remote round trip is tens to hundreds of milliseconds, five per turn is noticeable), or when the decision does not need a model at all.
- Likely follow-up: local stdio is cheap, so why not use it everywhere? Because the cost is not only transport — it is one more process to deploy, monitor, and authorize.
分析过程 · 先想清楚再作答
- 这题在筛「有没有真正接过工具」。把 MCP 说成「函数调用的升级版」就露馅了,因为两者根本不在同一层,答对的人第一句就会先把层次拆开。
- 拆法:问自己「这一步是模型 API 的事,还是工具从哪来的事」。函数调用是模型 API 的能力——你把工具定义放进请求,模型回一个要调谁;MCP 管的是那份定义和执行体住在哪个进程里、用什么语言交换。
- 接着点出两者是叠加而非替代:MCP 客户端拿到 tools/list 之后,还要把它翻译成模型 API 的工具参数,最终仍然走函数调用那条路。
- 结论:MCP 解决的是 M 个应用乘 N 个工具的重复接线,把乘法变成加法;它换来的代价是多一层进程、一层序列化、一层要排查的地方。
- 不该用的三种情况:工具只有自己这一个程序用;调用极频繁且对延迟敏感(远程一次往返几十到几百毫秒,一轮连调五次用户就有感);这件事根本不需要模型决定,产品逻辑本来就是确定的。
- 可预期的追问:那本机 stdio 的开销很小,是不是就可以随便用?答案是开销不只在传输,还在多一个要部署、要监控、要授权的进程上。
Key points
- Function calling is a model API capability; MCP is a distribution protocol for tool definitions and executors — they stack, not compete
- MCP converts M-by-N adapters into M plus N, paying with an extra process and serialization hop
- Skip it for single-consumer tools, latency-sensitive hot paths, and flows that are deterministic by design
- The test is whether a second program will ever need this capability; if yes, the protocol cost amortizes
答题要点
- 函数调用是模型 API 的能力,MCP 是工具定义与执行体的分发协议,两者叠加而不是替代
- MCP 的价值是把 M 乘 N 的适配器数量变成 M 加 N,代价是多一层进程与序列化
- 单一消费者、延迟敏感的热路径、以及本来就确定的产品流程,这三种情况不该用 MCP
- 判据是「这个能力要不要给第二个程序用」,只要答案是要,协议的成本就摊得开
Why does the MCP spec require one client per server instead of multiplexing many servers over one connection?MCP 规范为什么规定一个客户端只连一个服务端?多路复用不是更省资源吗?
Common in ChinaCommon overseasIntermediate#architecture#securityHow to reason about it · think before answering
- It reads like a performance question but is really about security boundaries. Answering only in terms of connection count signals you never read the design principles.
- Ask who can see whom once a channel is shared. The spec fixes two principles: servers should not read the whole conversation, and should not see into other servers. One-to-one is the most direct way to enforce both.
- Concrete consequence: with isolation, a third-party weather server sees only the city you passed. On a shared channel it could observe traffic between you and an internal database server — a data leak.
- Conclusion: full history stays with the host, each server receives only the arguments this call needs, and the host is the single place where boundaries are enforced and cross-server orchestration happens.
- State the cost yourself: N servers means N connections and N lifecycles, and that is where most client complexity lives, not in sending messages.
- Likely follow-up: how do you handle tool name collisions across servers? Aggregation and disambiguation belong to the host; the spec suggests prefixing with a server identifier and explicitly warns against relying on the server's self-reported name, which is neither unique nor verified.
分析过程 · 先想清楚再作答
- 这题看着在问性能,其实在问安全边界。只从连接数和资源占用切入的回答会被判为没读过设计原则那一节。
- 拆法:先问「共享一条通道之后,谁能看见谁」。规范写死了两条原则——服务端不应该读到整段对话,也不应该看得见别的服务端;一对一是实现这两条最直接的手段。
- 举一个具体后果:接一个第三方天气服务端时,一对一隔离让它只能看到你传的城市名;共享通道则可能让它读到你和内部数据库服务端之间的往来,那就是一次数据泄露。
- 结论:完整对话历史留在宿主,服务端只拿到这次真正需要的参数;宿主是唯一的安全边界执行者,也是唯一做跨服务端编排的地方。
- 代价要主动说:接 N 个服务端就有 N 条连接、N 套生命周期要管,客户端实现的复杂度大头正是在这里,而不是在发报文上。
- 可预期的追问:那多个服务端的工具重名怎么办?答案是聚合与消歧是宿主侧的职责,规范建议加服务端标识前缀,并且明确说不要依赖服务端自报的名字,因为它不保证唯一也未经验证。
Key points
- One-to-one is a security decision, not a performance one: servers cannot read the conversation or see peers
- Full history stays in the host; a server receives only the arguments for the current call
- Aggregation, disambiguation, and authorization all happen in the host, so there is a single boundary to harden
- The cost is connection and lifecycle management, which dominates client implementation complexity
答题要点
- 一对一是安全设计而非性能设计:服务端读不到整段对话,也看不见别的服务端
- 完整历史留在宿主,服务端只收到本次调用真正需要的参数
- 跨服务端的聚合、消歧、授权都由宿主统一做,边界只有一处需要加固
- 代价是连接与生命周期管理,这是客户端实现复杂度的主要来源
The 2026-07-28 revision made MCP stateless and removed the initialize handshake. What does that cost, and how should a server that still needs state handle it?2026-07-28 这一版把 MCP 改成了无状态协议,删掉了 initialize 握手。这么改的代价是什么?服务端还想保存状态该怎么办?
Common in ChinaCommon overseasDeep dive#protocol-versions#statelessnessHow to reason about it · think before answering
- The discriminator is whether you know what this revision changed. Anyone answering from memory about handshakes, session IDs, or stream resumption exposes themselves — all three were removed.
- State the change first: no initialize and no notifications/initialized; every request carries its protocol version and client capabilities in _meta, and a new server/discover method, which servers MUST implement, returns versions, capabilities, and identity in one call.
- Weigh it as saved versus paid. You pay in payload size, repeating the version and capability block on every request. You save three things: any replica can serve any request so scaling needs no sticky routing, unrelated requests can interleave on one connection, and after a restart in-flight requests simply get resent.
- Conclusion: it trades bandwidth for scalability — near-invisible on local stdio, valuable for multi-replica remote deployments.
- For state, use explicit handles: a creation tool returns a server-minted id, and later calls pass it back as an ordinary argument while the server keys its own storage on it and documents the lifetime in the tool description.
- Likely follow-up: is a handle safe? Say it unprompted — a handle is a name, not a credential. Re-authorize the caller on every call, generate handles with a secure random source, bind them to the authenticated principal, and expire them.
分析过程 · 先想清楚再作答
- 这题的区分度在「知不知道这一版改了什么」。凭旧记忆答握手、会话标识、断流续传的人会当场暴露,因为这三样在这一版全被删了。
- 先说改了什么:没有 initialize 与 notifications/initialized,每条请求在 _meta 里自带协议版本与客户端能力;新增 server/discover 供客户端一次性取回版本、能力与身份,服务端必须实现它。
- 拆代价的角度是「省了什么、贵了什么」。贵的是报文:每条请求都要重复带版本与能力块。省的是三件事——任意副本都能处理请求所以扩容不用粘性路由、一条连接可以穿插无关请求、进程重启后在途请求重发即可。
- 结论:这是一次拿带宽换可伸缩性的交易,对本机 stdio 几乎无感,对多副本的远程部署收益很大。
- 状态怎么办:显式句柄。创建工具返回一个服务端铸造的 id,后续调用把它当普通参数传回来;服务端把状态按这个 key 存在自己的库里,并在工具描述里写清有效期。
- 可预期的追问:句柄安全吗?必须补一句——句柄是名字不是凭证,服务端每次都要重新校验调用者身份,句柄要用安全随机数生成、绑定到已认证的主体、并设过期时间。
Key points
- The revision removed the initialize handshake, protocol-level sessions, the GET stream, and stream resumption; each request now carries version and capabilities
- It added server/discover, which servers must implement, letting clients fetch versions, capabilities, and identity up front
- The cost is larger payloads; the payoff is sticky-free horizontal scaling, interleaved unrelated requests, and cheap retry after restarts
- Cross-call state moves to server-minted explicit handles passed as ordinary tool arguments, and a handle is never authentication
答题要点
- 这一版删掉了 initialize 握手、协议级会话、GET 长连接与断流续传,改为每条请求自带版本与能力
- 新增 server/discover,服务端必须实现,客户端可在任何请求前一次性取回版本、能力与身份
- 代价是报文变胖,收益是无粘性路由的横向扩容、连接上可穿插无关请求、重启后重发即可
- 跨调用状态改用服务端铸造的显式句柄,作为普通工具参数传递,并且句柄不等于身份认证