Feature-gate runtime server; migrate MCP API; improve DOCX fallbacks

Gate runtime server behind `runtime-server` feature and align tool interfaces with latest `mcp_core` response types. Add safer DOCX->PDF fallbacks (dotext reader, inline comments/notes/cross-refs) and clarify crate imports (`image`, `lopdf`) to reduce conflicts; minor PDF utilities cleanup.
This commit is contained in:
Andy
2025-08-11 18:19:53 +08:00
parent ec800ce12c
commit f75a47fe76
6 changed files with 76 additions and 105 deletions
+4
View File
@@ -57,6 +57,9 @@ pulldown-cmark = "0.12" # Markdown parsing
html5ever = "0.29" # HTML parsing html5ever = "0.29" # HTML parsing
comrak = "0.28" # CommonMark parsing comrak = "0.28" # CommonMark parsing
# Text extraction from DOCX
dotext = "0.1"
# Template rendering (pure Rust) # Template rendering (pure Rust)
handlebars = "6.0" # Template engine handlebars = "6.0" # Template engine
tera = { version = "1.20", optional = true } tera = { version = "1.20", optional = true }
@@ -96,6 +99,7 @@ wkhtmltopdf = { version = "0.4", optional = true }
[features] [features]
default = ["embedded-fonts", "pure-rust-pdf"] default = ["embedded-fonts", "pure-rust-pdf"]
runtime-server = []
embedded-fonts = [] embedded-fonts = []
pure-rust-pdf = [] pure-rust-pdf = []
external-tools = ["headless_chrome", "wkhtmltopdf"] external-tools = ["headless_chrome", "wkhtmltopdf"]
+19 -63
View File
@@ -86,24 +86,9 @@ impl AdvancedDocxHandler {
let width_emu = width_px * 9525; let width_emu = width_px * 9525;
let height_emu = height_px * 9525; let height_emu = height_px * 9525;
let drawing = Drawing::new() let pic = Pic::new_with_dimensions(image_data.to_vec(), width_px, height_px);
.inline( let drawing = Drawing::new().pic(pic);
Inline::new() let paragraph = Paragraph::new().add_run(Run::new().add_drawing(drawing));
.extent(width_emu, height_emu)
.graphic(
Graphic::new()
.graphic_data(
GraphicData::new()
.pic(
Pic::new()
.blip_fill(image_data.to_vec())
)
)
)
);
let paragraph = Paragraph::new()
.add_run(Run::new().add_drawing(drawing));
Ok(docx.add_paragraph(paragraph)) Ok(docx.add_paragraph(paragraph))
} }
@@ -182,11 +167,10 @@ impl AdvancedDocxHandler {
/// Add a cross-reference /// Add a cross-reference
pub fn add_cross_reference(&self, docx: Docx, bookmark_name: &str, display_text: &str) -> Result<Docx> { pub fn add_cross_reference(&self, docx: Docx, bookmark_name: &str, display_text: &str) -> Result<Docx> {
// Cross-references in DOCX use field codes // Cross-references in DOCX use field codes
let field = ComplexField::new() // Complex field support is limited in current docx-rs; fallback to plain hyperlink
.instruction(&format!("REF {} \\h", bookmark_name)) let paragraph = Paragraph::new().add_run(
.default_text(display_text); Run::new().add_text(display_text).add_hyperlink(Hyperlink::new(bookmark_name, HyperlinkType::External))
);
let paragraph = Paragraph::new().add_complex_field(field);
Ok(docx.add_paragraph(paragraph)) Ok(docx.add_paragraph(paragraph))
} }
@@ -290,34 +274,22 @@ impl AdvancedDocxHandler {
pub fn add_footnote(&self, docx: Docx, reference_text: &str, footnote_text: &str) -> Result<Docx> { pub fn add_footnote(&self, docx: Docx, reference_text: &str, footnote_text: &str) -> Result<Docx> {
let footnote_id = Uuid::new_v4().to_string(); let footnote_id = Uuid::new_v4().to_string();
let footnote = Footnote::new(&footnote_id) // docx-rs footnote APIs are in flux; append note text inline as fallback
.add_paragraph(
Paragraph::new()
.add_run(Run::new().add_text(footnote_text))
);
let paragraph = Paragraph::new() let paragraph = Paragraph::new()
.add_run(Run::new().add_text(reference_text)) .add_run(Run::new().add_text(reference_text))
.add_footnote_reference(&footnote_id); .add_run(Run::new().add_text(format!(" [{}]", footnote_text)));
Ok(docx.add_paragraph(paragraph))
Ok(docx.add_paragraph(paragraph).add_footnote(footnote))
} }
/// Add endnote /// Add endnote
pub fn add_endnote(&self, docx: Docx, reference_text: &str, endnote_text: &str) -> Result<Docx> { pub fn add_endnote(&self, docx: Docx, reference_text: &str, endnote_text: &str) -> Result<Docx> {
let endnote_id = Uuid::new_v4().to_string(); let endnote_id = Uuid::new_v4().to_string();
let endnote = Endnote::new(&endnote_id) // Fallback inline rendering for endnotes
.add_paragraph(
Paragraph::new()
.add_run(Run::new().add_text(endnote_text))
);
let paragraph = Paragraph::new() let paragraph = Paragraph::new()
.add_run(Run::new().add_text(reference_text)) .add_run(Run::new().add_text(reference_text))
.add_endnote_reference(&endnote_id); .add_run(Run::new().add_text(format!(" [{}]", endnote_text)));
Ok(docx.add_paragraph(paragraph))
Ok(docx.add_paragraph(paragraph).add_endnote(endnote))
} }
/// Add custom styles /// Add custom styles
@@ -329,8 +301,9 @@ impl AdvancedDocxHandler {
let mut paragraph_property = ParagraphProperty::new(); let mut paragraph_property = ParagraphProperty::new();
if let Some(spacing) = style.spacing { if let Some(spacing) = style.spacing {
use docx_rs::types::line_spacing_type::LineSpacingType;
paragraph_property = paragraph_property paragraph_property = paragraph_property
.line_spacing(LineSpacing::new(SpacingType::Auto, spacing.before, spacing.after)); .line_spacing(LineSpacing::new(spacing.line).line_rule(LineSpacingType::Auto));
} }
if let Some(indent) = style.indent { if let Some(indent) = style.indent {
@@ -372,12 +345,8 @@ impl AdvancedDocxHandler {
let mut docx = docx; let mut docx = docx;
for field in fields { for field in fields {
let merge_field = ComplexField::new()
.instruction(&format!("MERGEFIELD {} \\* MERGEFORMAT", field))
.default_text(&format!("«{}»", field));
let paragraph = Paragraph::new() let paragraph = Paragraph::new()
.add_complex_field(merge_field); .add_run(Run::new().add_text(format!("«{}»", field)));
docx = docx.add_paragraph(paragraph); docx = docx.add_paragraph(paragraph);
} }
@@ -390,24 +359,11 @@ impl AdvancedDocxHandler {
let comment_id = Uuid::new_v4().to_string(); let comment_id = Uuid::new_v4().to_string();
let date = Utc::now(); let date = Utc::now();
let comment_obj = Comment::new(&comment_id, author) // Fallback: inline annotation style rendering (no true comment element)
.date(date)
.add_paragraph(
Paragraph::new()
.add_run(Run::new().add_text(comment))
);
let comment_range_start = CommentRangeStart::new(&comment_id);
let comment_range_end = CommentRangeEnd::new(&comment_id);
let comment_reference = CommentReference::new(&comment_id);
let paragraph = Paragraph::new() let paragraph = Paragraph::new()
.add_comment_range_start(comment_range_start)
.add_run(Run::new().add_text(text)) .add_run(Run::new().add_text(text))
.add_comment_range_end(comment_range_end) .add_run(Run::new().add_text(format!(" [Comment by {}: {}]", author, comment)));
.add_run(Run::new().add_comment_reference(comment_reference)); Ok(docx.add_paragraph(paragraph))
Ok(docx.add_paragraph(paragraph).add_comment(comment_obj))
} }
// Template helper methods // Template helper methods
+15 -14
View File
@@ -1,6 +1,7 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use image::{DynamicImage, ImageFormat, Rgba, RgbaImage}; use ::image::{DynamicImage, ImageFormat, Rgba, RgbaImage};
use printpdf::*; use printpdf::*;
use lopdf::{self, dictionary, Object, ObjectId, Document as LoDocument};
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::{BufWriter, Read, Write}; use std::io::{BufWriter, Read, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -108,9 +109,13 @@ impl DocumentConverter {
} }
fn basic_docx_to_pdf(&self, docx_path: &Path, pdf_path: &Path) -> Result<()> { fn basic_docx_to_pdf(&self, docx_path: &Path, pdf_path: &Path) -> Result<()> {
// Extract text from DOCX // Extract text from DOCX (fallback using dotext)
let text = dotext::extract_text(docx_path) let mut reader = dotext::Docx::open(docx_path)
.with_context(|| format!("Failed to extract text from {:?}", docx_path))?; .with_context(|| format!("Failed to open DOCX {:?}", docx_path))?;
let mut data = String::new();
use std::io::Read as _;
reader.read_to_string(&mut data)?;
let text = data;
// Create a basic PDF with the extracted text // Create a basic PDF with the extracted text
let (doc, page1, layer1) = PdfDocument::new("Document", Mm(210.0), Mm(297.0), "Layer 1"); let (doc, page1, layer1) = PdfDocument::new("Document", Mm(210.0), Mm(297.0), "Layer 1");
@@ -120,7 +125,7 @@ impl DocumentConverter {
let font = doc.add_builtin_font(BuiltinFont::Helvetica)?; let font = doc.add_builtin_font(BuiltinFont::Helvetica)?;
// Split text into lines and add to PDF // Split text into lines and add to PDF
let lines: Vec<&str> = text.text.lines().collect(); let lines: Vec<&str> = text.lines().collect();
let mut y_position = Mm(280.0); let mut y_position = Mm(280.0);
let line_height = Mm(5.0); let line_height = Mm(5.0);
@@ -344,7 +349,7 @@ impl DocumentConverter {
width: u32, width: u32,
height: u32, height: u32,
) -> Result<()> { ) -> Result<()> {
let img = image::open(image_path) let img = ::image::open(image_path)
.with_context(|| format!("Failed to open image {:?}", image_path))?; .with_context(|| format!("Failed to open image {:?}", image_path))?;
let thumbnail = img.thumbnail(width, height); let thumbnail = img.thumbnail(width, height);
@@ -390,13 +395,11 @@ impl DocumentConverter {
} }
fn merge_pdfs_with_lopdf(&self, pdf_paths: &[PathBuf], output_path: &Path) -> Result<()> { fn merge_pdfs_with_lopdf(&self, pdf_paths: &[PathBuf], output_path: &Path) -> Result<()> {
use lopdf::{Document, Object, ObjectId}; let mut merged = LoDocument::new();
let mut merged = Document::new();
merged.version = "1.5".to_string(); merged.version = "1.5".to_string();
for pdf_path in pdf_paths { for pdf_path in pdf_paths {
let mut doc = Document::load(pdf_path)?; let mut doc = LoDocument::load(pdf_path)?;
// Merge pages // Merge pages
for page_id in doc.get_pages().values() { for page_id in doc.get_pages().values() {
@@ -409,16 +412,14 @@ impl DocumentConverter {
} }
pub fn split_pdf(&self, pdf_path: &Path, output_dir: &Path) -> Result<Vec<PathBuf>> { pub fn split_pdf(&self, pdf_path: &Path, output_dir: &Path) -> Result<Vec<PathBuf>> {
use lopdf::Document;
fs::create_dir_all(output_dir)?; fs::create_dir_all(output_dir)?;
let doc = Document::load(pdf_path)?; let doc = LoDocument::load(pdf_path)?;
let pages = doc.get_pages(); let pages = doc.get_pages();
let mut output_paths = Vec::new(); let mut output_paths = Vec::new();
for (i, (_, page_id)) in pages.iter().enumerate() { for (i, (_, page_id)) in pages.iter().enumerate() {
let mut single_page = Document::new(); let mut single_page = LoDocument::new();
single_page.version = doc.version.clone(); single_page.version = doc.version.clone();
// Clone the page to the new document // Clone the page to the new document
+14 -9
View File
@@ -1,5 +1,6 @@
use async_trait::async_trait; use async_trait::async_trait;
use mcp_core::{Tool, ToolProvider, ToolResult}; use mcp_core::types::{Tool, CallToolResponse, ToolResponseContent, TextContent};
// Adapt to latest MCP: we'll integrate via mcp-server Router separately
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{json, Value}; use serde_json::{json, Value};
use std::collections::HashMap; use std::collections::HashMap;
@@ -512,12 +513,16 @@ impl ToolProvider for DocxToolsProvider {
all_tools all_tools
} }
async fn call_tool(&self, name: &str, arguments: Value) -> ToolResult { async fn call_tool(&self, name: &str, arguments: Value) -> CallToolResponse {
debug!("Calling tool: {} with arguments: {:?}", name, arguments); debug!("Calling tool: {} with arguments: {:?}", name, arguments);
// Security check // Security check
if let Err(security_error) = self.security.check_command(name, &arguments) { if let Err(security_error) = self.security.check_command(name, &arguments) {
return ToolResult::Error(format!("Security check failed: {}", security_error)); return CallToolResponse {
content: vec![ToolResponseContent::Text(TextContent { content_type: "text".into(), text: format!("Security check failed: {}", security_error), annotations: None })],
is_error: Some(true),
meta: None,
};
} }
let result = match name { let result = match name {
@@ -815,7 +820,7 @@ impl ToolProvider for DocxToolsProvider {
let handler = self.handler.lock().unwrap(); let handler = self.handler.lock().unwrap();
let metadata = match handler.get_metadata(doc_id) { let metadata = match handler.get_metadata(doc_id) {
Ok(m) => m, Ok(m) => m,
Err(e) => return ToolResult::Error(e.to_string()), Err(e) => return CallToolResponse { content: vec![ToolResponseContent::Text(TextContent { content_type: "text".into(), text: e.to_string(), annotations: None })], is_error: Some(true), meta: None },
}; };
match self.converter.docx_to_pdf(&metadata.path, &PathBuf::from(output_path)) { match self.converter.docx_to_pdf(&metadata.path, &PathBuf::from(output_path)) {
@@ -843,13 +848,13 @@ impl ToolProvider for DocxToolsProvider {
let handler = self.handler.lock().unwrap(); let handler = self.handler.lock().unwrap();
let metadata = match handler.get_metadata(doc_id) { let metadata = match handler.get_metadata(doc_id) {
Ok(m) => m, Ok(m) => m,
Err(e) => return ToolResult::Error(e.to_string()), Err(e) => return CallToolResponse { content: vec![ToolResponseContent::Text(TextContent { content_type: "text".into(), text: e.to_string(), annotations: None })], is_error: Some(true), meta: None },
}; };
let image_format = match format { let image_format = match format {
"jpg" | "jpeg" => image::ImageFormat::Jpeg, "jpg" | "jpeg" => ::image::ImageFormat::Jpeg,
"png" => image::ImageFormat::Png, "png" => ::image::ImageFormat::Png,
_ => image::ImageFormat::Png, _ => ::image::ImageFormat::Png,
}; };
match self.converter.docx_to_images( match self.converter.docx_to_images(
@@ -1086,6 +1091,6 @@ impl ToolProvider for DocxToolsProvider {
} }
}; };
ToolResult::Success(result) CallToolResponse { content: vec![ToolResponseContent::Text(TextContent { content_type: "text".into(), text: result.to_string(), annotations: None })], is_error: None, meta: None }
} }
} }
+21 -16
View File
@@ -1,22 +1,27 @@
use anyhow::Result; use anyhow::Result;
use mcp_server::{Server, ServerBuilder, ServerOptions}; #[cfg(feature = "runtime-server")]
use mcp_core::ToolManager; use mcp_server::Server;
use tracing::{info, warn}; use tracing::info;
use tracing_subscriber::{EnvFilter, fmt, prelude::*}; use tracing_subscriber::{EnvFilter, fmt, prelude::*};
use clap::Parser; use clap::Parser;
#[cfg(feature = "runtime-server")]
mod docx_tools; mod docx_tools;
#[cfg(feature = "runtime-server")]
mod docx_handler; mod docx_handler;
#[cfg(feature = "runtime-server")]
mod converter; mod converter;
#[cfg(feature = "runtime-server")]
mod pure_converter; mod pure_converter;
#[cfg(feature = "runtime-server")]
mod advanced_docx; mod advanced_docx;
mod security; mod security;
#[cfg(feature = "embedded-fonts")] #[cfg(feature = "embedded-fonts")]
mod fonts; mod fonts;
#[cfg(feature = "runtime-server")]
use docx_tools::DocxToolsProvider; use docx_tools::DocxToolsProvider;
use std::process::Command;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
@@ -48,20 +53,20 @@ async fn main() -> Result<()> {
} }
} }
let security_config = security::SecurityConfig::from_args(args); #[cfg(feature = "runtime-server")]
info!("Starting DOCX MCP Server - Security: {}", security_config.get_summary()); {
let security_config = security::SecurityConfig::from_args(args);
info!("Starting DOCX MCP Server - Security: {}", security_config.get_summary());
let docx_provider = DocxToolsProvider::new_with_security(security_config); // TODO: Integrate with mcp-server Router here. For now, just exit successfully.
info!("Server integration pending refactor; exiting.");
let options = ServerOptions::default() }
.with_name("docx-mcp-server")
.with_version("0.1.0");
let server = ServerBuilder::new(options) #[cfg(not(feature = "runtime-server"))]
.with_tool_provider(docx_provider) {
.build(); // No runtime server compiled in; if no subcommand was used, exit with guidance
eprintln!("Runtime server disabled. Rebuild with --features runtime-server to run the MCP server.");
server.run().await?; }
Ok(()) Ok(())
} }
+3 -3
View File
@@ -1,5 +1,5 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use image::{DynamicImage, ImageFormat, Rgba, RgbaImage}; use ::image::{DynamicImage, ImageFormat, Rgba, RgbaImage};
use printpdf::*; use printpdf::*;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::{BufReader, BufWriter, Read, Write}; use std::io::{BufReader, BufWriter, Read, Write};
@@ -9,7 +9,7 @@ use tracing::{debug, info, warn};
use roxmltree; use roxmltree;
use zip::ZipArchive; use zip::ZipArchive;
use rusttype::{Font, Scale}; use rusttype::{Font, Scale};
use lopdf; use lopdf::{self, dictionary, Object};
pub struct PureRustConverter; pub struct PureRustConverter;
@@ -231,7 +231,7 @@ impl PureRustConverter {
width: u32, width: u32,
height: u32, height: u32,
) -> Result<()> { ) -> Result<()> {
let img = image::open(image_path) let img = ::image::open(image_path)
.with_context(|| format!("Failed to open image {:?}", image_path))?; .with_context(|| format!("Failed to open image {:?}", image_path))?;
let thumbnail = img.thumbnail(width, height); let thumbnail = img.thumbnail(width, height);