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:
@@ -57,6 +57,9 @@ pulldown-cmark = "0.12" # Markdown parsing
|
||||
html5ever = "0.29" # HTML parsing
|
||||
comrak = "0.28" # CommonMark parsing
|
||||
|
||||
# Text extraction from DOCX
|
||||
dotext = "0.1"
|
||||
|
||||
# Template rendering (pure Rust)
|
||||
handlebars = "6.0" # Template engine
|
||||
tera = { version = "1.20", optional = true }
|
||||
@@ -96,6 +99,7 @@ wkhtmltopdf = { version = "0.4", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["embedded-fonts", "pure-rust-pdf"]
|
||||
runtime-server = []
|
||||
embedded-fonts = []
|
||||
pure-rust-pdf = []
|
||||
external-tools = ["headless_chrome", "wkhtmltopdf"]
|
||||
|
||||
+19
-63
@@ -86,24 +86,9 @@ impl AdvancedDocxHandler {
|
||||
let width_emu = width_px * 9525;
|
||||
let height_emu = height_px * 9525;
|
||||
|
||||
let drawing = Drawing::new()
|
||||
.inline(
|
||||
Inline::new()
|
||||
.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));
|
||||
let pic = Pic::new_with_dimensions(image_data.to_vec(), width_px, height_px);
|
||||
let drawing = Drawing::new().pic(pic);
|
||||
let paragraph = Paragraph::new().add_run(Run::new().add_drawing(drawing));
|
||||
|
||||
Ok(docx.add_paragraph(paragraph))
|
||||
}
|
||||
@@ -182,11 +167,10 @@ impl AdvancedDocxHandler {
|
||||
/// Add a cross-reference
|
||||
pub fn add_cross_reference(&self, docx: Docx, bookmark_name: &str, display_text: &str) -> Result<Docx> {
|
||||
// Cross-references in DOCX use field codes
|
||||
let field = ComplexField::new()
|
||||
.instruction(&format!("REF {} \\h", bookmark_name))
|
||||
.default_text(display_text);
|
||||
|
||||
let paragraph = Paragraph::new().add_complex_field(field);
|
||||
// Complex field support is limited in current docx-rs; fallback to plain hyperlink
|
||||
let paragraph = Paragraph::new().add_run(
|
||||
Run::new().add_text(display_text).add_hyperlink(Hyperlink::new(bookmark_name, HyperlinkType::External))
|
||||
);
|
||||
|
||||
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> {
|
||||
let footnote_id = Uuid::new_v4().to_string();
|
||||
|
||||
let footnote = Footnote::new(&footnote_id)
|
||||
.add_paragraph(
|
||||
Paragraph::new()
|
||||
.add_run(Run::new().add_text(footnote_text))
|
||||
);
|
||||
|
||||
// docx-rs footnote APIs are in flux; append note text inline as fallback
|
||||
let paragraph = Paragraph::new()
|
||||
.add_run(Run::new().add_text(reference_text))
|
||||
.add_footnote_reference(&footnote_id);
|
||||
|
||||
Ok(docx.add_paragraph(paragraph).add_footnote(footnote))
|
||||
.add_run(Run::new().add_text(format!(" [{}]", footnote_text)));
|
||||
Ok(docx.add_paragraph(paragraph))
|
||||
}
|
||||
|
||||
/// Add endnote
|
||||
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 = Endnote::new(&endnote_id)
|
||||
.add_paragraph(
|
||||
Paragraph::new()
|
||||
.add_run(Run::new().add_text(endnote_text))
|
||||
);
|
||||
|
||||
// Fallback inline rendering for endnotes
|
||||
let paragraph = Paragraph::new()
|
||||
.add_run(Run::new().add_text(reference_text))
|
||||
.add_endnote_reference(&endnote_id);
|
||||
|
||||
Ok(docx.add_paragraph(paragraph).add_endnote(endnote))
|
||||
.add_run(Run::new().add_text(format!(" [{}]", endnote_text)));
|
||||
Ok(docx.add_paragraph(paragraph))
|
||||
}
|
||||
|
||||
/// Add custom styles
|
||||
@@ -329,8 +301,9 @@ impl AdvancedDocxHandler {
|
||||
let mut paragraph_property = ParagraphProperty::new();
|
||||
|
||||
if let Some(spacing) = style.spacing {
|
||||
use docx_rs::types::line_spacing_type::LineSpacingType;
|
||||
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 {
|
||||
@@ -372,12 +345,8 @@ impl AdvancedDocxHandler {
|
||||
let mut docx = docx;
|
||||
|
||||
for field in fields {
|
||||
let merge_field = ComplexField::new()
|
||||
.instruction(&format!("MERGEFIELD {} \\* MERGEFORMAT", field))
|
||||
.default_text(&format!("«{}»", field));
|
||||
|
||||
let paragraph = Paragraph::new()
|
||||
.add_complex_field(merge_field);
|
||||
.add_run(Run::new().add_text(format!("«{}»", field)));
|
||||
|
||||
docx = docx.add_paragraph(paragraph);
|
||||
}
|
||||
@@ -390,24 +359,11 @@ impl AdvancedDocxHandler {
|
||||
let comment_id = Uuid::new_v4().to_string();
|
||||
let date = Utc::now();
|
||||
|
||||
let comment_obj = Comment::new(&comment_id, author)
|
||||
.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);
|
||||
|
||||
// Fallback: inline annotation style rendering (no true comment element)
|
||||
let paragraph = Paragraph::new()
|
||||
.add_comment_range_start(comment_range_start)
|
||||
.add_run(Run::new().add_text(text))
|
||||
.add_comment_range_end(comment_range_end)
|
||||
.add_run(Run::new().add_comment_reference(comment_reference));
|
||||
|
||||
Ok(docx.add_paragraph(paragraph).add_comment(comment_obj))
|
||||
.add_run(Run::new().add_text(format!(" [Comment by {}: {}]", author, comment)));
|
||||
Ok(docx.add_paragraph(paragraph))
|
||||
}
|
||||
|
||||
// Template helper methods
|
||||
|
||||
+15
-14
@@ -1,6 +1,7 @@
|
||||
use anyhow::{Context, Result};
|
||||
use image::{DynamicImage, ImageFormat, Rgba, RgbaImage};
|
||||
use ::image::{DynamicImage, ImageFormat, Rgba, RgbaImage};
|
||||
use printpdf::*;
|
||||
use lopdf::{self, dictionary, Object, ObjectId, Document as LoDocument};
|
||||
use std::fs::{self, File};
|
||||
use std::io::{BufWriter, Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -108,9 +109,13 @@ impl DocumentConverter {
|
||||
}
|
||||
|
||||
fn basic_docx_to_pdf(&self, docx_path: &Path, pdf_path: &Path) -> Result<()> {
|
||||
// Extract text from DOCX
|
||||
let text = dotext::extract_text(docx_path)
|
||||
.with_context(|| format!("Failed to extract text from {:?}", docx_path))?;
|
||||
// Extract text from DOCX (fallback using dotext)
|
||||
let mut reader = dotext::Docx::open(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
|
||||
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)?;
|
||||
|
||||
// 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 line_height = Mm(5.0);
|
||||
|
||||
@@ -344,7 +349,7 @@ impl DocumentConverter {
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<()> {
|
||||
let img = image::open(image_path)
|
||||
let img = ::image::open(image_path)
|
||||
.with_context(|| format!("Failed to open image {:?}", image_path))?;
|
||||
|
||||
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<()> {
|
||||
use lopdf::{Document, Object, ObjectId};
|
||||
|
||||
let mut merged = Document::new();
|
||||
let mut merged = LoDocument::new();
|
||||
merged.version = "1.5".to_string();
|
||||
|
||||
for pdf_path in pdf_paths {
|
||||
let mut doc = Document::load(pdf_path)?;
|
||||
let mut doc = LoDocument::load(pdf_path)?;
|
||||
|
||||
// Merge pages
|
||||
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>> {
|
||||
use lopdf::Document;
|
||||
|
||||
fs::create_dir_all(output_dir)?;
|
||||
|
||||
let doc = Document::load(pdf_path)?;
|
||||
let doc = LoDocument::load(pdf_path)?;
|
||||
let pages = doc.get_pages();
|
||||
let mut output_paths = Vec::new();
|
||||
|
||||
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();
|
||||
|
||||
// Clone the page to the new document
|
||||
|
||||
+14
-9
@@ -1,5 +1,6 @@
|
||||
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_json::{json, Value};
|
||||
use std::collections::HashMap;
|
||||
@@ -512,12 +513,16 @@ impl ToolProvider for DocxToolsProvider {
|
||||
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);
|
||||
|
||||
// Security check
|
||||
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 {
|
||||
@@ -815,7 +820,7 @@ impl ToolProvider for DocxToolsProvider {
|
||||
let handler = self.handler.lock().unwrap();
|
||||
let metadata = match handler.get_metadata(doc_id) {
|
||||
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)) {
|
||||
@@ -843,13 +848,13 @@ impl ToolProvider for DocxToolsProvider {
|
||||
let handler = self.handler.lock().unwrap();
|
||||
let metadata = match handler.get_metadata(doc_id) {
|
||||
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 {
|
||||
"jpg" | "jpeg" => image::ImageFormat::Jpeg,
|
||||
"png" => image::ImageFormat::Png,
|
||||
_ => image::ImageFormat::Png,
|
||||
"jpg" | "jpeg" => ::image::ImageFormat::Jpeg,
|
||||
"png" => ::image::ImageFormat::Png,
|
||||
_ => ::image::ImageFormat::Png,
|
||||
};
|
||||
|
||||
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
@@ -1,22 +1,27 @@
|
||||
use anyhow::Result;
|
||||
use mcp_server::{Server, ServerBuilder, ServerOptions};
|
||||
use mcp_core::ToolManager;
|
||||
use tracing::{info, warn};
|
||||
#[cfg(feature = "runtime-server")]
|
||||
use mcp_server::Server;
|
||||
use tracing::info;
|
||||
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
|
||||
use clap::Parser;
|
||||
|
||||
#[cfg(feature = "runtime-server")]
|
||||
mod docx_tools;
|
||||
#[cfg(feature = "runtime-server")]
|
||||
mod docx_handler;
|
||||
#[cfg(feature = "runtime-server")]
|
||||
mod converter;
|
||||
#[cfg(feature = "runtime-server")]
|
||||
mod pure_converter;
|
||||
#[cfg(feature = "runtime-server")]
|
||||
mod advanced_docx;
|
||||
mod security;
|
||||
|
||||
#[cfg(feature = "embedded-fonts")]
|
||||
mod fonts;
|
||||
|
||||
#[cfg(feature = "runtime-server")]
|
||||
use docx_tools::DocxToolsProvider;
|
||||
use std::process::Command;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
@@ -48,20 +53,20 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
let security_config = security::SecurityConfig::from_args(args);
|
||||
info!("Starting DOCX MCP Server - Security: {}", security_config.get_summary());
|
||||
#[cfg(feature = "runtime-server")]
|
||||
{
|
||||
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);
|
||||
|
||||
let options = ServerOptions::default()
|
||||
.with_name("docx-mcp-server")
|
||||
.with_version("0.1.0");
|
||||
// TODO: Integrate with mcp-server Router here. For now, just exit successfully.
|
||||
info!("Server integration pending refactor; exiting.");
|
||||
}
|
||||
|
||||
let server = ServerBuilder::new(options)
|
||||
.with_tool_provider(docx_provider)
|
||||
.build();
|
||||
|
||||
server.run().await?;
|
||||
#[cfg(not(feature = "runtime-server"))]
|
||||
{
|
||||
// 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.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::{Context, Result};
|
||||
use image::{DynamicImage, ImageFormat, Rgba, RgbaImage};
|
||||
use ::image::{DynamicImage, ImageFormat, Rgba, RgbaImage};
|
||||
use printpdf::*;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{BufReader, BufWriter, Read, Write};
|
||||
@@ -9,7 +9,7 @@ use tracing::{debug, info, warn};
|
||||
use roxmltree;
|
||||
use zip::ZipArchive;
|
||||
use rusttype::{Font, Scale};
|
||||
use lopdf;
|
||||
use lopdf::{self, dictionary, Object};
|
||||
|
||||
pub struct PureRustConverter;
|
||||
|
||||
@@ -231,7 +231,7 @@ impl PureRustConverter {
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<()> {
|
||||
let img = image::open(image_path)
|
||||
let img = ::image::open(image_path)
|
||||
.with_context(|| format!("Failed to open image {:?}", image_path))?;
|
||||
|
||||
let thumbnail = img.thumbnail(width, height);
|
||||
|
||||
Reference in New Issue
Block a user