把服务端立起来 — 并且不往 stdout 写任何东西

5 分钟
目标MCP 服务端课程第一篇。没有工具也没有资源,只把服务端立起来。这里要确认的只有一件事——客户端连上来,能不能拿到 initialize 的回答。以及一条现在不定下来、后面就会一直坏的规矩。

课程从这里开始。六篇里养大一个服务端。每一篇加一样东西,而每一级 本身就是能跑的成品。

第一篇做的是一个什么都不干的服务端。没有工具,没有资源。可是没有这一级,后面全都会塌。

全部就这些

import 'dart:async';
import 'dart:io';

import 'package:mcp_server/mcp_server.dart';

void main(List<String> args) async {
  const config = McpServerConfig(
    name: 'Course',
    version: '1.0.0',
    capabilities: ServerCapabilities(),
  );
  final server = McpServer.createServer(config);

  final transport = McpServer.createStdioTransport().get();
  server.connect(transport);

  stderr.writeln('course step1: up, nothing registered');
  await Completer<void>().future;
}

ServerCapabilities() 是空的。它在说自己还没有什么可以拿出来。从下一篇起这里会被填上。

最后那行 Completer<void>().future 永远不会完成。main 一返回进程就结束,所以要把它按住。

连上去问一句

不用客户端也能手动戳。它走 stdio,把一行推进去就行。

{ printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}\n'
  sleep 2; } | dart run bin/step1.dart

回来的东西。

{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","serverInfo":{"name":"Course","version":"1.0.0"},"capabilities":{}}}

serverInfo 里就是我们写的名字。第一篇要确认的全部就是这个。 服务端活着,并按契约自我介绍。

stdout 是协议专用的

注意上面代码里诊断信息走的是 stderr。这不是口味问题。

在 stdio 模式下,stdout 是只有 JSON-RPC 流动的通道。 用一次 print,那一行就混进流里,客户端会把它当消息去读,然后死掉。

print('server started');          // 这一行会把协议打断
stderr.writeln('server started'); // 这一行安全

这不是那种可以以后再修的错误,因为 症状表现为「有时候解析会失败」。 日志什么时候写,决定它这次过不过。所以在第一篇就把它钉死。

嵌入式里也是同一个位置。板子往同一个 UART 打给人看的日志,客户端就会被噎住——真板那篇之所以要在桥接里过滤,就是这个原因。

校验

课程样例每一级都带检查。第 1 级是这样:

ask step1 > captures/s1.txt
grep -q 'Course' captures/s1.txt || die "step1: no serverInfo"

dart run 还在编译时不能写输入,写完立刻关掉 stdin 也不行——服务端会把 EOF 当成断开,在答复之前就退出。所以探针前后都等一下。

sleep 3                      # 等编译结束、main 起来
printf '%s\n' "$INIT"
sleep 2                      # 等答案出来之前,stdin 保持打开

少了这两行,废掉了一天。响应是空的,而服务端还在正常打启动日志,于是时间全花在怀疑服务端上。

自己跑一遍

cd content/sample/course-server
dart pub get
dart run bin/step1.dart      # 连上去手动戳
bash verify.sh               # 六级全查

可以带走的

  1. 最小启动形态 — createServer → createStdioTransport → connect → 保持进程
  2. 只用 stderr — stdio 服务端里禁止 print
  3. 手动探针 — 不用客户端包,用 printf | dart run 确认往返

下一篇

注册一个工具。以及工具的说明文为什么比代码更要紧。

运行示例

makemind-academy/course_server/
git clone https://github.com/makemind-academy/course_server
cd course_server
dart pub get
dart run bin/step1.dart
在 GitHub 上打开 →