1use crate::client;
4use crate::envfile;
5use crate::error::{self, ErrorCode, MitosisError};
6use crate::names::{format_duration, parse_duration, parse_wait, validate_name};
7use crate::paths::Home;
8use crate::protocol::{BranchArgs, CloneCreateArgs, CloneInfo, Request, Response};
9use rmcp::handler::server::wrapper::Parameters;
10use rmcp::schemars::JsonSchema;
11use rmcp::{tool, tool_handler, tool_router, ServerHandler, ServiceExt};
12use serde::Deserialize;
13use std::path::PathBuf;
14use std::time::Duration;
15
16#[derive(Deserialize, JsonSchema)]
17#[schemars(crate = "rmcp::schemars")]
18pub struct CloneCreateParams {
19 pub template: Option<String>,
21 pub name: Option<String>,
25 pub ttl: Option<String>,
27 pub durable: Option<bool>,
29 pub fresh: Option<bool>,
32 pub env_file: Option<String>,
35 pub env_var: Option<String>,
37 pub wait: Option<String>,
40 pub branch: Option<String>,
44}
45
46#[derive(Deserialize, JsonSchema)]
47#[schemars(crate = "rmcp::schemars")]
48pub struct CloneDestroyParams {
49 pub id_or_name: String,
51}
52
53#[derive(Deserialize, JsonSchema)]
54#[schemars(crate = "rmcp::schemars")]
55pub struct CloneDiffParams {
56 pub id_or_name: String,
58 pub table: Option<String>,
61 pub limit: Option<u32>,
63}
64
65#[derive(Clone)]
66pub struct Mitosis {
67 home: Home,
68}
69
70fn text(r: error::Result<String>) -> Result<String, String> {
73 r.map_err(|e| e.to_string())
74}
75
76fn unexpected() -> MitosisError {
78 MitosisError::new(ErrorCode::ProtocolMismatch, "unexpected daemon reply")
79 .with_hint("restart the daemon: mitosis daemon stop")
80}
81
82#[tool_router(vis = "pub")]
83impl Mitosis {
84 #[tool(
85 description = "Create a private, throwaway Postgres database cloned from a template in about a second. Use it for migrations, seeds and destructive tests. It is deleted when this session ends or the TTL passes. Returns `url`, a stable router URL (postgres://postgres:<password>@127.0.0.1:<router_port>/<clone name or id>) that keeps working when the clone sleeps: an idle clone is stopped to save memory and the next connection to `url` wakes it. `direct_url` is the clone's own port and refuses connections while it sleeps. `branch` clones a template built for a git branch, so the database has that branch's schema. Use clone_diff afterwards to see what you changed."
86 )]
87 async fn clone_create(
88 &self,
89 Parameters(p): Parameters<CloneCreateParams>,
90 ) -> Result<String, String> {
91 text(self.create(p).await)
92 }
93
94 #[tool(
95 description = "List the clones this session created, as JSON. `status` is `ready` or `sleeping`; connecting to a sleeping clone's `url` wakes it."
96 )]
97 async fn clone_list(&self) -> Result<String, String> {
98 text(
99 async {
100 let mine: Vec<CloneInfo> = self
101 .all_clones()
102 .await?
103 .into_iter()
104 .filter(is_mine)
105 .collect();
106 Ok(serde_json::to_string_pretty(&mine)?)
107 }
108 .await,
109 )
110 }
111
112 #[tool(
113 description = "Destroy a clone this session created, by id or name. Works on a sleeping clone too. Returns `{\"destroyed\": \"<id>\"}`."
114 )]
115 async fn clone_destroy(
116 &self,
117 Parameters(p): Parameters<CloneDestroyParams>,
118 ) -> Result<String, String> {
119 text(self.destroy(p.id_or_name).await)
120 }
121
122 #[tool(
123 description = "Show what you changed in one of your clones, compared with the template generation it was made from, as JSON: schema changes (tables, columns, indexes, constraints, views, functions, sequences, extensions), row counts per table before and after, and with `table` the inserted, deleted and updated rows by primary key."
124 )]
125 async fn clone_diff(
126 &self,
127 Parameters(p): Parameters<CloneDiffParams>,
128 ) -> Result<String, String> {
129 text(self.diff(p).await)
130 }
131}
132
133fn is_mine(c: &CloneInfo) -> bool {
135 c.parent_pid == Some(std::process::id() as i32)
136}
137
138fn find<'a>(clones: &'a [CloneInfo], id_or_name: &str) -> Option<&'a CloneInfo> {
140 clones.iter().find(|c| c.id == id_or_name).or_else(|| {
141 clones
142 .iter()
143 .find(|c| c.name.as_deref() == Some(id_or_name))
144 })
145}
146
147impl Mitosis {
148 async fn all_clones(&self) -> error::Result<Vec<CloneInfo>> {
149 match client::request(&self.home, Request::CloneList).await? {
150 Response::Clones { clones } => Ok(clones),
151 _ => Err(unexpected()),
152 }
153 }
154
155 async fn destroy(&self, id_or_name: String) -> error::Result<String> {
157 let clones = self.all_clones().await?;
158 let c = find(&clones, &id_or_name).ok_or_else(|| {
159 MitosisError::new(
160 ErrorCode::CloneNotFound,
161 format!("clone {id_or_name} not found"),
162 )
163 .with_hint("see this session's clones with the clone_list tool")
164 })?;
165 if !is_mine(c) {
166 return Err(MitosisError::new(
167 ErrorCode::InvalidRequest,
168 format!(
169 "clone {} was not created by this session; it may be another agent's database",
170 c.id
171 ),
172 )
173 .with_hint("this tool only destroys clones from clone_create in this session; a person can run `mitosis destroy <id>`"));
174 }
175 match client::request(
177 &self.home,
178 Request::CloneDestroy {
179 id_or_name: c.id.clone(),
180 force: false,
181 forget: false,
182 },
183 )
184 .await?
185 {
186 Response::Destroyed { id } => Ok(serde_json::to_string_pretty(&destroy_output(&id))?),
187 _ => Err(unexpected()),
188 }
189 }
190
191 async fn diff(&self, p: CloneDiffParams) -> error::Result<String> {
193 let clones = self.all_clones().await?;
194 let c = find(&clones, &p.id_or_name).ok_or_else(|| {
195 MitosisError::new(
196 ErrorCode::CloneNotFound,
197 format!("clone {} not found", p.id_or_name),
198 )
199 .with_hint("see this session's clones with the clone_list tool")
200 })?;
201 if !is_mine(c) {
202 return Err(MitosisError::new(
203 ErrorCode::InvalidRequest,
204 format!(
205 "clone {} was not created by this session; it may be another agent's database",
206 c.id
207 ),
208 )
209 .with_hint("this tool only reads clones from clone_create in this session"));
210 }
211 let req = Request::CloneDiff {
214 id_or_name: c.id.clone(),
215 table: p.table,
216 limit: p.limit,
217 parent_pid: Some(std::process::id() as i32),
218 };
219 match client::request(&self.home, req).await? {
220 Response::Diff(report) => Ok(serde_json::to_string_pretty(&report)?),
221 _ => Err(unexpected()),
222 }
223 }
224
225 async fn create(&self, p: CloneCreateParams) -> error::Result<String> {
226 let template = p.template.unwrap_or_else(|| "main".into());
227 validate_name("template", &template)?;
228 if let Some(n) = &p.name {
229 validate_name("clone", n)?;
230 }
231 let ttl_secs = parse_duration(p.ttl.as_deref().unwrap_or("2h"))?.as_secs();
232 let args = CloneCreateArgs {
233 template: template.clone(),
234 name: p.name,
235 ttl_secs,
236 durable: p.durable.unwrap_or(false),
237 fresh: p.fresh.unwrap_or(false),
238 protect: false,
240 env: None,
241 wait_secs: p
242 .wait
243 .as_deref()
244 .map(parse_wait)
245 .transpose()?
246 .map(|d| d.as_secs()),
247 branch: match p.branch {
249 Some(name) => Some(BranchArgs {
250 name,
251 repo_root: std::env::current_dir()?,
252 path: None,
253 }),
254 None => None,
255 },
256 parent_pid: Some(std::process::id() as i32),
258 };
259 let info =
261 envfile::create_clone(&self.home, args, p.env_file.map(PathBuf::from), p.env_var)
262 .await?;
263 let mut warnings: Vec<String> =
265 match client::request(&self.home, Request::TemplateList).await {
266 Ok(Response::Templates { templates }) => templates
267 .into_iter()
268 .filter(|t| t.name == info.template)
269 .flat_map(|t| t.warnings)
270 .collect(),
271 _ => Vec::new(),
272 };
273 warnings.extend(info.warnings.iter().cloned());
274 Ok(serde_json::to_string_pretty(&create_output(
275 &info, warnings,
276 )?)?)
277 }
278}
279
280pub fn create_output(info: &CloneInfo, warnings: Vec<String>) -> error::Result<serde_json::Value> {
283 let mut out = serde_json::to_value(info)?;
284 if let Some(obj) = out.as_object_mut() {
285 obj.insert(
286 "template_age".into(),
287 format_duration(Duration::from_secs(info.template_age_secs)).into(),
288 );
289 obj.insert("warnings".into(), warnings.into());
290 }
291 Ok(out)
292}
293
294pub fn destroy_output(id: &str) -> serde_json::Value {
296 serde_json::json!({ "destroyed": id })
297}
298
299#[tool_handler(
300 name = "mitosis",
301 instructions = "Mitosis gives this session private, throwaway Postgres databases, cloned copy-on-write from a template in about a second. clone_create makes one and returns its connection URL (a router URL on 127.0.0.1 that stays valid while the clone sleeps; the next connection wakes it); clone_list shows the clones this session made; clone_destroy deletes one of them; clone_diff shows what you changed in one. Pass `branch` to clone_create to get the schema of a git branch. Clones are deleted when this session ends or their TTL passes."
302)]
303impl ServerHandler for Mitosis {}
304
305pub async fn serve_stdio(home: Home) -> error::Result<()> {
307 let internal = |e: String| MitosisError::new(ErrorCode::Internal, format!("mcp: {e}"));
308 for w in envfile::finish_pending_if_running(&home).await {
312 eprintln!("mitosis: {w}");
313 }
314 let service = Mitosis { home }
315 .serve(rmcp::transport::stdio())
316 .await
317 .map_err(|e| internal(e.to_string()))?;
318 service
319 .waiting()
320 .await
321 .map_err(|e| internal(e.to_string()))?;
322 Ok(())
323}