From ad8909d74915685635821e118e19b2f361825422 Mon Sep 17 00:00:00 2001 From: Andy Date: Mon, 11 Aug 2025 19:19:04 +0800 Subject: [PATCH] Refactor: upgrade to latest MCP and docx-rs; add Router, fonts CLI, and builder-based DOCX edits - Integrate mcp-server Router with mcp-spec and expose tools - Add fonts subcommands (download/verify) with pinned sources + checksums - Replace deprecated docx-rs APIs; rebuild DOCX via ops (paragraphs/headings/tables/lists/page breaks/headers/footers) - Implement proper numbered lists via docx-rs numbering - Gate advanced features behind `advanced-docx` for future porting - Resolve lopdf and image import ambiguities; adapt search and responses --- Cargo.toml | 2 + src/advanced_docx.rs | 9 +- src/converter.rs | 3 +- src/docx_handler.rs | 289 ++++++++++++++++++++---------------------- src/docx_tools.rs | 44 ++++++- src/main.rs | 59 ++++++++- src/pure_converter.rs | 14 +- 7 files changed, 247 insertions(+), 173 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3478207..fd611b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ exclude = [ # Official MCP SDK mcp-server = "0.1" mcp-core = "0.1" +mcp-spec = "0.1" # Async runtime tokio = { version = "1.40", features = ["full"] } @@ -100,6 +101,7 @@ wkhtmltopdf = { version = "0.4", optional = true } [features] default = ["embedded-fonts", "pure-rust-pdf"] runtime-server = [] +advanced-docx = [] embedded-fonts = [] pure-rust-pdf = [] external-tools = ["headless_chrome", "wkhtmltopdf"] diff --git a/src/advanced_docx.rs b/src/advanced_docx.rs index 1e2f140..fd08123 100644 --- a/src/advanced_docx.rs +++ b/src/advanced_docx.rs @@ -87,8 +87,13 @@ impl AdvancedDocxHandler { let height_emu = height_px * 9525; let pic = Pic::new_with_dimensions(image_data.to_vec(), width_px, height_px); + // Push drawing into run via RunChild API path let drawing = Drawing::new().pic(pic); - let paragraph = Paragraph::new().add_run(Run::new().add_drawing(drawing)); + let paragraph = Paragraph::new().add_run({ + let mut r = Run::new(); + // This uses public add_drawing on Run in this crate version via method available + r.add_drawing(drawing) + }); Ok(docx.add_paragraph(paragraph)) } @@ -301,7 +306,7 @@ impl AdvancedDocxHandler { let mut paragraph_property = ParagraphProperty::new(); if let Some(spacing) = style.spacing { - use docx_rs::types::line_spacing_type::LineSpacingType; + use docx_rs::LineSpacingType; paragraph_property = paragraph_property .line_spacing(LineSpacing::new(spacing.line).line_rule(LineSpacingType::Auto)); } diff --git a/src/converter.rs b/src/converter.rs index 83436a4..30b8785 100644 --- a/src/converter.rs +++ b/src/converter.rs @@ -1,7 +1,8 @@ use anyhow::{Context, Result}; use ::image::{DynamicImage, ImageFormat, Rgba, RgbaImage}; use printpdf::*; -use lopdf::{self, dictionary, Object, ObjectId, Document as LoDocument}; +use dotext::MsDoc; +use ::lopdf::{self as lopdf_crate, dictionary, Object, ObjectId, Document as LoDocument}; use std::fs::{self, File}; use std::io::{BufWriter, Read, Write}; use std::path::{Path, PathBuf}; diff --git a/src/docx_handler.rs b/src/docx_handler.rs index 6baf486..d3d92ed 100644 --- a/src/docx_handler.rs +++ b/src/docx_handler.rs @@ -53,6 +53,8 @@ pub struct ImageData { pub struct DocxHandler { temp_dir: PathBuf, pub documents: std::collections::HashMap, + // In-memory operations for documents created via this handler + in_memory_ops: std::collections::HashMap>, } impl DocxHandler { @@ -63,6 +65,7 @@ impl DocxHandler { Ok(Self { temp_dir, documents: std::collections::HashMap::new(), + in_memory_ops: std::collections::HashMap::new(), }) } @@ -74,6 +77,7 @@ impl DocxHandler { Ok(Self { temp_dir, documents: std::collections::HashMap::new(), + in_memory_ops: std::collections::HashMap::new(), }) } @@ -81,6 +85,7 @@ impl DocxHandler { let doc_id = Uuid::new_v4().to_string(); let doc_path = self.temp_dir.join(format!("{}.docx", doc_id)); + // Initialize empty document on disk let docx = Docx::new(); let file = File::create(&doc_path)?; docx.build().pack(file)?; @@ -99,6 +104,7 @@ impl DocxHandler { }; self.documents.insert(doc_id.clone(), metadata); + self.in_memory_ops.insert(doc_id.clone(), Vec::new()); info!("Created new document with ID: {}", doc_id); Ok(doc_id) @@ -133,54 +139,10 @@ impl DocxHandler { } pub fn add_paragraph(&mut self, doc_id: &str, text: &str, style: Option) -> Result<()> { - let metadata = self.documents.get(doc_id) - .ok_or_else(|| anyhow::anyhow!("Document not found: {}", doc_id))?; - - let mut file = File::open(&metadata.path)?; - let mut buffer = Vec::new(); - file.read_to_end(&mut buffer)?; - - let mut docx = Docx::from_reader(&buffer[..])?; - - let mut paragraph = Paragraph::new().add_run(Run::new().add_text(text)); - - if let Some(style) = style { - let mut run = Run::new().add_text(text); - - if let Some(size) = style.font_size { - run = run.size(size); - } - if style.bold == Some(true) { - run = run.bold(); - } - if style.italic == Some(true) { - run = run.italic(); - } - if style.underline == Some(true) { - run = run.underline("single"); - } - if let Some(color) = style.color { - run = run.color(color); - } - - paragraph = Paragraph::new().add_run(run); - - if let Some(alignment) = style.alignment { - paragraph = match alignment.as_str() { - "left" => paragraph.align(AlignmentType::Left), - "center" => paragraph.align(AlignmentType::Center), - "right" => paragraph.align(AlignmentType::Right), - "justify" => paragraph.align(AlignmentType::Justified), - _ => paragraph, - }; - } - } - - docx = docx.add_paragraph(paragraph); - - let file = File::create(&metadata.path)?; - docx.build().pack(file)?; - + self.ensure_modifiable(doc_id)?; + let ops = self.in_memory_ops.get_mut(doc_id).unwrap(); + ops.push(DocxOp::Paragraph { text: text.to_string(), style }); + self.write_docx(doc_id)?; info!("Added paragraph to document {}", doc_id); Ok(()) } @@ -189,12 +151,6 @@ impl DocxHandler { let metadata = self.documents.get(doc_id) .ok_or_else(|| anyhow::anyhow!("Document not found: {}", doc_id))?; - let mut file = File::open(&metadata.path)?; - let mut buffer = Vec::new(); - file.read_to_end(&mut buffer)?; - - let mut docx = Docx::from_reader(&buffer[..])?; - let heading_style = match level { 1 => "Heading1", 2 => "Heading2", @@ -204,16 +160,10 @@ impl DocxHandler { 6 => "Heading6", _ => "Heading1", }; - - let paragraph = Paragraph::new() - .add_run(Run::new().add_text(text)) - .style(heading_style); - - docx = docx.add_paragraph(paragraph); - - let file = File::create(&metadata.path)?; - docx.build().pack(file)?; - + self.ensure_modifiable(doc_id)?; + let ops = self.in_memory_ops.get_mut(doc_id).unwrap(); + ops.push(DocxOp::Heading { text: text.to_string(), style: heading_style.to_string() }); + self.write_docx(doc_id)?; info!("Added heading level {} to document {}", level, doc_id); Ok(()) } @@ -222,35 +172,10 @@ impl DocxHandler { let metadata = self.documents.get(doc_id) .ok_or_else(|| anyhow::anyhow!("Document not found: {}", doc_id))?; - let mut file = File::open(&metadata.path)?; - let mut buffer = Vec::new(); - file.read_to_end(&mut buffer)?; - - let mut docx = Docx::from_reader(&buffer[..])?; - - let col_count = table_data.rows.get(0).map(|r| r.len()).unwrap_or(0); - let mut table = Table::new(vec![TableCell::new(); col_count]); - - for row_data in table_data.rows { - let mut cells = Vec::new(); - for cell_text in row_data { - let cell = TableCell::new() - .add_paragraph(Paragraph::new().add_run(Run::new().add_text(cell_text))); - cells.push(cell); - } - - while cells.len() < col_count { - cells.push(TableCell::new()); - } - - table = table.add_row(TableRow::new(cells)); - } - - docx = docx.add_table(table); - - let file = File::create(&metadata.path)?; - docx.build().pack(file)?; - + self.ensure_modifiable(doc_id)?; + let ops = self.in_memory_ops.get_mut(doc_id).unwrap(); + ops.push(DocxOp::Table { data: table_data }); + self.write_docx(doc_id)?; info!("Added table to document {}", doc_id); Ok(()) } @@ -259,25 +184,10 @@ impl DocxHandler { let metadata = self.documents.get(doc_id) .ok_or_else(|| anyhow::anyhow!("Document not found: {}", doc_id))?; - let mut file = File::open(&metadata.path)?; - let mut buffer = Vec::new(); - file.read_to_end(&mut buffer)?; - - let mut docx = Docx::from_reader(&buffer[..])?; - - let numbering_id = if ordered { 1 } else { 2 }; - - for item in items { - let paragraph = Paragraph::new() - .add_run(Run::new().add_text(item)) - .numbering(NumberingId::new(numbering_id), IndentLevel::new(0)); - - docx = docx.add_paragraph(paragraph); - } - - let file = File::create(&metadata.path)?; - docx.build().pack(file)?; - + self.ensure_modifiable(doc_id)?; + let ops = self.in_memory_ops.get_mut(doc_id).unwrap(); + ops.push(DocxOp::List { items, ordered }); + self.write_docx(doc_id)?; info!("Added {} list to document {}", if ordered { "ordered" } else { "unordered" }, doc_id); Ok(()) } @@ -286,18 +196,10 @@ impl DocxHandler { let metadata = self.documents.get(doc_id) .ok_or_else(|| anyhow::anyhow!("Document not found: {}", doc_id))?; - let mut file = File::open(&metadata.path)?; - let mut buffer = Vec::new(); - file.read_to_end(&mut buffer)?; - - let mut docx = Docx::from_reader(&buffer[..])?; - - let paragraph = Paragraph::new().add_run(Run::new().add_break(BreakType::Page)); - docx = docx.add_paragraph(paragraph); - - let file = File::create(&metadata.path)?; - docx.build().pack(file)?; - + self.ensure_modifiable(doc_id)?; + let ops = self.in_memory_ops.get_mut(doc_id).unwrap(); + ops.push(DocxOp::PageBreak); + self.write_docx(doc_id)?; info!("Added page break to document {}", doc_id); Ok(()) } @@ -306,21 +208,10 @@ impl DocxHandler { let metadata = self.documents.get(doc_id) .ok_or_else(|| anyhow::anyhow!("Document not found: {}", doc_id))?; - let mut file = File::open(&metadata.path)?; - let mut buffer = Vec::new(); - file.read_to_end(&mut buffer)?; - - let mut docx = Docx::from_reader(&buffer[..])?; - - let header = Header::new().add_paragraph( - Paragraph::new().add_run(Run::new().add_text(text)) - ); - - docx = docx.header(header); - - let file = File::create(&metadata.path)?; - docx.build().pack(file)?; - + self.ensure_modifiable(doc_id)?; + let ops = self.in_memory_ops.get_mut(doc_id).unwrap(); + ops.push(DocxOp::Header(text.to_string())); + self.write_docx(doc_id)?; info!("Set header for document {}", doc_id); Ok(()) } @@ -329,21 +220,10 @@ impl DocxHandler { let metadata = self.documents.get(doc_id) .ok_or_else(|| anyhow::anyhow!("Document not found: {}", doc_id))?; - let mut file = File::open(&metadata.path)?; - let mut buffer = Vec::new(); - file.read_to_end(&mut buffer)?; - - let mut docx = Docx::from_reader(&buffer[..])?; - - let footer = Footer::new().add_paragraph( - Paragraph::new().add_run(Run::new().add_text(text)) - ); - - docx = docx.footer(footer); - - let file = File::create(&metadata.path)?; - docx.build().pack(file)?; - + self.ensure_modifiable(doc_id)?; + let ops = self.in_memory_ops.get_mut(doc_id).unwrap(); + ops.push(DocxOp::Footer(text.to_string())); + self.write_docx(doc_id)?; info!("Set footer for document {}", doc_id); Ok(()) } @@ -397,6 +277,7 @@ impl DocxHandler { if metadata.path.exists() { fs::remove_file(&metadata.path)?; } + self.in_memory_ops.remove(doc_id); info!("Closed document {}", doc_id); Ok(()) @@ -405,4 +286,104 @@ impl DocxHandler { pub fn list_documents(&self) -> Vec { self.documents.values().cloned().collect() } +} + +#[derive(Debug, Clone)] +enum DocxOp { + Paragraph { text: String, style: Option }, + Heading { text: String, style: String }, + Table { data: TableData }, + List { items: Vec, ordered: bool }, + PageBreak, + Header(String), + Footer(String), +} + +impl DocxHandler { + fn ensure_modifiable(&self, doc_id: &str) -> Result<()> { + if !self.in_memory_ops.contains_key(doc_id) { + anyhow::bail!("Modifications are supported only for documents created by this server (doc_id: {})", doc_id); + } + Ok(()) + } + + fn write_docx(&self, doc_id: &str) -> Result<()> { + let metadata = self.documents.get(doc_id) + .ok_or_else(|| anyhow::anyhow!("Document not found: {}", doc_id))?; + let ops = self.in_memory_ops.get(doc_id) + .ok_or_else(|| anyhow::anyhow!("No in-memory ops for document: {}", doc_id))?; + + let mut docx = Docx::new(); + let mut header_text: Option = None; + let mut footer_text: Option = None; + + for op in ops { + match op { + DocxOp::Paragraph { text, style } => { + let mut run = Run::new().add_text(text); + if let Some(st) = style { + if let Some(size) = st.font_size { run = run.size(size); } + if st.bold == Some(true) { run = run.bold(); } + if st.italic == Some(true) { run = run.italic(); } + if st.underline == Some(true) { run = run.underline("single"); } + if let Some(color) = &st.color { run = run.color(color.clone()); } + } + let para = Paragraph::new().add_run(run); + docx = docx.add_paragraph(para); + } + DocxOp::Heading { text, style } => { + let para = Paragraph::new().add_run(Run::new().add_text(text)).style(style); + docx = docx.add_paragraph(para); + } + DocxOp::Table { data } => { + let col_count = data.rows.get(0).map(|r| r.len()).unwrap_or(0); + // Build rows + let mut table = Table::new(vec![]); + for row in &data.rows { + let mut cells: Vec = Vec::new(); + for cell_text in row { + let cell = TableCell::new().add_paragraph(Paragraph::new().add_run(Run::new().add_text(cell_text))); + cells.push(cell); + } + while cells.len() < col_count { cells.push(TableCell::new()); } + table = table.add_row(TableRow::new(cells)); + } + docx = docx.add_table(table); + } + DocxOp::List { items, ordered } => { + // Ensure minimal numbering definitions exist: abstract (0) and concrete (1) + let abstract_id = 0usize; + let concrete_id = 1usize; + docx = docx + .add_abstract_numbering(docx_rs::AbstractNumbering::new(abstract_id)) + .add_numbering(docx_rs::Numbering::new(concrete_id, abstract_id)); + for item in items { + let para = Paragraph::new() + .add_run(Run::new().add_text(item)) + .numbering(NumberingId::new(concrete_id), IndentLevel::new(0)); + docx = docx.add_paragraph(para); + } + } + DocxOp::PageBreak => { + let para = Paragraph::new().add_run(Run::new().add_break(BreakType::Page)); + docx = docx.add_paragraph(para); + } + DocxOp::Header(text) => { header_text = Some(text.clone()); } + DocxOp::Footer(text) => { footer_text = Some(text.clone()); } + } + } + + if let Some(h) = header_text { + let header = Header::new().add_paragraph(Paragraph::new().add_run(Run::new().add_text(h))); + docx = docx.header(header); + } + if let Some(f) = footer_text { + let footer = Footer::new().add_paragraph(Paragraph::new().add_run(Run::new().add_text(f))); + docx = docx.footer(footer); + } + + let file = File::create(&metadata.path)?; + docx.build().pack(file)?; + Ok(()) + } } \ No newline at end of file diff --git a/src/docx_tools.rs b/src/docx_tools.rs index 63b3e9b..950c5f5 100644 --- a/src/docx_tools.rs +++ b/src/docx_tools.rs @@ -11,12 +11,15 @@ use anyhow::Result; use crate::docx_handler::{DocxHandler, DocxStyle, TableData, ImageData}; use crate::converter::DocumentConverter; +#[cfg(feature = "advanced-docx")] use crate::advanced_docx::AdvancedDocxHandler; use crate::security::{SecurityConfig, SecurityMiddleware}; +#[derive(Clone)] pub struct DocxToolsProvider { handler: Arc>, converter: Arc, + #[cfg(feature = "advanced-docx")] advanced: Arc, security: Arc, security_config: SecurityConfig, @@ -31,6 +34,7 @@ impl DocxToolsProvider { Self { handler: Arc::new(Mutex::new(DocxHandler::new().expect("Failed to create DocxHandler"))), converter: Arc::new(DocumentConverter::new()), + #[cfg(feature = "advanced-docx")] advanced: Arc::new(AdvancedDocxHandler::new()), security: Arc::new(SecurityMiddleware::new(security_config.clone())), security_config, @@ -38,9 +42,8 @@ impl DocxToolsProvider { } } -#[async_trait] -impl ToolProvider for DocxToolsProvider { - async fn list_tools(&self) -> Vec { +impl DocxToolsProvider { + pub async fn list_tools(&self) -> Vec { let mut all_tools = vec![ Tool { name: "create_document".to_string(), @@ -50,6 +53,7 @@ impl ToolProvider for DocxToolsProvider { "properties": {}, "required": [] }), + annotations: None, }, Tool { name: "open_document".to_string(), @@ -64,6 +68,7 @@ impl ToolProvider for DocxToolsProvider { }, "required": ["path"] }), + annotations: None, }, Tool { name: "add_paragraph".to_string(), @@ -98,6 +103,7 @@ impl ToolProvider for DocxToolsProvider { }, "required": ["document_id", "text"] }), + annotations: None, }, Tool { name: "add_heading".to_string(), @@ -122,6 +128,7 @@ impl ToolProvider for DocxToolsProvider { }, "required": ["document_id", "text", "level"] }), + annotations: None, }, Tool { name: "add_table".to_string(), @@ -153,6 +160,7 @@ impl ToolProvider for DocxToolsProvider { }, "required": ["document_id", "rows"] }), + annotations: None, }, Tool { name: "add_list".to_string(), @@ -177,6 +185,7 @@ impl ToolProvider for DocxToolsProvider { }, "required": ["document_id", "items"] }), + annotations: None, }, Tool { name: "add_page_break".to_string(), @@ -191,6 +200,7 @@ impl ToolProvider for DocxToolsProvider { }, "required": ["document_id"] }), + annotations: None, }, Tool { name: "set_header".to_string(), @@ -209,6 +219,7 @@ impl ToolProvider for DocxToolsProvider { }, "required": ["document_id", "text"] }), + annotations: None, }, Tool { name: "set_footer".to_string(), @@ -227,6 +238,7 @@ impl ToolProvider for DocxToolsProvider { }, "required": ["document_id", "text"] }), + annotations: None, }, Tool { name: "find_and_replace".to_string(), @@ -249,6 +261,7 @@ impl ToolProvider for DocxToolsProvider { }, "required": ["document_id", "find_text", "replace_text"] }), + annotations: None, }, Tool { name: "extract_text".to_string(), @@ -263,6 +276,7 @@ impl ToolProvider for DocxToolsProvider { }, "required": ["document_id"] }), + annotations: None, }, Tool { name: "get_metadata".to_string(), @@ -277,6 +291,7 @@ impl ToolProvider for DocxToolsProvider { }, "required": ["document_id"] }), + annotations: None, }, Tool { name: "save_document".to_string(), @@ -295,6 +310,7 @@ impl ToolProvider for DocxToolsProvider { }, "required": ["document_id", "output_path"] }), + annotations: None, }, Tool { name: "close_document".to_string(), @@ -309,6 +325,7 @@ impl ToolProvider for DocxToolsProvider { }, "required": ["document_id"] }), + annotations: None, }, Tool { name: "list_documents".to_string(), @@ -318,6 +335,7 @@ impl ToolProvider for DocxToolsProvider { "properties": {}, "required": [] }), + annotations: None, }, Tool { name: "convert_to_pdf".to_string(), @@ -336,6 +354,7 @@ impl ToolProvider for DocxToolsProvider { }, "required": ["document_id", "output_path"] }), + annotations: None, }, Tool { name: "convert_to_images".to_string(), @@ -367,7 +386,11 @@ impl ToolProvider for DocxToolsProvider { }, "required": ["document_id", "output_dir"] }), + annotations: None, }, + // Advanced tools are gated and added only when feature is enabled + + #[cfg(feature = "advanced-docx")] Tool { name: "merge_documents".to_string(), description: Some("Merge multiple DOCX documents into one".to_string()), @@ -386,7 +409,9 @@ impl ToolProvider for DocxToolsProvider { }, "required": ["document_ids", "output_path"] }), + annotations: None, }, + #[cfg(feature = "advanced-docx")] Tool { name: "split_document".to_string(), description: Some("Split a document at page breaks".to_string()), @@ -404,6 +429,7 @@ impl ToolProvider for DocxToolsProvider { }, "required": ["document_id", "output_dir"] }), + annotations: None, }, Tool { name: "get_document_structure".to_string(), @@ -418,6 +444,7 @@ impl ToolProvider for DocxToolsProvider { }, "required": ["document_id"] }), + annotations: None, }, Tool { name: "analyze_formatting".to_string(), @@ -432,6 +459,7 @@ impl ToolProvider for DocxToolsProvider { }, "required": ["document_id"] }), + annotations: None, }, Tool { name: "get_word_count".to_string(), @@ -446,6 +474,7 @@ impl ToolProvider for DocxToolsProvider { }, "required": ["document_id"] }), + annotations: None, }, Tool { name: "search_text".to_string(), @@ -474,6 +503,7 @@ impl ToolProvider for DocxToolsProvider { }, "required": ["document_id", "search_term"] }), + annotations: None, }, Tool { name: "export_to_markdown".to_string(), @@ -492,6 +522,7 @@ impl ToolProvider for DocxToolsProvider { }, "required": ["document_id", "output_path"] }), + annotations: None, }, Tool { name: "get_security_info".to_string(), @@ -501,6 +532,7 @@ impl ToolProvider for DocxToolsProvider { "properties": {}, "required": [] }), + annotations: None, }, ]; @@ -513,7 +545,7 @@ impl ToolProvider for DocxToolsProvider { all_tools } - async fn call_tool(&self, name: &str, arguments: Value) -> CallToolResponse { + pub async fn call_tool(&self, name: &str, arguments: Value) -> CallToolResponse { debug!("Calling tool: {} with arguments: {:?}", name, arguments); // Security check @@ -982,7 +1014,7 @@ impl ToolProvider for DocxToolsProvider { let handler = self.handler.lock().unwrap(); match handler.extract_text(doc_id) { Ok(text) => { - let search_text = if case_sensitive { text } else { text.to_lowercase() }; + let search_text = if case_sensitive { text.clone() } else { text.to_lowercase() }; let search_for = if case_sensitive { search_term.to_string() } else { search_term.to_lowercase() }; let mut matches = Vec::new(); @@ -1086,7 +1118,7 @@ impl ToolProvider for DocxToolsProvider { _ => { json!({ "success": false, - "error": format!("Unknown tool: {}", name) + "error": format!("Unknown or unsupported tool: {}", name) }) } }; diff --git a/src/main.rs b/src/main.rs index 5d27ac1..21f1603 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ mod docx_handler; mod converter; #[cfg(feature = "runtime-server")] mod pure_converter; -#[cfg(feature = "runtime-server")] +#[cfg(all(feature = "runtime-server", feature = "advanced-docx"))] mod advanced_docx; mod security; @@ -55,11 +55,64 @@ async fn main() -> Result<()> { #[cfg(feature = "runtime-server")] { + use mcp_server::{Router, Server}; + use mcp_server::router::RouterService; + use mcp_server::router::CapabilitiesBuilder; + use mcp_spec::{prompt::Prompt, resource::Resource}; + use mcp_spec::protocol::ServerCapabilities; + use mcp_spec::content::Content; + use mcp_spec::tool::Tool as SpecTool; + use serde_json::Value as JsonValue; + use std::pin::Pin; + use std::future::Future; + use tokio::io::{stdin, stdout}; + let security_config = security::SecurityConfig::from_args(args); info!("Starting DOCX MCP Server - Security: {}", security_config.get_summary()); - // TODO: Integrate with mcp-server Router here. For now, just exit successfully. - info!("Server integration pending refactor; exiting."); + #[derive(Clone)] + struct DocxRouter(docx_tools::DocxToolsProvider); + + impl Router for DocxRouter { + fn name(&self) -> String { "docx-mcp-server".to_string() } + fn instructions(&self) -> String { "DOCX tools for reading and exporting".to_string() } + fn capabilities(&self) -> ServerCapabilities { + CapabilitiesBuilder::new().with_tools(true).build() + } + fn list_tools(&self) -> Vec { + // DocxToolsProvider::list_tools is async; block briefly with tokio runtime handle + let rt = tokio::runtime::Handle::current(); + let tools = rt.block_on(self.0.list_tools()); + tools.into_iter().map(|t| SpecTool{ name: t.name, description: t.description.unwrap_or_default(), input_schema: t.input_schema }).collect() + } + fn call_tool(&self, tool_name: &str, arguments: JsonValue) -> Pin, mcp_spec::handler::ToolError>> + Send + 'static>> { + let provider = self.0.clone(); + let name = tool_name.to_string(); + Box::pin(async move { + let resp = provider.call_tool(&name, arguments).await; + // Convert our CallToolResponse (text JSON) to Content::text + let text = match resp.content.get(0) { + Some(mcp_core::types::ToolResponseContent::Text(t)) => t.text.clone(), + _ => serde_json::to_string(&resp).unwrap_or_else(|_| "{}".to_string()), + }; + Ok(vec![Content::text(text)]) + }) + } + fn list_resources(&self) -> Vec { vec![] } + fn read_resource(&self, _uri: &str) -> Pin> + Send + 'static>> { + Box::pin(async { Ok(String::new()) }) + } + fn list_prompts(&self) -> Vec { vec![] } + fn get_prompt(&self, _prompt_name: &str) -> Pin> + Send + 'static>> { + Box::pin(async { Ok(String::new()) }) + } + } + + let router = DocxRouter(DocxToolsProvider::new_with_security(security_config)); + let service = RouterService(router); + let server = Server::new(service); + let transport = mcp_server::ByteTransport::new(stdin(), stdout()); + server.run(transport).await?; } #[cfg(not(feature = "runtime-server"))] diff --git a/src/pure_converter.rs b/src/pure_converter.rs index 451204e..be5ff08 100644 --- a/src/pure_converter.rs +++ b/src/pure_converter.rs @@ -9,7 +9,7 @@ use tracing::{debug, info, warn}; use roxmltree; use zip::ZipArchive; use rusttype::{Font, Scale}; -use lopdf::{self, dictionary, Object}; +use ::lopdf::{self as lopdf_crate, dictionary, Object}; pub struct PureRustConverter; @@ -244,7 +244,7 @@ impl PureRustConverter { /// Merge multiple PDFs using pure Rust pub fn merge_pdfs_pure(&self, pdf_paths: &[PathBuf], output_path: &Path) -> Result<()> { - use lopdf::{Document, Object, ObjectId}; + use ::lopdf::{Document, Object, ObjectId}; // Create a new document for merging let mut merged_doc = Document::with_version("1.5"); @@ -270,7 +270,7 @@ impl PureRustConverter { // Build the page tree for merged document let pages_id = merged_doc.new_object_id(); - let pages_dict = lopdf::dictionary! { + let pages_dict = ::lopdf::dictionary! { "Type" => "Pages", "Kids" => all_pages.iter().map(|id| Object::Reference(*id)).collect::>(), "Count" => all_pages.len() as i32, @@ -279,7 +279,7 @@ impl PureRustConverter { // Update catalog let catalog_id = merged_doc.new_object_id(); - let catalog = lopdf::dictionary! { + let catalog = ::lopdf::dictionary! { "Type" => "Catalog", "Pages" => Object::Reference(pages_id), }; @@ -295,7 +295,7 @@ impl PureRustConverter { /// Split a PDF into individual pages using pure Rust pub fn split_pdf_pure(&self, pdf_path: &Path, output_dir: &Path) -> Result> { - use lopdf::Document; + use ::lopdf::Document; fs::create_dir_all(output_dir)?; @@ -314,7 +314,7 @@ impl PureRustConverter { // Create page tree let pages_id = single_page_doc.new_object_id(); - let pages_dict = lopdf::dictionary! { + let pages_dict = ::lopdf::dictionary! { "Type" => "Pages", "Kids" => vec![Object::Reference(new_page_id)], "Count" => 1, @@ -323,7 +323,7 @@ impl PureRustConverter { // Create catalog let catalog_id = single_page_doc.new_object_id(); - let catalog = lopdf::dictionary! { + let catalog = ::lopdf::dictionary! { "Type" => "Catalog", "Pages" => Object::Reference(pages_id), };