1use rmcp::ErrorData;
7use rmcp::model::{CallToolResult, ContentBlock};
8use serde_json::Value;
9
10use crate::mcp::host::{Despawned, McpCommand, Output, SpawnedEntity, to_value};
11use crate::mcp::server::{
12 CommandSender, EntityArg, GetComponent, ListEntities, McpServer, NoArgs, SetComponent,
13 SpawnEntity, ToolResult, ToolRouter, ToolSource, WorldArg, command, entity, tool,
14};
15use crate::reflect;
16use crate::{DynamicBundle, Entity, Scope, ScopeMut, World};
17
18fn summarize_world(scope: &Scope<'_>) -> Result<Value, String> {
20 to_value(&scope.info())
21}
22
23fn component_names(_world: &World) -> Result<Value, String> {
25 to_value(&reflect::names().collect::<Vec<_>>())
26}
27
28fn summarize_archetypes(scope: &Scope<'_>) -> Result<Value, String> {
30 to_value(&scope.archetype_infos())
31}
32
33fn summarize_entities(scope: &Scope<'_>, limit: usize) -> Result<Value, String> {
35 to_value(&scope.entity_infos(limit))
36}
37
38fn summarize_entity(scope: &Scope<'_>, entity: Entity) -> Result<Value, String> {
40 let info = scope
41 .entity_info(entity)
42 .map_err(|_| format!("no entity {entity:?}"))?;
43 to_value(&info)
44}
45
46fn read_component(scope: &Scope<'_>, entity: Entity, name: &str) -> Result<Value, String> {
48 if !scope.contains(entity) {
49 return Err(format!("no entity {entity:?}"));
50 }
51 let text = reflect::encode_json(scope, entity, name).map_err(|error| error.to_string())?;
52 serde_json::from_str(&text).map_err(|error| error.to_string())
53}
54
55fn write_component(
57 scope: &Scope<'_>,
58 entity: Entity,
59 name: &str,
60 value: &Value,
61) -> Result<Value, String> {
62 if !scope.contains(entity) {
63 return Err(format!("no entity {entity:?}"));
64 }
65 let patch = serde_json::to_string(value).map_err(|error| error.to_string())?;
66 reflect::set_json(scope, entity, name, &patch).map_err(|error| error.to_string())?;
67 Ok(Value::Null)
68}
69
70fn spawn_from_value(scope: &mut ScopeMut<'_>, components: &Value) -> Result<Value, String> {
72 let object = components
73 .as_object()
74 .ok_or_else(|| "components must be an object".to_string())?;
75 let mut bundle = DynamicBundle::new();
76 for (name, value) in object {
77 let patch = serde_json::to_string(value).map_err(|error| error.to_string())?;
78 reflect::push_json(&mut bundle, name, &patch).map_err(|error| error.to_string())?;
79 }
80 let spawned = scope.entity();
81 let name = "mcp spawned";
82 let entity = scope.spawn_named(name, bundle);
83 to_value(&SpawnedEntity {
84 entity,
85 name,
86 scope: spawned,
87 })
88}
89
90fn despawn_one(scope: &mut ScopeMut<'_>, entity: Entity) -> Result<Value, String> {
95 let name = scope.as_scope().entity_name(entity).unwrap_or("");
96 let removed = scope
97 .despawn(entity)
98 .map_err(|error| format!("cannot despawn {entity:?}: {error}"))?;
99 to_value(&Despawned { removed, name })
100}
101
102async fn list_worlds(commands: CommandSender, _args: NoArgs) -> ToolResult {
104 let (request, receiver) = McpCommand::list_worlds();
105 commands
106 .send(request)
107 .await
108 .map_err(|_| ErrorData::internal_error("the world thread is gone", None))?;
109 match receiver.recv().await {
110 Ok(Ok(Output::Json(value))) => Ok(CallToolResult::structured(value)),
111 Ok(Ok(Output::Image(_))) => Err(ErrorData::internal_error(
112 "list_worlds was answered with an image",
113 None,
114 )),
115 Ok(Ok(Output::Audio(_))) => Err(ErrorData::internal_error(
116 "list_worlds was answered with a sound",
117 None,
118 )),
119 Ok(Err(message)) => Ok(CallToolResult::error(vec![ContentBlock::text(message)])),
120 Err(_) => Err(ErrorData::internal_error(
121 "the world thread dropped the command",
122 None,
123 )),
124 }
125}
126
127async fn world_summary(commands: CommandSender, args: WorldArg) -> ToolResult {
129 command(&commands, args.world, move |world| {
130 summarize_world(&scoped(world, args.scope)?)
131 })
132 .await
133}
134
135async fn list_components(commands: CommandSender, args: WorldArg) -> ToolResult {
137 command(&commands, args.world, move |world| component_names(world)).await
138}
139
140async fn list_archetypes(commands: CommandSender, args: WorldArg) -> ToolResult {
142 command(&commands, args.world, move |world| {
143 summarize_archetypes(&scoped(world, args.scope)?)
144 })
145 .await
146}
147
148async fn list_entities(commands: CommandSender, args: ListEntities) -> ToolResult {
150 command(&commands, args.world, move |world| {
151 summarize_entities(&scoped(world, args.scope)?, args.limit)
152 })
153 .await
154}
155
156async fn get_entity(commands: CommandSender, args: EntityArg) -> ToolResult {
158 command(&commands, args.world, move |world| {
159 summarize_entity(&scoped(world, args.scope)?, entity(args.entity))
160 })
161 .await
162}
163
164async fn get_component(commands: CommandSender, args: GetComponent) -> ToolResult {
166 command(&commands, args.world, move |world| {
167 read_component(
168 &scoped(world, args.scope)?,
169 entity(args.entity),
170 &args.component,
171 )
172 })
173 .await
174}
175
176async fn set_component(commands: CommandSender, args: SetComponent) -> ToolResult {
178 command(&commands, args.world, move |world| {
179 write_component(
180 &scoped(world, args.scope)?,
181 entity(args.entity),
182 &args.component,
183 &args.value,
184 )
185 })
186 .await
187}
188
189async fn spawn_entity(commands: CommandSender, args: SpawnEntity) -> ToolResult {
191 command(&commands, args.world, move |world| {
192 let mut scope = scoped_mut(world, args.scope)?;
193 spawn_from_value(&mut scope, &args.components)
194 })
195 .await
196}
197
198async fn despawn_entity(commands: CommandSender, args: EntityArg) -> ToolResult {
200 command(&commands, args.world, move |world| {
201 let mut scope = scoped_mut(world, args.scope)?;
202 despawn_one(&mut scope, entity(args.entity))
203 })
204 .await
205}
206
207fn scoped(world: &World, scope: Option<u64>) -> Result<Scope<'_>, String> {
209 match scope {
210 None => Ok(world.root_scope()),
211 Some(bits) => {
212 let scope = Entity::from_bits(bits);
213 world
214 .scope(scope)
215 .map_err(|_| format!("no scope {scope:?}"))
216 }
217 }
218}
219
220fn scoped_mut(world: &mut World, scope: Option<u64>) -> Result<ScopeMut<'_>, String> {
222 match scope {
223 None => Ok(world.root_scope_mut()),
224 Some(bits) => {
225 let scope = Entity::from_bits(bits);
226
227 world
228 .scope_mut(scope)
229 .map_err(|_| format!("no scope {scope:?}"))
230 }
231 }
232}
233
234inventory::submit! {
235 ToolSource { extend: register }
236}
237
238fn register(router: &mut ToolRouter<McpServer>) {
240 router.add_route(tool(
241 "world_summary",
242 "Count a world's entities and archetypes and list the component names it has seen. Pass the host entity whose world to inspect, or omit for the host world.",
243 world_summary,
244 ));
245 router.add_route(tool(
246 "list_worlds",
247 "List the worlds the host carries: its own world and every world it routes commands to, including ones on other threads. Ask this before naming a world in another tool.",
248 list_worlds,
249 ));
250 router.add_route(tool(
251 "list_components",
252 "List the component names the server knows how to encode and decode.",
253 list_components,
254 ));
255 router.add_route(tool(
256 "list_archetypes",
257 "List every archetype of a world with the component names it stores and how many entities it holds.",
258 list_archetypes,
259 ));
260 router.add_route(tool(
261 "list_entities",
262 "List a world's spawned entities and their component names, up to a limit.",
263 list_entities,
264 ));
265 router.add_route(tool(
266 "get_entity",
267 "Describe one entity of a world: its components, archetype and row.",
268 get_entity,
269 ));
270 router.add_route(tool(
271 "get_component",
272 "Read one component of one entity of a world as JSON.",
273 get_component,
274 ));
275 router.add_route(tool(
276 "set_component",
277 "Overwrite one component of one entity of a world from a JSON value.",
278 set_component,
279 ));
280 router.add_route(tool(
281 "spawn_entity",
282 "Spawn an entity in a world from an object mapping component names to JSON values.",
283 spawn_entity,
284 ));
285 router.add_route(tool(
286 "despawn_entity",
287 "Despawn one entity of a world.",
288 despawn_entity,
289 ));
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use crate::bundle;
296
297 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, facet::Facet)]
299 struct Marker(u32);
300
301 inventory::submit! { crate::reflect::ComponentEntry::new::<Marker>() }
302
303 fn fixture() -> (World, Entity, Entity, Entity) {
305 let mut world = World::new();
306 let scene = world.root_scope_mut().spawn(bundle![0u32]);
307 let child = world.scope_mut(scene).unwrap().spawn(bundle![Marker(1)]);
308 let sibling = world.root_scope_mut().spawn(bundle![Marker(2)]);
309 (world, scene, child, sibling)
310 }
311
312 #[test]
313 fn an_absent_scope_means_the_root() {
314 let (world, _, child, sibling) = fixture();
315 let Ok(scope) = scoped(&world, None) else {
316 panic!("the root needs no handle");
317 };
318 assert_eq!(scope.entity(), world.root());
319 assert!(scope.contains(child));
320 assert!(scope.contains(sibling));
321 }
322
323 #[test]
324 fn a_scope_handle_narrows_the_view() {
325 let (world, scene, child, sibling) = fixture();
326 let Ok(scope) = scoped(&world, Some(scene.to_bits())) else {
327 panic!("a live scope");
328 };
329 assert_eq!(scope.entity(), scene);
330 assert!(scope.contains(child));
331 assert!(!scope.contains(sibling), "the sibling is beside it");
332 }
333
334 #[test]
335 fn a_dead_scope_handle_is_reported_by_its_bits() {
336 let (mut world, scene, _, _) = fixture();
337 let bits = scene.to_bits();
338 let restored = Entity::from_bits(bits);
339 world.root_scope_mut().despawn(scene).unwrap();
340 let error = scoped(&world, Some(bits)).expect_err("the scope is gone");
341 assert_eq!(
342 error,
343 format!("no scope {restored:?}"),
344 "the bits it came as"
345 );
346 }
347
348 #[test]
349 fn an_unknown_scope_reads_the_same_as_a_dead_one() {
350 let world = World::new();
353 let bits = 0x1234_5678_9abc_def0u64;
354 let restored = Entity::from_bits(bits);
355 let error = scoped(&world, Some(bits)).expect_err("no such scope");
356 assert_eq!(
357 error,
358 format!("no scope {restored:?}"),
359 "the bits it came as"
360 );
361 }
362
363 #[test]
364 fn a_scoped_write_refuses_an_entity_the_scope_cannot_see() {
365 let (world, scene, _, sibling) = fixture();
366 let scope = scoped(&world, Some(scene.to_bits())).unwrap();
367 let error = write_component(&scope, sibling, "Marker", &serde_json::json!({ "0": 9 }))
368 .expect_err("the sibling is out of reach");
369 assert!(error.contains(&sibling.to_bits().to_string()), "{error}");
370 assert_eq!(
371 world
372 .root_scope()
373 .query_one::<&Marker>(sibling)
374 .unwrap()
375 .get()
376 .0,
377 2
378 );
379 }
380
381 #[test]
382 fn a_scoped_read_refuses_an_entity_the_scope_cannot_see() {
383 let (world, scene, _, sibling) = fixture();
384 let scope = scoped(&world, Some(scene.to_bits())).unwrap();
385 let error = read_component(&scope, sibling, "Marker").expect_err("out of reach");
386 assert!(error.contains(&sibling.to_bits().to_string()), "{error}");
387 }
388
389 #[test]
390 fn a_scoped_spawn_lands_in_that_scope_and_reports_it() {
391 let (mut world, scene, _, _) = fixture();
392 let mut scope = scoped_mut(&mut world, Some(scene.to_bits())).unwrap();
393 let spawned = spawn_from_value(&mut scope, &serde_json::json!({ "Marker": { "0": 7 } }))
394 .expect("a marker is spawnable");
395 let entity = spawned["entity"].clone();
396 let reported = spawned["scope"].clone();
397 let entity = Entity::from_bits(entity.as_u64().expect("bits"));
398 assert_eq!(world.parent_scope(entity).unwrap(), scene);
399 assert_eq!(
400 Entity::from_bits(reported.as_u64().expect("bits")),
401 scene,
402 "the reply says which scope it went in"
403 );
404 }
405
406 #[test]
407 fn a_scoped_despawn_refuses_what_the_scope_does_not_own() {
408 let (mut world, scene, _, sibling) = fixture();
409 let mut scope = scoped_mut(&mut world, Some(scene.to_bits())).unwrap();
410 let error = despawn_one(&mut scope, sibling).expect_err("it is not its to remove");
411 assert!(error.contains("is not its to move or remove"), "{error}");
412 assert!(error.contains(&format!("{sibling:?}")), "{error}");
413 assert!(
414 world.root_scope().contains(sibling),
415 "the refusal removed nothing"
416 );
417 }
418
419 #[test]
420 fn a_scoped_despawn_takes_the_subtree() {
421 let (mut world, scene, child, _) = fixture();
422 let name = world.root_scope().entity_name(child).unwrap();
423 assert!(!name.is_empty(), "the row names the entity");
424 let mut scope = scoped_mut(&mut world, Some(scene.to_bits())).unwrap();
425 let answer = despawn_one(&mut scope, child).expect("its own descendant");
426 assert_eq!(
427 answer["removed"],
428 serde_json::json!(1),
429 "the child took no subtree with it"
430 );
431 assert_eq!(
432 answer["name"],
433 serde_json::json!(name),
434 "the reply carries the name the row stores"
435 );
436 assert!(!world.root_scope().contains(child));
437 }
438
439 #[test]
440 fn the_summaries_report_the_scope_not_the_whole_world() {
441 let (world, scene, child, sibling) = fixture();
442 let scope = scoped(&world, Some(scene.to_bits())).unwrap();
443 let world_info = summarize_world(&scope).unwrap();
444 assert_eq!(
445 world_info["entities"],
446 serde_json::json!(2),
447 "the scene and its child"
448 );
449 assert_eq!(
450 world_info["root"],
451 serde_json::json!(world.root().to_bits())
452 );
453 let entities = summarize_entities(&scope, usize::MAX).unwrap();
454 let listed: Vec<u64> = entities
455 .as_array()
456 .unwrap()
457 .iter()
458 .map(|info| info["entity"].as_u64().unwrap())
459 .collect();
460 assert!(listed.contains(&child.to_bits()));
461 assert!(!listed.contains(&sibling.to_bits()));
462 let archetypes = summarize_archetypes(&scope).unwrap();
463 let sum: u64 = archetypes
464 .as_array()
465 .unwrap()
466 .iter()
467 .map(|info| info["len"].as_u64().unwrap())
468 .sum();
469 assert_eq!(sum, 2, "the archetype lengths add up to the count");
470 }
471
472 #[test]
473 fn an_entity_listing_reports_the_scope_it_lives_in() {
474 let (world, scene, child, _) = fixture();
475 let scope = scoped(&world, None).unwrap();
476 let info = summarize_entity(&scope, child).unwrap();
477 assert_eq!(info["scope"], serde_json::json!(scene.to_bits()));
478 let names: Vec<&str> = info["components"]
479 .as_array()
480 .unwrap()
481 .iter()
482 .map(|name| name.as_str().unwrap())
483 .collect();
484 assert!(
485 !names.iter().any(|name| name.ends_with("ScopeOf")),
486 "the scope is not a component: {names:?}"
487 );
488 }
489
490 #[test]
491 fn the_scope_of_an_entity_is_not_addressed_as_a_component() {
492 let (world, scene, child, _) = fixture();
495 let scope = scoped(&world, None).unwrap();
496 let error = write_component(
497 &scope,
498 child,
499 "ScopeOf",
500 &serde_json::json!({ "0": scene.to_bits() }),
501 )
502 .expect_err("there is no such component");
503 assert!(!error.is_empty(), "{error}");
504 assert_eq!(
505 world.parent_scope(child).unwrap(),
506 scene,
507 "the failed write moved nothing"
508 );
509 }
510
511 #[test]
512 fn a_lifecycle_component_is_not_spawnable_through_the_tools() {
513 let (mut world, _, _, _) = fixture();
514 let mut scope = scoped_mut(&mut world, None).unwrap();
515 let error = spawn_from_value(&mut scope, &serde_json::json!({ "OnDespawn": {} }))
516 .expect_err("a callback has no wire form");
517 assert!(!error.is_empty());
518 }
519}