The course starts here. Over six pieces one server grows. Each piece adds one thing, and every step is a complete server you can run on its own.
The first piece builds a server that does nothing. No tools, no resources. And without this step everything after it falls over.
All of it
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() is empty. That is the server saying it has nothing to offer yet. From the next piece on, this fills in.
The Completer<void>().future on the last line never completes. When main returns the process ends, so this holds it open.
Connect and ask
You can poke it by hand, with no client at all. It speaks over stdio, so pushing a line in is enough.
{ 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
What came back.
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","serverInfo":{"name":"Course","version":"1.0.0"},"capabilities":{}}}
serverInfo carries the name we wrote. That is everything this first piece confirms. The server is alive and introduces itself by the contract.
stdout is reserved for the protocol
Notice that the diagnostic line above goes to stderr. That is not taste.
In stdio mode stdout is a channel where only JSON-RPC flows. Use print once and that line joins the stream, and the client dies trying to read it as a message.
print('server started'); // this one line breaks the protocol
stderr.writeln('server started'); // this one is safe
This is not the kind of mistake you fix later, because the symptom is "parsing fails sometimes." It works or does not depending on when the log happens to be written. So it gets set in the first piece.
It is the same place in embedded work. A board printing human-readable logs onto the same UART makes the client choke — that is why the real-board piece had to filter them in the bridge.
This content requires Developer or above
Sign in and upgrade your plan to continue reading.
View Plans