forked from 1b-agent/fieldcraft-upstream
--section x|y|z[=VALUE] hides the elements on the camera's side of a plane (at the mesh's middle by default) so the element faces inside show; --field contours a nodal field of the solved step (results.contour). The MCP server's headless screenshot draws with the viewport's pipeline on the shared device and its reply, like snapshot's, names the renderer. Refs #422 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
57 lines
2.8 KiB
Rust
57 lines
2.8 KiB
Rust
//! FieldCraft's MCP server (docs/mcp.md).
|
|
//!
|
|
//! JSON-RPC 2.0 over stdio, one message per line, implementing the Model Context Protocol's
|
|
//! lifecycle, tools and resources. Every tool runs engine commands (the same ones the UI,
|
|
//! scripts, the CLI and the control channel run) and never opens a dialog. A tool call is served
|
|
//! by a [`Backend`]: an in-process headless session ([`Headless`], pictures drawn with the CPU
|
|
//! rasterizer) or a running app reached through its control channel ([`Remote`],
|
|
//! docs/control-protocol.md), so a person can watch the agent work.
|
|
//!
|
|
//! The server treats its input as hostile: a message line is capped at [`MAX_LINE`] bytes and
|
|
//! answered with an error past it, a JSON-RPC batch at [`MAX_BATCH_MESSAGES`] messages, a
|
|
//! `batch` tool call at [`tools::MAX_STEPS`] commands, and tool arguments are checked against
|
|
//! the tool's schema before anything runs. Read and write roots ([`fieldcraft_io::roots`])
|
|
//! confine every file a tool touches.
|
|
//!
|
|
//! Adapted from SolidCraft's `crates/mcp` (MIT OR Apache-2.0), see ATTRIBUTION.md.
|
|
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
|
#![forbid(unsafe_code)]
|
|
|
|
mod backend;
|
|
mod server;
|
|
pub mod tools;
|
|
|
|
pub use backend::{Backend, Headless, Picture, PictureRequest, Remote, picture_report};
|
|
pub use server::{MAX_BATCH_MESSAGES, MAX_LINE, PROTOCOL_VERSION, SUPPORTED_VERSIONS, Server};
|
|
|
|
use fieldcraft_io::roots::Roots;
|
|
|
|
/// How the server is started (`fieldcraft-cli mcp`).
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct Options {
|
|
/// The control channel of a running app (`127.0.0.1:7070`); `None` runs headless.
|
|
pub connect: Option<String>,
|
|
/// Folders file access is confined to; unconfined by default.
|
|
pub roots: Roots,
|
|
}
|
|
|
|
/// The backend `options` ask for, and a note for stderr when the server falls back to headless
|
|
/// because the app cannot be reached. Confined roots with `connect` are refused: the app would
|
|
/// read and write files the roots do not cover.
|
|
pub fn backend(options: Options) -> Result<(Box<dyn Backend>, Option<String>), String> {
|
|
let headless = |roots: Roots| -> Box<dyn Backend> { Box::new(Headless::new(roots)) };
|
|
match options.connect {
|
|
Some(addr) if options.roots.is_confined() => Err(format!(
|
|
"--connect {addr} cannot be combined with --automation-read-root or --automation-write-root: the app is not confined by them (they imply --headless)"
|
|
)),
|
|
Some(addr) => match Remote::connect(&addr) {
|
|
Ok(r) => Ok((Box::new(r), None)),
|
|
Err(e) => Ok((headless(options.roots), Some(format!("FieldCraft at {addr} cannot be reached ({e}); running headless")))),
|
|
},
|
|
None => Ok((headless(options.roots), None)),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|