spreadsheet_mcp/runtime/
stateless.rs1use crate::config::{OutputProfile, RecalcBackendKind, ServerConfig, TransportKind};
2use crate::core;
3use crate::core::types::{CellEdit, RecalculateOutcome};
4use crate::model::WorkbookId;
5use crate::state::AppState;
6use crate::tools::filters::WorkbookFilter;
7use anyhow::{Result, anyhow};
8use serde_json::Value;
9use std::fs;
10use std::path::{Path, PathBuf};
11use std::sync::Arc;
12
13#[derive(Debug, Default, Clone)]
14pub struct StatelessRuntime;
15
16impl StatelessRuntime {
17 pub fn normalize_existing_file(&self, path: &Path) -> Result<PathBuf> {
18 core::read::normalize_existing_file(path)
19 }
20
21 pub fn normalize_destination_path(&self, path: &Path) -> Result<PathBuf> {
22 core::read::normalize_destination_path(path)
23 }
24
25 pub fn copy_file(&self, source: &Path, dest: &Path) -> Result<u64> {
26 fs::copy(source, dest).map_err(Into::into)
27 }
28
29 pub fn apply_edits(&self, path: &Path, sheet_name: &str, edits: &[CellEdit]) -> Result<()> {
30 core::write::apply_edits_to_file(path, sheet_name, edits)
31 }
32
33 pub fn diff_json(&self, original: &Path, modified: &Path) -> Result<Value> {
34 core::diff::diff_workbooks_json(original, modified)
35 }
36
37 pub async fn recalculate_file(&self, path: &Path) -> Result<RecalculateOutcome> {
38 #[cfg(not(feature = "recalc"))]
39 {
40 let _ = path;
41 core::recalc::unavailable()?;
42 unreachable!();
43 }
44
45 #[cfg(feature = "recalc")]
46 {
47 let backend = core::recalc::select_backend_from_env()?;
48 core::recalc::execute_with_backend(path, Some(30_000), backend).await
49 }
50 }
51
52 pub async fn open_state_for_file(&self, path: &Path) -> Result<(Arc<AppState>, WorkbookId)> {
53 let absolute = self.normalize_existing_file(path)?;
54 let config = Arc::new(self.build_cli_config(&absolute));
55 let state = Arc::new(AppState::new(config));
56
57 let workbook_list = state.list_workbooks(WorkbookFilter::default())?;
58 let workbook_id = workbook_list
59 .workbooks
60 .first()
61 .map(|entry| entry.workbook_id.clone())
62 .ok_or_else(|| anyhow!("no workbook found at '{}'", absolute.display()))?;
63 Ok((state, workbook_id))
64 }
65
66 fn build_cli_config(&self, file: &Path) -> ServerConfig {
67 let workspace_root = file
68 .parent()
69 .map(Path::to_path_buf)
70 .unwrap_or_else(|| PathBuf::from("."));
71 ServerConfig {
72 workspace_root,
73 screenshot_dir: PathBuf::from("screenshots"),
74 path_mappings: Vec::new(),
75 cache_capacity: 2,
76 supported_extensions: vec!["xlsx".into(), "xlsm".into(), "xls".into(), "xlsb".into()],
77 single_workbook: Some(file.to_path_buf()),
78 enabled_tools: None,
79 transport: TransportKind::Stdio,
80 http_bind_address: "127.0.0.1:8079"
81 .parse()
82 .expect("hardcoded bind address is valid"),
83 recalc_enabled: false,
84 recalc_backend: RecalcBackendKind::Auto,
85 vba_enabled: false,
86 max_concurrent_recalcs: 1,
87 tool_timeout_ms: Some(30_000),
88 max_response_bytes: Some(1_000_000),
89 output_profile: OutputProfile::Verbose,
90 max_payload_bytes: Some(65_536),
91 max_cells: Some(10_000),
92 max_items: Some(500),
93 allow_overwrite: true,
94 }
95 }
96}