서버를 띄운다 — 그리고 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               # 6단계 전부 검사

가져갈 것

  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에서 열기 →