Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
309
vendor/ruvector/examples/edge-net/src/mcp/handlers.rs
vendored
Normal file
309
vendor/ruvector/examples/edge-net/src/mcp/handlers.rs
vendored
Normal file
@@ -0,0 +1,309 @@
|
||||
//! Additional MCP Handlers
|
||||
//!
|
||||
//! Extended handler implementations for specialized edge-net capabilities.
|
||||
|
||||
use super::protocol::*;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
/// Vector search handler parameters
|
||||
pub struct VectorSearchParams {
|
||||
pub query: Vec<f32>,
|
||||
pub k: usize,
|
||||
pub filter: Option<Value>,
|
||||
}
|
||||
|
||||
/// Embedding generation parameters
|
||||
pub struct EmbeddingParams {
|
||||
pub text: String,
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
/// Semantic match parameters
|
||||
pub struct SemanticMatchParams {
|
||||
pub text: String,
|
||||
pub categories: Vec<String>,
|
||||
}
|
||||
|
||||
/// Task execution result
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TaskResult {
|
||||
pub task_id: String,
|
||||
pub status: TaskStatus,
|
||||
pub result: Option<Value>,
|
||||
pub error: Option<String>,
|
||||
pub cost: u64,
|
||||
}
|
||||
|
||||
/// Task status
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TaskStatus {
|
||||
Queued,
|
||||
Running,
|
||||
Completed,
|
||||
Failed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl TaskStatus {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
TaskStatus::Queued => "queued",
|
||||
TaskStatus::Running => "running",
|
||||
TaskStatus::Completed => "completed",
|
||||
TaskStatus::Failed => "failed",
|
||||
TaskStatus::Cancelled => "cancelled",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for vector operations
|
||||
pub struct VectorHandler;
|
||||
|
||||
impl VectorHandler {
|
||||
/// Create vector search response
|
||||
pub fn search_response(id: Option<Value>, results: Vec<(String, f32)>) -> McpResponse {
|
||||
let result_list: Vec<Value> = results
|
||||
.into_iter()
|
||||
.map(|(id, score)| json!({ "id": id, "score": score }))
|
||||
.collect();
|
||||
|
||||
McpResponse::success(id, json!({
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": format!("Found {} results", result_list.len())
|
||||
}],
|
||||
"results": result_list
|
||||
}))
|
||||
}
|
||||
|
||||
/// Create embedding response
|
||||
pub fn embedding_response(id: Option<Value>, embedding: Vec<f32>) -> McpResponse {
|
||||
McpResponse::success(id, json!({
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": format!("Generated {}-dimensional embedding", embedding.len())
|
||||
}],
|
||||
"embedding": embedding,
|
||||
"dimensions": embedding.len()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for RAC coherence operations
|
||||
pub struct CoherenceHandler;
|
||||
|
||||
impl CoherenceHandler {
|
||||
/// Create conflict detection response
|
||||
pub fn conflict_response(
|
||||
id: Option<Value>,
|
||||
conflicts: Vec<(String, String, f32)>,
|
||||
) -> McpResponse {
|
||||
let conflict_list: Vec<Value> = conflicts
|
||||
.into_iter()
|
||||
.map(|(id1, id2, severity)| {
|
||||
json!({
|
||||
"claim1": id1,
|
||||
"claim2": id2,
|
||||
"severity": severity
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
McpResponse::success(id, json!({
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": format!("Detected {} conflicts", conflict_list.len())
|
||||
}],
|
||||
"conflicts": conflict_list
|
||||
}))
|
||||
}
|
||||
|
||||
/// Create resolution response
|
||||
pub fn resolution_response(
|
||||
id: Option<Value>,
|
||||
resolution_id: &str,
|
||||
accepted: Vec<String>,
|
||||
deprecated: Vec<String>,
|
||||
) -> McpResponse {
|
||||
McpResponse::success(id, json!({
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": format!(
|
||||
"Resolution {}: accepted {}, deprecated {}",
|
||||
resolution_id,
|
||||
accepted.len(),
|
||||
deprecated.len()
|
||||
)
|
||||
}],
|
||||
"resolutionId": resolution_id,
|
||||
"accepted": accepted,
|
||||
"deprecated": deprecated
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for economic operations
|
||||
pub struct EconomicsHandler;
|
||||
|
||||
impl EconomicsHandler {
|
||||
/// Create stake response
|
||||
pub fn stake_response(
|
||||
id: Option<Value>,
|
||||
staked: u64,
|
||||
locked_until: u64,
|
||||
multiplier: f32,
|
||||
) -> McpResponse {
|
||||
McpResponse::success(id, json!({
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": format!(
|
||||
"Staked {} rUv ({}x multiplier, locked until {})",
|
||||
staked, multiplier, locked_until
|
||||
)
|
||||
}],
|
||||
"staked": staked,
|
||||
"lockedUntil": locked_until,
|
||||
"multiplier": multiplier
|
||||
}))
|
||||
}
|
||||
|
||||
/// Create reward distribution response
|
||||
pub fn reward_response(
|
||||
id: Option<Value>,
|
||||
recipients: Vec<(String, u64)>,
|
||||
total: u64,
|
||||
) -> McpResponse {
|
||||
let recipient_list: Vec<Value> = recipients
|
||||
.into_iter()
|
||||
.map(|(node, amount)| json!({ "nodeId": node, "amount": amount }))
|
||||
.collect();
|
||||
|
||||
McpResponse::success(id, json!({
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": format!("Distributed {} rUv to {} recipients", total, recipient_list.len())
|
||||
}],
|
||||
"recipients": recipient_list,
|
||||
"totalDistributed": total
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for network operations
|
||||
pub struct NetworkHandler;
|
||||
|
||||
impl NetworkHandler {
|
||||
/// Create peer list response
|
||||
pub fn peers_response(id: Option<Value>, peers: Vec<PeerInfo>) -> McpResponse {
|
||||
let peer_list: Vec<Value> = peers
|
||||
.into_iter()
|
||||
.map(|p| {
|
||||
json!({
|
||||
"nodeId": p.node_id,
|
||||
"publicKey": p.public_key,
|
||||
"reputation": p.reputation,
|
||||
"latency": p.latency_ms,
|
||||
"connected": p.connected
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
McpResponse::success(id, json!({
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": format!("{} peers connected", peer_list.len())
|
||||
}],
|
||||
"peers": peer_list,
|
||||
"count": peer_list.len()
|
||||
}))
|
||||
}
|
||||
|
||||
/// Create network health response
|
||||
pub fn health_response(id: Option<Value>, health: NetworkHealth) -> McpResponse {
|
||||
McpResponse::success(id, json!({
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": format!(
|
||||
"Network Health: {}% (peers: {}, avg latency: {}ms)",
|
||||
(health.score * 100.0) as u32,
|
||||
health.peer_count,
|
||||
health.avg_latency_ms
|
||||
)
|
||||
}],
|
||||
"score": health.score,
|
||||
"peerCount": health.peer_count,
|
||||
"avgLatency": health.avg_latency_ms,
|
||||
"messageRate": health.message_rate,
|
||||
"bandwidth": health.bandwidth_kbps
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Peer information
|
||||
pub struct PeerInfo {
|
||||
pub node_id: String,
|
||||
pub public_key: String,
|
||||
pub reputation: f32,
|
||||
pub latency_ms: u32,
|
||||
pub connected: bool,
|
||||
}
|
||||
|
||||
/// Network health metrics
|
||||
pub struct NetworkHealth {
|
||||
pub score: f32,
|
||||
pub peer_count: usize,
|
||||
pub avg_latency_ms: u32,
|
||||
pub message_rate: f32,
|
||||
pub bandwidth_kbps: u32,
|
||||
}
|
||||
|
||||
/// Helper for creating error responses
|
||||
pub fn error_response(id: Option<Value>, code: i32, message: &str) -> McpResponse {
|
||||
McpResponse::error(id, McpError::new(code, message))
|
||||
}
|
||||
|
||||
/// Helper for creating not implemented responses
|
||||
pub fn not_implemented(id: Option<Value>, feature: &str) -> McpResponse {
|
||||
McpResponse::success(id, json!({
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": format!("{} is not yet implemented", feature)
|
||||
}],
|
||||
"status": "not_implemented",
|
||||
"feature": feature
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_vector_search_response() {
|
||||
let results = vec![
|
||||
("doc1".to_string(), 0.95),
|
||||
("doc2".to_string(), 0.87),
|
||||
];
|
||||
let response = VectorHandler::search_response(Some(json!(1)), results);
|
||||
assert!(response.result.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_status() {
|
||||
assert_eq!(TaskStatus::Completed.as_str(), "completed");
|
||||
assert_eq!(TaskStatus::Running.as_str(), "running");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_health_response() {
|
||||
let health = NetworkHealth {
|
||||
score: 0.85,
|
||||
peer_count: 10,
|
||||
avg_latency_ms: 50,
|
||||
message_rate: 100.0,
|
||||
bandwidth_kbps: 1000,
|
||||
};
|
||||
let response = NetworkHandler::health_response(Some(json!(1)), health);
|
||||
assert!(response.result.is_some());
|
||||
}
|
||||
}
|
||||
1198
vendor/ruvector/examples/edge-net/src/mcp/mod.rs
vendored
Normal file
1198
vendor/ruvector/examples/edge-net/src/mcp/mod.rs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
207
vendor/ruvector/examples/edge-net/src/mcp/protocol.rs
vendored
Normal file
207
vendor/ruvector/examples/edge-net/src/mcp/protocol.rs
vendored
Normal file
@@ -0,0 +1,207 @@
|
||||
//! MCP Protocol Types
|
||||
//!
|
||||
//! JSON-RPC 2.0 based protocol types for Model Context Protocol.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
/// MCP Request message (JSON-RPC 2.0)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct McpRequest {
|
||||
/// JSON-RPC version (always "2.0")
|
||||
pub jsonrpc: String,
|
||||
/// Request ID (can be null for notifications)
|
||||
pub id: Option<Value>,
|
||||
/// Method name
|
||||
pub method: String,
|
||||
/// Optional parameters
|
||||
pub params: Option<Value>,
|
||||
}
|
||||
|
||||
/// MCP Response message
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct McpResponse {
|
||||
/// JSON-RPC version
|
||||
pub jsonrpc: String,
|
||||
/// Request ID (matches request)
|
||||
pub id: Option<Value>,
|
||||
/// Result (on success)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub result: Option<Value>,
|
||||
/// Error (on failure)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<McpError>,
|
||||
}
|
||||
|
||||
impl McpResponse {
|
||||
/// Create a success response
|
||||
pub fn success(id: Option<Value>, result: Value) -> Self {
|
||||
Self {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(result),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an error response
|
||||
pub fn error(id: Option<Value>, error: McpError) -> Self {
|
||||
Self {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: None,
|
||||
error: Some(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// MCP Error
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct McpError {
|
||||
/// Error code (JSON-RPC standard codes)
|
||||
pub code: i32,
|
||||
/// Human-readable message
|
||||
pub message: String,
|
||||
/// Optional additional data
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub data: Option<Value>,
|
||||
}
|
||||
|
||||
impl McpError {
|
||||
/// Create a new error
|
||||
pub fn new(code: i32, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
code,
|
||||
message: message.into(),
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add additional data to error
|
||||
pub fn with_data(mut self, data: Value) -> Self {
|
||||
self.data = Some(data);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Standard JSON-RPC error codes
|
||||
pub struct ErrorCodes;
|
||||
|
||||
impl ErrorCodes {
|
||||
/// Parse error - Invalid JSON
|
||||
pub const PARSE_ERROR: i32 = -32700;
|
||||
/// Invalid request - Not a valid Request object
|
||||
pub const INVALID_REQUEST: i32 = -32600;
|
||||
/// Method not found
|
||||
pub const METHOD_NOT_FOUND: i32 = -32601;
|
||||
/// Invalid params
|
||||
pub const INVALID_PARAMS: i32 = -32602;
|
||||
/// Internal error
|
||||
pub const INTERNAL_ERROR: i32 = -32603;
|
||||
}
|
||||
|
||||
/// MCP Tool definition
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct McpTool {
|
||||
/// Tool name (unique identifier)
|
||||
pub name: String,
|
||||
/// Human-readable description
|
||||
pub description: String,
|
||||
/// JSON Schema for input parameters
|
||||
#[serde(rename = "inputSchema")]
|
||||
pub input_schema: Value,
|
||||
}
|
||||
|
||||
/// MCP Resource definition
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct McpResource {
|
||||
/// Resource URI
|
||||
pub uri: String,
|
||||
/// Human-readable name
|
||||
pub name: String,
|
||||
/// Description
|
||||
pub description: String,
|
||||
/// MIME type
|
||||
#[serde(rename = "mimeType")]
|
||||
pub mime_type: String,
|
||||
}
|
||||
|
||||
/// MCP Prompt definition
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct McpPrompt {
|
||||
/// Prompt name
|
||||
pub name: String,
|
||||
/// Description
|
||||
pub description: String,
|
||||
/// Optional arguments
|
||||
pub arguments: Option<Vec<PromptArgument>>,
|
||||
}
|
||||
|
||||
/// Prompt argument definition
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PromptArgument {
|
||||
/// Argument name
|
||||
pub name: String,
|
||||
/// Description
|
||||
pub description: String,
|
||||
/// Whether argument is required
|
||||
pub required: bool,
|
||||
}
|
||||
|
||||
/// MCP Notification (no response expected)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct McpNotification {
|
||||
/// JSON-RPC version
|
||||
pub jsonrpc: String,
|
||||
/// Method name
|
||||
pub method: String,
|
||||
/// Optional parameters
|
||||
pub params: Option<Value>,
|
||||
}
|
||||
|
||||
impl McpNotification {
|
||||
/// Create a new notification
|
||||
pub fn new(method: impl Into<String>, params: Option<Value>) -> Self {
|
||||
Self {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
method: method.into(),
|
||||
params,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_request_serialization() {
|
||||
let req = McpRequest {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id: Some(json!(1)),
|
||||
method: "tools/list".to_string(),
|
||||
params: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&req).unwrap();
|
||||
assert!(json.contains("tools/list"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_success() {
|
||||
let resp = McpResponse::success(Some(json!(1)), json!({"status": "ok"}));
|
||||
assert!(resp.error.is_none());
|
||||
assert!(resp.result.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_error() {
|
||||
let resp = McpResponse::error(
|
||||
Some(json!(1)),
|
||||
McpError::new(ErrorCodes::METHOD_NOT_FOUND, "Not found"),
|
||||
);
|
||||
assert!(resp.error.is_some());
|
||||
assert!(resp.result.is_none());
|
||||
}
|
||||
}
|
||||
353
vendor/ruvector/examples/edge-net/src/mcp/transport.rs
vendored
Normal file
353
vendor/ruvector/examples/edge-net/src/mcp/transport.rs
vendored
Normal file
@@ -0,0 +1,353 @@
|
||||
//! Browser-based MCP Transport Layer
|
||||
//!
|
||||
//! Provides MessagePort and BroadcastChannel transports for browser environments.
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ```javascript
|
||||
//! // Main thread
|
||||
//! const worker = new Worker('edge-net-worker.js');
|
||||
//! const transport = new WasmMcpTransport(worker);
|
||||
//!
|
||||
//! // Send request
|
||||
//! const response = await transport.send({
|
||||
//! method: "tools/call",
|
||||
//! params: { name: "credits_balance", arguments: { node_id: "..." } }
|
||||
//! });
|
||||
//!
|
||||
//! // Worker thread
|
||||
//! import { WasmMcpServer, WasmMcpWorkerHandler } from '@ruvector/edge-net';
|
||||
//!
|
||||
//! const server = new WasmMcpServer();
|
||||
//! const handler = new WasmMcpWorkerHandler(server, self);
|
||||
//! handler.start();
|
||||
//! ```
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{MessageEvent, MessagePort, Worker, BroadcastChannel};
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::{McpRequest, McpResponse, WasmMcpServer};
|
||||
|
||||
/// Pending request tracker
|
||||
struct PendingRequest {
|
||||
resolve: js_sys::Function,
|
||||
reject: js_sys::Function,
|
||||
}
|
||||
|
||||
/// Browser-based MCP transport using MessagePort
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmMcpTransport {
|
||||
/// Message port for communication
|
||||
port: MessagePort,
|
||||
/// Pending requests awaiting responses
|
||||
pending: Arc<RwLock<HashMap<String, PendingRequest>>>,
|
||||
/// Request ID counter
|
||||
next_id: Arc<RwLock<u64>>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmMcpTransport {
|
||||
/// Create transport from a Worker
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(worker: &Worker) -> Result<WasmMcpTransport, JsValue> {
|
||||
// Create a message channel
|
||||
let channel = web_sys::MessageChannel::new()?;
|
||||
let port1 = channel.port1();
|
||||
let port2 = channel.port2();
|
||||
|
||||
// Send port2 to worker
|
||||
let transfer = js_sys::Array::new();
|
||||
transfer.push(&port2);
|
||||
worker.post_message_with_transfer(&port2, &transfer)?;
|
||||
|
||||
Ok(Self {
|
||||
port: port1,
|
||||
pending: Arc::new(RwLock::new(HashMap::new())),
|
||||
next_id: Arc::new(RwLock::new(0)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Create transport from existing MessagePort
|
||||
#[wasm_bindgen(js_name = fromPort)]
|
||||
pub fn from_port(port: MessagePort) -> WasmMcpTransport {
|
||||
Self {
|
||||
port,
|
||||
pending: Arc::new(RwLock::new(HashMap::new())),
|
||||
next_id: Arc::new(RwLock::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize transport (set up message handler)
|
||||
#[wasm_bindgen]
|
||||
pub fn init(&self) -> Result<(), JsValue> {
|
||||
let pending = self.pending.clone();
|
||||
|
||||
// Create message handler closure
|
||||
let handler = Closure::wrap(Box::new(move |event: MessageEvent| {
|
||||
let data = event.data();
|
||||
|
||||
// Parse response
|
||||
if let Ok(json_str) = data.dyn_into::<js_sys::JsString>() {
|
||||
let json: String = json_str.into();
|
||||
if let Ok(response) = serde_json::from_str::<McpResponse>(&json) {
|
||||
// Find pending request
|
||||
if let Some(id) = &response.id {
|
||||
let id_str = id.to_string();
|
||||
let mut pending = pending.write();
|
||||
if let Some(req) = pending.remove(&id_str) {
|
||||
let response_js = JsValue::from_str(&json);
|
||||
if response.error.is_some() {
|
||||
let _ = req.reject.call1(&JsValue::NULL, &response_js);
|
||||
} else {
|
||||
let _ = req.resolve.call1(&JsValue::NULL, &response_js);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}) as Box<dyn FnMut(MessageEvent)>);
|
||||
|
||||
self.port.set_onmessage(Some(handler.as_ref().unchecked_ref()));
|
||||
handler.forget(); // Don't drop the closure
|
||||
|
||||
self.port.start();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send an MCP request and get a Promise for the response
|
||||
#[wasm_bindgen]
|
||||
pub fn send(&self, request: JsValue) -> js_sys::Promise {
|
||||
let port = self.port.clone();
|
||||
let pending = self.pending.clone();
|
||||
let next_id = self.next_id.clone();
|
||||
|
||||
js_sys::Promise::new(&mut move |resolve, reject| {
|
||||
// Generate request ID
|
||||
let id = {
|
||||
let mut counter = next_id.write();
|
||||
*counter += 1;
|
||||
*counter
|
||||
};
|
||||
|
||||
// Parse and augment request
|
||||
let request: Result<McpRequest, _> = serde_wasm_bindgen::from_value(request.clone());
|
||||
let mut req = match request {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
let _ = reject.call1(&JsValue::NULL, &JsValue::from_str(&e.to_string()));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Set request ID
|
||||
req.id = Some(json!(id));
|
||||
req.jsonrpc = "2.0".to_string();
|
||||
|
||||
// Store pending request
|
||||
{
|
||||
let mut pending = pending.write();
|
||||
pending.insert(id.to_string(), PendingRequest {
|
||||
resolve: resolve.clone(),
|
||||
reject: reject.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
// Send request
|
||||
let json = serde_json::to_string(&req).unwrap();
|
||||
if let Err(e) = port.post_message(&JsValue::from_str(&json)) {
|
||||
let _ = reject.call1(&JsValue::NULL, &e);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Close the transport
|
||||
#[wasm_bindgen]
|
||||
pub fn close(&self) {
|
||||
self.port.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Worker-side handler for MCP requests
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmMcpWorkerHandler {
|
||||
server: WasmMcpServer,
|
||||
port: Option<MessagePort>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmMcpWorkerHandler {
|
||||
/// Create handler with MCP server
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(server: WasmMcpServer) -> WasmMcpWorkerHandler {
|
||||
Self {
|
||||
server,
|
||||
port: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Start handling messages (call in worker)
|
||||
#[wasm_bindgen]
|
||||
pub fn start(&mut self) -> Result<(), JsValue> {
|
||||
let server = std::mem::replace(&mut self.server, WasmMcpServer::new()?);
|
||||
let server = Arc::new(server);
|
||||
|
||||
// In worker context, listen for port from main thread
|
||||
let global = js_sys::global();
|
||||
|
||||
let onmessage = Closure::wrap(Box::new(move |event: MessageEvent| {
|
||||
let data = event.data();
|
||||
|
||||
// Check if this is a MessagePort being transferred
|
||||
if let Ok(port) = data.dyn_into::<MessagePort>() {
|
||||
// Set up handler for this port
|
||||
let server_for_handler = Arc::clone(&server);
|
||||
let port_for_response = port.clone();
|
||||
|
||||
let handler = Closure::wrap(Box::new(move |event: MessageEvent| {
|
||||
let data = event.data();
|
||||
|
||||
if let Ok(json_str) = data.dyn_into::<js_sys::JsString>() {
|
||||
let json: String = json_str.into();
|
||||
let server_for_async = Arc::clone(&server_for_handler);
|
||||
let port_clone = port_for_response.clone();
|
||||
|
||||
// Parse and handle request
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
if let Ok(response) = server_for_async.handle_request(&json).await {
|
||||
// Send response back
|
||||
let _ = port_clone.post_message(&JsValue::from_str(&response));
|
||||
}
|
||||
});
|
||||
}
|
||||
}) as Box<dyn FnMut(MessageEvent)>);
|
||||
|
||||
port.set_onmessage(Some(handler.as_ref().unchecked_ref()));
|
||||
handler.forget();
|
||||
port.start();
|
||||
}
|
||||
}) as Box<dyn FnMut(MessageEvent)>);
|
||||
|
||||
// Set global onmessage
|
||||
js_sys::Reflect::set(
|
||||
&global,
|
||||
&JsValue::from_str("onmessage"),
|
||||
onmessage.as_ref(),
|
||||
)?;
|
||||
onmessage.forget();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for WasmMcpServer {
|
||||
fn clone(&self) -> Self {
|
||||
// Create a new server with shared state
|
||||
// NOTE: Identity is not cloned (contains private key)
|
||||
// NOTE: Learning engine is not cloned (state is complex)
|
||||
Self {
|
||||
identity: None,
|
||||
ledger: self.ledger.clone(),
|
||||
coherence: self.coherence.clone(),
|
||||
learning: None,
|
||||
config: self.config.clone(),
|
||||
request_counter: self.request_counter.clone(),
|
||||
rate_limit: self.rate_limit.clone(), // Share rate limit state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// BroadcastChannel-based transport for multi-tab communication
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmMcpBroadcast {
|
||||
channel: BroadcastChannel,
|
||||
server: Option<WasmMcpServer>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmMcpBroadcast {
|
||||
/// Create a broadcast transport
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(channel_name: &str) -> Result<WasmMcpBroadcast, JsValue> {
|
||||
let channel = BroadcastChannel::new(channel_name)?;
|
||||
|
||||
Ok(Self {
|
||||
channel,
|
||||
server: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Set as server mode (responds to requests)
|
||||
#[wasm_bindgen(js_name = setServer)]
|
||||
pub fn set_server(&mut self, server: WasmMcpServer) {
|
||||
self.server = Some(server);
|
||||
}
|
||||
|
||||
/// Start listening for requests (server mode)
|
||||
#[wasm_bindgen]
|
||||
pub fn listen(&self) -> Result<(), JsValue> {
|
||||
if self.server.is_none() {
|
||||
return Err(JsValue::from_str("No server set"));
|
||||
}
|
||||
|
||||
let channel = self.channel.clone();
|
||||
let server = self.server.as_ref().unwrap().clone();
|
||||
|
||||
let handler = Closure::wrap(Box::new(move |event: MessageEvent| {
|
||||
let data = event.data();
|
||||
|
||||
if let Ok(json_str) = data.dyn_into::<js_sys::JsString>() {
|
||||
let json: String = json_str.into();
|
||||
let channel_clone = channel.clone();
|
||||
let server_clone = server.clone();
|
||||
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
if let Ok(response) = server_clone.handle_request(&json).await {
|
||||
let _ = channel_clone.post_message(&JsValue::from_str(&response));
|
||||
}
|
||||
});
|
||||
}
|
||||
}) as Box<dyn FnMut(MessageEvent)>);
|
||||
|
||||
self.channel.set_onmessage(Some(handler.as_ref().unchecked_ref()));
|
||||
handler.forget();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a request (client mode)
|
||||
#[wasm_bindgen]
|
||||
pub fn send(&self, request_json: &str) -> Result<(), JsValue> {
|
||||
self.channel.post_message(&JsValue::from_str(request_json))
|
||||
}
|
||||
|
||||
/// Close the channel
|
||||
#[wasm_bindgen]
|
||||
pub fn close(&self) {
|
||||
self.channel.close();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Transport tests require browser environment
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod wasm_tests {
|
||||
use super::*;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_broadcast_creation() {
|
||||
let broadcast = WasmMcpBroadcast::new("test-channel").unwrap();
|
||||
broadcast.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user