Skip to main content

spreadsheet_mcp/runtime/
stateless.rs

1use 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}