サーバーを立てる — そして 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 で開く →