#
tokens: 28644/50000 1/231 files (page 7/8)
lines: off (toggle) GitHub
raw markdown copy
This is page 7 of 8. Use http://codebase.md/tuananh/hyper-mcp?page={x} to view the full context.

# Directory Structure

```
├── .cursor
│   └── rules
│       └── print-ctx-size.mdc
├── .dockerignore
├── .github
│   ├── renovate.json5
│   └── workflows
│       ├── ci.yml
│       ├── nightly.yml
│       └── release.yml
├── .gitignore
├── .gitmodules
├── .hadolint.yaml
├── .pre-commit-config.yaml
├── .windsurf
│   └── rules
│       ├── print-ctx-size.md
│       └── think.md
├── assets
│   ├── cursor-mcp-1.png
│   ├── cursor-mcp.png
│   ├── eval-py.jpg
│   └── logo.png
├── Cargo.lock
├── Cargo.toml
├── config.example.json
├── config.example.yaml
├── CREATING_PLUGINS.md
├── DEPLOYMENT.md
├── Dockerfile
├── examples
│   └── plugins
│       ├── v1
│       │   ├── arxiv
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── context7
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── crates-io
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── crypto-price
│       │   │   ├── Dockerfile
│       │   │   ├── go.mod
│       │   │   ├── go.sum
│       │   │   ├── main.go
│       │   │   ├── pdk.gen.go
│       │   │   └── README.md
│       │   ├── eval-py
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── fetch
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── fs
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── github
│       │   │   ├── .gitignore
│       │   │   ├── branches.go
│       │   │   ├── Dockerfile
│       │   │   ├── files.go
│       │   │   ├── gists.go
│       │   │   ├── go.mod
│       │   │   ├── go.sum
│       │   │   ├── issues.go
│       │   │   ├── main.go
│       │   │   ├── pdk.gen.go
│       │   │   ├── README.md
│       │   │   └── repo.go
│       │   ├── gitlab
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── gomodule
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── hash
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.lock
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── maven
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── meme-generator
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── generate_embedded.py
│       │   │   ├── README.md
│       │   │   ├── src
│       │   │   │   ├── embedded.rs
│       │   │   │   ├── lib.rs
│       │   │   │   └── pdk.rs
│       │   │   └── templates.json
│       │   ├── memory
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── myip
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.lock
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── qdrant
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       ├── pdk.rs
│       │   │       └── qdrant_client.rs
│       │   ├── qr-code
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.lock
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── serper
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── sqlite
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── think
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── time
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   ├── src
│       │   │   │   ├── lib.rs
│       │   │   │   └── pdk.rs
│       │   │   └── time.wasm
│       │   └── tool-list-changed
│       │       ├── .gitignore
│       │       ├── Cargo.toml
│       │       ├── Dockerfile
│       │       ├── README.md
│       │       ├── src
│       │       │   ├── lib.rs
│       │       │   └── pdk.rs
│       │       └── tool_list_changed.wasm
│       └── v2
│           └── rstime
│               ├── .cargo
│               │   └── config.toml
│               ├── .gitignore
│               ├── Cargo.toml
│               ├── Dockerfile
│               ├── README.md
│               ├── rstime.wasm
│               └── src
│                   ├── lib.rs
│                   └── pdk
│                       ├── exports.rs
│                       ├── imports.rs
│                       ├── mod.rs
│                       └── types.rs
├── iac
│   ├── .terraform.lock.hcl
│   ├── main.tf
│   ├── outputs.tf
│   └── variables.tf
├── justfile
├── LICENSE
├── README.md
├── RUNTIME_CONFIG.md
├── rust-toolchain.toml
├── server.json
├── SKIP_TOOLS_GUIDE.md
├── src
│   ├── cli.rs
│   ├── config.rs
│   ├── https_auth.rs
│   ├── logging.rs
│   ├── main.rs
│   ├── naming.rs
│   ├── plugin.rs
│   ├── service.rs
│   └── wasm
│       ├── http.rs
│       ├── mod.rs
│       ├── oci.rs
│       └── s3.rs
├── templates
│   └── plugins
│       ├── go
│       │   ├── .gitignore
│       │   ├── Dockerfile
│       │   ├── exports.go
│       │   ├── go.mod
│       │   ├── go.sum
│       │   ├── imports.go
│       │   ├── main.go
│       │   ├── README.md
│       │   └── types.go
│       ├── README.md
│       └── rust
│           ├── .cargo
│           │   └── config.toml
│           ├── .gitignore
│           ├── Cargo.toml
│           ├── Dockerfile
│           ├── README.md
│           └── src
│               ├── lib.rs
│               └── pdk
│                   ├── exports.rs
│                   ├── imports.rs
│                   ├── mod.rs
│                   └── types.rs
├── tests
│   └── fixtures
│       ├── config_with_auths.json
│       ├── config_with_auths.yaml
│       ├── documentation_example.json
│       ├── documentation_example.yaml
│       ├── invalid_auth_config.yaml
│       ├── invalid_plugin_name.yaml
│       ├── invalid_structure.yaml
│       ├── invalid_url.yaml
│       ├── keyring_auth_config.yaml
│       ├── skip_tools_examples.yaml
│       ├── unsupported_config.txt
│       ├── valid_config.json
│       └── valid_config.yaml
└── xtp-plugin-schema.json
```

# Files

--------------------------------------------------------------------------------
/src/service.rs:
--------------------------------------------------------------------------------

```rust
use crate::{
    config::{Config, PluginName},
    naming::{
        create_namespaced_name, create_namespaced_uri, parse_namespaced_name, parse_namespaced_uri,
    },
    plugin::{Plugin, PluginV1, PluginV2},
    wasm,
};
use anyhow::{Error, Result};
use bytesize::ByteSize;
use dashmap::{DashMap, DashSet, Entry};
use extism::{EXTISM_USER_MODULE, Function, Manifest, UserData, Wasm, host_fn};
use extism_convert::Json;
use rmcp::{
    ErrorData as McpError, ServerHandler,
    model::*,
    service::{NotificationContext, Peer, RequestContext, RoleServer},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_with::{DurationSeconds, serde_as};
use std::{
    collections::HashMap,
    fmt::Debug,
    ops::Deref,
    str::FromStr,
    sync::{Arc, LazyLock, Mutex, RwLock, Weak},
    time::Duration,
};
use tokio::{runtime::Handle, sync::SetOnce};
use uuid::Uuid;

/// Check if a value contains an environment variable reference in the format ${ENVVARKEY}
/// and replace it with the actual environment variable value if it exists.
/// If the environment variable doesn't exist, returns the original value.
fn check_env_reference(value: &str) -> String {
    // Check if the value matches the pattern ${ENVVARKEY}
    if let Some(stripped) = value.strip_prefix("${").and_then(|s| s.strip_suffix("}")) {
        // Try to get the environment variable
        match std::env::var(stripped) {
            Ok(env_value) => {
                tracing::debug!(
                    "Resolved environment variable reference ${{{stripped}}} to actual value"
                );
                env_value
            }
            Err(_) => {
                tracing::warn!(
                    "Environment variable {stripped} not found, keeping original value {value}"
                );
                value.to_string()
            }
        }
    } else {
        value.to_string()
    }
}

static PLUGIN_SERVICE_INNER_REGISTRY: LazyLock<DashMap<Uuid, Weak<PluginServiceInner>>> =
    LazyLock::new(DashMap::new);
static WASM_DATA_CACHE: LazyLock<DashMap<PluginName, Vec<u8>>> = LazyLock::new(DashMap::new);

#[allow(dead_code)]
#[serde_as]
#[derive(Clone, Debug, Serialize)]
struct CreateElicitationRequestParamWithTimeout {
    #[serde(flatten)]
    pub inner: CreateElicitationRequestParam,
    #[serde_as(as = "Option<DurationSeconds<f64>>")]
    pub timeout: Option<Duration>,
}

impl<'de> Deserialize<'de> for CreateElicitationRequestParamWithTimeout {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let mut value = Value::deserialize(deserializer)?;

        fn patch_formats(value: &mut Value) {
            match value {
                Value::Object(map) => {
                    if let Some(Value::String(s)) = map.get_mut("format")
                        && s == "date_time"
                    {
                        *s = "date-time".to_string();
                    }
                    for val in map.values_mut() {
                        patch_formats(val);
                    }
                }
                Value::Array(arr) => {
                    for val in arr.iter_mut() {
                        patch_formats(val);
                    }
                }
                _ => {}
            }
        }

        patch_formats(&mut value);

        #[serde_as]
        #[derive(Deserialize)]
        struct Helper {
            #[serde(flatten)]
            inner: CreateElicitationRequestParam,
            #[serde_as(as = "Option<DurationSeconds<f64>>")]
            timeout: Option<Duration>,
        }

        let Helper { inner, timeout } =
            Helper::deserialize(value).map_err(serde::de::Error::custom)?;
        Ok(CreateElicitationRequestParamWithTimeout { inner, timeout })
    }
}

#[derive(Clone, Debug)]
struct PluginServiceContext {
    handle: Handle,
    plugin_service_id: Uuid,
    plugin_name: String,
}

pub struct PluginServiceInner {
    config: Config,
    id: Uuid,
    logging_level: RwLock<LoggingLevel>,
    names: SetOnce<HashMap<Uuid, PluginName>>,
    peer: SetOnce<Peer<RoleServer>>,
    plugins: SetOnce<HashMap<PluginName, Box<dyn Plugin>>>,
    subscriptions: DashSet<String>,
}

impl Drop for PluginServiceInner {
    fn drop(&mut self) {
        PLUGIN_SERVICE_INNER_REGISTRY.remove(&self.id);
    }
}

pub struct PluginService(Arc<PluginServiceInner>);

impl Clone for PluginService {
    fn clone(&self) -> Self {
        Self(self.0.clone())
    }
}

impl Deref for PluginService {
    type Target = Arc<PluginServiceInner>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl PluginService {
    pub async fn new(config: &Config) -> Result<Self> {
        let inner = Arc::new(PluginServiceInner {
            config: config.clone(),
            id: Uuid::new_v4(),
            logging_level: RwLock::new(LoggingLevel::Error),
            names: SetOnce::new(),
            peer: SetOnce::new(),
            plugins: SetOnce::new(),
            subscriptions: DashSet::new(),
        });
        PLUGIN_SERVICE_INNER_REGISTRY.insert(inner.id, Arc::downgrade(&inner));
        let service = Self(inner);

        service.load_plugins().await?;
        Ok(service)
    }

    fn get(id: Uuid) -> Option<PluginService> {
        if let Some(weak_inner) = PLUGIN_SERVICE_INNER_REGISTRY.get(&id)
            && let Some(inner) = weak_inner.upgrade()
        {
            return Some(PluginService(inner));
        }
        PLUGIN_SERVICE_INNER_REGISTRY.remove(&id);
        None
    }

    async fn load_plugins(&self) -> Result<()> {
        let mut names = HashMap::new();
        let mut plugins: HashMap<PluginName, Box<dyn Plugin>> = HashMap::new();

        host_fn!(create_elicitation(ctx: PluginServiceContext; elicitation_msg: Json<CreateElicitationRequestParamWithTimeout>) -> Json<CreateElicitationResult> {
            let elicitation_msg = elicitation_msg.into_inner();
            let ctx = ctx.get()?.lock().unwrap().clone();
            let plugin_service = PluginService::get(ctx.plugin_service_id).ok_or_else(|| {
                anyhow::anyhow!("PluginService with ID {:?} not found", ctx.plugin_service_id)
            })?;
            match plugin_service.peer.get() {
                Some(peer) => {
                    if peer.supports_elicitation() {
                        if let Some(timeout) = elicitation_msg.timeout {
                            tracing::info!("Creating elicitation from {} with timeout {:?}", ctx.plugin_name, timeout);
                            ctx.handle.block_on(peer.create_elicitation_with_timeout(elicitation_msg.inner, Some(timeout))).map(Json).map_err(Error::from)
                        } else {
                            tracing::info!("Creating elicitation from {}", ctx.plugin_name);
                            ctx.handle.block_on(peer.create_elicitation(elicitation_msg.inner)).map(Json).map_err(Error::from)
                        }
                    } else {
                        tracing::info!("Peer does not support elicitation, declining from {}", ctx.plugin_name);
                        Ok(Json(CreateElicitationResult {
                            action: ElicitationAction::Decline,
                            content: None,
                        }))
                    }
                },
                None => Err(anyhow::anyhow!("No peer available")),
            }
        });

        host_fn!(create_message(ctx: PluginServiceContext; sampling_msg: Json<CreateMessageRequestParam>) -> Json<CreateMessageResult> {
            let sampling_msg = sampling_msg.into_inner();
            let ctx = ctx.get()?.lock().unwrap().clone();
            let plugin_service = PluginService::get(ctx.plugin_service_id).ok_or_else(|| {
                anyhow::anyhow!("PluginService with ID {:?} not found", ctx.plugin_service_id)
            })?;
            match plugin_service.peer.get() {
                Some(peer) => {
                    if let Some(peer_info) = peer.peer_info() && peer_info.capabilities.sampling.is_some() {
                        tracing::info!("Creating sampling message from {}", ctx.plugin_name);
                        ctx.handle.block_on(peer.create_message(sampling_msg)).map(Json).map_err(Error::from)
                    } else {
                        Err(anyhow::anyhow!("Peer does not support sampling"))
                    }
                },
                None => Err(anyhow::anyhow!("No peer available")),
            }
        });

        // Declares a host function `list_roots` that plugins can call
        host_fn!(list_roots(ctx: PluginServiceContext;) -> Json<ListRootsResult> {
            let ctx = ctx.get()?.lock().unwrap().clone();
            let plugin_service = PluginService::get(ctx.plugin_service_id).ok_or_else(|| {
                anyhow::anyhow!("PluginService with ID {:?} not found", ctx.plugin_service_id)
            })?;
            match plugin_service.peer.get() {
                Some(peer) => {
                    if let Some(peer_info) = peer.peer_info() && peer_info.capabilities.roots.is_some() {
                        tracing::info!("Listing roots from {}", ctx.plugin_name);
                        ctx.handle.block_on(peer.list_roots()).map(Json).map_err(Error::from)
                    } else {
                        Ok(Json(ListRootsResult::default()))
                    }
                },
                None => Err(anyhow::anyhow!("No peer available")),
            }
        });

        // Declares a host function `notify_logging_message` that plugins can call
        host_fn!(notify_logging_message(ctx: PluginServiceContext; log_msg: Json<LoggingMessageNotificationParam>) {
            let log_msg = log_msg.into_inner();
            let ctx = ctx.get()?.lock().unwrap().clone();
            let plugin_service = PluginService::get(ctx.plugin_service_id).ok_or_else(|| {
                anyhow::anyhow!("PluginService with ID {:?} not found", ctx.plugin_service_id)
            })?;
            if (plugin_service.logging_level() as u8) <= (log_msg.level as u8) && let Some(peer) = plugin_service.peer.get() {
                tracing::debug!("Logging message from {}", ctx.plugin_name);
                return ctx.handle.block_on(peer.notify_logging_message(log_msg)).map_err(Error::from);
            }
            Ok(())
        });

        // Declares a host function `notify_progress` that plugins can call
        host_fn!(notify_progress(ctx: PluginServiceContext; progress_msg: Json<ProgressNotificationParam>) {
            let progress_msg = progress_msg.into_inner();
            let ctx = ctx.get()?.lock().unwrap().clone();
            let plugin_service = PluginService::get(ctx.plugin_service_id).ok_or_else(|| {
                anyhow::anyhow!("PluginService with ID {:?} not found", ctx.plugin_service_id)
            })?;
            match plugin_service.peer.get() {
                Some(peer) => {
                    tracing::debug!("Progress notification from {}", ctx.plugin_name);
                    ctx.handle.block_on(peer.notify_progress(progress_msg)).map_err(Error::from)
                },
                None => Ok(()),
            }
        });

        host_fn!(notify_prompt_list_changed(ctx: PluginServiceContext;) {
            let ctx = ctx.get()?.lock().unwrap().clone();
            let plugin_service = PluginService::get(ctx.plugin_service_id).ok_or_else(|| {
                anyhow::anyhow!("PluginService with ID {:?} not found", ctx.plugin_service_id)
            })?;

            match plugin_service.peer.get() {
                Some(peer) => {
                    tracing::info!("Notifying tool list changed from {}", ctx.plugin_name);
                    ctx.handle.block_on(peer.notify_prompt_list_changed()).map_err(Error::from)
                },
                None => Ok(()),
            }
        });

        host_fn!(notify_resource_list_changed(ctx: PluginServiceContext;) {
            let ctx = ctx.get()?.lock().unwrap().clone();
            let plugin_service = PluginService::get(ctx.plugin_service_id).ok_or_else(|| {
                anyhow::anyhow!("PluginService with ID {:?} not found", ctx.plugin_service_id)
            })?;

            match plugin_service.peer.get() {
                Some(peer) => {
                    tracing::info!("Notifying tool list changed from {}", ctx.plugin_name);
                    ctx.handle.block_on(peer.notify_resource_list_changed()).map_err(Error::from)
                },
                None => Ok(()),
            }
        });

        host_fn!(notify_resource_updated(ctx: PluginServiceContext; update_msg: Json<ResourceUpdatedNotificationParam>) {
            let update_msg = update_msg.into_inner();
            let ctx = ctx.get()?.lock().unwrap().clone();
            let plugin_service = PluginService::get(ctx.plugin_service_id).ok_or_else(|| {
                anyhow::anyhow!("PluginService with ID {:?} not found", ctx.plugin_service_id)
            })?;
            if plugin_service.subscriptions.contains(&update_msg.uri) {
                match plugin_service.peer.get() {
                    Some(peer) => {
                        tracing::info!("Notifying resource {} updated from {}", update_msg.uri, ctx.plugin_name);
                        ctx.handle.block_on(peer.notify_resource_updated(update_msg)).map_err(Error::from)
                    },
                    None => Ok(()),
                }
            }
            else {
                Ok(())
            }
        });

        // Declares a host function `notify_tool_list_changed` that plugins can call
        host_fn!(notify_tool_list_changed(ctx: PluginServiceContext;) {
            let ctx = ctx.get()?.lock().unwrap().clone();
            let plugin_service = PluginService::get(ctx.plugin_service_id).ok_or_else(|| {
                anyhow::anyhow!("PluginService with ID {:?} not found", ctx.plugin_service_id)
            })?;

            match plugin_service.peer.get() {
                Some(peer) => {
                    tracing::info!("Notifying tool list changed from {}", ctx.plugin_name);
                    ctx.handle.block_on(peer.notify_tool_list_changed()).map_err(Error::from)
                },
                None => Ok(()),
            }
        });

        for (plugin_name, plugin_cfg) in &self.config.plugins {
            let wasm_data = match WASM_DATA_CACHE.entry(plugin_name.clone()) {
                Entry::Occupied(entry) => entry.get().clone(),
                Entry::Vacant(entry) => {
                    let content = match plugin_cfg.url.scheme() {
                        "file" => tokio::fs::read(plugin_cfg.url.path()).await?,
                        "http" => wasm::http::load_wasm(&plugin_cfg.url, &None).await?,
                        "https" => {
                            wasm::http::load_wasm(&plugin_cfg.url, &self.config.auths).await?
                        }
                        "oci" => {
                            wasm::oci::load_wasm(&plugin_cfg.url, &self.config.oci, plugin_name)
                                .await?
                        }
                        "s3" => wasm::s3::load_wasm(&plugin_cfg.url).await?,
                        unsupported => {
                            tracing::error!("Unsupported plugin URL scheme: {unsupported}");
                            return Err(anyhow::anyhow!(
                                "Unsupported plugin URL scheme: {unsupported}"
                            ));
                        }
                    };
                    entry.insert(content.clone());
                    content
                }
            };
            let mut manifest = Manifest::new([Wasm::data(wasm_data)]);
            if let Some(runtime_cfg) = &plugin_cfg.runtime_config {
                tracing::info!("runtime_cfg: {runtime_cfg:?}");
                if let Some(hosts) = &runtime_cfg.allowed_hosts {
                    for host in hosts {
                        manifest = manifest.with_allowed_host(host);
                    }
                }
                if let Some(paths) = &runtime_cfg.allowed_paths {
                    for path in paths {
                        // path will be available in the plugin with exact same path
                        manifest = manifest.with_allowed_path(path.clone(), path.clone());
                    }
                }

                // Add plugin configurations if present
                if let Some(env_vars) = &runtime_cfg.env_vars {
                    for (key, value) in env_vars {
                        let resolved_value = check_env_reference(value);
                        manifest = manifest.with_config_key(key, &resolved_value);
                    }
                }

                if let Some(memory_limit) = &runtime_cfg.memory_limit {
                    match ByteSize::from_str(memory_limit) {
                        Ok(b) => {
                            // Wasm page size 64KiB, convert to number of pages
                            let num_pages = b.as_u64() / (64 * 1024);
                            manifest = manifest.with_memory_max(num_pages as u32);
                        }
                        Err(e) => {
                            tracing::error!(
                                "Failed to parse memory_limit '{memory_limit}': {e}. Using default memory limit."
                            );
                        }
                    }
                }
            }
            let extism_plugin = extism::Plugin::new(
                &manifest,
                [
                    Function::new(
                        "create_elicitation",
                        [extism::PTR],
                        [extism::PTR],
                        UserData::new(PluginServiceContext {
                            plugin_service_id: self.id,
                            handle: Handle::current(),
                            plugin_name: plugin_name.to_string(),
                        }),
                        create_elicitation,
                    )
                    .with_namespace(EXTISM_USER_MODULE),
                    Function::new(
                        "create_message",
                        [extism::PTR],
                        [extism::PTR],
                        UserData::new(PluginServiceContext {
                            plugin_service_id: self.id,
                            handle: Handle::current(),
                            plugin_name: plugin_name.to_string(),
                        }),
                        create_message,
                    )
                    .with_namespace(EXTISM_USER_MODULE),
                    Function::new(
                        "list_roots",
                        [],
                        [extism::PTR],
                        UserData::new(PluginServiceContext {
                            plugin_service_id: self.id,
                            handle: Handle::current(),
                            plugin_name: plugin_name.to_string(),
                        }),
                        list_roots,
                    )
                    .with_namespace(EXTISM_USER_MODULE),
                    Function::new(
                        "notify_logging_message",
                        [extism::PTR],
                        [],
                        UserData::new(PluginServiceContext {
                            plugin_service_id: self.id,
                            handle: Handle::current(),
                            plugin_name: plugin_name.to_string(),
                        }),
                        notify_logging_message,
                    )
                    .with_namespace(EXTISM_USER_MODULE),
                    Function::new(
                        "notify_progress",
                        [extism::PTR],
                        [],
                        UserData::new(PluginServiceContext {
                            plugin_service_id: self.id,
                            handle: Handle::current(),
                            plugin_name: plugin_name.to_string(),
                        }),
                        notify_progress,
                    )
                    .with_namespace(EXTISM_USER_MODULE),
                    Function::new(
                        "notify_prompt_list_changed",
                        [],
                        [],
                        UserData::new(PluginServiceContext {
                            plugin_service_id: self.id,
                            handle: Handle::current(),
                            plugin_name: plugin_name.to_string(),
                        }),
                        notify_prompt_list_changed,
                    )
                    .with_namespace(EXTISM_USER_MODULE),
                    Function::new(
                        "notify_resource_list_changed",
                        [],
                        [],
                        UserData::new(PluginServiceContext {
                            plugin_service_id: self.id,
                            handle: Handle::current(),
                            plugin_name: plugin_name.to_string(),
                        }),
                        notify_resource_list_changed,
                    )
                    .with_namespace(EXTISM_USER_MODULE),
                    Function::new(
                        "notify_resource_updated",
                        [extism::PTR],
                        [],
                        UserData::new(PluginServiceContext {
                            plugin_service_id: self.id,
                            handle: Handle::current(),
                            plugin_name: plugin_name.to_string(),
                        }),
                        notify_resource_updated,
                    )
                    .with_namespace(EXTISM_USER_MODULE),
                    Function::new(
                        "notify_tool_list_changed",
                        [],
                        [],
                        UserData::new(PluginServiceContext {
                            plugin_service_id: self.id,
                            handle: Handle::current(),
                            plugin_name: plugin_name.to_string(),
                        }),
                        notify_tool_list_changed,
                    )
                    .with_namespace(EXTISM_USER_MODULE),
                ],
                true,
            )
            .unwrap();

            let plugin_id = extism_plugin.id;
            let plugin: Box<dyn Plugin> = if extism_plugin.function_exists("call")
                && extism_plugin.function_exists("describe")
            {
                Box::new(PluginV1::new(
                    plugin_name.clone(),
                    Arc::new(Mutex::new(extism_plugin)),
                ))
            } else {
                Box::new(PluginV2::new(
                    plugin_name.clone(),
                    Arc::new(Mutex::new(extism_plugin)),
                ))
            };

            names.insert(plugin_id, plugin_name.clone());
            plugins.insert(plugin_name.clone(), plugin);
            tracing::info!("Loaded plugin {plugin_name}");
        }
        self.names.set(names).expect("Names already set");
        self.plugins.set(plugins).expect("Plugins already set");
        Ok(())
    }

    pub fn logging_level(&self) -> LoggingLevel {
        *self.logging_level.read().unwrap()
    }

    pub fn set_logging_level(&self, level: LoggingLevel) {
        *self.logging_level.write().unwrap() = level;
    }
}

impl ServerHandler for PluginService {
    async fn call_tool(
        &self,
        request: CallToolRequestParam,
        context: RequestContext<RoleServer>,
    ) -> Result<CallToolResult, McpError> {
        tracing::info!("got tools/call request {:?}", request);
        let (plugin_name, tool_name) = match parse_namespaced_name(request.name.to_string()) {
            Ok((plugin_name, tool_name)) => (plugin_name, tool_name),
            Err(e) => {
                return Err(McpError::invalid_request(
                    format!("Failed to parse tool name: {e}"),
                    None,
                ));
            }
        };
        let plugin_config = match self.config.plugins.get(&plugin_name) {
            Some(config) => config,
            None => {
                return Err(McpError::method_not_found::<CallToolRequestMethod>());
            }
        };
        if let Some(skip_tools) = &plugin_config
            .runtime_config
            .as_ref()
            .and_then(|rc| rc.skip_tools.clone())
            && skip_tools.is_match(&tool_name)
        {
            tracing::warn!("Tool {tool_name} in skip_tools");
            return Err(McpError::method_not_found::<CallToolRequestMethod>());
        }

        let request = CallToolRequestParam {
            name: std::borrow::Cow::Owned(tool_name.clone()),
            arguments: request.arguments,
        };

        let Some(plugins) = self.plugins.get() else {
            return Err(McpError::internal_error(
                "Plugins not initialized".to_string(),
                None,
            ));
        };

        let Some(plugin) = plugins.get(&plugin_name) else {
            return Err(McpError::method_not_found::<CallToolRequestMethod>());
        };
        plugin.call_tool(request, context).await
    }

    async fn complete(
        &self,
        request: CompleteRequestParam,
        context: RequestContext<RoleServer>,
    ) -> Result<CompleteResult, McpError> {
        tracing::info!("got completion/complete request {:?}", request);
        let (plugin_name, request) = match request.r#ref {
            Reference::Prompt(PromptReference { name, title }) => {
                let (plugin_name, prompt_name) = match parse_namespaced_name(name.to_string()) {
                    Ok((plugin_name, prompt_name)) => (plugin_name, prompt_name),
                    Err(e) => {
                        return Err(McpError::invalid_request(
                            format!("Failed to parse prompt name: {e}"),
                            None,
                        ));
                    }
                };
                let plugin_config = match self.config.plugins.get(&plugin_name) {
                    Some(config) => config,
                    None => {
                        return Err(McpError::method_not_found::<CompleteRequestMethod>());
                    }
                };
                if let Some(skip_prompts) = &plugin_config
                    .runtime_config
                    .as_ref()
                    .and_then(|rc| rc.skip_prompts.clone())
                    && skip_prompts.is_match(&prompt_name)
                {
                    tracing::warn!("Prompt {prompt_name} in skip_prompts");
                    return Err(McpError::method_not_found::<CompleteRequestMethod>());
                }
                (
                    plugin_name,
                    CompleteRequestParam {
                        r#ref: Reference::Prompt(PromptReference {
                            name: prompt_name,
                            title,
                        }),
                        argument: request.argument,
                        context: request.context,
                    },
                )
            }
            Reference::Resource(ResourceReference { uri }) => {
                let (plugin_name, resource_uri) = match parse_namespaced_uri(uri.to_string()) {
                    Ok((plugin_name, resource_uri)) => (plugin_name, resource_uri),
                    Err(e) => {
                        return Err(McpError::invalid_request(
                            format!("Failed to parse prompt name: {e}"),
                            None,
                        ));
                    }
                };
                let plugin_config = match self.config.plugins.get(&plugin_name) {
                    Some(config) => config,
                    None => {
                        return Err(McpError::method_not_found::<CompleteRequestMethod>());
                    }
                };
                if let Some(skip_resource_templates) = &plugin_config
                    .runtime_config
                    .as_ref()
                    .and_then(|rc| rc.skip_resource_templates.clone())
                    && skip_resource_templates.is_match(&resource_uri)
                {
                    tracing::warn!("Resource {resource_uri} in skip_resources");
                    return Err(McpError::method_not_found::<CompleteRequestMethod>());
                }
                (
                    plugin_name,
                    CompleteRequestParam {
                        r#ref: Reference::Resource(ResourceReference { uri: resource_uri }),
                        argument: request.argument,
                        context: request.context,
                    },
                )
            }
        };

        let Some(plugins) = self.plugins.get() else {
            return Err(McpError::internal_error(
                "Plugins not initialized".to_string(),
                None,
            ));
        };

        let Some(plugin) = plugins.get(&plugin_name) else {
            return Err(McpError::method_not_found::<CallToolRequestMethod>());
        };
        plugin.complete(request, context).await
    }

    fn get_info(&self) -> ServerInfo {
        ServerInfo {
            server_info: Implementation {
                name: "hyper-mcp".to_string(),
                title: Some("Hyper MCP".to_string()),
                version: env!("CARGO_PKG_VERSION").to_string(),
                website_url: Some("https://github.com/tuananh/hyper-mcp".to_string()),

                ..Default::default()
            },
            capabilities: ServerCapabilities::builder()
                .enable_completions()
                .enable_logging()
                .enable_prompts()
                .enable_prompts_list_changed()
                .enable_resources()
                .enable_resources_list_changed()
                .enable_resources_subscribe()
                .enable_tools()
                .enable_tool_list_changed()
                .build(),

            ..Default::default()
        }
    }

    async fn get_prompt(
        &self,
        request: GetPromptRequestParam,
        context: RequestContext<RoleServer>,
    ) -> Result<GetPromptResult, McpError> {
        tracing::info!("got prompts/get request {:?}", request);
        let (plugin_name, prompt_name) = match parse_namespaced_name(request.name.to_string()) {
            Ok((plugin_name, prompt_name)) => (plugin_name, prompt_name),
            Err(e) => {
                return Err(McpError::invalid_request(
                    format!("Failed to parse prompt name: {e}"),
                    None,
                ));
            }
        };
        let plugin_config = match self.config.plugins.get(&plugin_name) {
            Some(config) => config,
            None => {
                return Err(McpError::method_not_found::<GetPromptRequestMethod>());
            }
        };
        if let Some(skip_prompts) = &plugin_config
            .runtime_config
            .as_ref()
            .and_then(|rc| rc.skip_prompts.clone())
            && skip_prompts.is_match(&prompt_name)
        {
            tracing::warn!("Prompt {prompt_name} in skip_prompts");
            return Err(McpError::method_not_found::<GetPromptRequestMethod>());
        }

        let request = GetPromptRequestParam {
            name: prompt_name.clone(),
            arguments: request.arguments,
        };

        let Some(plugins) = self.plugins.get() else {
            return Err(McpError::internal_error(
                "Plugins not initialized".to_string(),
                None,
            ));
        };

        let Some(plugin) = plugins.get(&plugin_name) else {
            return Err(McpError::method_not_found::<GetPromptRequestMethod>());
        };
        plugin.get_prompt(request, context).await
    }

    async fn list_prompts(
        &self,
        request: Option<PaginatedRequestParam>,
        context: RequestContext<RoleServer>,
    ) -> Result<ListPromptsResult, McpError> {
        tracing::info!("got prompts/list request {:?}", request);
        let Some(plugins) = self.plugins.get() else {
            return Err(McpError::internal_error(
                "Plugins not initialized".to_string(),
                None,
            ));
        };

        let mut list_prompts_result = ListPromptsResult::default();

        for (plugin_name, plugin) in plugins.iter() {
            let plugin_prompts = plugin
                .list_prompts(request.clone(), context.clone())
                .await?;
            let plugin_cfg = self.config.plugins.get(plugin_name).ok_or_else(|| {
                McpError::internal_error(
                    format!("Plugin configuration not found for {plugin_name}"),
                    None,
                )
            })?;
            let skip_prompts = plugin_cfg
                .runtime_config
                .as_ref()
                .and_then(|rc| rc.skip_prompts.clone())
                .unwrap_or_default();
            for prompt in plugin_prompts.prompts {
                let prompt_name = prompt.name.as_ref() as &str;
                if skip_prompts.is_match(prompt_name) {
                    tracing::info!(
                        "Skipping prompt {} as requested in skip_prompts",
                        prompt.name
                    );
                    continue;
                }
                let mut new_prompt = prompt.clone();
                new_prompt.name = create_namespaced_name(plugin_name, &prompt.name);
                list_prompts_result.prompts.push(new_prompt);
            }
        }

        Ok(list_prompts_result)
    }

    async fn list_resources(
        &self,
        request: Option<PaginatedRequestParam>,
        context: RequestContext<RoleServer>,
    ) -> Result<ListResourcesResult, McpError> {
        tracing::info!("got resources/list request {:?}", request);
        let Some(plugins) = self.plugins.get() else {
            return Err(McpError::internal_error(
                "Plugins not initialized".to_string(),
                None,
            ));
        };

        let mut list_resources_result = ListResourcesResult::default();

        for (plugin_name, plugin) in plugins.iter() {
            let plugin_resources = plugin
                .list_resources(request.clone(), context.clone())
                .await?;
            let plugin_cfg = self.config.plugins.get(plugin_name).ok_or_else(|| {
                McpError::internal_error(
                    format!("Plugin configuration not found for {plugin_name}"),
                    None,
                )
            })?;
            let skip_resources = plugin_cfg
                .runtime_config
                .as_ref()
                .and_then(|rc| rc.skip_resources.clone())
                .unwrap_or_default();
            for resource in plugin_resources.resources {
                if skip_resources.is_match(resource.uri.as_str()) {
                    tracing::info!(
                        "Skipping resource {} as requested in skip_resources",
                        resource.uri
                    );
                    continue;
                }
                let mut raw = resource.raw.clone();
                raw.uri = create_namespaced_uri(plugin_name, &resource.uri)
                    .map_err(|e| McpError::internal_error(e.to_string(), None))?;
                list_resources_result.resources.push(Resource {
                    raw,
                    annotations: resource.annotations.clone(),
                });
            }
        }

        Ok(list_resources_result)
    }

    async fn list_resource_templates(
        &self,
        request: Option<PaginatedRequestParam>,
        context: RequestContext<RoleServer>,
    ) -> Result<ListResourceTemplatesResult, McpError> {
        tracing::info!("got resources/templates/list request {:?}", request);
        let Some(plugins) = self.plugins.get() else {
            return Err(McpError::internal_error(
                "Plugins not initialized".to_string(),
                None,
            ));
        };

        let mut list_resource_templates_result = ListResourceTemplatesResult::default();

        for (plugin_name, plugin) in plugins.iter() {
            let plugin_resource_templates = plugin
                .list_resource_templates(request.clone(), context.clone())
                .await?;
            let plugin_cfg = self.config.plugins.get(plugin_name).ok_or_else(|| {
                McpError::internal_error(
                    format!("Plugin configuration not found for {plugin_name}"),
                    None,
                )
            })?;
            let skip_resource_templates = plugin_cfg
                .runtime_config
                .as_ref()
                .and_then(|rc| rc.skip_resource_templates.clone())
                .unwrap_or_default();
            for resource_template in plugin_resource_templates.resource_templates {
                if skip_resource_templates.is_match(resource_template.uri_template.as_str()) {
                    tracing::info!(
                        "Skipping resource template {} as requested in skip_resources",
                        resource_template.uri_template
                    );
                    continue;
                }
                let mut raw = resource_template.raw.clone();
                raw.uri_template =
                    create_namespaced_uri(plugin_name, &resource_template.uri_template)
                        .map_err(|e| McpError::internal_error(e.to_string(), None))?;
                list_resource_templates_result
                    .resource_templates
                    .push(ResourceTemplate {
                        raw,
                        annotations: resource_template.annotations.clone(),
                    });
            }
        }

        Ok(list_resource_templates_result)
    }

    async fn list_tools(
        &self,
        request: Option<PaginatedRequestParam>,
        context: RequestContext<RoleServer>,
    ) -> Result<ListToolsResult, McpError> {
        tracing::info!("got tools/list request {:?}", request);
        let Some(plugins) = self.plugins.get() else {
            return Err(McpError::internal_error(
                "Plugins not initialized".to_string(),
                None,
            ));
        };

        let mut list_tools_result = ListToolsResult::default();

        for (plugin_name, plugin) in plugins.iter() {
            let plugin_tools = plugin.list_tools(request.clone(), context.clone()).await?;
            let plugin_cfg = self.config.plugins.get(plugin_name).ok_or_else(|| {
                McpError::internal_error(
                    format!("Plugin configuration not found for {plugin_name}"),
                    None,
                )
            })?;
            let skip_tools = plugin_cfg
                .runtime_config
                .as_ref()
                .and_then(|rc| rc.skip_tools.clone())
                .unwrap_or_default();
            for tool in plugin_tools.tools {
                let tool_name = tool.name.as_ref() as &str;
                if skip_tools.is_match(tool_name) {
                    tracing::info!("Skipping tool {} as requested in skip_tools", tool.name);
                    continue;
                }
                let mut new_tool = tool.clone();
                new_tool.name =
                    std::borrow::Cow::Owned(create_namespaced_name(plugin_name, &tool.name));
                list_tools_result.tools.push(new_tool);
            }
        }

        Ok(list_tools_result)
    }

    fn on_initialized(
        &self,
        context: NotificationContext<RoleServer>,
    ) -> impl Future<Output = ()> + Send + '_ {
        tracing::info!("client initialized");
        self.peer.set(context.peer).expect("Peer already set");
        std::future::ready(())
    }

    async fn on_roots_list_changed(&self, context: NotificationContext<RoleServer>) -> () {
        tracing::info!("got roots/list_changed notification");
        let Some(plugins) = self.plugins.get() else {
            tracing::error!("Plugins not initialized");
            return;
        };
        for (plugin_name, plugin) in plugins.iter() {
            if let Err(e) = plugin.on_roots_list_changed(context.clone()).await {
                tracing::error!("Failed to notify plugin {plugin_name} of roots list change: {e}");
            }
        }
    }

    async fn read_resource(
        &self,
        request: ReadResourceRequestParam,
        context: RequestContext<RoleServer>,
    ) -> Result<ReadResourceResult, McpError> {
        tracing::info!("got resources/read request {:?}", request);
        let (plugin_name, resource_uri) = match parse_namespaced_uri(request.uri.to_string()) {
            Ok((plugin_name, resource_uri)) => (plugin_name, resource_uri),
            Err(e) => {
                return Err(McpError::invalid_request(
                    format!("Failed to parse prompt name: {e}"),
                    None,
                ));
            }
        };
        let plugin_config = match self.config.plugins.get(&plugin_name) {
            Some(config) => config,
            None => {
                return Err(McpError::method_not_found::<ReadResourceRequestMethod>());
            }
        };
        if let Some(skip_resources) = &plugin_config
            .runtime_config
            .as_ref()
            .and_then(|rc| rc.skip_resources.clone())
            && skip_resources.is_match(&resource_uri)
        {
            tracing::warn!("Resource {resource_uri} in skip_resources");
            return Err(McpError::method_not_found::<ReadResourceRequestMethod>());
        }

        let request = ReadResourceRequestParam {
            uri: resource_uri.clone(),
        };

        let Some(plugins) = self.plugins.get() else {
            return Err(McpError::internal_error(
                "Plugins not initialized".to_string(),
                None,
            ));
        };

        let Some(plugin) = plugins.get(&plugin_name) else {
            return Err(McpError::method_not_found::<GetPromptRequestMethod>());
        };
        plugin.read_resource(request, context).await
    }

    fn set_level(
        &self,
        request: SetLevelRequestParam,
        _context: RequestContext<RoleServer>,
    ) -> impl Future<Output = Result<(), McpError>> + Send + '_ {
        self.set_logging_level(request.level);
        std::future::ready(Ok(()))
    }

    fn subscribe(
        &self,
        request: SubscribeRequestParam,
        _context: RequestContext<RoleServer>,
    ) -> impl Future<Output = std::result::Result<(), McpError>> + Send + '_ {
        self.subscriptions.insert(request.uri);
        std::future::ready(Ok(()))
    }

    fn unsubscribe(
        &self,
        request: UnsubscribeRequestParam,
        _context: RequestContext<RoleServer>,
    ) -> impl Future<Output = std::result::Result<(), McpError>> + Send + '_ {
        self.subscriptions.remove(&request.uri);
        std::future::ready(Ok(()))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{cli::Cli, config::load_config};
    use rmcp::{
        ClientHandler,
        model::ClientInfo,
        service::{RoleClient, RunningService, Service, serve_client, serve_server},
    };
    use std::{
        path::PathBuf,
        sync::atomic::{AtomicUsize, Ordering},
    };
    use tempfile::TempDir;
    use tokio::io::duplex;
    use tokio_test::assert_ok;
    use tokio_util::sync::CancellationToken;

    struct TestClientInner {
        tool_list_changed_count: AtomicUsize,
    }

    struct TestClient(Arc<TestClientInner>);

    impl Clone for TestClient {
        fn clone(&self) -> Self {
            Self(Arc::clone(&self.0))
        }
    }

    impl Deref for TestClient {
        type Target = Arc<TestClientInner>;

        fn deref(&self) -> &Self::Target {
            &self.0
        }
    }

    impl ClientHandler for TestClient {
        fn on_tool_list_changed(
            &self,
            _context: NotificationContext<RoleClient>,
        ) -> impl Future<Output = ()> + Send + '_ {
            self.tool_list_changed_count.fetch_add(1, Ordering::SeqCst);
            std::future::ready(())
        }
    }

    impl TestClient {
        fn new() -> Self {
            Self(Arc::new(TestClientInner {
                tool_list_changed_count: AtomicUsize::new(0),
            }))
        }

        fn get_tool_list_changed_count(&self) -> usize {
            self.tool_list_changed_count.load(Ordering::SeqCst)
        }
    }

    async fn create_temp_config_file(content: &str) -> anyhow::Result<(TempDir, PathBuf)> {
        let temp_dir = TempDir::new()?;
        let config_path = temp_dir.path().join("test_config.yaml");
        tokio::fs::write(&config_path, content).await?;
        Ok((temp_dir, config_path))
    }

    fn create_test_cli() -> Cli {
        crate::cli::Cli::default()
    }

    fn create_test_ctx(
        running: &RunningService<RoleServer, PluginService>,
    ) -> RequestContext<RoleServer> {
        RequestContext {
            ct: CancellationToken::new(),
            extensions: Extensions::default(),
            id: RequestId::Number(1),
            meta: Meta::default(),
            peer: running.peer().clone(),
        }
    }

    fn create_test_service(config: Config) -> PluginService {
        PluginService(Arc::new(PluginServiceInner {
            config,
            id: Uuid::new_v4(),
            logging_level: RwLock::new(LoggingLevel::Info),
            names: SetOnce::new(),
            peer: SetOnce::new(),
            plugins: SetOnce::new(),
            subscriptions: DashSet::new(),
        }))
    }

    async fn create_test_pair<S, C>(
        service: S,
        client: C,
    ) -> (RunningService<RoleServer, S>, RunningService<RoleClient, C>)
    where
        S: Service<RoleServer>,
        C: Service<RoleClient>,
    {
        let (srv_io, cli_io) = duplex(64 * 1024);
        tokio::try_join!(
            async {
                serve_server(service, srv_io)
                    .await
                    .map_err(anyhow::Error::from)
            },
            async {
                serve_client(client, cli_io)
                    .await
                    .map_err(anyhow::Error::from)
            }
        )
        .expect("Failed to create test pair")
    }

    fn get_test_wasm_path() -> PathBuf {
        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        path.push("examples");
        path.push("plugins");
        path.push("v1");
        path.push("time");
        path.push("time.wasm");
        path
    }

    fn test_wasm_exists() -> bool {
        get_test_wasm_path().exists()
    }

    fn get_tool_list_changed_wasm_path() -> PathBuf {
        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        path.push("examples");
        path.push("plugins");
        path.push("v1");
        path.push("tool-list-changed");
        path.push("tool_list_changed.wasm");
        path
    }

    fn test_tool_list_changed_wasm_exists() -> bool {
        get_tool_list_changed_wasm_path().exists()
    }

    fn get_rstime_wasm_path() -> PathBuf {
        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        path.push("examples");
        path.push("plugins");
        path.push("v2");
        path.push("rstime");
        path.push("rstime.wasm");
        path
    }

    fn test_rstime_wasm_exists() -> bool {
        get_rstime_wasm_path().exists()
    }

    // Helper function to create a dummy request context for compilation
    // These tests will be skipped at runtime since we can't easily mock contexts
    // PluginService creation tests

    #[tokio::test]
    async fn test_plugin_service_creation_empty_config() {
        let config_content = r#"
plugins: {}
"#;
        let (_temp_dir, config_path) = create_temp_config_file(config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let result = PluginService::new(&config).await;
        assert!(
            result.is_ok(),
            "Should create service with empty plugin config"
        );

        let service = result.unwrap();
        let Some(plugins) = service.plugins.get() else {
            panic!("Plugins should be initialized");
        };
        assert!(plugins.is_empty(), "Should have no plugins loaded");
    }

    #[tokio::test]
    async fn test_plugin_service_creation_with_file_plugin() {
        let wasm_path = get_test_wasm_path();
        if !test_wasm_exists() {
            println!("Skipping test - WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  time_plugin:
    url: "file://{}"
    runtime_config:
      memory_limit: "1MB"
      env_vars:
        TEST_MODE: "true"
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let result = PluginService::new(&config).await;
        assert!(
            result.is_ok(),
            "Should create service with valid WASM plugin"
        );

        let service = result.unwrap();
        let Some(plugins) = service.plugins.get() else {
            panic!("Plugins should be initialized");
        };
        assert_eq!(plugins.len(), 1, "Should have one plugin loaded");
        assert!(plugins.contains_key(&PluginName::from_str("time_plugin").unwrap()));
    }

    #[tokio::test]
    async fn test_plugin_service_creation_with_nonexistent_file() {
        let config_content = r#"
plugins:
  missing_plugin:
    url: "file:///nonexistent/path/plugin.wasm"
"#;

        let (_temp_dir, config_path) = create_temp_config_file(config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let result = PluginService::new(&config).await;
        assert!(result.is_err(), "Should fail with nonexistent plugin file");
    }

    #[tokio::test]
    async fn test_plugin_service_creation_with_invalid_memory_limit() {
        let wasm_path = get_test_wasm_path();
        if !test_wasm_exists() {
            println!("Skipping test - WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  time_plugin:
    url: "file://{}"
    runtime_config:
      memory_limit: "invalid_size"
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let result = PluginService::new(&config).await;
        // Should still succeed but log an error about invalid memory limit
        assert!(
            result.is_ok(),
            "Should handle invalid memory limit gracefully"
        );
    }

    // ServerHandler tests

    #[test]
    fn test_plugin_service_get_info() {
        let config = Config::default();
        let service = create_test_service(config);

        let info = rmcp::ServerHandler::get_info(&service);
        assert_eq!(info.protocol_version, ProtocolVersion::LATEST);
        assert_eq!(info.server_info.name, "hyper-mcp");
        assert!(!info.server_info.version.is_empty());
        assert!(info.capabilities.tools.is_some());
    }

    #[tokio::test]
    async fn test_plugin_service_list_tools_with_plugin() {
        let wasm_path = get_test_wasm_path();
        if !test_wasm_exists() {
            println!("Skipping test - WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  time_plugin:
    url: "file://{}"
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;
        // Verify the service was created successfully
        let Some(plugins) = server.service().plugins.get() else {
            panic!("Plugins should be initialized");
        };
        assert!(!plugins.is_empty(), "Should have loaded plugin");

        // Test the list_tools function
        let request = None; // No pagination for this test
        let ctx = create_test_ctx(&server);
        let result = server.service().list_tools(request, ctx).await;
        assert!(result.is_ok(), "list_tools should succeed");

        let list_tools_result = result.unwrap();
        assert!(
            !list_tools_result.tools.is_empty(),
            "Should have tools from the loaded plugin"
        );

        // Verify we get the expected tools from time.wasm plugin
        let expected_tools = vec!["time_plugin-time"];

        let actual_tool_names: Vec<String> = list_tools_result
            .tools
            .iter()
            .map(|tool| tool.name.to_string())
            .collect();

        for expected_tool in &expected_tools {
            assert!(
                actual_tool_names.contains(&expected_tool.to_string()),
                "Expected tool '{expected_tool}' not found in actual tools: {actual_tool_names:?}"
            );
        }

        assert_eq!(
            list_tools_result.tools.len(),
            expected_tools.len(),
            "Expected {} tools but got {}: {:?}",
            expected_tools.len(),
            list_tools_result.tools.len(),
            actual_tool_names
        );

        // Verify the time tool has the expected operations in its schema
        let time_tool = list_tools_result
            .tools
            .iter()
            .find(|tool| tool.name == "time_plugin-time")
            .expect("time_plugin-time tool should exist");

        // Check that the tool description mentions the expected operations
        let description = time_tool
            .description
            .as_ref()
            .expect("Tool should have description");
        let expected_operations = vec!["get_time_utc", "parse_time", "time_offset"];
        for operation in &expected_operations {
            assert!(
                description.contains(operation),
                "Tool description should mention operation '{operation}': {description}"
            );
        }

        // Check that the input schema includes the expected operations in the enum
        let schema_value = &time_tool.input_schema;
        if let Some(properties) = schema_value.get("properties") {
            if let Some(name_property) = properties.get("name") {
                if let Some(enum_values) = name_property.get("enum") {
                    if let Some(enum_array) = enum_values.as_array() {
                        let schema_operations: Vec<String> = enum_array
                            .iter()
                            .filter_map(|v| v.as_str().map(|s| s.to_string()))
                            .collect();

                        for operation in &expected_operations {
                            assert!(
                                schema_operations.contains(&operation.to_string()),
                                "Input schema should include operation '{operation}' in enum: {schema_operations:?}"
                            );
                        }
                    }
                }
            }
        }
        // Cleanup
        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_plugin_service_list_tools_with_skip_tools() {
        let wasm_path = get_test_wasm_path();
        if !test_wasm_exists() {
            println!("Skipping test - WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  time_plugin:
    url: "file://{}"
    runtime_config:
      skip_tools:
        - "time"
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;
        let Some(plugins) = server.service().plugins.get() else {
            panic!("Plugins should be initialized");
        };
        assert!(!plugins.is_empty(), "Should have loaded plugin");

        // Test the list_tools function with skip_tools configuration
        let request = None; // No pagination for this test
        let ctx = create_test_ctx(&server);
        let result = server.service().list_tools(request, ctx).await;
        assert!(result.is_ok(), "list_tools should succeed");

        let list_tools_result = result.unwrap();

        // Since we're skipping the "time" tool, the tools list should be empty
        assert!(
            list_tools_result.tools.is_empty(),
            "Should have no tools since 'time' tool is skipped. Found tools: {:?}",
            list_tools_result
                .tools
                .iter()
                .map(|t| t.name.as_ref() as &str)
                .collect::<Vec<&str>>()
        );

        // Verify specifically that the time-plugin::time tool is not present
        let tool_names: Vec<String> = list_tools_result
            .tools
            .iter()
            .map(|tool| tool.name.to_string())
            .collect();

        assert!(
            !tool_names.contains(&"time_plugin-time".to_string()),
            "time_plugin-time should be skipped but was found in tools: {tool_names:?}"
        );

        // Verify that the plugin itself was loaded (skip_tools should not prevent plugin loading)
        {
            let plugin_name: PluginName = "time_plugin".parse().unwrap();
            assert!(
                plugins.contains_key(&plugin_name),
                "Plugin 'time_plugin' should still be loaded even with skip_tools configuration"
            );
        } // plugins guard dropped here

        // Verify the plugin configuration includes skip_tools
        let plugin_name: PluginName = "time_plugin".parse().unwrap();
        let plugin_config = server.service().config.plugins.get(&plugin_name).unwrap();
        let skip_tools = plugin_config
            .runtime_config
            .as_ref()
            .and_then(|rc| rc.skip_tools.as_ref())
            .unwrap();

        assert!(
            skip_tools.is_match(&"time"),
            "Configuration should include 'time' in skip_tools list: {skip_tools:?}"
        );

        assert_eq!(
            skip_tools.len(),
            1,
            "Should have exactly one tool in skip_tools list: {skip_tools:?}"
        );

        // Cleanup
        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_plugin_service_call_tool_invalid_format() {
        let config = Config::default();
        let (server, client) =
            create_test_pair(create_test_service(config), ClientInfo::default()).await;

        // Test calling tool with invalid format (missing plugin name separator)
        let request = CallToolRequestParam {
            name: std::borrow::Cow::Borrowed("invalid_tool_name"),
            arguments: None,
        };

        let ctx = create_test_ctx(&server);
        let result = server.service().call_tool(request, ctx).await;
        assert!(result.is_err(), "Should fail with invalid tool name format");

        if let Err(error) = result {
            // Should be an invalid_request error
            assert!(
                error.to_string().contains("Failed to parse tool name"),
                "Error should mention parsing failure: {error}"
            );
        }

        // Test with empty tool name
        let request = CallToolRequestParam {
            name: std::borrow::Cow::Borrowed(""),
            arguments: None,
        };

        let ctx = create_test_ctx(&server);
        let result = server.service().call_tool(request, ctx).await;
        assert!(result.is_err(), "Should fail with empty tool name");
        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_plugin_service_call_tool_nonexistent_plugin() {
        let config = Config::default();
        let (server, client) =
            create_test_pair(create_test_service(config), ClientInfo::default()).await;

        // Test calling tool on nonexistent plugin
        let request = CallToolRequestParam {
            name: std::borrow::Cow::Borrowed("nonexistent_plugin-some_tool"),
            arguments: None,
        };

        let ctx = create_test_ctx(&server);
        let result = server.service().call_tool(request, ctx).await;
        assert!(result.is_err(), "Should fail with nonexistent plugin");

        if let Err(error) = result {
            // Should be a method_not_found error since plugin doesn't exist
            let error_str = error.to_string();
            assert!(
                error_str.contains("-32601") || error_str.contains("tools/call"),
                "Error should indicate method not found: {error}"
            );
        }
        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_plugin_service_call_tool_with_plugin() {
        let wasm_path = get_test_wasm_path();
        if !test_wasm_exists() {
            println!("Skipping test - WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  time_plugin:
    url: "file://{}"
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;
        let Some(plugins) = server.service().plugins.get() else {
            panic!("Plugins should be initialized");
        };
        assert!(!plugins.is_empty(), "Should have loaded plugin");

        // Test calling the time tool with get_time_utc operation
        let request = CallToolRequestParam {
            name: std::borrow::Cow::Borrowed("time_plugin-time"),
            arguments: Some({
                let mut map = serde_json::Map::new();
                map.insert(
                    "name".to_string(),
                    serde_json::Value::String("get_time_utc".to_string()),
                );
                map
            }),
        };

        let ctx = create_test_ctx(&server);
        let result = server.service().call_tool(request, ctx).await;
        assert!(
            result.is_ok(),
            "Should successfully call time tool: {result:?}"
        );

        let call_result = result.unwrap();

        assert!(
            !call_result.content.is_empty(),
            "call_result.content should not be empty"
        );

        // Test calling with parse_time operation
        let request = CallToolRequestParam {
            name: std::borrow::Cow::Borrowed("time_plugin-time"),
            arguments: Some({
                let mut map = serde_json::Map::new();
                map.insert(
                    "name".to_string(),
                    serde_json::Value::String("parse_time".to_string()),
                );
                map.insert(
                    "time_rfc2822".to_string(),
                    serde_json::Value::String("Wed, 18 Feb 2015 23:16:09 GMT".to_string()),
                );
                map
            }),
        };

        let ctx = create_test_ctx(&server);
        let result = server.service().call_tool(request, ctx).await;
        assert!(
            result.is_ok(),
            "Should successfully call parse_time operation: {result:?}"
        );

        let call_result = result.unwrap();
        // Verify the parse_time operation returns content

        assert!(
            !call_result.content.is_empty(),
            "Parse time operation should return non-empty content"
        );
        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_plugin_service_call_tool_with_skipped_tool() {
        let wasm_path = get_test_wasm_path();
        if !test_wasm_exists() {
            println!("Skipping test - WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  time_plugin:
    url: "file://{}"
    runtime_config:
      skip_tools:
        - "time"
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;
        let Some(plugins) = server.service().plugins.get() else {
            panic!("Plugins should be initialized");
        };
        assert!(!plugins.is_empty(), "Should have loaded plugin");

        // Test calling the skipped time tool
        let request = CallToolRequestParam {
            name: std::borrow::Cow::Borrowed("time_plugin-time"),
            arguments: Some({
                let mut map = serde_json::Map::new();
                map.insert(
                    "name".to_string(),
                    serde_json::Value::String("get_time_utc".to_string()),
                );
                map
            }),
        };

        let ctx = create_test_ctx(&server);
        let result = server.service().call_tool(request, ctx).await;
        assert!(result.is_err(), "Should fail when calling skipped tool");

        if let Err(error) = result {
            // Should be a method_not_found error since tool is skipped
            let error_str = error.to_string();
            assert!(
                error_str.contains("-32601") || error_str.contains("tools/call"),
                "Error should indicate method not found for skipped tool: {error}"
            );
        }
        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[test]
    fn test_plugin_service_ping() {
        let config = Config::default();
        let service = create_test_service(config);

        // Test that the service implements ServerHandler
        assert_eq!(
            rmcp::ServerHandler::get_info(&service).server_info.name,
            "hyper-mcp"
        );
    }

    #[test]
    fn test_plugin_service_initialize() {
        let config = Config::default();
        let service = create_test_service(config);

        // Test server info
        let info = rmcp::ServerHandler::get_info(&service);
        assert_eq!(info.protocol_version, ProtocolVersion::LATEST);
        assert_eq!(info.server_info.name, "hyper-mcp");
    }

    #[test]
    fn test_plugin_service_methods_exist() {
        let config = Config::default();
        let service = create_test_service(config);

        // Test that ServerHandler methods exist by calling get_info
        let info = rmcp::ServerHandler::get_info(&service);
        assert_eq!(info.server_info.name, "hyper-mcp");
        assert!(info.capabilities.tools.is_some());
    }

    #[tokio::test]
    async fn test_plugin_service_multiple_plugins() {
        let wasm_path = get_test_wasm_path();
        if !test_wasm_exists() {
            println!("Skipping test - WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  time_plugin_1:
    url: "file://{}"
    runtime_config:
      memory_limit: "1MB"
  time_plugin_2:
    url: "file://{}"
    runtime_config:
      memory_limit: "2MB"
"#,
            wasm_path.display(),
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let service = PluginService::new(&config).await.unwrap();
        let Some(plugins) = service.plugins.get() else {
            panic!("Plugins should be initialized");
        };

        assert_eq!(plugins.len(), 2, "Should have loaded two plugins");
        assert!(plugins.contains_key(&PluginName::from_str("time_plugin_1").unwrap()));
        assert!(plugins.contains_key(&PluginName::from_str("time_plugin_2").unwrap()));
    }

    #[tokio::test]
    async fn test_plugin_service_call_tool_with_cancellation() {
        let wasm_path = get_test_wasm_path();
        if !test_wasm_exists() {
            println!("Skipping test - WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  time_plugin:
    url: "file://{}"
    runtime_config:
      max_memory_mb: 10
      max_execution_time_ms: 5000
"#,
            wasm_path.to_string_lossy()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;

        // Create a cancellation token
        let cancellation_token = CancellationToken::new();

        // Create request context with the cancellation token
        let ctx = RequestContext {
            ct: cancellation_token.clone(),
            extensions: Extensions::default(),
            id: RequestId::Number(1),
            meta: Meta::default(),
            peer: server.peer().clone(),
        };

        let request = CallToolRequestParam {
            name: std::borrow::Cow::Borrowed("time_plugin-time"),
            arguments: Some({
                let mut map = serde_json::Map::new();
                map.insert(
                    "name".to_string(),
                    serde_json::Value::String("get_time_utc".to_string()),
                );
                map
            }),
        };

        // Cancel the token before executing call_tool to force cancellation path
        cancellation_token.cancel();

        // Execute call_tool with the already-cancelled token
        let result = server.service().call_tool(request, ctx).await;

        assert!(result.is_err(), "Expected cancellation error");
        let error = result.unwrap_err();
        let error_message = error.to_string();
        assert!(
            error_message.contains("cancelled") || error_message.contains("canceled"),
            "Expected cancellation error message, got: {error_message}"
        );
        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_plugin_service_list_tools_with_cancellation() {
        let wasm_path = get_test_wasm_path();
        if !test_wasm_exists() {
            println!("Skipping test - WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  time_plugin:
    url: "file://{}"
    runtime_config:
      max_memory_mb: 10
      max_execution_time_ms: 5000
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;

        // Create a cancellation token
        let cancellation_token = CancellationToken::new();

        // Create request context with the cancellation token
        let ctx = RequestContext {
            ct: cancellation_token.clone(),
            extensions: Extensions::default(),
            id: RequestId::Number(1),
            meta: Meta::default(),
            peer: server.peer().clone(),
        };

        // Cancel the token before executing list_tools to force cancellation path
        cancellation_token.cancel();

        // Execute list_tools with the already-cancelled token
        let result = server.service().list_tools(None, ctx).await;

        assert!(result.is_err(), "Expected cancellation error");
        let error = result.unwrap_err();
        let error_message = error.to_string();
        assert!(
            error_message.contains("cancelled") || error_message.contains("canceled"),
            "Expected cancellation error message, got: {error_message}"
        );
        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    // ========================================================================
    // Tests for notify_tool_list_changed host function
    // ========================================================================

    #[tokio::test]
    async fn test_notify_tool_list_changed_basic() {
        let wasm_path = get_tool_list_changed_wasm_path();
        if !test_tool_list_changed_wasm_exists() {
            println!("Skipping test - tool-list-changed WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  tool_list_changed_plugin:
    url: "file://{}"
    runtime_config:
      max_memory_mb: 10
      max_execution_time_ms: 5000
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;
        let ctx = create_test_ctx(&server);

        // List tools to verify the plugin loaded and has initial tools
        let result = server.service().list_tools(None, ctx).await;
        assert!(result.is_ok(), "list_tools should succeed");

        let tools = result.unwrap();
        assert!(
            !tools.tools.is_empty(),
            "tool_list_changed_plugin should have at least one tool"
        );

        // Verify add_tool exists
        let tool_names: Vec<String> = tools.tools.iter().map(|t| t.name.to_string()).collect();
        assert!(
            tool_names.contains(&"tool_list_changed_plugin-add_tool".to_string()),
            "add_tool should be in the tool list"
        );

        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_notify_tool_list_changed_triggers_on_add() {
        let wasm_path = get_tool_list_changed_wasm_path();
        if !test_tool_list_changed_wasm_exists() {
            println!("Skipping test - tool-list-changed WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  tool_list_changed_plugin:
    url: "file://{}"
    runtime_config:
      max_memory_mb: 10
      max_execution_time_ms: 5000
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            TestClient::new(),
        )
        .await;
        let ctx = create_test_ctx(&server);

        // Get initial tool list
        let initial_tools = server.service().list_tools(None, ctx.clone()).await;
        assert!(initial_tools.is_ok());
        let initial_result = initial_tools.unwrap();
        let initial_count = initial_result.tools.len();

        // Call add_tool
        let add_tool_request = CallToolRequestParam {
            name: std::borrow::Cow::Borrowed("tool_list_changed_plugin-add_tool"),
            arguments: Some(serde_json::Map::new()),
        };

        let result = server
            .service()
            .call_tool(add_tool_request, ctx.clone())
            .await;
        assert!(
            result.is_ok(),
            "add_tool should succeed. Error: {:?}",
            result.err()
        );

        assert!(client.service().get_tool_list_changed_count() == 1);

        // Get updated tool list
        let ctx2 = create_test_ctx(&server);
        let updated_tools = server.service().list_tools(None, ctx2).await;
        assert!(updated_tools.is_ok());
        let updated_result = updated_tools.unwrap();
        let updated_count = updated_result.tools.len();

        // Verify tool list grew
        assert!(
            updated_count > initial_count,
            "Tool count should increase after add_tool. Initial: {}, Updated: {}",
            initial_count,
            updated_count
        );

        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_notify_tool_list_changed_multiple_additions() {
        let wasm_path = get_tool_list_changed_wasm_path();
        if !test_tool_list_changed_wasm_exists() {
            println!("Skipping test - tool-list-changed WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  tool_list_changed_plugin:
    url: "file://{}"
    runtime_config:
      max_memory_mb: 10
      max_execution_time_ms: 5000
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            TestClient::new(),
        )
        .await;

        // Call add_tool three times
        for i in 1..=3 {
            let ctx = create_test_ctx(&server);
            let add_tool_request = CallToolRequestParam {
                name: std::borrow::Cow::Borrowed("tool_list_changed_plugin-add_tool"),
                arguments: Some(serde_json::Map::new()),
            };

            let result = server.service().call_tool(add_tool_request, ctx).await;
            assert!(result.is_ok(), "add_tool call {} should succeed", i);
        }

        assert!(client.service().get_tool_list_changed_count() == 3);

        // Get final tool list
        let ctx = create_test_ctx(&server);
        let final_tools = server.service().list_tools(None, ctx).await;
        assert!(final_tools.is_ok());

        let final_result = final_tools.unwrap();
        let tool_names: Vec<String> = final_result
            .tools
            .iter()
            .map(|t| t.name.to_string())
            .collect();

        // Verify all three tools exist
        assert!(
            tool_names.contains(&"tool_list_changed_plugin-tool_1".to_string()),
            "tool_1 should exist in tool list"
        );
        assert!(
            tool_names.contains(&"tool_list_changed_plugin-tool_2".to_string()),
            "tool_2 should exist in tool list"
        );
        assert!(
            tool_names.contains(&"tool_list_changed_plugin-tool_3".to_string()),
            "tool_3 should exist in tool list"
        );

        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_notify_tool_list_changed_tool_callable_after_add() {
        let wasm_path = get_tool_list_changed_wasm_path();
        if !test_tool_list_changed_wasm_exists() {
            println!("Skipping test - tool-list-changed WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  tool_list_changed_plugin:
    url: "file://{}"
    runtime_config:
      max_memory_mb: 10
      max_execution_time_ms: 5000
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;

        // Add a tool
        let ctx = create_test_ctx(&server);
        let add_tool_request = CallToolRequestParam {
            name: std::borrow::Cow::Borrowed("tool_list_changed_plugin-add_tool"),
            arguments: Some(serde_json::Map::new()),
        };

        let result = server.service().call_tool(add_tool_request, ctx).await;
        assert!(result.is_ok(), "add_tool should succeed");

        // Call the newly created tool_1
        let ctx2 = create_test_ctx(&server);
        let tool_request = CallToolRequestParam {
            name: std::borrow::Cow::Borrowed("tool_list_changed_plugin-tool_1"),
            arguments: Some(serde_json::Map::new()),
        };

        let result = server.service().call_tool(tool_request, ctx2).await;
        assert!(result.is_ok(), "tool_1 should be callable after creation");

        let response = result.unwrap();
        assert!(!response.content.is_empty(), "tool_1 should return content");

        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_notify_tool_list_changed_response_format() {
        let wasm_path = get_tool_list_changed_wasm_path();
        if !test_tool_list_changed_wasm_exists() {
            println!("Skipping test - tool-list-changed WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  tool_list_changed_plugin:
    url: "file://{}"
    runtime_config:
      max_memory_mb: 10
      max_execution_time_ms: 5000
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;
        let ctx = create_test_ctx(&server);

        // Call add_tool and verify response format
        let add_tool_request = CallToolRequestParam {
            name: std::borrow::Cow::Borrowed("tool_list_changed_plugin-add_tool"),
            arguments: Some(serde_json::Map::new()),
        };

        let result = server.service().call_tool(add_tool_request, ctx).await;
        assert!(result.is_ok());

        let response = result.unwrap();
        assert!(!response.content.is_empty(), "Response should have content");

        // Just verify that we got content back - the content structure is handled by rmcp
        assert_eq!(
            response.is_error,
            Some(false),
            "Response should not be an error"
        );

        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_notify_tool_list_changed_sequential_tool_numbers() {
        let wasm_path = get_tool_list_changed_wasm_path();
        if !test_tool_list_changed_wasm_exists() {
            println!("Skipping test - tool-list-changed WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  tool_list_changed_plugin:
    url: "file://{}"
    runtime_config:
      max_memory_mb: 10
      max_execution_time_ms: 5000
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;

        // Add 5 tools and verify tool_count in responses
        for expected_count in 1..=5 {
            let ctx = create_test_ctx(&server);
            let add_tool_request = CallToolRequestParam {
                name: std::borrow::Cow::Borrowed("tool_list_changed_plugin-add_tool"),
                arguments: Some(serde_json::Map::new()),
            };

            let result = server.service().call_tool(add_tool_request, ctx).await;
            assert!(result.is_ok());

            let response = result.unwrap();
            // Verify response indicates success
            assert_eq!(
                response.is_error,
                Some(false),
                "add_tool call {} should succeed",
                expected_count
            );
        }

        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_notify_tool_list_changed_invalid_tool_call() {
        let wasm_path = get_tool_list_changed_wasm_path();
        if !test_tool_list_changed_wasm_exists() {
            println!("Skipping test - tool-list-changed WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  tool_list_changed_plugin:
    url: "file://{}"
    runtime_config:
      max_memory_mb: 10
      max_execution_time_ms: 5000
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;

        // Try to call a tool that doesn't exist yet (tool_5 when only tool_1 exists)
        let ctx = create_test_ctx(&server);
        let invalid_tool_request = CallToolRequestParam {
            name: std::borrow::Cow::Borrowed("tool_list_changed_plugin-tool_5"),
            arguments: Some(serde_json::Map::new()),
        };

        let result = server.service().call_tool(invalid_tool_request, ctx).await;
        assert!(
            result.is_ok(),
            "Tool call should complete, but indicate error"
        );

        let response = result.unwrap();
        assert!(
            response.is_error == Some(true),
            "Calling non-existent tool should return error"
        );

        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_notify_tool_list_changed_add_tool_failure_propagates() {
        let wasm_path = get_tool_list_changed_wasm_path();
        if !test_tool_list_changed_wasm_exists() {
            println!("Skipping test - tool-list-changed WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  tool_list_changed_plugin:
    url: "file://{}"
    runtime_config:
      max_memory_mb: 10
      max_execution_time_ms: 5000
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;

        // Call add_tool with additional arguments (should still work but they're ignored)
        let ctx = create_test_ctx(&server);
        let mut args = serde_json::Map::new();
        args.insert(
            "extra_param".to_string(),
            serde_json::Value::String("should_be_ignored".to_string()),
        );

        let add_tool_request = CallToolRequestParam {
            name: std::borrow::Cow::Borrowed("tool_list_changed_plugin-add_tool"),
            arguments: Some(args),
        };

        let result = server.service().call_tool(add_tool_request, ctx).await;
        assert!(
            result.is_ok(),
            "add_tool should succeed even with extra params"
        );

        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_notify_tool_list_changed_new_tools_appear_in_list() {
        let wasm_path = get_tool_list_changed_wasm_path();
        if !test_tool_list_changed_wasm_exists() {
            println!("Skipping test - tool-list-changed WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  tool_list_changed_plugin:
    url: "file://{}"
    runtime_config:
      max_memory_mb: 10
      max_execution_time_ms: 5000
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;

        // Get initial tools
        let ctx = create_test_ctx(&server);
        let initial_result = server.service().list_tools(None, ctx).await;
        assert!(initial_result.is_ok());
        let initial_tools = initial_result.unwrap();
        let initial_names: Vec<String> = initial_tools
            .tools
            .iter()
            .map(|t| t.name.to_string())
            .collect();

        // Verify tool_1 doesn't exist yet
        assert!(
            !initial_names.contains(&"tool_list_changed_plugin-tool_1".to_string()),
            "tool_1 should not exist initially"
        );

        // Add tool_1
        let ctx = create_test_ctx(&server);
        let add_tool_request = CallToolRequestParam {
            name: std::borrow::Cow::Borrowed("tool_list_changed_plugin-add_tool"),
            arguments: Some(serde_json::Map::new()),
        };
        let _ = server.service().call_tool(add_tool_request, ctx).await;

        // Get updated tools
        let ctx = create_test_ctx(&server);
        let updated_result = server.service().list_tools(None, ctx).await;
        assert!(updated_result.is_ok());
        let updated_tools = updated_result.unwrap();
        let updated_names: Vec<String> = updated_tools
            .tools
            .iter()
            .map(|t| t.name.to_string())
            .collect();

        // Verify tool_1 exists now
        assert!(
            updated_names.contains(&"tool_list_changed_plugin-tool_1".to_string()),
            "tool_1 should exist after add_tool"
        );

        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_notify_tool_list_changed_tool_descriptions() {
        let wasm_path = get_tool_list_changed_wasm_path();
        if !test_tool_list_changed_wasm_exists() {
            println!("Skipping test - tool-list-changed WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  tool_list_changed_plugin:
    url: "file://{}"
    runtime_config:
      max_memory_mb: 10
      max_execution_time_ms: 5000
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;

        // Add two tools
        for _ in 0..2 {
            let ctx = create_test_ctx(&server);
            let add_tool_request = CallToolRequestParam {
                name: std::borrow::Cow::Borrowed("tool_list_changed_plugin-add_tool"),
                arguments: Some(serde_json::Map::new()),
            };
            let _ = server.service().call_tool(add_tool_request, ctx).await;
        }

        // Get tool list and verify descriptions
        let ctx = create_test_ctx(&server);
        let result = server.service().list_tools(None, ctx).await;
        assert!(result.is_ok());

        let tools = result.unwrap();
        let tool_map: std::collections::HashMap<String, &Tool> = tools
            .tools
            .iter()
            .map(|t| (t.name.to_string(), t))
            .collect();

        // Verify tool descriptions exist and are meaningful
        if let Some(add_tool) = tool_map.get("tool_list_changed_plugin-add_tool") {
            if let Some(desc) = &add_tool.description {
                assert!(!desc.is_empty(), "add_tool should have a description");
                assert!(
                    desc.to_lowercase().contains("add"),
                    "add_tool description should mention 'add'"
                );
            }
        }

        if let Some(tool_1) = tool_map.get("tool_list_changed_plugin-tool_1") {
            if let Some(desc) = &tool_1.description {
                assert!(!desc.is_empty(), "tool_1 should have a description");
                assert!(
                    desc.to_lowercase().contains("tool"),
                    "tool_1 description should mention 'tool'"
                );
            }
        }

        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    // Comprehensive tests for rstime v2 plugin

    #[tokio::test]
    async fn test_rstime_list_tools() {
        let wasm_path = get_rstime_wasm_path();
        if !test_rstime_wasm_exists() {
            println!("Skipping test - WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  rstime:
    url: "file://{}"
    runtime_config:
      allowed_hosts:
        - "www.timezoneconverter.com"
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;

        let Some(plugins) = server.service().plugins.get() else {
            panic!("Plugins should be initialized");
        };
        assert!(!plugins.is_empty(), "Should have loaded rstime plugin");

        let request = None;
        let ctx = create_test_ctx(&server);
        let result = server.service().list_tools(request, ctx).await;
        assert!(result.is_ok(), "list_tools should succeed");

        let list_tools_result = result.unwrap();
        assert!(
            !list_tools_result.tools.is_empty(),
            "Should have tools from rstime plugin"
        );

        // Verify expected tools: get_time and parse_time
        let tool_names: Vec<String> = list_tools_result
            .tools
            .iter()
            .map(|tool| tool.name.to_string())
            .collect();

        assert!(
            tool_names.iter().any(|name| name.contains("get_time")),
            "Should have get_time tool"
        );
        assert!(
            tool_names.iter().any(|name| name.contains("parse_time")),
            "Should have parse_time tool"
        );

        // Verify tool properties
        for tool in &list_tools_result.tools {
            assert!(!tool.name.is_empty(), "Tool should have a name");
            assert!(tool.description.is_some(), "Tool should have a description");
            // Just verify the tool exists, schema validation happens at plugin level
        }

        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_rstime_list_prompts() {
        let wasm_path = get_rstime_wasm_path();
        if !test_rstime_wasm_exists() {
            println!("Skipping test - WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  rstime:
    url: "file://{}"
    runtime_config:
      allowed_hosts:
        - "www.timezoneconverter.com"
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;

        let Some(plugins) = server.service().plugins.get() else {
            panic!("Plugins should be initialized");
        };
        assert!(!plugins.is_empty(), "Should have loaded rstime plugin");

        let request = None;
        let ctx = create_test_ctx(&server);
        let result = server.service().list_prompts(request, ctx).await;
        assert!(result.is_ok(), "list_prompts should succeed");

        let list_prompts_result = result.unwrap();
        assert!(
            !list_prompts_result.prompts.is_empty(),
            "Should have prompts from rstime plugin"
        );

        // Verify the get_time_with_timezone prompt exists
        let prompt_names: Vec<String> = list_prompts_result
            .prompts
            .iter()
            .map(|p| p.name.to_string())
            .collect();

        assert!(
            prompt_names
                .iter()
                .any(|name| name.contains("get_time_with_timezone")),
            "Should have get_time_with_timezone prompt"
        );

        // Verify prompt properties
        for prompt in &list_prompts_result.prompts {
            assert!(!prompt.name.is_empty(), "Prompt should have a name");
            assert!(
                prompt.description.is_some(),
                "Prompt should have a description"
            );
            assert!(prompt.arguments.is_some(), "Prompt should have arguments");
        }

        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_rstime_list_resource_templates() {
        let wasm_path = get_rstime_wasm_path();
        if !test_rstime_wasm_exists() {
            println!("Skipping test - WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  rstime:
    url: "file://{}"
    runtime_config:
      allowed_hosts:
        - "www.timezoneconverter.com"
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;

        let Some(plugins) = server.service().plugins.get() else {
            panic!("Plugins should be initialized");
        };
        assert!(!plugins.is_empty(), "Should have loaded rstime plugin");

        let request = None;
        let ctx = create_test_ctx(&server);
        let result = server.service().list_resource_templates(request, ctx).await;
        assert!(result.is_ok(), "list_resource_templates should succeed");

        let list_templates_result = result.unwrap();
        assert!(
            !list_templates_result.resource_templates.is_empty(),
            "Should have resource templates from rstime plugin"
        );

        // Verify the time_zone_converter template exists
        let template_names: Vec<String> = list_templates_result
            .resource_templates
            .iter()
            .map(|t| t.name.to_string())
            .collect();

        assert!(
            template_names
                .iter()
                .any(|name| name.contains("time_zone_converter")),
            "Should have time_zone_converter resource template"
        );

        // Verify template properties
        for template in &list_templates_result.resource_templates {
            assert!(!template.name.is_empty(), "Template should have a name");
            assert!(
                template.description.is_some(),
                "Template should have a description"
            );
            assert!(
                template.uri_template.contains("{timezone}"),
                "Template should have URI template with timezone placeholder"
            );
            assert!(
                template.mime_type.is_some(),
                "Template should have a MIME type"
            );
        }

        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_rstime_list_resources() {
        let wasm_path = get_rstime_wasm_path();
        if !test_rstime_wasm_exists() {
            println!("Skipping test - WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  rstime:
    url: "file://{}"
    runtime_config:
      allowed_hosts:
        - "www.timezoneconverter.com"
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;

        let Some(plugins) = server.service().plugins.get() else {
            panic!("Plugins should be initialized");
        };
        assert!(!plugins.is_empty(), "Should have loaded rstime plugin");

        let request = None;
        let ctx = create_test_ctx(&server);
        let result = server.service().list_resources(request, ctx).await;
        assert!(result.is_ok(), "list_resources should succeed");

        let list_resources_result = result.unwrap();
        // rstime plugin returns empty resources list, which is expected
        assert_eq!(
            list_resources_result.resources.len(),
            0,
            "rstime should return empty resources"
        );

        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_rstime_call_get_time_tool() {
        let wasm_path = get_rstime_wasm_path();
        if !test_rstime_wasm_exists() {
            println!("Skipping test - WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  rstime:
    url: "file://{}"
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;

        // Test calling get_time with UTC (default)
        let request = CallToolRequestParam {
            name: std::borrow::Cow::Owned("rstime-get_time".to_string()),
            arguments: None,
        };

        let ctx = create_test_ctx(&server);
        let result = server.service().call_tool(request, ctx).await;
        assert!(
            result.is_ok(),
            "Should successfully call get_time tool: {result:?}"
        );

        let call_result = result.unwrap();
        assert!(
            !call_result.content.is_empty(),
            "get_time should return content"
        );

        // Verify structured content contains current_time
        assert!(
            call_result.structured_content.is_some(),
            "Should have structured content"
        );

        let structured = call_result.structured_content.unwrap();
        let has_current_time = if let Some(map) = structured.as_object() {
            map.contains_key("current_time")
        } else {
            false
        };
        assert!(
            has_current_time,
            "Structured content should have current_time field"
        );

        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_rstime_call_get_time_with_timezone() {
        let wasm_path = get_rstime_wasm_path();
        if !test_rstime_wasm_exists() {
            println!("Skipping test - WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  rstime:
    url: "file://{}"
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;

        // Test calling get_time with a specific timezone
        let mut args = serde_json::Map::new();
        args.insert(
            "timezone".to_string(),
            serde_json::Value::String("America/New_York".to_string()),
        );

        let request = CallToolRequestParam {
            name: std::borrow::Cow::Owned("rstime-get_time".to_string()),
            arguments: Some(args),
        };

        let ctx = create_test_ctx(&server);
        let result = server.service().call_tool(request, ctx).await;
        assert!(
            result.is_ok(),
            "Should successfully call get_time with timezone: {result:?}"
        );

        let call_result = result.unwrap();
        assert!(
            !call_result.content.is_empty(),
            "get_time with timezone should return content"
        );
        assert!(
            call_result.structured_content.is_some(),
            "Should have structured content"
        );

        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_rstime_call_parse_time_tool() {
        let wasm_path = get_rstime_wasm_path();
        if !test_rstime_wasm_exists() {
            println!("Skipping test - WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  rstime:
    url: "file://{}"
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;

        // Test calling parse_time with a valid RFC2822 timestamp
        let mut args = serde_json::Map::new();
        args.insert(
            "time".to_string(),
            serde_json::Value::String("Wed, 18 Feb 2015 23:16:09 GMT".to_string()),
        );

        let request = CallToolRequestParam {
            name: std::borrow::Cow::Owned("rstime-parse_time".to_string()),
            arguments: Some(args),
        };

        let ctx = create_test_ctx(&server);
        let result = server.service().call_tool(request, ctx).await;
        assert!(
            result.is_ok(),
            "Should successfully call parse_time tool: {result:?}"
        );

        let call_result = result.unwrap();
        assert!(
            !call_result.content.is_empty(),
            "parse_time should return content"
        );

        // Verify it parsed correctly and returned a timestamp
        assert!(
            call_result.structured_content.is_some(),
            "Should have structured content"
        );

        let structured = call_result.structured_content.unwrap();
        let has_timestamp = if let Some(map) = structured.as_object() {
            map.contains_key("timestamp")
        } else {
            false
        };
        assert!(
            has_timestamp,
            "Structured content should have timestamp field"
        );

        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_rstime_call_parse_time_invalid() {
        let wasm_path = get_rstime_wasm_path();
        if !test_rstime_wasm_exists() {
            println!("Skipping test - WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  rstime:
    url: "file://{}"
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;

        // Test calling parse_time with invalid timestamp
        let mut args = serde_json::Map::new();
        args.insert(
            "time".to_string(),
            serde_json::Value::String("invalid timestamp".to_string()),
        );

        let request = CallToolRequestParam {
            name: std::borrow::Cow::Owned("rstime-parse_time".to_string()),
            arguments: Some(args),
        };

        let ctx = create_test_ctx(&server);
        let result = server.service().call_tool(request, ctx).await;
        assert!(
            result.is_ok(),
            "Should return result (may indicate error in content)"
        );

        let call_result = result.unwrap();
        // Tool returns error flag when parsing fails
        assert_eq!(
            call_result.is_error,
            Some(true),
            "Should mark result as error for invalid input"
        );

        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_rstime_get_prompt() {
        let wasm_path = get_rstime_wasm_path();
        if !test_rstime_wasm_exists() {
            println!("Skipping test - WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  rstime:
    url: "file://{}"
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;

        // Test getting the prompt without timezone argument
        let request = GetPromptRequestParam {
            name: "rstime-get_time_with_timezone".to_string(),
            arguments: None,
        };

        let ctx = create_test_ctx(&server);
        let result = server.service().get_prompt(request, ctx).await;
        assert!(result.is_ok(), "Should successfully get prompt: {result:?}");

        let prompt_result = result.unwrap();
        assert!(
            !prompt_result.messages.is_empty(),
            "Prompt should have messages"
        );

        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_rstime_get_prompt_with_timezone() {
        let wasm_path = get_rstime_wasm_path();
        if !test_rstime_wasm_exists() {
            println!("Skipping test - WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  rstime:
    url: "file://{}"
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;

        // Test getting the prompt with timezone argument
        let mut args = serde_json::Map::new();
        args.insert(
            "timezone".to_string(),
            serde_json::Value::String("Europe/London".to_string()),
        );

        let request = GetPromptRequestParam {
            name: "rstime-get_time_with_timezone".to_string(),
            arguments: Some(args),
        };

        let ctx = create_test_ctx(&server);
        let result = server.service().get_prompt(request, ctx).await;
        assert!(
            result.is_ok(),
            "Should successfully get prompt with timezone: {result:?}"
        );

        let prompt_result = result.unwrap();
        assert!(
            !prompt_result.messages.is_empty(),
            "Prompt should have messages"
        );

        // Verify description mentions the timezone
        assert!(
            prompt_result.description.is_some(),
            "Prompt should have description"
        );
        let desc = prompt_result.description.unwrap();
        assert!(
            desc.contains("London"),
            "Prompt description should mention the timezone"
        );

        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_rstime_read_resource() {
        let wasm_path = get_rstime_wasm_path();
        if !test_rstime_wasm_exists() {
            println!("Skipping test - WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  rstime:
    url: "file://{}"
    runtime_config:
      allowed_hosts:
        - "www.timezoneconverter.com"
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;

        // Test reading a resource with timezone - use namespaced URI
        // Format: scheme://host/plugin-name/path?query (as created by create_namespaced_uri)
        // Test reading a resource with timezone
        // Resource URIs are namespaced with plugin name inserted into the path
        // Format: scheme://host/plugin-name/rest-of-path
        // With allowed_hosts configured, the plugin can make HTTP requests
        let request = ReadResourceRequestParam {
            uri: "https://www.timezoneconverter.com/rstime/cgi-bin/zoneinfo?tz=America/New_York"
                .to_string(),
        };

        let ctx = create_test_ctx(&server);
        let result = server.service().read_resource(request, ctx).await;
        // With allowed_hosts configured, the plugin should be able to fetch the resource
        match result {
            Ok(read_result) => {
                // If successful, verify we got contents
                assert!(
                    !read_result.contents.is_empty(),
                    "Should return resource contents from HTTP response"
                );
            }
            Err(e) => {
                // If there's an error (e.g., network unavailable in test env),
                // at least verify it's a reasonable error and not a parsing error
                let error_msg = e.message.to_lowercase();
                assert!(
                    !error_msg.contains("parse"),
                    "Should not have parsing errors with allowed_hosts: {:?}",
                    e.message
                );
            }
        }

        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_rstime_complete_prompt_timezone() {
        let wasm_path = get_rstime_wasm_path();
        if !test_rstime_wasm_exists() {
            println!("Skipping test - WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  rstime:
    url: "file://{}"
    runtime_config:
      allowed_hosts:
        - "www.timezoneconverter.com"
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;

        // Test calling the complete() function for prompt timezone argument
        let argument_info = ArgumentInfo {
            name: "timezone".to_string(),
            value: "Ame".to_string(),
        };

        let complete_request = CompleteRequestParam {
            r#ref: Reference::Prompt(PromptReference {
                name: "rstime-get_time_with_timezone".to_string(),
                title: None,
            }),
            argument: argument_info,
            context: Some(CompletionContext {
                arguments: Some(HashMap::new()),
            }),
        };

        let ctx = create_test_ctx(&server);
        let result = server.service().complete(complete_request, ctx).await;
        assert!(
            result.is_ok(),
            "Should successfully call complete() for prompt timezone: {result:?}"
        );

        let complete_result = result.unwrap();
        // Verify completion results include timezone suggestions
        assert!(
            !complete_result.completion.values.is_empty(),
            "Completion should return timezone suggestions"
        );

        // Verify we get timezone suggestions starting with "Ame"
        let suggestions: Vec<String> = complete_result
            .completion
            .values
            .iter()
            .map(|v| v.to_string())
            .collect();

        assert!(
            suggestions
                .iter()
                .any(|s| s.contains("America") || s.contains("ame")),
            "Should suggest timezones matching 'Ame' pattern: {suggestions:?}"
        );

        // Verify completion metadata
        assert!(
            complete_result.completion.total.unwrap_or(0) > 0,
            "Completion should have total count > 0"
        );

        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }

    #[tokio::test]
    async fn test_rstime_complete_resource_template_timezone() {
        let wasm_path = get_rstime_wasm_path();
        if !test_rstime_wasm_exists() {
            println!("Skipping test - WASM file not found at {wasm_path:?}");
            return;
        }

        let config_content = format!(
            r#"
plugins:
  rstime:
    url: "file://{}"
    runtime_config:
      allowed_hosts:
        - "www.timezoneconverter.com"
"#,
            wasm_path.display()
        );

        let (_temp_dir, config_path) = create_temp_config_file(&config_content).await.unwrap();
        let mut cli = create_test_cli();
        cli.config_file = Some(config_path);
        let config = load_config(&cli).await.unwrap();

        let (server, client) = create_test_pair(
            PluginService::new(&config).await.unwrap(),
            ClientInfo::default(),
        )
        .await;

        // First verify that resource templates exist and have proper structure
        let list_ctx = create_test_ctx(&server);
        let list_result = server
            .service()
            .list_resource_templates(None, list_ctx)
            .await;
        assert!(
            list_result.is_ok(),
            "Should successfully list resource templates"
        );

        let templates = list_result.unwrap();
        assert!(
            !templates.resource_templates.is_empty(),
            "Should have resource templates available"
        );

        // Verify the time_zone_converter template exists with proper URI template
        let tz_template = templates
            .resource_templates
            .iter()
            .find(|t| t.name.contains("time_zone_converter"))
            .expect("Should have time_zone_converter resource template");

        assert!(
            tz_template.uri_template.contains("{timezone}"),
            "Resource template should have timezone parameter placeholder"
        );

        // Now test calling the complete() function for resource template timezone parameter
        // Use the namespaced URI format with plugin name inserted
        let resource_uri =
            "https://www.timezoneconverter.com/rstime/cgi-bin/zoneinfo?tz=Eur".to_string();

        let argument_info = ArgumentInfo {
            name: "timezone".to_string(),
            value: "Eur".to_string(),
        };

        let complete_request = CompleteRequestParam {
            r#ref: Reference::Resource(ResourceReference { uri: resource_uri }),
            argument: argument_info,
            context: None,
        };

        let ctx = create_test_ctx(&server);
        let result = server.service().complete(complete_request, ctx).await;

        // The rstime plugin may not implement completion for resource URIs,
        // so we verify the interface works even if completion isn't supported
        match result {
            Ok(complete_result) => {
                // If completion is supported, verify results
                assert!(
                    !complete_result.completion.values.is_empty(),
                    "Completion should return timezone suggestions for resource template"
                );

                let suggestions: Vec<String> = complete_result
                    .completion
                    .values
                    .iter()
                    .map(|v| v.to_string())
                    .collect();

                assert!(
                    suggestions
                        .iter()
                        .any(|s| s.contains("Europe") || s.contains("eur")),
                    "Should suggest timezones matching 'Eur' pattern: {suggestions:?}"
                );

                assert!(
                    complete_result.completion.total.unwrap_or(0) > 0,
                    "Completion should have total count > 0 for resource templates"
                );
            }
            Err(e) => {
                // If resource completion is not implemented, that's acceptable
                // The important part is that the complete() method was called successfully
                let error_msg = e.message.to_lowercase();
                assert!(
                    error_msg.contains("not implemented") || error_msg.contains("completion"),
                    "If completion fails for resources, it should be a clear error: {}",
                    e.message
                );
            }
        }

        assert_ok!(server.cancel().await);
        assert_ok!(client.cancel().await);
    }
}

```
Page 7/8FirstPrevNextLast