Skip to main content

unlit_ecs/mcp/
server.rs

1//! The MCP service: the tool router, the shared argument types and the transport.
2//!
3//! Each tool turns its JSON arguments into a [McpCommand] and sends it to the
4//! thread that owns the world. The tool then awaits the result: structured JSON
5//! for most tools, an image or audio content block for the ones that read
6//! pixels or samples back inline, and a written path for the ones that save
7//! to a file. A tool that fails returns a tool-level error result rather
8//! than a protocol error, so a model sees the message instead of a transport
9//! failure.
10//!
11//! The router starts empty and the engine crates fill it in: a crate submits a
12//! [ToolSource] whose `extend` registers the tools that crate defines. The
13//! server therefore carries tools this crate knows nothing about, and a tool
14//! whose types live elsewhere still runs against the world through the same
15//! command channel.
16
17use std::error::Error;
18use std::future::Future;
19use std::sync::Arc;
20
21use rmcp::handler::server::tool::schema_for_input;
22use rmcp::handler::server::wrapper::Parameters;
23use rmcp::model::{
24    CallToolResponse, CallToolResult, ContentBlock, ServerCapabilities, ServerConfig, Tool,
25};
26use rmcp::{ErrorData, ServerHandler, tool_handler};
27use schemars::JsonSchema;
28use serde::Deserialize;
29use serde::de::DeserializeOwned;
30use serde_json::Value;
31
32use crate::mcp::host::{McpCommand, Output, WorldId};
33use crate::{Entity, World};
34
35pub use rmcp::handler::server::router::tool::{ToolRoute, ToolRouter};
36
37/// The channel a tool sends its commands over.
38pub type CommandSender = async_channel::Sender<McpCommand>;
39
40/// What a tool handler answers with.
41pub type ToolResult = Result<CallToolResult, ErrorData>;
42
43/// A crate's contribution to the server's tool set.
44///
45/// A crate that defines tools of its own submits one of these from a link-time
46/// registration; the server calls every `extend` once, while it is built.
47pub struct ToolSource {
48    /// Register this crate's tools on the router.
49    pub extend: fn(&mut ToolRouter<McpServer>),
50}
51
52inventory::collect!(ToolSource);
53
54/// An entity handle from the `u64` bits a JSON argument carries.
55pub fn entity(bits: u64) -> Entity {
56    Entity::from_bits(bits)
57}
58
59/// A tool route from a name, a description and an asynchronous handler.
60///
61/// The handler receives the command channel it sends its work over and the
62/// arguments the client passed. Its argument type derives [JsonSchema], so the
63/// advertised input schema is the one this crate's own tools advertise too.
64pub fn tool<A, F, Fut>(
65    name: &'static str,
66    description: &'static str,
67    handler: F,
68) -> ToolRoute<McpServer>
69where
70    A: DeserializeOwned + JsonSchema + Send + 'static,
71    F: Fn(CommandSender, A) -> Fut + Send + Sync + 'static,
72    Fut: Future<Output = ToolResult> + Send + 'static,
73{
74    let schema =
75        schema_for_input::<Parameters<A>>().expect("a tool's arguments describe an object");
76    let attribute = Tool::new(name, description, schema);
77    let handler = Arc::new(handler);
78    ToolRoute::<McpServer>::new_dyn(attribute, move |context| {
79        let handler = Arc::clone(&handler);
80        let commands = context.service.commands.clone();
81        let arguments = context.arguments;
82        Box::pin(async move {
83            let args = serde_json::from_value(Value::Object(arguments.unwrap_or_default()))
84                .map_err(|error| ErrorData::invalid_params(error.to_string(), None))?;
85            let result: CallToolResponse = handler(commands, args).await?.into();
86            Ok(result)
87        })
88    })
89}
90
91/// The MCP server driving one or more worlds.
92#[derive(Debug, Clone)]
93pub struct McpServer {
94    commands: CommandSender,
95    tool_router: ToolRouter<Self>,
96}
97
98impl McpServer {
99    /// A server that sends its commands over `commands`.
100    ///
101    /// Every submitted [ToolSource] registers its tools as the server is built.
102    pub fn new(commands: CommandSender) -> Self {
103        let mut tool_router = ToolRouter::new();
104        for source in inventory::iter::<ToolSource> {
105            (source.extend)(&mut tool_router);
106        }
107        Self {
108            commands,
109            tool_router,
110        }
111    }
112}
113
114/// Send a job to the world `world` names and await what it answered.
115///
116/// The host - not this crate - turns `world` into a world: it routes the
117/// command to the thread that owns that world, which calls
118/// [`McpCommand::dispatch`] on it.
119pub async fn send(
120    commands: &CommandSender,
121    world: WorldId,
122    job: impl FnOnce(&mut World) -> Result<Output, String> + Send + 'static,
123) -> Result<Result<Output, String>, ErrorData> {
124    let (command, receiver) = McpCommand::new(job);
125    let command = command.with_world(world);
126    commands
127        .send(command)
128        .await
129        .map_err(|_| ErrorData::internal_error("the world thread is gone", None))?;
130    receiver
131        .recv()
132        .await
133        .map_err(|_| ErrorData::internal_error("the world thread dropped the command", None))
134}
135
136/// Send a job to the world `world` names and await its output.
137pub async fn run(
138    commands: &CommandSender,
139    world: WorldId,
140    job: impl FnOnce(&mut World) -> Result<Output, String> + Send + 'static,
141) -> ToolResult {
142    match send(commands, world, job).await? {
143        Ok(Output::Json(value)) => Ok(CallToolResult::structured(value)),
144        // An image answers with an MCP image content block so the client
145        // renders the pixels, and carries its size as structured content so a
146        // client reads it as data instead of from the picture.
147        Ok(Output::Image(image)) => {
148            let mut result =
149                CallToolResult::success(vec![ContentBlock::image(image.data, image.mime_type)]);
150            result.structured_content = Some(image.structured);
151            Ok(result)
152        }
153        // A sound answers the same way, in an audio content block, so a
154        // client decodes the stream itself rather than a base64 field it has
155        // to find in the JSON.
156        Ok(Output::Audio(audio)) => {
157            let mut result =
158                CallToolResult::success(vec![ContentBlock::audio(audio.data, audio.mime_type)]);
159            result.structured_content = Some(audio.structured);
160            Ok(result)
161        }
162        Err(message) => Ok(CallToolResult::error(vec![ContentBlock::text(message)])),
163    }
164}
165
166/// Send a job to the world `world` names and await its JSON result.
167pub async fn command(
168    commands: &CommandSender,
169    world: WorldId,
170    job: impl FnOnce(&mut World) -> Result<Value, String> + Send + 'static,
171) -> ToolResult {
172    run(commands, world, move |world| job(world).map(Output::from)).await
173}
174
175#[tool_handler(router = self.tool_router)]
176impl ServerHandler for McpServer {
177    fn get_info(&self) -> ServerConfig {
178        ServerConfig::new(ServerCapabilities::builder().enable_tools().build()).with_instructions(
179            "Drives one or more worlds through the public API of the crates that registered their tools: inspect and edit entities and components, inspect a world's resource graph, read or save buffers and textures, send input, and render or screenshot frames. Every tool names its world with a WorldId (\"Host\", or {\"Handle\": bits} for a world a host entity carries), and a tool that needs engine handles names them explicitly.",
180        )
181    }
182}
183
184/// No arguments.
185#[derive(Debug, Default, Deserialize, JsonSchema)]
186pub struct NoArgs {}
187
188/// Serve MCP over stdio, handing every command to `sink`.
189///
190/// `sink` is how a command reaches the thread that owns the world its
191/// [WorldId] names: a host sends it to its own render thread, or a windowed host
192/// posts it to its event loop, where it resolves the id to a world and calls
193/// [`McpCommand::dispatch`]. Returning `false` means the command could not be
194/// delivered - the event loop is gone, say - and the waiting tool call is
195/// answered with an internal error rather than left hanging.
196///
197/// The host answers the `list_worlds` tool itself: when a command's
198/// [`wants_worlds`](McpCommand::wants_worlds) is true, it names every world it
199/// carries - with the name each was spawned under - through
200/// [`answer_worlds`](McpCommand::answer_worlds) instead of dispatching a job.
201/// This crate keeps no list of its own, because the worlds on other threads are
202/// not types it can recognise.
203///
204/// # Errors
205///
206/// Fails when the tokio runtime cannot be built or the transport cannot start.
207pub fn serve_stdio_with(
208    sink: impl Fn(McpCommand) -> bool + Send + 'static,
209) -> Result<(), Box<dyn Error + Send + Sync>> {
210    let runtime = tokio::runtime::Builder::new_current_thread()
211        .enable_time()
212        .build()?;
213    runtime.block_on(async move {
214        let service = serve_with(rmcp::transport::stdio(), sink)
215            .await
216            .map_err(|error| -> Box<dyn Error + Send + Sync> { error })?;
217        service.waiting().await?;
218        Ok::<(), Box<dyn Error + Send + Sync>>(())
219    })
220}
221
222/// Serve MCP over any transport, handing every command to `sink`.
223///
224/// The transport is whatever rmcp accepts - stdio, or a pair of streams for a
225/// test that wants the server in its own process. The returned service is
226/// running; await [`waiting`](rmcp::service::RunningService::waiting) to serve
227/// until the peer leaves.
228///
229/// # Errors
230///
231/// Fails when the service cannot complete its handshake over `transport`.
232pub async fn serve_with<T, E, A>(
233    transport: T,
234    sink: impl Fn(McpCommand) -> bool + Send + 'static,
235) -> Result<
236    rmcp::service::RunningService<rmcp::RoleServer, McpServer>,
237    // Boxed because the error is large and this is the only place it is
238    // constructed: a caller reports it, it does not match on it.
239    Box<rmcp::service::ServerInitializeError>,
240>
241where
242    T: rmcp::transport::IntoTransport<rmcp::RoleServer, E, A>,
243    E: std::error::Error + Send + Sync + 'static,
244{
245    let (sender, receiver) = async_channel::bounded::<McpCommand>(4);
246    // A pump rather than a task per command: the sink is synchronous, and the
247    // service would otherwise be blocked on it.
248    tokio::task::spawn_blocking(move || {
249        while let Ok(command) = receiver.recv_blocking() {
250            if !sink(command) {
251                break;
252            }
253        }
254    });
255    rmcp::serve_server(McpServer::new(sender), transport)
256        .await
257        .map_err(Box::new)
258}
259
260/// Which world a tool runs against.
261#[derive(Debug, Deserialize, JsonSchema)]
262pub struct WorldArg {
263    /// The world to drive: `"Host"` for the world the transport thread itself
264    /// holds, or `{"Handle": bits}` for a world a host entity carries.
265    #[serde(default)]
266    pub world: WorldId,
267    /// The entity whose scope the tool runs in, as its `u64` bits. Omit for the
268    /// world's root scope, which sees everything.
269    #[serde(default)]
270    pub scope: Option<u64>,
271}
272
273/// Which world and render context a GPU tool runs against.
274#[derive(Debug, Deserialize, JsonSchema)]
275pub struct ContextArg {
276    /// The world to drive: `"Host"` for the world the transport thread itself
277    /// holds, or `{"Handle": bits}` for a world a host entity carries.
278    #[serde(default)]
279    pub world: WorldId,
280    /// Any entity carrying the world's render context, as its `u64` bits —
281    /// its device, queue, graph or capabilities. Omit when the world holds
282    /// exactly one.
283    #[serde(default)]
284    pub context: Option<u64>,
285}
286
287/// Which world and resource a handle tool runs against.
288#[derive(Debug, Deserialize, JsonSchema)]
289pub struct HandleArg {
290    /// The world to drive: `"Host"` for the world the transport thread itself
291    /// holds, or `{"Handle": bits}` for a world a host entity carries.
292    #[serde(default)]
293    pub world: WorldId,
294    /// Any entity carrying the world's render context, as its `u64` bits.
295    /// Omit when the world holds exactly one.
296    #[serde(default)]
297    pub context: Option<u64>,
298    /// The resource's id, as `list_resources` reports it. A texture view is
299    /// read through the texture it views.
300    pub id: u64,
301    /// The image format to answer with: `"webp"` (lossless, the default),
302    /// `"png"`, or `"jpeg"`.
303    #[serde(default)]
304    pub format: Option<String>,
305}
306
307/// Arguments for one entity in a world.
308#[derive(Debug, Deserialize, JsonSchema)]
309pub struct EntityArg {
310    /// The world to drive: `"Host"` for the world the transport thread itself
311    /// holds, or `{"Handle": bits}` for a world a host entity carries.
312    #[serde(default)]
313    pub world: WorldId,
314    /// The entity whose scope the tool runs in, as its `u64` bits. Omit for the
315    /// world's root scope, which sees everything.
316    #[serde(default)]
317    pub scope: Option<u64>,
318    /// The entity's bits.
319    pub entity: u64,
320    /// The entity carrying the world's mesh source, as its `u64` bits, for the tools
321    /// that need one. Omit for the world's only mesh source.
322    #[serde(default)]
323    pub source: Option<u64>,
324}
325
326/// Arguments for listing entities.
327#[derive(Debug, Deserialize, JsonSchema)]
328pub struct ListEntities {
329    /// The world to drive: `"Host"` for the world the transport thread itself
330    /// holds, or `{"Handle": bits}` for a world a host entity carries.
331    #[serde(default)]
332    pub world: WorldId,
333    /// The entity whose scope the tool runs in, as its `u64` bits. Omit for the
334    /// world's root scope, which sees everything.
335    #[serde(default)]
336    pub scope: Option<u64>,
337    /// How many entities to report.
338    #[serde(default = "default_limit")]
339    pub limit: usize,
340}
341
342fn default_limit() -> usize {
343    256
344}
345
346/// Arguments for reading one component.
347#[derive(Debug, Deserialize, JsonSchema)]
348pub struct GetComponent {
349    /// The world to drive: `"Host"` for the world the transport thread itself
350    /// holds, or `{"Handle": bits}` for a world a host entity carries.
351    #[serde(default)]
352    pub world: WorldId,
353    /// The entity whose scope the tool runs in, as its `u64` bits. Omit for the
354    /// world's root scope, which sees everything.
355    #[serde(default)]
356    pub scope: Option<u64>,
357    /// The entity's bits.
358    pub entity: u64,
359    /// The component's name.
360    pub component: String,
361}
362
363/// Arguments for writing one component.
364#[derive(Debug, Deserialize, JsonSchema)]
365pub struct SetComponent {
366    /// The world to drive: `"Host"` for the world the transport thread itself
367    /// holds, or `{"Handle": bits}` for a world a host entity carries.
368    #[serde(default)]
369    pub world: WorldId,
370    /// The entity whose scope the tool runs in, as its `u64` bits. Omit for the
371    /// world's root scope, which sees everything.
372    #[serde(default)]
373    pub scope: Option<u64>,
374    /// The entity's bits.
375    pub entity: u64,
376    /// The component's name.
377    pub component: String,
378    /// The new value.
379    pub value: Value,
380}
381
382/// Arguments for spawning an entity.
383#[derive(Debug, Deserialize, JsonSchema)]
384pub struct SpawnEntity {
385    /// The world to drive: `"Host"` for the world the transport thread itself
386    /// holds, or `{"Handle": bits}` for a world a host entity carries.
387    #[serde(default)]
388    pub world: WorldId,
389    /// The entity whose scope the tool runs in, as its `u64` bits. Omit for the
390    /// world's root scope, which sees everything.
391    #[serde(default)]
392    pub scope: Option<u64>,
393    /// The components, as an object of name to value.
394    pub components: Value,
395}
396
397/// Arguments for reading a buffer.
398#[derive(Debug, Deserialize, JsonSchema)]
399pub struct ReadBuffer {
400    /// The world to drive: `"Host"` for the world the transport thread itself
401    /// holds, or `{"Handle": bits}` for a world a host entity carries.
402    #[serde(default)]
403    pub world: WorldId,
404    /// Any entity carrying the world's render context, as its `u64` bits.
405    /// Omit when the world holds exactly one.
406    #[serde(default)]
407    pub context: Option<u64>,
408    /// The buffer's id, as `list_resources` reports it.
409    pub id: u64,
410    /// The byte offset to read from.
411    #[serde(default)]
412    pub offset: u64,
413    /// How many bytes to read; the rest of the buffer when omitted.
414    #[serde(default)]
415    pub size: Option<u64>,
416}
417
418/// Arguments for saving a buffer to a file.
419#[derive(Debug, Deserialize, JsonSchema)]
420pub struct SaveBuffer {
421    /// The world to drive: `"Host"` for the world the transport thread itself
422    /// holds, or `{"Handle": bits}` for a world a host entity carries.
423    #[serde(default)]
424    pub world: WorldId,
425    /// Any entity carrying the world's render context, as its `u64` bits.
426    /// Omit when the world holds exactly one.
427    #[serde(default)]
428    pub context: Option<u64>,
429    /// The buffer's id, as `list_resources` reports it.
430    pub id: u64,
431    /// The byte offset to read from.
432    #[serde(default)]
433    pub offset: u64,
434    /// How many bytes to read; the rest of the buffer when omitted.
435    #[serde(default)]
436    pub size: Option<u64>,
437    /// The directory to write the file into; created when it does not exist.
438    pub directory: String,
439    /// The file name to write; it is given a `.bin` extension when it carries
440    /// none.
441    #[serde(default)]
442    pub file_name: Option<String>,
443}
444
445/// Arguments for saving a texture as an image.
446#[derive(Debug, Deserialize, JsonSchema)]
447pub struct SaveTexture {
448    /// The world to drive: `"Host"` for the world the transport thread itself
449    /// holds, or `{"Handle": bits}` for a world a host entity carries.
450    #[serde(default)]
451    pub world: WorldId,
452    /// Any entity carrying the world's render context, as its `u64` bits.
453    /// Omit when the world holds exactly one.
454    #[serde(default)]
455    pub context: Option<u64>,
456    /// The resource's id, as `list_resources` reports it. A texture view is
457    /// read through the texture it views.
458    pub id: u64,
459    /// The directory to write the file into; created when it does not exist.
460    pub directory: String,
461    /// The file name to write; its extension picks the image format, and it is
462    /// given a `.webp` one when it carries none.
463    #[serde(default)]
464    pub file_name: Option<String>,
465}
466
467/// Arguments for saving a screenshot as an image.
468#[derive(Debug, Deserialize, JsonSchema)]
469pub struct SaveScreenshot {
470    /// The world to drive: `"Host"` for the world the transport thread itself
471    /// holds, or `{"Handle": bits}` for a world a host entity carries.
472    #[serde(default)]
473    pub world: WorldId,
474    /// The entity carrying the renderer, as its `u64` bits. Omit for the
475    /// world's only one.
476    #[serde(default)]
477    pub renderer: Option<u64>,
478    /// The id of the texture to read, as `list_resources` reports it, or the
479    /// id of any view of it. Omit for the world's only readable texture.
480    #[serde(default)]
481    pub target: Option<u64>,
482    /// The directory to write the file into; created when it does not exist.
483    pub directory: String,
484    /// The file name to write; its extension picks the image format, and it is
485    /// given a `.webp` one when it carries none.
486    #[serde(default)]
487    pub file_name: Option<String>,
488}
489
490/// Arguments for reading or sending input.
491#[derive(Debug, Deserialize, JsonSchema)]
492pub struct InputArg {
493    /// The world to drive: `"Host"` for the world the transport thread itself
494    /// holds, or `{"Handle": bits}` for a world a host entity carries.
495    #[serde(default)]
496    pub world: WorldId,
497    /// The entity carrying the input state, as its `u64` bits. Omit for the
498    /// world's only one.
499    #[serde(default)]
500    pub entity: Option<u64>,
501}
502
503/// Arguments for sending input events.
504#[derive(Debug, Deserialize, JsonSchema)]
505pub struct SendInput {
506    /// The world to drive: `"Host"` for the world the transport thread itself
507    /// holds, or `{"Handle": bits}` for a world a host entity carries.
508    #[serde(default)]
509    pub world: WorldId,
510    /// The entity carrying the input state, as its `u64` bits. Omit for the
511    /// world's only one.
512    #[serde(default)]
513    pub entity: Option<u64>,
514    /// The events to deliver.
515    pub events: Vec<Value>,
516}
517
518/// Arguments for rendering a frame.
519#[derive(Debug, Deserialize, JsonSchema)]
520pub struct RendererArg {
521    /// The world to drive: `"Host"` for the world the transport thread itself
522    /// holds, or `{"Handle": bits}` for a world a host entity carries.
523    #[serde(default)]
524    pub world: WorldId,
525    /// The entity carrying the renderer, as its `u64` bits. Omit for the
526    /// world's only one.
527    #[serde(default)]
528    pub renderer: Option<u64>,
529}
530
531/// Arguments for taking a screenshot.
532#[derive(Debug, Deserialize, JsonSchema)]
533pub struct Screenshot {
534    /// The world to drive: `"Host"` for the world the transport thread itself
535    /// holds, or `{"Handle": bits}` for a world a host entity carries.
536    #[serde(default)]
537    pub world: WorldId,
538    /// The entity carrying the renderer, as its `u64` bits. Omit for the
539    /// world's only one.
540    #[serde(default)]
541    pub renderer: Option<u64>,
542    /// The id of the texture to read, as `list_resources` reports it, or the
543    /// id of any view of it. Omit for the world's only readable texture.
544    #[serde(default)]
545    pub target: Option<u64>,
546    /// The image format to answer with: `"webp"` (lossless, the default),
547    /// `"png"`, or `"jpeg"`.
548    #[serde(default)]
549    pub format: Option<String>,
550}
551
552/// Arguments for creating a mesh.
553#[derive(Debug, Deserialize, JsonSchema)]
554pub struct CreateMesh {
555    /// The world to drive: `"Host"` for the world the transport thread itself
556    /// holds, or `{"Handle": bits}` for a world a host entity carries.
557    #[serde(default)]
558    pub world: WorldId,
559    /// The scope to spawn into, as its `u64` bits. Omit for the entity
560    /// carrying the mesh source.
561    #[serde(default)]
562    pub scope: Option<u64>,
563    /// The entity carrying the world's mesh source, as its `u64` bits. Omit
564    /// for the world's only one.
565    #[serde(default)]
566    pub source: Option<u64>,
567    /// Vertex positions, three floats each.
568    pub positions: Vec<[f32; 3]>,
569    /// Vertex UVs, two floats each.
570    #[serde(default)]
571    pub uvs: Option<Vec<[f32; 2]>>,
572    /// Vertex colours, RGBA bytes each.
573    #[serde(default)]
574    pub colors: Option<Vec<[u8; 4]>>,
575    /// Vertex normals, three floats each. Omit to shade the surface by the
576    /// normals its positions give it.
577    #[serde(default)]
578    pub normals: Option<Vec<[f32; 3]>>,
579    /// Triangle indices.
580    #[serde(default)]
581    pub indices: Option<Vec<u32>>,
582    /// The entity's transform.
583    #[serde(default)]
584    pub transform: Option<MeshTransform>,
585    /// The instance colour; opaque white by default.
586    #[serde(default)]
587    pub color: Option<[f32; 4]>,
588    /// The alpha cutoff, when the mesh should be cut out.
589    #[serde(default)]
590    pub cutoff: Option<f32>,
591    /// How far the mesh's inverted hull is pushed along its normals, in world
592    /// units. Spawning one entity with this width beside a plain draw of the
593    /// same mesh is what outlines it; an entity without it draws no hull.
594    #[serde(default)]
595    pub hull_width: Option<f32>,
596}
597
598/// A transform as JSON.
599#[derive(Debug, Deserialize, JsonSchema)]
600pub struct MeshTransform {
601    /// The translation.
602    #[serde(default = "zero3")]
603    pub translation: [f32; 3],
604    /// The rotation quaternion, xyzw.
605    #[serde(default = "identity4")]
606    pub rotation: [f32; 4],
607    /// The scale.
608    #[serde(default = "one3")]
609    pub scale: [f32; 3],
610}
611
612fn zero3() -> [f32; 3] {
613    [0.0; 3]
614}
615
616fn one3() -> [f32; 3] {
617    [1.0; 3]
618}
619
620fn identity4() -> [f32; 4] {
621    [0.0, 0.0, 0.0, 1.0]
622}
623
624/// Arguments for load_gltf.
625#[derive(Debug, Deserialize, JsonSchema)]
626pub struct LoadGltf {
627    /// The world to drive: `"Host"` for the world the transport thread itself
628    /// holds, or `{"Handle": bits}` for a world a host entity carries.
629    #[serde(default)]
630    pub world: WorldId,
631    /// The scope to load into, as its `u64` bits. Omit for the entity
632    /// carrying the mesh source.
633    #[serde(default)]
634    pub scope: Option<u64>,
635    /// The entity carrying the world's mesh source, as its `u64` bits. Omit
636    /// for the world's only one.
637    #[serde(default)]
638    pub source: Option<u64>,
639    /// The path to the glTF or GLB file.
640    pub path: String,
641}