Dayward AI
Week 1 · D2About 5 hours

Writing Your First MCP Server: stdio Transport, the Official SDK, Parameter Schemas, Tool Annotations, and Debugging With Inspector

Use the official SDK to write your first server a real client can load, fill in the parameter schema, output schema, tool annotations, and error classification in one pass, and learn to debug it with Inspector.

Today's goals 0/3

Sign in to tick these off and save your progress.

Today's Goals

  1. Stand up a stdio server with the official SDK, and see its registered tools appear in a client
  2. Write a usable input schema, output schema, and description for a tool, and explain who the description is written for
  3. Distinguish protocol errors from tool execution errors, and give an example of when to use each

Yesterday you took a tools/call message apart field by field. Today, the reverse: write a process that genuinely emits messages like that. There is not much code, and the hard part is a few decisions — how detailed the description gets, how the schema is divided, which class an error falls into. Come back and tick off the three goals above.

Plain-Language Walkthrough

The stdio transport: one message per line, and one extra character breaks everything

Yesterday said the protocol is the plug standard. The transport, then, is the cable, and stdio is a cable run across the room: the client launches the server as a child process, and the two talk directly over standard input and standard output. No port, no domain, no authentication, because they were already on the same machine under the same user.

The rules are few enough to state in one breath. Messages are JSON-RPC, one per line, newline-delimited, with no bare newline inside a message. The client writes requests to the server's standard input and the server writes responses back to standard output. A server must not write anything to standard output that is not an MCP message; a client must not write JSON-RPC responses to the server's standard input (a client only sends requests and notifications). For logging, use standard error — the spec explicitly permits a server to write arbitrary UTF-8 text to standard error and reminds clients not to treat output on standard error as a signal of failure.

Shutdown is simple too: the client closes the input stream, and on reading end-of-file the server should exit promptly. The spec calls that the primary and only portable signal for graceful shutdown, so handle it honestly and save yourself a pile of process-killing trouble.

The engineering trade-off reads like this: stdio brings zero configuration, zero network attack surface, and process isolation for free; against that, it only serves the local machine, cannot run multiple replicas or be shared with a team, and the server's lifecycle is entirely in the client's hands — the client crashes and your server becomes an orphan process. Serving a team means changing transport, which is day 4's business.

Standing up a server with the official SDK: what registering a tool takes

Yesterday's hand-assembled _meta existed to show you the bottom layer. Nobody does that when actually writing a server, because the SDK already handles transport, message framing, and schema validation.

Registering a tool takes four things: a name, a description written for the model, an input schema, and a body. The output schema and the annotations are optional and should both be written in production.

server.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'
 
const server = new McpServer({ name: 'weather-fx-server', version: '0.1.0' })
 
server.registerTool(
  'get_weather',
  {
    title: 'Weather lookup', // for humans; the client renders its UI from this
    description:
      "Look up a city's current weather, returning the temperature in Celsius and the conditions. Pass a city name in city, for example Shanghai.",
    inputSchema: {
      city: z.string().min(1).describe('a city name, for example Shanghai or Seattle'),
    },
  },
  async ({ city }) => {
    const hit = await fetchWeather(city)
    return { content: [{ type: 'text', text: JSON.stringify(hit) }] }
  }
)
 
// The transport comes last, and is the one place where changing transport changes one line
await server.connect(new StdioServerTransport())

The two shapes are alike: declare the metadata, then write an ordinary function. Python is less work because it can infer the schema from type annotations and the docstring; TypeScript declares it explicitly with zod, which buys compile-time detection of a mistyped parameter name.

Now something slightly uncomfortable has to be said, or following tutorials online will leave you confused throughout. Measured on 2026-09-06: the latest JavaScript @modelcontextprotocol/sdk is 1.30.0, its internal latest-protocol-version constant is still 2025-11-25, and the package contains no resultType and no server/discover. Python's mcp package at 2.1.1 has caught up to 2026-07-28, with the discovery request, the input-required result, and subscription listen requests all present in its types.

The line is one sentence: the spec says this, and current SDKs implement that. A server written with the JS SDK genuinely emits the previous revision's messages on the wire; yesterday's hand-written message is what the spec looks like. That affects none of today's concepts — tools, schemas, annotations, and error classification did not change between revisions — but it affects what you see when capturing traffic. The fastest way to judge whether a piece of material is current is to search it for server/discover and resultType.

The input schema is a contract written for the model

This section is the most valuable of the day, because it decides whether your tool gets called and whether it gets called wrongly.

The description first. A tool description is written for the model, not for a colleague. The model cannot see your wiki, your code comments, or your requirements document — it has only the sentence you wrote. So "look up an order, see the documentation" is the same as writing nothing. An adequate description answers three questions: what this tool does, what the parameters look like (with an example, ideally), and under what circumstances it should be used. That last one is most often omitted and most damaging: without stated applicability, the model calls it when it should not.

Now the schema. MCP's schemas are JSON Schema 2020-12 by default (that is what applies with no $schema). The spec has a set of recommendations for tool names: 1 to 128 characters, using only letters, digits, underscores, hyphens, and dots, case-sensitive, unique within a server, and no spaces or commas. The form for a no-argument tool is worth remembering separately, and this one is recommended:

JSONJSON
{
  "name": "get_current_time",
  "description": "Returns the current server time",
  "inputSchema": { "type": "object", "additionalProperties": false }
}

additionalProperties: false means it accepts only an empty object, which is more explicit than a bare { "type": "object" } (which accepts any object). The input schema must be a valid JSON Schema object and cannot be null.

Parameter naming is part of the contract too. city beats q, and start_date beats d1, because the model infers meaning from names and descriptions together. Write a description on every field; the return on that is absurdly high — one sentence on one field often beats three more lines in the overall tool description.

The output schema and structured content: so downstream code stops parsing natural language

Tool returns come in two kinds. Unstructured content goes in content and is text, images, or audio for the model to read; structured content goes in structuredContent and is JSON for code to use.

If you declare an outputSchema, the spec's requirement is hard: the server must return a structured result conforming to that schema, and the client should validate against it. The benefit is direct — downstream code no longer needs a regex to dig the number out of "New York is currently 22 degrees and cloudy."

There is also an easily missed compatibility rule: a tool returning structured content should also return a serialized JSON text copy in content. Older clients recognize only content, and giving them structuredContent alone leaves them with nothing.

structured.ts
server.registerTool(
  'get_weather',
  {
    description: "Look up a city's current weather…",
    inputSchema: { city: z.string().describe('a city name') },
    // Declaring the output shape lets the client validate and spares downstream code from parsing prose
    outputSchema: {
      celsius: z.number().describe('the current temperature in Celsius'),
      conditions: z.string().describe('a description of the conditions'),
    },
  },
  async ({ city }) => {
    const hit = await fetchWeather(city)
    return {
      // Give both: content for older clients, structuredContent for code
      content: [{ type: 'text', text: JSON.stringify(hit) }],
      structuredContent: hit,
    }
  }
)

The engineering cost: an output schema is an external contract once published, so renaming a field is a breaking change. Do not dump your internal data structure out verbatim from the start — expose only the few fields you are willing to maintain long term. Day 7 returns to this when covering versioning.

The four annotation hints, and why they are untrustworthy

Annotations are a set of behavioural hints attached to a tool definition. The four field names, verified in practice, are readOnlyHint (read-only, changes no external state), destructiveHint (may make a destructive update), idempotentHint (repeating the call with the same arguments is equivalent to calling it once), and openWorldHint (acts on an open external world rather than a closed set), plus a title for display.

What does a client do with them? Mainly decide whether to raise a confirmation dialog and how to mark things in the interface. A read-only, idempotent lookup can run silently; a delete carrying a destructive hint should make the user click to confirm.

But the spec adds a very heavy warning here: a client must treat tool annotations as untrusted input unless they come from a trusted server.

That sentence deserves one more layer of thought. Annotations are written by the server itself, and a malicious server can perfectly well stamp readOnlyHint: true on delete_all_files and fool any client that skips confirmation for read-only tools. So:

Annotations are interface hints, not access control. Real permissions must be set by the host according to the server's provenance, never decided by the server's own account of itself.

That is the same reasoning as yesterday's clientInfo rule — anything the other side reports about itself cannot be used for a security decision. Day 6 unrolls this thread into a full attack surface analysis.

Two classes of error: one for the program to fix, one for the model to correct

The place beginners most often get wrong. MCP tools have two error reporting mechanisms, and using the wrong one traps the model in pointless retries.

Protocol errors go through JSON-RPC's error field. They mean the request itself was wrong: the tool name does not exist, the request fails the schema for calling the tool, the server blew up internally.

JSONJSON
{
  "jsonrpc": "2.0",
  "id": 3,
  "error": { "code": -32602, "message": "Unknown tool: invalid_tool_name" }
}

Tool execution errors go through isError: true in the result. They mean the tool ran and did not succeed on business grounds: a downstream API failed, an argument failed a business rule, business logic refused. This is still a successful JSON-RPC response.

JSONJSON
{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "resultType": "complete",
    "content": [{ "type": "text", "text": "The departure date must be later than today. Today is 2026-09-06." }],
    "isError": true
  }
}

The difference is who can fix it. The spec says a client should hand an execution error to the model so it can self-correct and retry; a protocol error may be shown to the model too and is basically useless there. So the criterion is one sentence:

Could the model succeed by changing an argument? If yes, use isError; if no, return a JSON-RPC error.

The corollary is that an execution error's wording is written for the model: list the valid values, the correct format, the boundary conditions. That "Today is 2026-09-06" above is the model of it — it tells the model directly which way to change. Writing "invalid argument" instead leaves the model guessing.

Today's lab has a more insidious counter-example: skipping argument validation so an illegal input computes a NaN and returns silently. No error, no isError, and the model carries that wrong result forward as a correct answer. That is far more dangerous than crashing, because it leaves no trace at all.

Debugging with Inspector: seeing the messages beats guessing

The most painful moment writing a server is "the model just will not call my tool." Two roads open then: tweak the description and try again, purely by superstition; or pull up the messages and look.

There is an official MCP Inspector, a debugging tool that wires a server in, shows messages in both directions one at a time, and triggers tool calls by hand. How to launch it and what its interface looks like are in the official documentation and change with versions, so no commands are copied here. What to remember is the approach: first confirm the tool really appears in tools/list and the schema really is the shape you think, and only then suspect the model. Over half of "the model will not call my tool" turns out to be a tool that never registered, or a misspelled parameter name.

Today's lab ships a lighter substitute: the SDK's in-memory transport wires client and server into one process, running assertions one at a time and printing what happened. It suits continuous integration better than Inspector, because it has a definite exit code. The two are complementary: Inspector for exploring, the self-test entry point for regression.

One last easily overlooked recommendation from the spec: tools/list should return tools in a deterministic order (the same order every time while the underlying tool set is unchanged). The reason is not fussiness — a stable order lets clients cache the tool list reliably, and the model side's prompt cache hits more often too. Generating the list by iterating a hash map casually destroys that optimization.

Source Reading

Hands-On Lab

🧪 D2 lab: a stdio MCP server with a weather tool and a currency tool

Code location: labs/mcp-7days/day-02-weather-fx-server

Acceptance criteria:

  1. tools/list returns both get_weather and convert_currency, each with an input and an output schema
  2. A normal get_weather call returns structuredContent, and an unknown city returns isError with the available cities listed in the message
  3. convert_currency converts 100 USD to 712 CNY correctly, and returns a model-useful isError for a negative amount or an unknown currency
  4. Both tools declare the readOnlyHint and idempotentHint annotations
  5. MOCK=1 SELFTEST=1 pnpm start shows 7 of 7 ✅ under solution and exits 0

This lab makes no network calls and needs no API key, with weather and rates both from built-in fake tables. The starter has 5 exercise points cut out, and running it as-is shows 5 ❌ — each one you finish turns one ✅, which is your progress bar. When stuck, read what that item printed; it tells you directly what is missing.

  1. Read the solution's src/server.ts first, find tool registration, schema declaration, and transport startup, and confirm each tool is metadata plus an ordinary function.
  2. Complete get_weather in the starter: rewrite the perfunctory description into something the model can act on, add the output schema and structuredContent, and run once to see items 1 and 3 go green.
  3. Change the unknown-city throw into deliberately returning isError with the available cities in the message, and watch item 4 go green — while feeling how useless a thrown message is to the model.
  4. Add argument validation to convert_currency so a negative amount and an unknown currency both return an execution error; note the starter would otherwise silently return a negative result, which is the most dangerous case.
  5. Write a set of annotations for each tool, thinking through what the read-only, idempotent, and open-world hints should each say, and get to 7 of 7 ✅.

Interview Questions

Today's 3 questions are in the question bank below, weighted toward writing tool descriptions, schema design, the division between the two error classes, and stdio's boundaries. 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

  • Stand up a stdio server with the official SDK, and see its registered tools appear in a client
  • Write a usable input schema, output schema, and description for a tool, and explain who the description is written for
  • Distinguish protocol errors from tool execution errors, and give an example of when to use each
  • Explain why tool annotations must be treated as untrusted input
  • All 5 acceptance criteria of the lab pass
  • Answer at least 2 of the 3 interview questions without looking at the key points

Tomorrow (D3) we fill in the other two primitives: resources and prompts. The order is again deliberate — a tool is picked by the model while resources and prompts are picked by the application and the user, and a different controlling party means an entirely different design method. And once the data volume rises (a directory of several thousand notes, say), you immediately hit pagination, caching, and change notifications, three things today never touched.

Interview questions

  • Who is a tool's description actually written for, and what concretely goes wrong in production when it is too vague?工具的 description 到底写给谁看?写得太泛,在生产里会造成什么具体后果?
    Common in ChinaCommon overseasBasic#tool-design#prompt-surface

    How to reason about it · think before answering

    1. The screen is whether you have ever debugged a tool the model refuses to call. Answering 'write it clearly so colleagues understand' reveals doc-thinking; the point is that the description is the model's only evidence.
    2. Ask what the model has when it makes the decision: the tool name, this one description, and the parameter schema. It cannot see your wiki, comments, or spec. The description is a decision input, not documentation.
    3. Split vagueness into two failure directions. Under-calling: the model never realizes the tool solves the current problem, so the task silently fails with no error. Over-calling: fuzzy boundaries make the model invoke it when it should not, which is a real incident if the tool has side effects.
    4. Conclusion: a usable description answers three things — what it does, what the parameters look like with an example, and when it should be used. The third is the one people omit, and it is the gate that prevents over-calling.
    5. Add the engineering view: a description is an external contract, so changing it changes behavior, and the same wording performs differently across models. It belongs in version control with an eval set, not in post-launch eyeballing.
    6. Likely follow-up: is longer always better? No. Descriptions consume context budget and crowd out the actual conversation once you have many tools. Keep the summary short and push detail into each parameter's own description.

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

    1. 这题在筛「有没有真的排查过模型不调工具」。答成「写清楚一点,方便别人理解」就落到文档思维了;面试官想听的是描述是模型唯一的判断依据这件事。
    2. 拆法:先问自己「模型做这个决定时手上有什么」。它看不到你的 wiki、代码注释、需求文档,只有工具名加这一句描述加参数 schema。所以描述不是文档,是决策依据。
    3. 把「太泛」拆成两个方向的后果:一是**漏调**,模型不知道这个工具能解决当前问题,任务默默做不成,而且不会报错;二是**误调**,描述边界不清,模型在不该调的时候调它——如果这个工具有副作用,那就是一次真实的线上事故。
    4. 结论:一句合格的描述要回答三件事——做什么、参数长什么样(给例子)、什么情况下才该用。第三条最常被漏掉,也最要命,因为它才是防误调的那道闸。
    5. 补一条工程视角:描述是对外契约,改它等于改行为。同一段描述在不同模型上表现还不一样,所以描述要进版本管理、要有评估集,不能靠上线后人肉观察。
    6. 可预期的追问:那把描述写得越长越好吗?不是。描述会占上下文预算,工具一多就挤掉真正的对话内容;正确做法是短而准,把细节放进每个参数各自的 description 里。

    Key points

    • The description is read by the model and is its only basis for deciding whether to call the tool
    • Vagueness causes silent under-calling or dangerous over-calling of side-effecting tools
    • A good description states what it does, what the parameters look like with an example, and when it applies
    • Treat it as an external contract with version control and evals; push detail into per-parameter descriptions to save context

    答题要点

    • 描述是给模型看的,是它决定调不调这个工具的唯一依据,不是给同事看的文档
    • 写得太泛有两类后果:漏调导致任务静默失败,误调则可能触发有副作用的操作
    • 合格描述回答三件事:做什么、参数长什么样并给例子、什么情况下才该用
    • 描述是对外契约,要进版本管理并配评估集;细节放进每个参数的 description,总描述保持短而准
  • When should a tool return a JSON-RPC error versus a result with isError set to true? Give me a decision rule.什么时候该返回 JSON-RPC 的 error,什么时候该返回 isError 为真的工具结果?给我一个判据。
    Common in ChinaCommon overseasIntermediate#error-handling#tool-design

    How to reason about it · think before answering

    1. This is close to a pass/fail line for MCP server work. Reciting 'two kinds of errors' is baseline; the discriminator is producing an actionable rule and knowing who the error text is written for.
    2. Ask who can fix it. If the request itself is invalid — unknown tool, arguments failing the call-tool schema, an internal server fault — no amount of parameter tweaking helps, so return a JSON-RPC error, typically -32602. If the tool ran but the business case failed — downstream API error, bad date format, amount out of range — a different argument might work, so return isError in the result.
    3. The rule in one line: could the model succeed by changing an argument? If yes use isError, if no use error. Note that isError is still a successful JSON-RPC response with resultType complete.
    4. Carry the text requirement into the conclusion: the spec says clients should hand execution errors to the model for self-correction, so the message is written for the model. List allowed values, the correct format, the boundary. 'Invalid parameter' just makes it guess.
    5. The production trap is not misclassifying but returning neither — skipping validation so an illegal input yields NaN or an empty result that is silently returned. The model then uses a wrong answer with no trace. Leaning on output-schema validation is not handling it either, since the model receives a schema stack trace.
    6. Likely follow-up: should clients feed protocol errors to the model too? The spec permits it but it rarely helps, because the model cannot fix them. Log and alert instead — that one is your bug.

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

    1. 这题几乎是 MCP 服务端的入门分水岭。能背出「两类错误」只算及格,区分度在于能不能给出一条可执行的判据,以及知不知道错误文案是写给谁的。
    2. 拆法:问「谁能修好这个错」。请求本身不合法——工具名不存在、参数不满足调用工具的 schema、服务端内部异常——模型再怎么改参数都没用,这类走 JSON-RPC 的 error,典型是 -32602。工具跑了但业务没成——下游 API 失败、日期格式不对、金额越界——模型换个参数就可能成功,这类走 result 里的 isError。
    3. 判据一句话:**模型换个参数有没有可能成功?有就用 isError,没有就用 error。** 注意 isError 仍然是一个成功的 JSON-RPC 响应,resultType 照样是 complete。
    4. 结论要带上文案要求:规范说客户端应当把执行错误交给模型自我纠正,所以文案是写给模型看的,要列出可选值、正确格式、边界条件。写「参数错误」等于让模型瞎猜。
    5. 生产视角的坑:最危险的不是分错类,而是**两类都不返回**——不做校验,让非法输入算出 NaN 或空结果静默返回。模型会把错误答案当正确答案用下去,且不留痕迹。靠输出 schema 校验去兜底也不算处理,因为模型拿到的是一段 schema 堆栈。
    6. 可预期的追问:客户端要不要把协议错误也喂给模型?规范说可以,但基本没用,因为模型改不了;更该做的是记日志报警,那是你的 bug 不是模型的。

    Key points

    • Protocol errors use the JSON-RPC error field: unknown tool, schema-invalid request, internal fault — unfixable by the model
    • Execution errors use isError true in the result and remain a successful JSON-RPC response
    • The rule: if a different argument could succeed, use isError; otherwise use error
    • Write execution-error text for the model with allowed values and formats; the worst case is neither, silently returning a wrong result

    答题要点

    • 协议错误走 JSON-RPC 的 error:未知工具、请求不满足 schema、服务端内部错,模型改参数也无济于事
    • 执行错误走结果里的 isError 为真:下游失败、业务校验不过,它仍是成功的 JSON-RPC 响应
    • 判据是模型换个参数有没有可能成功,有就 isError,没有就 error
    • 执行错误的文案写给模型看,要列出可选值与正确格式;最危险的是两类都不返回、静默给出错误结果
  • What is the most common way a stdio MCP server breaks, and how do you prevent it in code and in process?一个 stdio 的 MCP 服务端最常见的翻车原因是什么?你会在代码和流程上分别怎么堵住它?
    Common in ChinaCommon overseasDeep dive#stdio-transport#debugging

    How to reason about it · think before answering

    1. This checks whether you have actually run one. People who have not will say 'the process did not start' or 'wrong path'; anyone who has been bitten leads with stdout contamination.
    2. Restate the hard rules first: messages are newline-delimited JSON, one per line, with no embedded newlines, and the server must not write anything to stdout that is not an MCP message. Logging goes to stderr. Once the rules are stated the failure mode is obvious.
    3. Call out the symptom, because that is the discriminator: the client only reports a JSON parse failure and cannot point at your console.log, and the polluter is often a third-party library printing a banner or deprecation warning at import time rather than your own code.
    4. Conclusion in two layers. In code: wrap a stderr-only logger, ban direct printing, vet third-party libraries for stdout writes, and ensure serialized JSON carries no raw newlines. In process: add a self-test entry point that links a client and server over an in-memory transport in one process, asserts, and exits with a real status code, then run it in CI so contamination is caught before merge.
    5. Add the adjacent one: graceful shutdown. The spec makes closing stdin and exiting on EOF the primary and only portable shutdown signal; ignoring it leaves orphan processes that show up locally as mysteriously held ports and file locks.
    6. Likely follow-up: if it is this fragile, why use stdio? Zero configuration, zero network attack surface, and process isolation for free — the tradeoff is clearly worth it locally. You switch transports when you need team sharing or multiple replicas.

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

    1. 这题在验有没有真跑过。没实际接过的人会答「进程没起来」「路径不对」这类泛泛的,真踩过的人第一句就会说标准输出被污染。
    2. 拆法:先复述 stdio 的硬规矩——消息是一行一条换行分隔的 JSON、内部不许有裸换行,服务端不得往标准输出写任何不是 MCP 消息的东西,日志一律走标准错误。规矩一说完,翻车原因就自明了。
    3. 现场特征值得单独说,因为它是这题的区分点:客户端只会报一句 JSON 解析失败,指不到你哪一行 console.log;而且污染源常常不是你自己的代码,而是某个第三方库在启动时打的横幅或弃用警告。
    4. 结论分两层。代码上:封一个只写标准错误的日志函数并全局禁用直接打印,接第三方库之前先确认它不往标准输出写东西,把 JSON 序列化后确保不含裸换行。流程上:加一个自测入口,用内存传输在同一个进程里把客户端和服务端接起来跑断言,有明确退出码,进持续集成——这样污染一出现就会在合并前被拦下。
    5. 再补一条相关的:优雅停机。规范说客户端关掉输入流、服务端读到文件结束就应尽快退出,这是主要且唯一可移植的停机信号;不处理它就会留下孤儿进程,本机开发时表现为端口和文件锁莫名被占。
    6. 可预期的追问:既然这么脆,为什么还用 stdio?因为它零配置、零网络攻击面、进程隔离天生就有,本机场景收益远大于代价;要给团队共享或多副本才需要换成远程传输。

    Key points

    • Stdout contamination: stdio reserves stdout for MCP messages, so a single console.log breaks the client's parser
    • The symptom is only a JSON parse failure with no line number, and the culprit is often a third-party library's startup banner
    • In code, use a stderr-only logger, ban direct printing, and vet dependencies for stdout writes
    • In process, add an in-memory-transport self-test with a real exit code in CI, and exit promptly on stdin EOF to avoid orphan processes

    答题要点

    • 最常见的是标准输出被污染:stdio 规定 stdout 只能有 MCP 消息,一行 console.log 就让客户端解析失败
    • 现场只报 JSON 解析失败,指不到具体行,污染源常常是第三方库启动时打的横幅或警告
    • 代码上封一个只写标准错误的日志函数并禁用直接打印,接库之前先验它不写 stdout
    • 流程上加一个用内存传输的自测入口,有明确退出码并进持续集成;同时处理 stdin 关闭时的优雅退出

Comments