@smbcloud / cli / commits / 312fccf

Add project create/update/delete MCP tools

Extend the MCP server with the project write operations, reusing the same networking-project calls the CLI handlers use: - `project_create { name, description? }` — creates a project. - `project_update { id, description }` — updates the description, fetching the project first to preserve its runner (which `update_project` requires). - `project_delete { id }` — deletes a project; destructive and irreversible. Tools are non-interactive by nature, so writes apply immediately; the tool descriptions and docs flag `project_delete` accordingly. Verified over stdio: all seven tools list with correct required-argument schemas and dispatch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019fkbx6fUq1uDR5N75Ph5zi

Claude committed Jul 17, 2026 at 13:18 UTC 312fccfd1042609bc5da60d3f41e82a96bb7b171
2 files changed +96 -1
crates/cli/src/mcp.rs
+89 -1
@@ -22,10 +22,12 @@ use {
22 schemars::JsonSchema,
23 serde::Deserialize,
24 smbcloud_auth::me::me,
25 + smbcloud_model::project::ProjectCreate,
26 smbcloud_network::environment::Environment,
27 smbcloud_networking_project::{
28 + crud_project_create::create_project, crud_project_delete::delete_project,
29 crud_project_deployment_read::get_deployments, crud_project_read::get_project,
28 - crud_project_read::get_projects,
30 + crud_project_read::get_projects, crud_project_update::update_project,
31 },
32 };
33
@@ -70,6 +72,29 @@ struct DeploymentsArgs {
72 project_id: i32,
73 }
74
75 +#[derive(Debug, Deserialize, JsonSchema)]
76 +struct ProjectCreateArgs {
77 + /// Name for the new project.
78 + name: String,
79 + /// Optional description for the new project.
80 + #[serde(default)]
81 + description: String,
82 +}
83 +
84 +#[derive(Debug, Deserialize, JsonSchema)]
85 +struct ProjectUpdateArgs {
86 + /// The project ID to update.
87 + id: String,
88 + /// The new description.
89 + description: String,
90 +}
91 +
92 +#[derive(Debug, Deserialize, JsonSchema)]
93 +struct ProjectDeleteArgs {
94 + /// The project ID to delete.
95 + id: String,
96 +}
97 +
98 #[tool_router]
99 impl SmbMcpServer {
100 #[tool(description = "Get the authenticated smbCloud user's account info. \
@@ -114,6 +139,69 @@ impl SmbMcpServer {
139 .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
140 json_result(&deployments)
141 }
142 +
143 + #[tool(
144 + description = "Create a new smbCloud project with a name and optional description. \
145 + Returns the created project as JSON."
146 + )]
147 + async fn project_create(
148 + &self,
149 + Parameters(args): Parameters<ProjectCreateArgs>,
150 + ) -> Result<CallToolResult, ErrorData> {
151 + let token = self.access_token()?;
152 + let payload = ProjectCreate {
153 + name: args.name,
154 + description: args.description,
155 + };
156 + let project = create_project(self.environment, client(), token, payload)
157 + .await
158 + .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
159 + json_result(&project)
160 + }
161 +
162 + #[tool(
163 + description = "Update a project's description by ID, preserving its runner. \
164 + Returns the updated project as JSON."
165 + )]
166 + async fn project_update(
167 + &self,
168 + Parameters(args): Parameters<ProjectUpdateArgs>,
169 + ) -> Result<CallToolResult, ErrorData> {
170 + let token = self.access_token()?;
171 + // `update_project` requires the runner; fetch the current project so the
172 + // description-only update doesn't clobber it.
173 + let current = get_project(self.environment, client(), token.clone(), args.id.clone())
174 + .await
175 + .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
176 + let project = update_project(
177 + self.environment,
178 + client(),
179 + token,
180 + args.id,
181 + &args.description,
182 + current.runner,
183 + )
184 + .await
185 + .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
186 + json_result(&project)
187 + }
188 +
189 + #[tool(
190 + description = "Delete a project by ID. This is destructive and irreversible — the \
191 + project and its deploy configuration are removed."
192 + )]
193 + async fn project_delete(
194 + &self,
195 + Parameters(args): Parameters<ProjectDeleteArgs>,
196 + ) -> Result<CallToolResult, ErrorData> {
197 + let token = self.access_token()?;
198 + delete_project(self.environment, client(), token, args.id)
199 + .await
200 + .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
201 + Ok(CallToolResult::success(vec![ContentBlock::text(
202 + "Project deleted.",
203 + )]))
204 + }
205 }
206
207 #[tool_handler]
docs/interfaces.md
+7
@@ -63,6 +63,13 @@ starting the server. Tools run non-interactively and return structured JSON.
63 | `project_list` | _(none)_ | The user's projects. |
64 | `project_show` | `id` | A single project by ID. |
65 | `deployments` | `project_id` | A project's deployments. |
66 +| `project_create` | `name`, `description` (optional) | The created project. |
67 +| `project_update` | `id`, `description` | The updated project (runner preserved). |
68 +| `project_delete` | `id` | Confirmation. **Destructive and irreversible.** |
69 +
70 +Tools run non-interactively, so the write tools apply immediately without a
71 +confirmation prompt — the calling client is responsible for confirming intent
72 +before invoking `project_delete`.
73
74 ### Wiring it into an MCP client
75