# Best Practices for Developing and Debugging MCP Servers for Use with ChatMCP

> Learn the best practices for developing and debugging MCP servers with ChatMCP. Ensure JSON-RPC 2.0 compliance, expose required methods, and use processStateStream for effective integration.

- Repository: [刀刀/chatmcp](https://github.com/daodao97/chatmcp)
- Tags: best-practices
- Published: 2026-02-28

---

**To successfully integrate with ChatMCP, MCP servers must implement strict JSON-RPC 2.0 compliance, expose mandatory methods (`initialize`, `ping`, `tools/list`), declare transport type (`stdio`, `streamable`, or `sse`) in [`assets/mcp_server.json`](https://github.com/daodao97/chatmcp/blob/main/assets/mcp_server.json), and utilize `processStateStream` and Dart's `logging` package for real-time debugging.**

ChatMCP is a cross-platform AI client that communicates with MCP (Message-Calling-Protocol) servers via three transport layers. When developing and debugging MCP servers for use with ChatMCP, understanding the client implementation details in the `daodao97/chatmcp` repository ensures your server handles initialization, tool discovery, and graceful shutdown correctly across all supported transports.

## Core Architectural Guidelines

All ChatMCP clients implement the abstract `McpClient` interface defined in `lib/mcp/client/mcp_client_interface.dart`. This uniform contract means your server must conform to specific protocols regardless of whether ChatMCP connects via STDIO, Streamable HTTP, or Server-Sent Events (SSE).

### Enforce JSON-RPC 2.0 Schema Compliance

Every request must include an `"id"` field (string or number) and a `"method"` string. Responses must echo the same `"id"` and contain either `"result"` or `"error"`. In `lib/mcp/stdio/stdio_client.dart`, lines 66-68 validate this contract during `sendMessage`; non-conforming payloads trigger a `Failed to parse server output` error, immediately terminating the connection.

### Implement Mandatory RPC Methods

Your server must handle four critical methods to satisfy the ChatMCP client lifecycle:

1. **`initialize`** – Return a JSON-RPC success response to complete the handshake. Optionally emit a `notifications/initialized` notification afterward, following the pattern in `StdioClient.sendInitialize`.
2. **`ping`** – Echo back the incoming `id` with an empty result object. The `SSEClient._handleMessage` implementation (around lines 80-87) specifically expects this for liveness checks.
3. **`tools/list`** – Return an array of tool descriptors, each containing `name`, `description`, and a JSON Schema `parameters` object. Names must be unique as the UI relies on this for deduplication.
4. **Tool execution methods** – Handle `params` for any custom methods declared in your tool list, returning structured results or standard JSON-RPC error objects.

### Configure Server Metadata Correctly

ChatMCP discovers servers through [`assets/mcp_server.json`](https://github.com/daodao97/chatmcp/blob/main/assets/mcp_server.json), parsed by `McpServerProvider._loadServers`. Each entry requires:

```json
{
  "command": "path/to/executable",
  "args": [],
  "env": {},
  "type": "stdio|streamable|sse",
  "oauth": {}
}

```

The **`type`** field is mandatory and determines which client class (`StdioClient`, `StreamableClient`, or `SSEClient`) instantiates the connection. If your server requires authentication, implement either a `/.well-known/oauth-authorization-server` endpoint or `/oauth/metadata` endpoint; `OAuthDiscoveryService` in `lib/utils/oauth_discovery.dart` automatically probes these URLs via `_tryWellKnownEndpoint`, `_tryDirectServerProbe`, and `_tryOAuthMetadataEndpoint`.

### Manage Process Lifecycle Gracefully

For **STDIO** transports, ensure your process responds to `dispose()` calls. The `StdioClient` implementation (lines 36-38) kills the process and closes streams to prevent zombie processes. For **SSE**, monitor the `processStateStream` broadcast stream; the client emits `ProcessState.exited` (lines 133-135 in `sse_client.dart`) when connections drop. **Streamable** implementations should reuse a single `http.Client` instance and close it in `dispose()`, supporting request cancellation via `StreamableClient._abortController`.

## Debugging Techniques for MCP Development

Effective debugging relies on the observable state exposed by ChatMCP's client implementations rather than external proxies alone.

### Monitor Process State Streams

All three clients expose a `processStateStream` broadcast stream. Subscribe to this in your test harness or UI:

```dart
client.processStateStream.listen((state) {
  if (state is ProcessStateError) {
    debugPrint('Connection error: ${state.error}');
  }
});

```

For STDIO, watch for transitions from `ProcessState.starting` to `running` and finally `exited`. For SSE, the stream reports `ConnectionState.reconnecting` during automatic retries.

### Enable Verbose Client Logging

Set the logging level before initialization to capture internal client operations:

```dart
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((r) => print('[${r.level.name}] ${r.message}'));

```

The client emits critical events such as `Starting process`, `Failed to parse server output`, and `SSE connection error`. Correlate these timestamps with your server logs to identify protocol violations.

### Capture Raw JSON Traffic

For STDIO servers, the parsing logic in `StdioClient` reads lines from `stdoutStream`. Temporarily modify the client or wrap your server executable to dump raw lines before JSON parsing. For HTTP-based transports (Streamable/SSE), use a proxy like **mitmproxy** or inject an `http.LoggingClient` wrapper to inspect headers and bodies.

### Verify OAuth Discovery

Test your authentication flow programmatically using the same service ChatMCP employs:

```dart
final result = await OAuthDiscoveryService.discoverOAuth('http://localhost:8080');
assert(result.requiresOAuth);
assert(result.authorizationUrl != null && result.tokenUrl != null);

```

Ensure at least one of the three discovery strategies returns a complete `OAuthDiscoveryResult` with valid authorization and token URLs.

### Handle Crash Recovery

STDIO servers that crash emit `ProcessState.exited`; you can restart by calling `client.initialize()` again. SSE clients implement automatic reconnection via `EventFlux` with exponential back-off (configured in `ReconnectConfig` at lines 14-18 of `sse_client.dart`), defaulting to `_maxReconnectAttempts` of 5. Adjust this constant if your server requires longer recovery windows.

## Implementation Examples

### Building a Minimal STDIO Server in Python

The following Python script implements the required JSON-RPC interface for ChatMCP integration:

```python
#!/usr/bin/env python3
import sys, json

def read():
    return json.loads(sys.stdin.readline())

def write(obj):
    sys.stdout.write(json.dumps(obj) + "\n")
    sys.stdout.flush()

def handle(message):
    if message["method"] == "initialize":
        return {"jsonrpc":"2.0","id":message["id"],"result":{}}
    if message["method"] == "ping":
        return {"jsonrpc":"2.0","id":message["id"],"result":{}}
    if message["method"] == "tools/list":
        tools = [{
            "name": "echo",
            "description": "Echoes the input text",
            "parameters": {
                "type": "object",
                "properties": {"text": {"type": "string"}}
            }
        }]
        return {"jsonrpc":"2.0","id":message["id"],"result":tools}
    raise Exception("unknown method")

while True:
    try:
        msg = read()
        resp = handle(msg)
    except Exception as e:
        resp = {"jsonrpc":"2.0","id":msg.get("id"),"error":{"code":-32603,"message":str(e)}}
    write(resp)

```

Register this in [`assets/mcp_server.json`](https://github.com/daodao97/chatmcp/blob/main/assets/mcp_server.json) with `"type": "stdio"`.

### Integrating the Dart Client

When building Flutter widgets that interact with MCP servers, use the provider pattern to resolve configurations:

```dart
class McpDemo extends StatefulWidget {
  @override
  _McpDemoState createState() => _McpDemoState();
}

class _McpDemoState extends State<McpDemo> {
  late McpClient _client;

  @override
  void initState() {
    super.initState();
    final provider = McpServerProvider();
    final config = provider.clients['Echo']!.serverConfig;
    _client = StdioClient(serverConfig: config);
    
    _client.initialize().then((_) async {
      await _client.sendInitialize();
      final ping = await _client.sendPing();
      // Handle successful connection
    });
  }

  @override
  void dispose() async {
    await _client.dispose();
    super.dispose();
  }
}

```

Always call `initialize()` before `sendInitialize()`, and ensure `dispose()` runs during widget cleanup to release OS resources.

### Configuring SSE Reconnection

For long-running SSE connections, adjust the reconnection behavior:

```dart
final client = SSEClient(serverConfig: sseConfig);
await client.initialize();

client.processStateStream.listen((state) {
  if (state is ProcessStateError) {
    // Implement custom UI feedback or logging
  }
});

```

The client automatically retries up to 5 times with exponential back-off. For servers requiring different behavior, modify the `reconnectConfig` parameter in `SSEClient`.

## Summary

- **Implement strict JSON-RPC 2.0** compliance with valid `id` echoing and error objects to avoid parse failures in `StdioClient` (lines 66-68).
- **Declare transport type** explicitly in [`assets/mcp_server.json`](https://github.com/daodao97/chatmcp/blob/main/assets/mcp_server.json) (`stdio`, `streamable`, or `sse`) so `McpServerProvider` instantiates the correct client class.
- **Support mandatory methods**: `initialize`, `ping`, and `tools/list` with proper response schemas.
- **Monitor `processStateStream`** across all transport types to detect disconnections and crashes in real-time.
- **Leverage `Logger.root`** for verbose logging of client internals, correlating with server-side logs during debugging.
- **Implement OAuth discovery** endpoints if authentication is required, ensuring compatibility with `OAuthDiscoveryService` probing strategies.
- **Handle graceful shutdown** by responding to process kills (STDIO) or closing HTTP clients (Streamable/SSE) to prevent resource leaks.

## Frequently Asked Questions

### What transport type should I use for my MCP server?

**Choose STDIO for local executables** like Python scripts or compiled binaries that run on the same machine as ChatMCP, as implemented in `lib/mcp/stdio/stdio_client.dart`. **Use Streamable HTTP** for remote stateless services that accept JSON-RPC over POST requests. **Select SSE** for long-running services requiring bidirectional streaming or server-pushed events, managed by `lib/mcp/sse/sse_client.dart` with automatic reconnection logic.

### Why does ChatMCP fail to parse my server's output?

**The client validates strict JSON-RPC 2.0 formatting.** In `StdioClient` (lines 66-68), any line written to stdout that lacks a valid `jsonrpc`, `id`, and either `result` or `error` field triggers a `Failed to parse server output` error and terminates the connection. Ensure your server flushes output buffers immediately after each JSON object and does not print debug logs to stdout.

### How do I debug authentication issues with my MCP server?

**Verify OAuth discovery using the same logic as ChatMCP.** Call `OAuthDiscoveryService.discoverOAuth()` with your server URL to test if your `/.well-known/oauth-authorization-server` or `/oauth/metadata` endpoints return valid `authorization_url` and `token_url` values. The service tries three fallback strategies, so ensure at least one endpoint returns a complete `OAuthDiscoveryResult` object.

### Can I hot-reload my MCP server during development?

**For STDIO servers, yes.** Listen to `ProcessState.exited` events on `processStateStream` and automatically re-invoke `client.initialize()` to spawn a new process. For SSE, the client handles reconnection automatically with exponential back-off (default 5 attempts), but you can force a refresh by disposing and recreating the `SSEClient` instance, ensuring you call `dispose()` to clean up the underlying HTTP connection.