Skip to content

Commit a932bd1

Browse files
committed
clarify stdio a bit more
1 parent a487982 commit a932bd1

File tree

1 file changed

+51
-10
lines changed

1 file changed

+51
-10
lines changed

exercises/01.ping/01.solution.connect/README.mdx

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,27 +30,68 @@ const args = [
3030
const child = spawn(command, args)
3131
```
3232

33-
Then it listens for messages from the child process and sends them to the MCP
34-
server:
33+
Once started, the client and server communicate by sending and receiving messages through standard input and output (stdio). These messages follow the JSON-RPC format, which is a standard way to structure requests and responses.
34+
35+
### Example: Ping Request and Response
36+
37+
Suppose the client wants to check if the server is running. It sends a `ping` request to the server by writing a line of JSON to the server's standard input:
38+
39+
```json
40+
{ "jsonrpc": "2.0", "id": "123", "method": "ping" }
41+
```
42+
43+
The server reads this line from its standard input, processes the request, and writes a response to its standard output:
44+
45+
```json
46+
{ "jsonrpc": "2.0", "id": "123", "result": {} }
47+
```
48+
49+
This is a typical request-response cycle:
50+
51+
- The **client** writes a request (like `ping`) to the server's stdin.
52+
- The **server** reads the request, processes it, and writes a response to stdout.
53+
- The **client** reads the response from the server's stdout.
54+
55+
Here's a simplified TypeScript example of how this might look in code:
3556

3657
```ts
58+
// Client sends a ping request
59+
child.stdin.write(
60+
JSON.stringify({ jsonrpc: '2.0', id: '123', method: 'ping' }) + '\n',
61+
)
62+
63+
// Client listens for the response
3764
child.stdout.on('data', (data) => {
38-
console.log(data)
65+
console.log('Received from server:', data.toString())
66+
// Should log: {"jsonrpc": "2.0", "id": "123", "result": {}}
3967
})
4068
```
4169

42-
This is why we use `console.error` in our server, so that the MCP Client can
43-
print it to the console without interfering with the server's output.
70+
> **Note:** Each message is typically sent as a single line of JSON, so a newline character (`\n`) is used to separate messages.
4471
45-
It also listens for messages from the MCP server and sends them to the child
46-
process:
72+
#### ⚠️ What happens if you use `console.log` for logging?
73+
74+
If your server uses `console.log` for regular logging, those log messages will be sent to standard output (stdout) along with your protocol messages. This can confuse the client, which expects only valid JSON-RPC messages on stdout.
75+
76+
For example, if your server does this:
4777

4878
```ts
49-
child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'ping' }))
79+
console.log('Server started!')
80+
```
81+
82+
Then the output on stdout might look like:
83+
84+
```
85+
Server started!
86+
{"jsonrpc": "2.0", "id": "123", "result": {}}
5087
```
5188

89+
When the client tries to read and parse the response, it will encounter the plain text `Server started!` and likely throw a parsing error, because it expects only JSON.
90+
91+
That's why you should use `console.error` for logging and debugging output. Standard error (stderr) is separate from stdout, so log messages won't interfere with the protocol communication.
92+
5293
Other transport mechanisms exist as well, and while the code for them is
53-
different the underlying concept is the same. Just a server and client
54-
communicating with each other.
94+
different the underlying concept is the same: a server and client
95+
communicating with each other using a standard protocol.
5596

5697
</details>

0 commit comments

Comments
 (0)