| 1 | use { |
| 2 | serde::Deserialize, |
| 3 | std::env, |
| 4 | zed_extension_api::{ |
| 5 | self as zed, serde_json, settings::ContextServerSettings, Command, ContextServerId, |
| 6 | ContextServerConfiguration, Project, Result, |
| 7 | }, |
| 8 | }; |
| 9 | |
| 10 | /// The npm package that ships the `smb` CLI. Running it with `--mcp` turns it |
| 11 | /// into an MCP server over stdio instead of a one-shot command. |
| 12 | const PACKAGE_NAME: &str = "@smbcloud/cli"; |
| 13 | const SERVER_PATH: &str = "node_modules/@smbcloud/cli/lib/index.js"; |
| 14 | |
| 15 | /// User-facing settings, read from the `settings` object of the |
| 16 | /// `context_servers.smbcloud` entry in Zed's settings. |
| 17 | #[derive(Debug, Default, Deserialize)] |
| 18 | struct SmbcloudSettings { |
| 19 | /// Tool profile: `cloud` (default) or `automation`. |
| 20 | scope: Option<String>, |
| 21 | /// API environment: `production` (default) or `dev`. |
| 22 | environment: Option<String>, |
| 23 | /// Mail app API key (`smb_mail_...`), only needed by the `mail_send` tool. |
| 24 | mail_api_key: Option<String>, |
| 25 | } |
| 26 | |
| 27 | struct SmbcloudExtension; |
| 28 | |
| 29 | impl SmbcloudExtension { |
| 30 | fn server_args(settings: &SmbcloudSettings) -> Vec<String> { |
| 31 | let mut args = vec!["--mcp".to_string()]; |
| 32 | if let Some(scope) = &settings.scope { |
| 33 | args.push("--scope".into()); |
| 34 | args.push(scope.clone()); |
| 35 | } |
| 36 | if let Some(environment) = &settings.environment { |
| 37 | args.push("--environment".into()); |
| 38 | args.push(environment.clone()); |
| 39 | } |
| 40 | args |
| 41 | } |
| 42 | |
| 43 | fn server_env(settings: &SmbcloudSettings) -> Vec<(String, String)> { |
| 44 | settings |
| 45 | .mail_api_key |
| 46 | .iter() |
| 47 | .filter(|key| !key.is_empty()) |
| 48 | .map(|key| ("SMB_MAIL_API_KEY".to_string(), key.clone())) |
| 49 | .collect() |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | impl zed::Extension for SmbcloudExtension { |
| 54 | fn new() -> Self { |
| 55 | Self |
| 56 | } |
| 57 | |
| 58 | fn context_server_command( |
| 59 | &mut self, |
| 60 | context_server_id: &ContextServerId, |
| 61 | project: &Project, |
| 62 | ) -> Result<Command> { |
| 63 | let settings = ContextServerSettings::for_project(context_server_id.as_ref(), project)?; |
| 64 | let options: SmbcloudSettings = settings |
| 65 | .settings |
| 66 | .map(serde_json::from_value) |
| 67 | .transpose() |
| 68 | .map_err(|err| format!("invalid `settings` for the smbCloud MCP server: {err}"))? |
| 69 | .unwrap_or_default(); |
| 70 | |
| 71 | let args = Self::server_args(&options); |
| 72 | let mut env_vars = Self::server_env(&options); |
| 73 | |
| 74 | // An explicit `command.path` wins: it lets people point at an `smb` |
| 75 | // installed via Homebrew, cargo, or the install script instead of |
| 76 | // pulling the npm package. |
| 77 | if let Some(command) = settings.command { |
| 78 | if let Some(path) = command.path.filter(|path| !path.is_empty()) { |
| 79 | env_vars.extend(command.env.into_iter().flatten()); |
| 80 | return Ok(Command { |
| 81 | command: path, |
| 82 | args: command.arguments.unwrap_or(args), |
| 83 | env: env_vars, |
| 84 | }); |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | let latest_version = zed::npm_package_latest_version(PACKAGE_NAME)?; |
| 89 | if zed::npm_package_installed_version(PACKAGE_NAME)?.as_deref() |
| 90 | != Some(latest_version.as_str()) |
| 91 | { |
| 92 | zed::npm_install_package(PACKAGE_NAME, &latest_version)?; |
| 93 | } |
| 94 | |
| 95 | // The npm entrypoint is a shim that spawns the platform binary from |
| 96 | // `optionalDependencies` with inherited stdio, so it speaks MCP fine. |
| 97 | let server_path = env::current_dir() |
| 98 | .map(|dir| dir.join(SERVER_PATH).to_string_lossy().into_owned()) |
| 99 | .unwrap_or_else(|_| SERVER_PATH.to_string()); |
| 100 | |
| 101 | Ok(Command { |
| 102 | command: zed::node_binary_path()?, |
| 103 | args: std::iter::once(server_path).chain(args).collect(), |
| 104 | env: env_vars, |
| 105 | }) |
| 106 | } |
| 107 | |
| 108 | fn context_server_configuration( |
| 109 | &mut self, |
| 110 | _context_server_id: &ContextServerId, |
| 111 | _project: &Project, |
| 112 | ) -> Result<Option<ContextServerConfiguration>> { |
| 113 | Ok(Some(ContextServerConfiguration { |
| 114 | installation_instructions: include_str!("../configuration/installation_instructions.md") |
| 115 | .to_string(), |
| 116 | settings_schema: include_str!("../configuration/settings_schema.json").to_string(), |
| 117 | default_settings: include_str!("../configuration/default_settings.jsonc").to_string(), |
| 118 | })) |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | zed::register_extension!(SmbcloudExtension); |