@smbcloud / cli / commits / 212a6d3

Add project_list, project_show, and deployments MCP tools

Expand the MCP server beyond the `me` slice with the read-only project and deployment tools, all reusing the existing library fetch functions: - `project_list` — lists the user's projects as JSON. - `project_show { id }` — a single project by ID (parameterized tool). - `deployments { project_id }` — a project's deployments as JSON. Auth handling is factored into `SmbMcpServer::access_token`, and a shared `json_result` helper serializes any value into a tool result. Parameterized tools use `schemars`-derived arg structs (added as a direct dependency) so `tools/list` advertises correct input schemas. Verified over stdio: `tools/list` shows all four tools with their required arguments, and a parameterized `project_show` call deserializes and dispatches correctly. CI gate (fmt, clippy -D warnings, tests) is green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019fkbx6fUq1uDR5N75Ph5zi

Claude committed Jul 17, 2026 at 12:56 UTC 212a6d32d2d358d6945eac7609b226f6820b0a4a
4 files changed +74 -11
Cargo.lock
+1
@@ -2605,6 +2605,7 @@ dependencies = [
2605 "regex",
2606 "reqwest",
2607 "rmcp",
2608 + "schemars",
2609 "serde",
2610 "serde_json",
2611 "serde_repr",
Cargo.toml
+1
@@ -24,6 +24,7 @@ ratatui = { version = "0.29", default-features = false, features = ["crossterm"]
24 regex = "1.11"
25 reqwest = { version = "0.12.18", default-features = false }
26 rmcp = { version = "2.2.0", features = ["server", "macros", "transport-io", "schemars"] }
27 +schemars = "1"
28 serde = "1"
29 serde_json = "1.0.82"
30 serde_repr = "0.1"
crates/cli/Cargo.toml
+1
@@ -41,6 +41,7 @@ ratatui = { workspace = true }
41 regex = { workspace = true }
42 reqwest = { workspace = true, features = ["json", "rustls-tls-native-roots"] }
43 rmcp = { workspace = true }
44 +schemars = { workspace = true }
45 serde = { workspace = true, features = ["derive"] }
46 serde_json = { workspace = true }
47 serde_repr = { workspace = true }
crates/cli/src/mcp.rs
+71 -11
@@ -13,14 +13,20 @@ use {
13 crate::{account::lib::is_logged_in, client, token::get_smb_token::get_smb_token},
14 anyhow::{anyhow, Result},
15 rmcp::{
16 - handler::server::ServerHandler,
16 + handler::server::{wrapper::Parameters, ServerHandler},
17 model::{CallToolResult, ContentBlock, Implementation, ServerCapabilities, ServerInfo},
18 tool, tool_handler, tool_router,
19 transport::stdio,
20 ErrorData, ServiceExt,
21 },
22 + schemars::JsonSchema,
23 + serde::Deserialize,
24 smbcloud_auth::me::me,
25 smbcloud_network::environment::Environment,
26 + smbcloud_networking_project::{
27 + crud_project_deployment_read::get_deployments, crud_project_read::get_project,
28 + crud_project_read::get_projects,
29 + },
30 };
31
32 /// The smbCloud MCP server. Holds the environment selected on the command line
@@ -33,26 +39,80 @@ impl SmbMcpServer {
39 pub fn new(environment: Environment) -> Self {
40 Self { environment }
41 }
36 -}
42
38 -#[tool_router]
39 -impl SmbMcpServer {
40 - #[tool(description = "Get the authenticated smbCloud user's account info. \
41 - Requires a prior `smb login`; returns the user as JSON.")]
42 - async fn me(&self) -> Result<CallToolResult, ErrorData> {
43 + /// Resolve the stored auth token, mapping "not logged in" and read failures
44 + /// to MCP errors. Every tool that hits the API goes through this.
45 + fn access_token(&self) -> Result<String, ErrorData> {
46 if !is_logged_in(self.environment) {
47 return Err(ErrorData::invalid_request(
48 "Not logged in. Run `smb login` first.",
49 None,
50 ));
51 }
49 - let token = get_smb_token(self.environment)
50 - .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
52 + get_smb_token(self.environment).map_err(|e| ErrorData::internal_error(e.to_string(), None))
53 + }
54 +}
55 +
56 +/// Serialize any JSON-able value into a single-content successful tool result.
57 +fn json_result<T: serde::Serialize>(value: &T) -> Result<CallToolResult, ErrorData> {
58 + Ok(CallToolResult::success(vec![ContentBlock::json(value)?]))
59 +}
60 +
61 +#[derive(Debug, Deserialize, JsonSchema)]
62 +struct ProjectShowArgs {
63 + /// The project ID to show.
64 + id: String,
65 +}
66 +
67 +#[derive(Debug, Deserialize, JsonSchema)]
68 +struct DeploymentsArgs {
69 + /// The project ID whose deployments to list.
70 + project_id: i32,
71 +}
72 +
73 +#[tool_router]
74 +impl SmbMcpServer {
75 + #[tool(description = "Get the authenticated smbCloud user's account info. \
76 + Requires a prior `smb login`; returns the user as JSON.")]
77 + async fn me(&self) -> Result<CallToolResult, ErrorData> {
78 + let token = self.access_token()?;
79 let user = me(self.environment, client(), &token)
80 .await
81 .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
54 - let content = ContentBlock::json(&user)?;
55 - Ok(CallToolResult::success(vec![content]))
82 + json_result(&user)
83 + }
84 +
85 + #[tool(description = "List the authenticated user's smbCloud projects as a JSON array.")]
86 + async fn project_list(&self) -> Result<CallToolResult, ErrorData> {
87 + let token = self.access_token()?;
88 + let projects = get_projects(self.environment, client(), token)
89 + .await
90 + .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
91 + json_result(&projects)
92 + }
93 +
94 + #[tool(description = "Show a single smbCloud project by ID, returned as JSON.")]
95 + async fn project_show(
96 + &self,
97 + Parameters(args): Parameters<ProjectShowArgs>,
98 + ) -> Result<CallToolResult, ErrorData> {
99 + let token = self.access_token()?;
100 + let project = get_project(self.environment, client(), token, args.id)
101 + .await
102 + .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
103 + json_result(&project)
104 + }
105 +
106 + #[tool(description = "List deployments for a project by project ID, returned as a JSON array.")]
107 + async fn deployments(
108 + &self,
109 + Parameters(args): Parameters<DeploymentsArgs>,
110 + ) -> Result<CallToolResult, ErrorData> {
111 + let token = self.access_token()?;
112 + let deployments = get_deployments(self.environment, client(), token, args.project_id)
113 + .await
114 + .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
115 + json_result(&deployments)
116 }
117 }
118