# 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:
--------------------------------------------------------------------------------
```
/target
.env
.DS_Store
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# MCP Server Runner
> **Note**: This project is currently under active development and in WIP (Work In Progress) status. Features and APIs may change significantly.
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.
## Development Status
- 🚧 **Work In Progress**: This software is in active development
- ⚠️ **API Stability**: APIs and features may change without notice
- 🧪 **Testing**: Currently undergoing testing and refinement
- 📝 **Documentation**: Documentation is being actively updated
## Overview
MCP Server Runner acts as a bridge between WebSocket clients and MCP server implementations. It:
- Launches an MCP server process
- Manages WebSocket connections
- Handles bidirectional communication between clients and the MCP server
- Supports graceful shutdown and error handling
## Features
- WebSocket server implementation with single-client support
- Process management for MCP server instances
- Bidirectional message passing between client and server
- Graceful shutdown handling
- Comprehensive error logging
- Cross-platform support (Unix/Windows)
## Prerequisites
- Rust 1.70 or higher
- An MCP server implementation executable
## Configuration
### Environment Variables
The application can be configured through environment variables:
```env
PROGRAM= # Path to the MCP server executable (required if no config file)
ARGS= # Comma-separated list of arguments for the MCP server
HOST=0.0.0.0 # Host address to bind to (default: 0.0.0.0)
PORT=8080 # Port to listen on (default: 8080)
CONFIG_FILE= # Path to JSON configuration file
```
Additional environment variables will be passed through to the MCP server process.
### JSON Configuration
Alternatively, you can provide a JSON configuration file:
```json
{
"servers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/path/to/workspace"
]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "your_token_here"
}
}
},
"default_server": "filesystem",
"host": "0.0.0.0",
"port": 8080
}
```
You can specify the configuration file in two ways:
1. As a command-line argument: `mcp-server-runner config.json`
2. Using the `CONFIG_FILE` environment variable: `CONFIG_FILE=config.json mcp-server-runner`
The JSON configuration allows you to define multiple server configurations and select one as the default.
### Configuration Priority
1. Command-line specified config file
2. `CONFIG_FILE` environment variable
3. Environment variables (`PROGRAM`, `ARGS`, etc.)
4. Default values
## Usage
1. Using environment variables:
```bash
export PROGRAM=npx
export ARGS=-y,@modelcontextprotocol/server-github
export PORT=8080
export GITHUB_PERSONAL_ACCESS_TOKEN=github_pat_***
cargo run
```
2. Using a configuration file:
```bash
# Either specify the config file as an argument
cargo run config.json
# Or use the CONFIG_FILE environment variable
CONFIG_FILE=config.json cargo run
```
3. Connect to the WebSocket server:
```javascript
const ws = new WebSocket("ws://localhost:8080");
```
## Docker Support
A Dockerfile and docker-compose.yml are provided for containerized deployment:
```bash
docker-compose up --build
```
## Development
Build the project:
```bash
cargo build
```
Run tests:
```bash
cargo test
```
Run with debug logging:
```bash
RUST_LOG=debug cargo run
```
## Architecture
The application follows a modular architecture:
- `main.rs`: Application entry point and server setup
- `config/`: Configuration loading and management
- `process/`: Process management and I/O handling
- `websocket/`: WebSocket connection management
- `state.rs`: Global state management
- `shutdown.rs`: Graceful shutdown handling
## Error Handling
- Standard error output from the MCP server is logged but not forwarded to clients
- WebSocket connection errors are handled gracefully
- Process errors are logged with detailed information
## Limitations
- Supports only one client connection at a time
- Does not support WebSocket SSL/TLS (use a reverse proxy for secure connections)
- No built-in authentication mechanism
## Contributing
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## License
This project is licensed under the MIT License - see the LICENSE file for details.
## Additional Resources
- [Model Context Protocol Specification](https://github.com/modelcontextprotocol/specification)
- [WebSocket Protocol (RFC 6455)](https://tools.ietf.org/html/rfc6455)
```
--------------------------------------------------------------------------------
/src/process/mod.rs:
--------------------------------------------------------------------------------
```rust
mod io;
mod manager;
pub use manager::ProcessManager;
```
--------------------------------------------------------------------------------
/src/config/mod.rs:
--------------------------------------------------------------------------------
```rust
pub mod model;
mod loader;
pub use loader::load_config;
```
--------------------------------------------------------------------------------
/src/constants.rs:
--------------------------------------------------------------------------------
```rust
/// Size of the message buffer for communication channels.
/// This value affects the capacity of mpsc channels used for
/// process and WebSocket communication.
pub const MESSAGE_BUFFER_SIZE: usize = 100;
```
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
```json
{
"servers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "your_token_here"
}
}
},
"default_server": "github",
"host": "0.0.0.0",
"port": 8080
}
```
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
```rust
pub mod config;
mod constants;
mod process;
mod shutdown;
mod state;
mod websocket;
// Re-export public API
pub use constants::MESSAGE_BUFFER_SIZE;
pub use process::ProcessManager;
pub use shutdown::shutdown_signal;
pub use websocket::handle_connection;
pub use state::{CONNECTED, SHUTDOWN};
```
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
```toml
[package]
name = "mcp-server-runner"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1.0", features = ["full"] }
tokio-tungstenite = "0.20"
futures-util = "0.3"
log = "0.4"
env_logger = "0.10"
anyhow = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
```
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
```yaml
version: "3.9"
services:
mcp-server:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:8080"
environment:
- PROGRAM=${PROGRAM}
- ARGS=${ARGS}
- GITHUB_PERSONAL_ACCESS_TOKEN=${GITHUB_PERSONAL_ACCESS_TOKEN}
- RUST_LOG=debug
volumes:
- ./data:/app/data
restart: always
```
--------------------------------------------------------------------------------
/src/state.rs:
--------------------------------------------------------------------------------
```rust
use std::sync::atomic::AtomicBool;
/// Indicates whether a client is currently connected to the WebSocket server.
/// Used to ensure only one client can be connected at a time.
pub static CONNECTED: AtomicBool = AtomicBool::new(false);
/// Global shutdown flag to signal all components to terminate.
/// When set to true, all async tasks should gracefully shut down.
pub static SHUTDOWN: AtomicBool = AtomicBool::new(false);
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
FROM rust:latest AS chef
RUN cargo install cargo-chef
WORKDIR /app
FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
FROM chef AS builder
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim AS runtime
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
nodejs npm python3 python3-pip curl && \
curl -LsSf https://astral.sh/uv/install.sh | sh && \
apt-get clean && rm -rf /var/lib/apt/lists/* /root/.npm /root/.cache
COPY --from=builder /app/target/release/mcp-server-runner /usr/local/bin/
CMD ["mcp-server-runner"]
```
--------------------------------------------------------------------------------
/src/shutdown.rs:
--------------------------------------------------------------------------------
```rust
use log::info;
use tokio::signal;
use std::sync::atomic::Ordering;
use crate::state::SHUTDOWN;
/// Handles shutdown signals for the application.
/// Listens for Ctrl+C and termination signals (on Unix systems),
/// and sets the global shutdown flag when received.
pub async fn shutdown_signal() {
wait_for_shutdown_signal().await;
initiate_shutdown();
}
/// Waits for either Ctrl+C or termination signal.
async fn wait_for_shutdown_signal() {
let ctrl_c = setup_ctrl_c();
let terminate = setup_terminate();
tokio::select! {
_ = ctrl_c => info!("Ctrl+C received"),
_ = terminate => info!("Termination signal received"),
}
}
/// Sets up Ctrl+C signal handler.
async fn setup_ctrl_c() {
signal::ctrl_c()
.await
.expect("Failed to install Ctrl+C handler");
}
/// Sets up termination signal handler (Unix only).
#[cfg(unix)]
async fn setup_terminate() {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("Failed to install signal handler")
.recv()
.await;
}
/// Placeholder for non-Unix systems.
#[cfg(not(unix))]
async fn setup_terminate() {
std::future::pending::<()>().await
}
/// Initiates the shutdown process by setting the global shutdown flag.
fn initiate_shutdown() {
info!("Initiating shutdown sequence");
SHUTDOWN.store(true, Ordering::SeqCst);
}
```
--------------------------------------------------------------------------------
/src/websocket/mod.rs:
--------------------------------------------------------------------------------
```rust
mod message;
use anyhow::Result;
use log::info;
use tokio::net::TcpStream;
use tokio::sync::mpsc;
use tokio_tungstenite::accept_async;
use std::sync::atomic::Ordering;
use futures_util::StreamExt;
use crate::state::CONNECTED;
use self::message::{handle_incoming_messages, handle_outgoing_messages};
/// Handle a new WebSocket connection
pub async fn handle_connection(
stream: TcpStream,
process_tx: mpsc::Sender<String>,
ws_rx: mpsc::Receiver<String>,
) -> Result<()> {
let addr = setup_connection(&stream)?;
let ws_stream = accept_async(stream).await?;
info!("WebSocket connection established: {}", addr);
let (ws_writer, ws_reader) = ws_stream.split();
let ws_to_process = handle_incoming_messages(ws_reader, process_tx);
let process_to_ws = handle_outgoing_messages(ws_writer, ws_rx);
tokio::select! {
_ = ws_to_process => info!("WebSocket -> Process handling completed"),
_ = process_to_ws => info!("Process -> WebSocket handling completed"),
}
cleanup_connection(addr);
Ok(())
}
/// Set up initial connection state
fn setup_connection(stream: &TcpStream) -> Result<std::net::SocketAddr> {
let addr = stream.peer_addr()?;
CONNECTED.store(true, Ordering::SeqCst);
Ok(addr)
}
/// Clean up connection state
fn cleanup_connection(addr: std::net::SocketAddr) {
CONNECTED.store(false, Ordering::SeqCst);
info!("Client disconnected: {}", addr);
}
```
--------------------------------------------------------------------------------
/src/websocket/message.rs:
--------------------------------------------------------------------------------
```rust
use std::sync::atomic::Ordering;
use futures_util::{SinkExt, StreamExt};
use log::{debug, error};
use tokio::sync::mpsc;
use tokio_tungstenite::tungstenite::protocol::Message;
use futures_util::sink::Sink;
use crate::state::SHUTDOWN;
pub async fn handle_incoming_messages<S>(
mut reader: S,
process_tx: mpsc::Sender<String>,
) where
S: StreamExt<Item = Result<Message, tokio_tungstenite::tungstenite::Error>> + Unpin,
{
while let Some(msg) = reader.next().await {
if SHUTDOWN.load(Ordering::SeqCst) {
break;
}
match process_incoming_message(msg, &process_tx).await {
Ok(should_break) => {
if should_break {
break;
}
}
Err(e) => {
error!("Error processing incoming message: {}", e);
break;
}
}
}
}
pub async fn handle_outgoing_messages<S>(
mut writer: S,
mut ws_rx: mpsc::Receiver<String>,
) where
S: Sink<Message> + Unpin,
S::Error: std::fmt::Debug,
{
while let Some(msg) = ws_rx.recv().await {
if SHUTDOWN.load(Ordering::SeqCst) {
break;
}
debug!("Sending process response: {}", msg);
if let Err(e) = writer.send(Message::Text(msg)).await {
error!("Error sending to WebSocket: {:?}", e);
break;
}
}
}
async fn process_incoming_message(
msg: Result<Message, tokio_tungstenite::tungstenite::Error>,
process_tx: &mpsc::Sender<String>,
) -> Result<bool, Box<dyn std::error::Error>> {
match msg {
Ok(msg) => {
if msg.is_close() {
return Ok(true);
}
if let Ok(text) = msg.into_text() {
debug!("Received from client: {}", text);
process_tx.send(text).await?;
}
}
Err(e) => {
error!("Error receiving from WebSocket: {}", e);
return Ok(true);
}
}
Ok(false)
}
```
--------------------------------------------------------------------------------
/src/process/manager.rs:
--------------------------------------------------------------------------------
```rust
use anyhow::{Context, Result};
use log::{debug, error};
use std::collections::HashMap;
use tokio::process::{Child, Command};
use tokio::sync::mpsc;
use super::io::{handle_stdin, handle_stdout, handle_stderr};
use crate::constants::MESSAGE_BUFFER_SIZE;
pub struct ProcessManager {
child: Option<Child>,
}
impl ProcessManager {
pub fn new() -> Self {
Self { child: None }
}
pub async fn start_process(
&mut self,
program: &str,
args: &[String],
env_vars: &HashMap<String, String>,
websocket_tx: mpsc::Sender<String>,
) -> Result<mpsc::Sender<String>> {
let child = self.spawn_process(program, args, env_vars)?;
let (process_tx, process_rx) = mpsc::channel::<String>(MESSAGE_BUFFER_SIZE);
self.setup_io_handlers(child, process_rx, websocket_tx)?;
Ok(process_tx)
}
fn spawn_process(
&mut self,
program: &str,
args: &[String],
env_vars: &HashMap<String, String>,
) -> Result<Child> {
let mut command = Command::new(program);
if !args.is_empty() {
command.args(args);
}
for (key, value) in env_vars {
command.env(key, value);
}
debug!("Spawning process: {} {:?}", program, args);
let child = command
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()?;
Ok(child)
}
fn setup_io_handlers(
&mut self,
mut child: Child,
process_rx: mpsc::Receiver<String>,
websocket_tx: mpsc::Sender<String>,
) -> Result<()> {
let stdin = child.stdin.take().context("Failed to get child stdin")?;
let stdout = child.stdout.take().context("Failed to get child stdout")?;
let stderr = child.stderr.take().context("Failed to get child stderr")?;
self.child = Some(child);
tokio::spawn(handle_stdin(stdin, process_rx));
tokio::spawn(handle_stdout(stdout, websocket_tx));
tokio::spawn(handle_stderr(stderr));
Ok(())
}
pub async fn shutdown(&mut self) {
if let Some(mut child) = self.child.take() {
debug!("Stopping child process...");
if let Err(e) = child.kill().await {
error!("Failed to stop child process: {}", e);
}
if let Err(e) = child.wait().await {
error!("Error waiting for child process to exit: {}", e);
}
debug!("Child process stopped");
}
}
}
```
--------------------------------------------------------------------------------
/src/process/io.rs:
--------------------------------------------------------------------------------
```rust
use log::{debug, error, info, warn};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter};
use tokio::process::{ChildStdin, ChildStdout, ChildStderr};
use tokio::sync::mpsc;
use std::sync::atomic::Ordering;
use crate::state::SHUTDOWN;
pub async fn handle_stdin(
stdin: ChildStdin,
mut process_rx: mpsc::Receiver<String>,
) {
let mut writer = BufWriter::new(stdin);
debug!("Started stdin handler for child process");
while let Some(message) = process_rx.recv().await {
if SHUTDOWN.load(Ordering::SeqCst) {
debug!("Shutdown signal received, stopping stdin handler");
break;
}
debug!("Received message to send to process. Length: {}", message.len());
if let Err(e) = write_to_process(&mut writer, &message).await {
error!("Error in stdin handling: {}. Message was: {}", e, message);
break;
}
debug!("Successfully wrote message to process");
}
info!("Stdin handler finished");
}
pub async fn handle_stdout(
stdout: ChildStdout,
websocket_tx: mpsc::Sender<String>,
) {
let mut reader = BufReader::new(stdout);
let mut line = String::new();
debug!("Started stdout handler for child process");
while let Ok(n) = reader.read_line(&mut line).await {
if should_stop(n) {
debug!("Stopping stdout handler: {}",
if n == 0 { "EOF reached" } else { "shutdown requested" });
break;
}
let trimmed = line.trim().to_string();
debug!("Received from process (stdout) - Length: {}, Content: {}",
trimmed.len(), trimmed);
if let Err(e) = websocket_tx.send(trimmed).await {
error!("Error sending to WebSocket: {}", e);
break;
}
debug!("Successfully sent process output to WebSocket");
line.clear();
}
info!("Stdout handler finished");
}
pub async fn handle_stderr(stderr: ChildStderr) {
let mut reader = BufReader::new(stderr);
let mut line = String::new();
debug!("Started stderr handler for child process");
while let Ok(n) = reader.read_line(&mut line).await {
if should_stop(n) {
debug!("Stopping stderr handler: {}",
if n == 0 { "EOF reached" } else { "shutdown requested" });
break;
}
let trimmed = line.trim();
warn!("Process stderr: {}", trimmed);
line.clear();
}
info!("Stderr handler finished");
}
async fn write_to_process(
writer: &mut BufWriter<ChildStdin>,
message: &str,
) -> tokio::io::Result<()> {
debug!("Writing to process - Length: {}, Content: {}", message.len(), message);
writer.write_all(message.as_bytes()).await?;
writer.write_all(b"\n").await?;
writer.flush().await?;
debug!("Successfully flushed message to process");
Ok(())
}
fn should_stop(n: usize) -> bool {
n == 0 || SHUTDOWN.load(Ordering::SeqCst)
}
```
--------------------------------------------------------------------------------
/test.html:
--------------------------------------------------------------------------------
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MCP WebSocket Test Client</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/json-formatter/0.7.2/json-formatter.min.js"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
.container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.status {
padding: 10px;
margin-bottom: 20px;
border-radius: 4px;
}
.status.connected {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.disconnected {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.message-box {
margin: 20px 0;
}
.message-input {
width: 100%;
height: 100px;
margin: 10px 0;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: monospace;
}
.button {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
}
.button:hover {
background-color: #0056b3;
}
.button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.log {
margin-top: 20px;
border: 1px solid #ddd;
padding: 10px;
border-radius: 4px;
background: #f8f9fa;
height: 400px;
overflow-y: auto;
}
.log-entry {
margin: 5px 0;
padding: 5px;
border-bottom: 1px solid #eee;
}
.log-entry.sent {
color: #004085;
background-color: #cce5ff;
}
.log-entry.received {
color: #155724;
background-color: #d4edda;
}
.log-entry.error {
color: #721c24;
background-color: #f8d7da;
}
#requestTemplates {
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<h1>MCP WebSocket Test Client</h1>
<div id="connectionStatus" class="status disconnected">Disconnected</div>
<div class="connection-controls">
<input
type="text"
id="wsUrl"
value="ws://localhost:3000"
style="width: 200px; margin-right: 10px"
/>
<button id="connectButton" class="button">Connect</button>
<button id="disconnectButton" class="button" disabled>
Disconnect
</button>
</div>
<div class="message-box">
<h3>Request Templates</h3>
<select id="requestTemplates" style="width: 100%; padding: 5px">
<option value="initialize">Initialize</option>
<option value="ping">Ping</option>
<option value="resourcesList">List Resources</option>
<option value="toolsList">List Tools</option>
<option value="promptsList">List Prompts</option>
</select>
<h3>Message</h3>
<textarea
id="messageInput"
class="message-input"
placeholder="Enter JSON message"
></textarea>
<button id="sendButton" class="button" disabled>Send Message</button>
</div>
<div class="log" id="messageLog"></div>
</div>
<script>
let ws = null;
let nextRequestId = 1;
const templates = {
initialize: {
jsonrpc: "2.0",
method: "initialize",
id: 1,
params: {
protocolVersion: "2024-11-05",
capabilities: {
sampling: {},
},
clientInfo: {
name: "Web Test Client",
version: "1.0.0",
},
},
},
ping: {
jsonrpc: "2.0",
method: "ping",
id: 1,
},
resourcesList: {
jsonrpc: "2.0",
method: "resources/list",
id: 1,
},
toolsList: {
jsonrpc: "2.0",
method: "tools/list",
id: 1,
},
promptsList: {
jsonrpc: "2.0",
method: "prompts/list",
id: 1,
},
};
function updateConnectionStatus(connected) {
const statusDiv = document.getElementById("connectionStatus");
const connectButton = document.getElementById("connectButton");
const disconnectButton = document.getElementById("disconnectButton");
const sendButton = document.getElementById("sendButton");
statusDiv.textContent = connected ? "Connected" : "Disconnected";
statusDiv.className = `status ${
connected ? "connected" : "disconnected"
}`;
connectButton.disabled = connected;
disconnectButton.disabled = !connected;
sendButton.disabled = !connected;
}
function addLogEntry(message, type = "received") {
const logDiv = document.getElementById("messageLog");
const entry = document.createElement("div");
entry.className = `log-entry ${type}`;
try {
let jsonData = message;
if (typeof message === "string") {
jsonData = JSON.parse(message);
}
const formatter = new JSONFormatter(jsonData, 2, {
hoverPreviewEnabled: true,
hoverPreviewArrayCount: 100,
hoverPreviewFieldCount: 5,
});
entry.appendChild(formatter.render());
} catch (e) {
console.error("Failed to format message:", e);
entry.innerHTML = `<pre>${
typeof message === "object"
? JSON.stringify(message, null, 2)
: String(message)
}</pre>`;
}
logDiv.appendChild(entry);
logDiv.scrollTop = logDiv.scrollHeight;
}
function connect() {
const url = document.getElementById("wsUrl").value;
try {
ws = new WebSocket(url);
ws.onopen = () => {
updateConnectionStatus(true);
addLogEntry("WebSocket connection established", "received");
};
ws.onclose = () => {
updateConnectionStatus(false);
addLogEntry("WebSocket connection closed", "error");
ws = null;
};
ws.onerror = (error) => {
addLogEntry(`WebSocket error: ${error}`, "error");
};
ws.onmessage = (event) => {
// イベントデータがJSON形式の文字列でない場合に備えて処理
let data = event.data;
try {
// オブジェクトが文字列化されていない場合は文字列化
if (typeof data === "object") {
data = JSON.stringify(data);
}
// 文字列をJSONとしてパースしてフォーマット
const jsonData = JSON.parse(data);
addLogEntry(jsonData, "received");
} catch (e) {
// JSONとしてパースできない場合は生のデータを表示
console.error("Failed to parse message:", e);
addLogEntry(
{
error: "Failed to parse message",
data: data,
details: e.toString(),
},
"error"
);
}
};
} catch (error) {
addLogEntry(`Failed to connect: ${error}`, "error");
}
}
function disconnect() {
if (ws) {
ws.close();
}
}
function sendMessage() {
if (!ws) {
addLogEntry("Not connected to server", "error");
return;
}
try {
const messageInput = document.getElementById("messageInput");
const message = JSON.parse(messageInput.value);
ws.send(JSON.stringify(message));
addLogEntry(message, "sent");
} catch (error) {
addLogEntry(`Failed to send message: ${error}`, "error");
}
}
document.getElementById("connectButton").onclick = connect;
document.getElementById("disconnectButton").onclick = disconnect;
document.getElementById("sendButton").onclick = sendMessage;
document.getElementById("requestTemplates").onchange = (e) => {
const template = templates[e.target.value];
if (template) {
template.id = nextRequestId++;
document.getElementById("messageInput").value = JSON.stringify(
template,
null,
2
);
}
};
// 初期化時にテンプレートを選択
document
.getElementById("requestTemplates")
.dispatchEvent(new Event("change"));
</script>
</body>
</html>
```