main
rs 122 lines 4.35 KB
Raw
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);