#
tokens: 9253/50000 17/17 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── config.json
├── docker-compose.yaml
├── Dockerfile
├── LICENSE
├── README.md
├── src
│   ├── config
│   │   ├── loader.rs
│   │   ├── mod.rs
│   │   └── model.rs
│   ├── constants.rs
│   ├── lib.rs
│   ├── lib.rs.bak
│   ├── main.rs
│   ├── process
│   │   ├── io.rs
│   │   ├── manager.rs
│   │   └── mod.rs
│   ├── shutdown.rs
│   ├── state.rs
│   └── websocket
│       ├── message.rs
│       └── mod.rs
└── test.html
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | /target
2 | .env
3 | .DS_Store
4 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # MCP Server Runner
  2 | 
  3 | > **Note**: This project is currently under active development and in WIP (Work In Progress) status. Features and APIs may change significantly.
  4 | 
  5 | A WebSocket server implementation for running [Model Context Protocol](https://github.com/modelcontextprotocol) (MCP) servers. This application enables MCP servers to be accessed via WebSocket connections, facilitating integration with web applications and other network-enabled clients.
  6 | 
  7 | ## Development Status
  8 | 
  9 | - 🚧 **Work In Progress**: This software is in active development
 10 | - ⚠️ **API Stability**: APIs and features may change without notice
 11 | - 🧪 **Testing**: Currently undergoing testing and refinement
 12 | - 📝 **Documentation**: Documentation is being actively updated
 13 | 
 14 | ## Overview
 15 | 
 16 | MCP Server Runner acts as a bridge between WebSocket clients and MCP server implementations. It:
 17 | 
 18 | - Launches an MCP server process
 19 | - Manages WebSocket connections
 20 | - Handles bidirectional communication between clients and the MCP server
 21 | - Supports graceful shutdown and error handling
 22 | 
 23 | ## Features
 24 | 
 25 | - WebSocket server implementation with single-client support
 26 | - Process management for MCP server instances
 27 | - Bidirectional message passing between client and server
 28 | - Graceful shutdown handling
 29 | - Comprehensive error logging
 30 | - Cross-platform support (Unix/Windows)
 31 | 
 32 | ## Prerequisites
 33 | 
 34 | - Rust 1.70 or higher
 35 | - An MCP server implementation executable
 36 | 
 37 | ## Configuration
 38 | 
 39 | ### Environment Variables
 40 | 
 41 | The application can be configured through environment variables:
 42 | 
 43 | ```env
 44 | PROGRAM=        # Path to the MCP server executable (required if no config file)
 45 | ARGS=           # Comma-separated list of arguments for the MCP server
 46 | HOST=0.0.0.0    # Host address to bind to (default: 0.0.0.0)
 47 | PORT=8080       # Port to listen on (default: 8080)
 48 | CONFIG_FILE=    # Path to JSON configuration file
 49 | ```
 50 | 
 51 | Additional environment variables will be passed through to the MCP server process.
 52 | 
 53 | ### JSON Configuration
 54 | 
 55 | Alternatively, you can provide a JSON configuration file:
 56 | 
 57 | ```json
 58 | {
 59 |   "servers": {
 60 |     "filesystem": {
 61 |       "command": "npx",
 62 |       "args": [
 63 |         "-y",
 64 |         "@modelcontextprotocol/server-filesystem",
 65 |         "/path/to/workspace"
 66 |       ]
 67 |     },
 68 |     "github": {
 69 |       "command": "npx",
 70 |       "args": ["-y", "@modelcontextprotocol/server-github"],
 71 |       "env": {
 72 |         "GITHUB_PERSONAL_ACCESS_TOKEN": "your_token_here"
 73 |       }
 74 |     }
 75 |   },
 76 |   "default_server": "filesystem",
 77 |   "host": "0.0.0.0",
 78 |   "port": 8080
 79 | }
 80 | ```
 81 | 
 82 | You can specify the configuration file in two ways:
 83 | 
 84 | 1. As a command-line argument: `mcp-server-runner config.json`
 85 | 2. Using the `CONFIG_FILE` environment variable: `CONFIG_FILE=config.json mcp-server-runner`
 86 | 
 87 | The JSON configuration allows you to define multiple server configurations and select one as the default.
 88 | 
 89 | ### Configuration Priority
 90 | 
 91 | 1. Command-line specified config file
 92 | 2. `CONFIG_FILE` environment variable
 93 | 3. Environment variables (`PROGRAM`, `ARGS`, etc.)
 94 | 4. Default values
 95 | 
 96 | ## Usage
 97 | 
 98 | 1. Using environment variables:
 99 | 
100 |    ```bash
101 |    export PROGRAM=npx
102 |    export ARGS=-y,@modelcontextprotocol/server-github
103 |    export PORT=8080
104 |    export GITHUB_PERSONAL_ACCESS_TOKEN=github_pat_***
105 |    cargo run
106 |    ```
107 | 
108 | 2. Using a configuration file:
109 | 
110 |    ```bash
111 |    # Either specify the config file as an argument
112 |    cargo run config.json
113 | 
114 |    # Or use the CONFIG_FILE environment variable
115 |    CONFIG_FILE=config.json cargo run
116 |    ```
117 | 
118 | 3. Connect to the WebSocket server:
119 |    ```javascript
120 |    const ws = new WebSocket("ws://localhost:8080");
121 |    ```
122 | 
123 | ## Docker Support
124 | 
125 | A Dockerfile and docker-compose.yml are provided for containerized deployment:
126 | 
127 | ```bash
128 | docker-compose up --build
129 | ```
130 | 
131 | ## Development
132 | 
133 | Build the project:
134 | 
135 | ```bash
136 | cargo build
137 | ```
138 | 
139 | Run tests:
140 | 
141 | ```bash
142 | cargo test
143 | ```
144 | 
145 | Run with debug logging:
146 | 
147 | ```bash
148 | RUST_LOG=debug cargo run
149 | ```
150 | 
151 | ## Architecture
152 | 
153 | The application follows a modular architecture:
154 | 
155 | - `main.rs`: Application entry point and server setup
156 | - `config/`: Configuration loading and management
157 | - `process/`: Process management and I/O handling
158 | - `websocket/`: WebSocket connection management
159 | - `state.rs`: Global state management
160 | - `shutdown.rs`: Graceful shutdown handling
161 | 
162 | ## Error Handling
163 | 
164 | - Standard error output from the MCP server is logged but not forwarded to clients
165 | - WebSocket connection errors are handled gracefully
166 | - Process errors are logged with detailed information
167 | 
168 | ## Limitations
169 | 
170 | - Supports only one client connection at a time
171 | - Does not support WebSocket SSL/TLS (use a reverse proxy for secure connections)
172 | - No built-in authentication mechanism
173 | 
174 | ## Contributing
175 | 
176 | 1. Fork the repository
177 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
178 | 3. Commit your changes (`git commit -m 'Add amazing feature'`)
179 | 4. Push to the branch (`git push origin feature/amazing-feature`)
180 | 5. Open a Pull Request
181 | 
182 | ## License
183 | 
184 | This project is licensed under the MIT License - see the LICENSE file for details.
185 | 
186 | ## Additional Resources
187 | 
188 | - [Model Context Protocol Specification](https://github.com/modelcontextprotocol/specification)
189 | - [WebSocket Protocol (RFC 6455)](https://tools.ietf.org/html/rfc6455)
190 | 
```

--------------------------------------------------------------------------------
/src/process/mod.rs:
--------------------------------------------------------------------------------

```rust
1 | mod io;
2 | mod manager;
3 | 
4 | pub use manager::ProcessManager;
```

--------------------------------------------------------------------------------
/src/config/mod.rs:
--------------------------------------------------------------------------------

```rust
1 | pub mod model;
2 | mod loader;
3 | 
4 | pub use loader::load_config;
```

--------------------------------------------------------------------------------
/src/constants.rs:
--------------------------------------------------------------------------------

```rust
1 | /// Size of the message buffer for communication channels.
2 | /// This value affects the capacity of mpsc channels used for
3 | /// process and WebSocket communication.
4 | pub const MESSAGE_BUFFER_SIZE: usize = 100;
```

--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "servers": {
 3 |     "github": {
 4 |       "command": "npx",
 5 |       "args": ["-y", "@modelcontextprotocol/server-github"],
 6 |       "env": {
 7 |         "GITHUB_PERSONAL_ACCESS_TOKEN": "your_token_here"
 8 |       }
 9 |     }
10 |   },
11 |   "default_server": "github",
12 |   "host": "0.0.0.0",
13 |   "port": 8080
14 | }
15 | 
16 | 
```

--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------

```rust
 1 | pub mod config;
 2 | mod constants;
 3 | mod process;
 4 | mod shutdown;
 5 | mod state;
 6 | mod websocket;
 7 | 
 8 | // Re-export public API
 9 | pub use constants::MESSAGE_BUFFER_SIZE;
10 | pub use process::ProcessManager;
11 | pub use shutdown::shutdown_signal;
12 | pub use websocket::handle_connection;
13 | pub use state::{CONNECTED, SHUTDOWN};
```

--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------

```toml
 1 | [package]
 2 | name = "mcp-server-runner"
 3 | version = "0.1.0"
 4 | edition = "2021"
 5 | 
 6 | [dependencies]
 7 | tokio = { version = "1.0", features = ["full"] }
 8 | tokio-tungstenite = "0.20"
 9 | futures-util = "0.3"
10 | log = "0.4"
11 | env_logger = "0.10"
12 | anyhow = "1.0"
13 | serde = { version = "1.0", features = ["derive"] }
14 | serde_json = "1.0"
15 | 
```

--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | version: "3.9"
 2 | 
 3 | services:
 4 |   mcp-server:
 5 |     build:
 6 |       context: .
 7 |       dockerfile: Dockerfile
 8 |     ports:
 9 |       - "3000:8080"
10 |     environment:
11 |       - PROGRAM=${PROGRAM}
12 |       - ARGS=${ARGS}
13 |       - GITHUB_PERSONAL_ACCESS_TOKEN=${GITHUB_PERSONAL_ACCESS_TOKEN}
14 |       - RUST_LOG=debug
15 |     volumes:
16 |       - ./data:/app/data
17 |     restart: always
18 | 
```

--------------------------------------------------------------------------------
/src/state.rs:
--------------------------------------------------------------------------------

```rust
1 | use std::sync::atomic::AtomicBool;
2 | 
3 | /// Indicates whether a client is currently connected to the WebSocket server.
4 | /// Used to ensure only one client can be connected at a time.
5 | pub static CONNECTED: AtomicBool = AtomicBool::new(false);
6 | 
7 | /// Global shutdown flag to signal all components to terminate.
8 | /// When set to true, all async tasks should gracefully shut down.
9 | pub static SHUTDOWN: AtomicBool = AtomicBool::new(false);
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | FROM rust:latest AS chef
 2 | RUN cargo install cargo-chef
 3 | WORKDIR /app
 4 | 
 5 | FROM chef AS planner
 6 | COPY . .
 7 | RUN cargo chef prepare --recipe-path recipe.json
 8 | 
 9 | FROM chef AS builder
10 | COPY --from=planner /app/recipe.json recipe.json
11 | RUN cargo chef cook --release --recipe-path recipe.json
12 | COPY . .
13 | RUN cargo build --release
14 | 
15 | FROM debian:bookworm-slim AS runtime
16 | WORKDIR /app
17 | RUN apt-get update && apt-get install -y --no-install-recommends \
18 |   nodejs npm python3 python3-pip curl && \
19 |   curl -LsSf https://astral.sh/uv/install.sh | sh && \
20 |   apt-get clean && rm -rf /var/lib/apt/lists/* /root/.npm /root/.cache
21 | COPY --from=builder /app/target/release/mcp-server-runner /usr/local/bin/
22 | CMD ["mcp-server-runner"]
23 | 
```

--------------------------------------------------------------------------------
/src/shutdown.rs:
--------------------------------------------------------------------------------

```rust
 1 | use log::info;
 2 | use tokio::signal;
 3 | use std::sync::atomic::Ordering;
 4 | 
 5 | use crate::state::SHUTDOWN;
 6 | 
 7 | /// Handles shutdown signals for the application.
 8 | /// Listens for Ctrl+C and termination signals (on Unix systems),
 9 | /// and sets the global shutdown flag when received.
10 | pub async fn shutdown_signal() {
11 |     wait_for_shutdown_signal().await;
12 |     initiate_shutdown();
13 | }
14 | 
15 | /// Waits for either Ctrl+C or termination signal.
16 | async fn wait_for_shutdown_signal() {
17 |     let ctrl_c = setup_ctrl_c();
18 |     let terminate = setup_terminate();
19 | 
20 |     tokio::select! {
21 |         _ = ctrl_c => info!("Ctrl+C received"),
22 |         _ = terminate => info!("Termination signal received"),
23 |     }
24 | }
25 | 
26 | /// Sets up Ctrl+C signal handler.
27 | async fn setup_ctrl_c() {
28 |     signal::ctrl_c()
29 |         .await
30 |         .expect("Failed to install Ctrl+C handler");
31 | }
32 | 
33 | /// Sets up termination signal handler (Unix only).
34 | #[cfg(unix)]
35 | async fn setup_terminate() {
36 |     signal::unix::signal(signal::unix::SignalKind::terminate())
37 |         .expect("Failed to install signal handler")
38 |         .recv()
39 |         .await;
40 | }
41 | 
42 | /// Placeholder for non-Unix systems.
43 | #[cfg(not(unix))]
44 | async fn setup_terminate() {
45 |     std::future::pending::<()>().await
46 | }
47 | 
48 | /// Initiates the shutdown process by setting the global shutdown flag.
49 | fn initiate_shutdown() {
50 |     info!("Initiating shutdown sequence");
51 |     SHUTDOWN.store(true, Ordering::SeqCst);
52 | }
```

--------------------------------------------------------------------------------
/src/websocket/mod.rs:
--------------------------------------------------------------------------------

```rust
 1 | mod message;
 2 | 
 3 | use anyhow::Result;
 4 | use log::info;
 5 | use tokio::net::TcpStream;
 6 | use tokio::sync::mpsc;
 7 | use tokio_tungstenite::accept_async;
 8 | use std::sync::atomic::Ordering;
 9 | use futures_util::StreamExt;
10 | 
11 | use crate::state::CONNECTED;
12 | use self::message::{handle_incoming_messages, handle_outgoing_messages};
13 | 
14 | /// Handle a new WebSocket connection
15 | pub async fn handle_connection(
16 |     stream: TcpStream,
17 |     process_tx: mpsc::Sender<String>,
18 |     ws_rx: mpsc::Receiver<String>,
19 | ) -> Result<()> {
20 |     let addr = setup_connection(&stream)?;
21 |     let ws_stream = accept_async(stream).await?;
22 |     
23 |     info!("WebSocket connection established: {}", addr);
24 |     let (ws_writer, ws_reader) = ws_stream.split();
25 | 
26 |     let ws_to_process = handle_incoming_messages(ws_reader, process_tx);
27 |     let process_to_ws = handle_outgoing_messages(ws_writer, ws_rx);
28 | 
29 |     tokio::select! {
30 |         _ = ws_to_process => info!("WebSocket -> Process handling completed"),
31 |         _ = process_to_ws => info!("Process -> WebSocket handling completed"),
32 |     }
33 |     
34 |     cleanup_connection(addr);
35 |     Ok(())
36 | }
37 | 
38 | /// Set up initial connection state
39 | fn setup_connection(stream: &TcpStream) -> Result<std::net::SocketAddr> {
40 |     let addr = stream.peer_addr()?;
41 |     CONNECTED.store(true, Ordering::SeqCst);
42 |     Ok(addr)
43 | }
44 | 
45 | /// Clean up connection state
46 | fn cleanup_connection(addr: std::net::SocketAddr) {
47 |     CONNECTED.store(false, Ordering::SeqCst);
48 |     info!("Client disconnected: {}", addr);
49 | }
```

--------------------------------------------------------------------------------
/src/websocket/message.rs:
--------------------------------------------------------------------------------

```rust
 1 | use std::sync::atomic::Ordering;
 2 | use futures_util::{SinkExt, StreamExt};
 3 | use log::{debug, error};
 4 | use tokio::sync::mpsc;
 5 | use tokio_tungstenite::tungstenite::protocol::Message;
 6 | use futures_util::sink::Sink;
 7 | 
 8 | use crate::state::SHUTDOWN;
 9 | 
10 | pub async fn handle_incoming_messages<S>(
11 |     mut reader: S,
12 |     process_tx: mpsc::Sender<String>,
13 | ) where
14 |     S: StreamExt<Item = Result<Message, tokio_tungstenite::tungstenite::Error>> + Unpin,
15 | {
16 |     while let Some(msg) = reader.next().await {
17 |         if SHUTDOWN.load(Ordering::SeqCst) {
18 |             break;
19 |         }
20 | 
21 |         match process_incoming_message(msg, &process_tx).await {
22 |             Ok(should_break) => {
23 |                 if should_break {
24 |                     break;
25 |                 }
26 |             }
27 |             Err(e) => {
28 |                 error!("Error processing incoming message: {}", e);
29 |                 break;
30 |             }
31 |         }
32 |     }
33 | }
34 | 
35 | pub async fn handle_outgoing_messages<S>(
36 |     mut writer: S,
37 |     mut ws_rx: mpsc::Receiver<String>,
38 | ) where
39 |     S: Sink<Message> + Unpin,
40 |     S::Error: std::fmt::Debug,
41 | {
42 |     while let Some(msg) = ws_rx.recv().await {
43 |         if SHUTDOWN.load(Ordering::SeqCst) {
44 |             break;
45 |         }
46 | 
47 |         debug!("Sending process response: {}", msg);
48 |         if let Err(e) = writer.send(Message::Text(msg)).await {
49 |             error!("Error sending to WebSocket: {:?}", e);
50 |             break;
51 |         }
52 |     }
53 | }
54 | 
55 | async fn process_incoming_message(
56 |     msg: Result<Message, tokio_tungstenite::tungstenite::Error>,
57 |     process_tx: &mpsc::Sender<String>,
58 | ) -> Result<bool, Box<dyn std::error::Error>> {
59 |     match msg {
60 |         Ok(msg) => {
61 |             if msg.is_close() {
62 |                 return Ok(true);
63 |             }
64 |             if let Ok(text) = msg.into_text() {
65 |                 debug!("Received from client: {}", text);
66 |                 process_tx.send(text).await?;
67 |             }
68 |         }
69 |         Err(e) => {
70 |             error!("Error receiving from WebSocket: {}", e);
71 |             return Ok(true);
72 |         }
73 |     }
74 |     Ok(false)
75 | }
```

--------------------------------------------------------------------------------
/src/process/manager.rs:
--------------------------------------------------------------------------------

```rust
 1 | use anyhow::{Context, Result};
 2 | use log::{debug, error};
 3 | use std::collections::HashMap;
 4 | use tokio::process::{Child, Command};
 5 | use tokio::sync::mpsc;
 6 | 
 7 | use super::io::{handle_stdin, handle_stdout, handle_stderr};
 8 | use crate::constants::MESSAGE_BUFFER_SIZE;
 9 | 
10 | pub struct ProcessManager {
11 |     child: Option<Child>,
12 | }
13 | 
14 | impl ProcessManager {
15 |     pub fn new() -> Self {
16 |         Self { child: None }
17 |     }
18 | 
19 |     pub async fn start_process(
20 |         &mut self,
21 |         program: &str,
22 |         args: &[String],
23 |         env_vars: &HashMap<String, String>,
24 |         websocket_tx: mpsc::Sender<String>,
25 |     ) -> Result<mpsc::Sender<String>> {
26 |         let child = self.spawn_process(program, args, env_vars)?;
27 |         let (process_tx, process_rx) = mpsc::channel::<String>(MESSAGE_BUFFER_SIZE);
28 |         
29 |         self.setup_io_handlers(child, process_rx, websocket_tx)?;
30 |         
31 |         Ok(process_tx)
32 |     }
33 | 
34 |     fn spawn_process(
35 |         &mut self,
36 |         program: &str,
37 |         args: &[String],
38 |         env_vars: &HashMap<String, String>,
39 |     ) -> Result<Child> {
40 |         let mut command = Command::new(program);
41 |         
42 |         if !args.is_empty() {
43 |             command.args(args);
44 |         }
45 |         
46 |         for (key, value) in env_vars {
47 |             command.env(key, value);
48 |         }
49 | 
50 |         debug!("Spawning process: {} {:?}", program, args);
51 |         
52 |         let child = command
53 |             .stdin(std::process::Stdio::piped())
54 |             .stdout(std::process::Stdio::piped())
55 |             .stderr(std::process::Stdio::piped())
56 |             .spawn()?;
57 | 
58 |         Ok(child)
59 |     }
60 | 
61 |     fn setup_io_handlers(
62 |         &mut self,
63 |         mut child: Child,
64 |         process_rx: mpsc::Receiver<String>,
65 |         websocket_tx: mpsc::Sender<String>,
66 |     ) -> Result<()> {
67 |         let stdin = child.stdin.take().context("Failed to get child stdin")?;
68 |         let stdout = child.stdout.take().context("Failed to get child stdout")?;
69 |         let stderr = child.stderr.take().context("Failed to get child stderr")?;
70 | 
71 |         self.child = Some(child);
72 | 
73 |         tokio::spawn(handle_stdin(stdin, process_rx));
74 |         tokio::spawn(handle_stdout(stdout, websocket_tx));
75 |         tokio::spawn(handle_stderr(stderr));
76 | 
77 |         Ok(())
78 |     }
79 | 
80 |     pub async fn shutdown(&mut self) {
81 |         if let Some(mut child) = self.child.take() {
82 |             debug!("Stopping child process...");
83 |             if let Err(e) = child.kill().await {
84 |                 error!("Failed to stop child process: {}", e);
85 |             }
86 |             if let Err(e) = child.wait().await {
87 |                 error!("Error waiting for child process to exit: {}", e);
88 |             }
89 |             debug!("Child process stopped");
90 |         }
91 |     }
92 | }
```

--------------------------------------------------------------------------------
/src/process/io.rs:
--------------------------------------------------------------------------------

```rust
 1 | use log::{debug, error, info, warn};
 2 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter};
 3 | use tokio::process::{ChildStdin, ChildStdout, ChildStderr};
 4 | use tokio::sync::mpsc;
 5 | use std::sync::atomic::Ordering;
 6 | 
 7 | use crate::state::SHUTDOWN;
 8 | 
 9 | pub async fn handle_stdin(
10 |     stdin: ChildStdin,
11 |     mut process_rx: mpsc::Receiver<String>,
12 | ) {
13 |     let mut writer = BufWriter::new(stdin);
14 |     debug!("Started stdin handler for child process");
15 | 
16 |     while let Some(message) = process_rx.recv().await {
17 |         if SHUTDOWN.load(Ordering::SeqCst) {
18 |             debug!("Shutdown signal received, stopping stdin handler");
19 |             break;
20 |         }
21 | 
22 |         debug!("Received message to send to process. Length: {}", message.len());
23 |         if let Err(e) = write_to_process(&mut writer, &message).await {
24 |             error!("Error in stdin handling: {}. Message was: {}", e, message);
25 |             break;
26 |         }
27 |         debug!("Successfully wrote message to process");
28 |     }
29 |     info!("Stdin handler finished");
30 | }
31 | 
32 | pub async fn handle_stdout(
33 |     stdout: ChildStdout,
34 |     websocket_tx: mpsc::Sender<String>,
35 | ) {
36 |     let mut reader = BufReader::new(stdout);
37 |     let mut line = String::new();
38 |     debug!("Started stdout handler for child process");
39 | 
40 |     while let Ok(n) = reader.read_line(&mut line).await {
41 |         if should_stop(n) {
42 |             debug!("Stopping stdout handler: {}", 
43 |                 if n == 0 { "EOF reached" } else { "shutdown requested" });
44 |             break;
45 |         }
46 | 
47 |         let trimmed = line.trim().to_string();
48 |         debug!("Received from process (stdout) - Length: {}, Content: {}", 
49 |             trimmed.len(), trimmed);
50 | 
51 |         if let Err(e) = websocket_tx.send(trimmed).await {
52 |             error!("Error sending to WebSocket: {}", e);
53 |             break;
54 |         }
55 |         debug!("Successfully sent process output to WebSocket");
56 |         line.clear();
57 |     }
58 |     info!("Stdout handler finished");
59 | }
60 | 
61 | pub async fn handle_stderr(stderr: ChildStderr) {
62 |     let mut reader = BufReader::new(stderr);
63 |     let mut line = String::new();
64 |     debug!("Started stderr handler for child process");
65 | 
66 |     while let Ok(n) = reader.read_line(&mut line).await {
67 |         if should_stop(n) {
68 |             debug!("Stopping stderr handler: {}", 
69 |                 if n == 0 { "EOF reached" } else { "shutdown requested" });
70 |             break;
71 |         }
72 | 
73 |         let trimmed = line.trim();
74 |         warn!("Process stderr: {}", trimmed);
75 |         line.clear();
76 |     }
77 |     info!("Stderr handler finished");
78 | }
79 | 
80 | async fn write_to_process(
81 |     writer: &mut BufWriter<ChildStdin>,
82 |     message: &str,
83 | ) -> tokio::io::Result<()> {
84 |     debug!("Writing to process - Length: {}, Content: {}", message.len(), message);
85 |     writer.write_all(message.as_bytes()).await?;
86 |     writer.write_all(b"\n").await?;
87 |     writer.flush().await?;
88 |     debug!("Successfully flushed message to process");
89 |     Ok(())
90 | }
91 | 
92 | fn should_stop(n: usize) -> bool {
93 |     n == 0 || SHUTDOWN.load(Ordering::SeqCst)
94 | }
95 | 
```

--------------------------------------------------------------------------------
/test.html:
--------------------------------------------------------------------------------

```html
  1 | <!DOCTYPE html>
  2 | <html lang="en">
  3 |   <head>
  4 |     <meta charset="UTF-8" />
  5 |     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6 |     <title>MCP WebSocket Test Client</title>
  7 |     <script src="https://cdnjs.cloudflare.com/ajax/libs/json-formatter/0.7.2/json-formatter.min.js"></script>
  8 |     <style>
  9 |       body {
 10 |         font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
 11 |           "Helvetica Neue", Arial, sans-serif;
 12 |         max-width: 1200px;
 13 |         margin: 0 auto;
 14 |         padding: 20px;
 15 |         background: #f5f5f5;
 16 |       }
 17 |       .container {
 18 |         background: white;
 19 |         padding: 20px;
 20 |         border-radius: 8px;
 21 |         box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
 22 |       }
 23 |       .status {
 24 |         padding: 10px;
 25 |         margin-bottom: 20px;
 26 |         border-radius: 4px;
 27 |       }
 28 |       .status.connected {
 29 |         background-color: #d4edda;
 30 |         color: #155724;
 31 |         border: 1px solid #c3e6cb;
 32 |       }
 33 |       .status.disconnected {
 34 |         background-color: #f8d7da;
 35 |         color: #721c24;
 36 |         border: 1px solid #f5c6cb;
 37 |       }
 38 |       .message-box {
 39 |         margin: 20px 0;
 40 |       }
 41 |       .message-input {
 42 |         width: 100%;
 43 |         height: 100px;
 44 |         margin: 10px 0;
 45 |         padding: 10px;
 46 |         border: 1px solid #ddd;
 47 |         border-radius: 4px;
 48 |         font-family: monospace;
 49 |       }
 50 |       .button {
 51 |         background-color: #007bff;
 52 |         color: white;
 53 |         border: none;
 54 |         padding: 10px 20px;
 55 |         border-radius: 4px;
 56 |         cursor: pointer;
 57 |       }
 58 |       .button:hover {
 59 |         background-color: #0056b3;
 60 |       }
 61 |       .button:disabled {
 62 |         background-color: #ccc;
 63 |         cursor: not-allowed;
 64 |       }
 65 |       .log {
 66 |         margin-top: 20px;
 67 |         border: 1px solid #ddd;
 68 |         padding: 10px;
 69 |         border-radius: 4px;
 70 |         background: #f8f9fa;
 71 |         height: 400px;
 72 |         overflow-y: auto;
 73 |       }
 74 |       .log-entry {
 75 |         margin: 5px 0;
 76 |         padding: 5px;
 77 |         border-bottom: 1px solid #eee;
 78 |       }
 79 |       .log-entry.sent {
 80 |         color: #004085;
 81 |         background-color: #cce5ff;
 82 |       }
 83 |       .log-entry.received {
 84 |         color: #155724;
 85 |         background-color: #d4edda;
 86 |       }
 87 |       .log-entry.error {
 88 |         color: #721c24;
 89 |         background-color: #f8d7da;
 90 |       }
 91 |       #requestTemplates {
 92 |         margin-bottom: 20px;
 93 |       }
 94 |     </style>
 95 |   </head>
 96 |   <body>
 97 |     <div class="container">
 98 |       <h1>MCP WebSocket Test Client</h1>
 99 | 
100 |       <div id="connectionStatus" class="status disconnected">Disconnected</div>
101 | 
102 |       <div class="connection-controls">
103 |         <input
104 |           type="text"
105 |           id="wsUrl"
106 |           value="ws://localhost:3000"
107 |           style="width: 200px; margin-right: 10px"
108 |         />
109 |         <button id="connectButton" class="button">Connect</button>
110 |         <button id="disconnectButton" class="button" disabled>
111 |           Disconnect
112 |         </button>
113 |       </div>
114 | 
115 |       <div class="message-box">
116 |         <h3>Request Templates</h3>
117 |         <select id="requestTemplates" style="width: 100%; padding: 5px">
118 |           <option value="initialize">Initialize</option>
119 |           <option value="ping">Ping</option>
120 |           <option value="resourcesList">List Resources</option>
121 |           <option value="toolsList">List Tools</option>
122 |           <option value="promptsList">List Prompts</option>
123 |         </select>
124 |         <h3>Message</h3>
125 |         <textarea
126 |           id="messageInput"
127 |           class="message-input"
128 |           placeholder="Enter JSON message"
129 |         ></textarea>
130 |         <button id="sendButton" class="button" disabled>Send Message</button>
131 |       </div>
132 | 
133 |       <div class="log" id="messageLog"></div>
134 |     </div>
135 | 
136 |     <script>
137 |       let ws = null;
138 |       let nextRequestId = 1;
139 | 
140 |       const templates = {
141 |         initialize: {
142 |           jsonrpc: "2.0",
143 |           method: "initialize",
144 |           id: 1,
145 |           params: {
146 |             protocolVersion: "2024-11-05",
147 |             capabilities: {
148 |               sampling: {},
149 |             },
150 |             clientInfo: {
151 |               name: "Web Test Client",
152 |               version: "1.0.0",
153 |             },
154 |           },
155 |         },
156 |         ping: {
157 |           jsonrpc: "2.0",
158 |           method: "ping",
159 |           id: 1,
160 |         },
161 |         resourcesList: {
162 |           jsonrpc: "2.0",
163 |           method: "resources/list",
164 |           id: 1,
165 |         },
166 |         toolsList: {
167 |           jsonrpc: "2.0",
168 |           method: "tools/list",
169 |           id: 1,
170 |         },
171 |         promptsList: {
172 |           jsonrpc: "2.0",
173 |           method: "prompts/list",
174 |           id: 1,
175 |         },
176 |       };
177 | 
178 |       function updateConnectionStatus(connected) {
179 |         const statusDiv = document.getElementById("connectionStatus");
180 |         const connectButton = document.getElementById("connectButton");
181 |         const disconnectButton = document.getElementById("disconnectButton");
182 |         const sendButton = document.getElementById("sendButton");
183 | 
184 |         statusDiv.textContent = connected ? "Connected" : "Disconnected";
185 |         statusDiv.className = `status ${
186 |           connected ? "connected" : "disconnected"
187 |         }`;
188 | 
189 |         connectButton.disabled = connected;
190 |         disconnectButton.disabled = !connected;
191 |         sendButton.disabled = !connected;
192 |       }
193 | 
194 |       function addLogEntry(message, type = "received") {
195 |         const logDiv = document.getElementById("messageLog");
196 |         const entry = document.createElement("div");
197 |         entry.className = `log-entry ${type}`;
198 | 
199 |         try {
200 |           let jsonData = message;
201 |           if (typeof message === "string") {
202 |             jsonData = JSON.parse(message);
203 |           }
204 |           const formatter = new JSONFormatter(jsonData, 2, {
205 |             hoverPreviewEnabled: true,
206 |             hoverPreviewArrayCount: 100,
207 |             hoverPreviewFieldCount: 5,
208 |           });
209 |           entry.appendChild(formatter.render());
210 |         } catch (e) {
211 |           console.error("Failed to format message:", e);
212 |           entry.innerHTML = `<pre>${
213 |             typeof message === "object"
214 |               ? JSON.stringify(message, null, 2)
215 |               : String(message)
216 |           }</pre>`;
217 |         }
218 | 
219 |         logDiv.appendChild(entry);
220 |         logDiv.scrollTop = logDiv.scrollHeight;
221 |       }
222 | 
223 |       function connect() {
224 |         const url = document.getElementById("wsUrl").value;
225 |         try {
226 |           ws = new WebSocket(url);
227 | 
228 |           ws.onopen = () => {
229 |             updateConnectionStatus(true);
230 |             addLogEntry("WebSocket connection established", "received");
231 |           };
232 | 
233 |           ws.onclose = () => {
234 |             updateConnectionStatus(false);
235 |             addLogEntry("WebSocket connection closed", "error");
236 |             ws = null;
237 |           };
238 | 
239 |           ws.onerror = (error) => {
240 |             addLogEntry(`WebSocket error: ${error}`, "error");
241 |           };
242 | 
243 |           ws.onmessage = (event) => {
244 |             // イベントデータがJSON形式の文字列でない場合に備えて処理
245 |             let data = event.data;
246 |             try {
247 |               // オブジェクトが文字列化されていない場合は文字列化
248 |               if (typeof data === "object") {
249 |                 data = JSON.stringify(data);
250 |               }
251 |               // 文字列をJSONとしてパースしてフォーマット
252 |               const jsonData = JSON.parse(data);
253 |               addLogEntry(jsonData, "received");
254 |             } catch (e) {
255 |               // JSONとしてパースできない場合は生のデータを表示
256 |               console.error("Failed to parse message:", e);
257 |               addLogEntry(
258 |                 {
259 |                   error: "Failed to parse message",
260 |                   data: data,
261 |                   details: e.toString(),
262 |                 },
263 |                 "error"
264 |               );
265 |             }
266 |           };
267 |         } catch (error) {
268 |           addLogEntry(`Failed to connect: ${error}`, "error");
269 |         }
270 |       }
271 | 
272 |       function disconnect() {
273 |         if (ws) {
274 |           ws.close();
275 |         }
276 |       }
277 | 
278 |       function sendMessage() {
279 |         if (!ws) {
280 |           addLogEntry("Not connected to server", "error");
281 |           return;
282 |         }
283 | 
284 |         try {
285 |           const messageInput = document.getElementById("messageInput");
286 |           const message = JSON.parse(messageInput.value);
287 |           ws.send(JSON.stringify(message));
288 |           addLogEntry(message, "sent");
289 |         } catch (error) {
290 |           addLogEntry(`Failed to send message: ${error}`, "error");
291 |         }
292 |       }
293 | 
294 |       document.getElementById("connectButton").onclick = connect;
295 |       document.getElementById("disconnectButton").onclick = disconnect;
296 |       document.getElementById("sendButton").onclick = sendMessage;
297 | 
298 |       document.getElementById("requestTemplates").onchange = (e) => {
299 |         const template = templates[e.target.value];
300 |         if (template) {
301 |           template.id = nextRequestId++;
302 |           document.getElementById("messageInput").value = JSON.stringify(
303 |             template,
304 |             null,
305 |             2
306 |           );
307 |         }
308 |       };
309 | 
310 |       // 初期化時にテンプレートを選択
311 |       document
312 |         .getElementById("requestTemplates")
313 |         .dispatchEvent(new Event("change"));
314 |     </script>
315 |   </body>
316 | </html>
317 | 
```