Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,240 changes: 384 additions & 2,856 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,16 @@
"@supabase/supabase-js": "^2.89.0",
"@vscode/ripgrep": "^1.15.9",
"cross-fetch": "^4.1.0",
"@xmldom/xmldom": "^0.8.10",
"docx": "^8.5.0",
"docxtemplater": "^3.44.0",
"exceljs": "^4.4.0",
"fastest-levenshtein": "^1.0.16",
"file-type": "^21.1.1",
"glob": "^10.3.10",
"isbinaryfile": "^5.0.4",
"jszip": "^3.10.1",
"pizzip": "^3.1.6",
"md-to-pdf": "^5.2.5",
"open": "^10.2.0",
"pdf-lib": "^1.17.1",
Expand Down
101 changes: 100 additions & 1 deletion src/handlers/filesystem-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
moveFile,
getFileInfo,
writePdf,
writeDocx,
type FileResult,
type MultiFileResult
} from '../tools/filesystem.js';
Expand All @@ -25,7 +26,8 @@ import {
ListDirectoryArgsSchema,
MoveFileArgsSchema,
GetFileInfoArgsSchema,
WritePdfArgsSchema
WritePdfArgsSchema,
WriteDocxArgsSchema
} from '../tools/schemas.js';

/**
Expand Down Expand Up @@ -106,6 +108,50 @@ export async function handleReadFile(args: unknown): Promise<ServerResult> {
};
}

// Handle DOCX files
if (fileResult.metadata?.isDocx) {
const meta = fileResult.metadata;
const contentItems: Array<{ type: string, text?: string, data?: string, mimeType?: string }> = [];

// Add document info
const infoParts = [`DOCX file: ${parsed.path}`];
if (meta?.title) infoParts.push(`Title: "${meta.title}"`);
if (meta?.author) infoParts.push(`Author: ${meta.author}`);
if (meta?.images && meta.images.length > 0) {
infoParts.push(`${meta.images.length} embedded images`);
}

contentItems.push({
type: "text",
text: infoParts.join(', ') + '\n\n'
});

// Add embedded images first
if (meta?.images && meta.images.length > 0) {
for (const image of meta.images) {
contentItems.push({
type: "image",
data: image.data,
mimeType: image.mimeType
});
}
}

// Add the main document content as markdown
const textContent = typeof fileResult.content === 'string'
? fileResult.content
: fileResult.content.toString('utf8');

contentItems.push({
type: "text",
text: textContent
});

return {
content: contentItems
};
}

// Handle image files
if (fileResult.metadata?.isImage) {
// For image files, return as an image content type
Expand Down Expand Up @@ -164,6 +210,8 @@ export async function handleReadMultipleFiles(args: unknown): Promise<ServerResu
return `${result.path}: Error - ${result.error}`;
} else if (result.isPdf) {
return `${result.path}: PDF file with ${result.payload?.pages?.length} pages`;
} else if (result.isDocx) {
return `${result.path}: DOCX file (Word document)`;
} else if (result.mimeType) {
return `${result.path}: ${result.mimeType} ${result.isImage ? '(image)' : '(text)'}`;
} else {
Expand Down Expand Up @@ -376,3 +424,54 @@ export async function handleWritePdf(args: unknown): Promise<ServerResult> {
return createErrorResponse(errorMessage);
}
}

/**
* Handle write_docx command
*/
export async function handleWriteDocx(args: unknown): Promise<ServerResult> {
try {
const parsed = WriteDocxArgsSchema.parse(args);
await writeDocx(parsed.path, parsed.content, parsed.outputPath, parsed.options);
const targetPath = parsed.outputPath || parsed.path;

// Build success message with operation details
let message: string;
if (parsed.outputPath) {
message = `Successfully created new DOCX file: ${targetPath}\nOriginal file preserved: ${parsed.path}`;
} else {
message = `Successfully updated DOCX file: ${targetPath}`;
}

// Add operation summary if using operations mode
if (Array.isArray(parsed.content)) {
const opCounts = {
replaceText: 0,
appendMarkdown: 0,
insertTable: 0,
insertImage: 0,
};

for (const op of parsed.content) {
if (op.type in opCounts) {
opCounts[op.type as keyof typeof opCounts]++;
}
}

const details = Object.entries(opCounts)
.filter(([_, count]) => count > 0)
.map(([type, count]) => `${count} ${type}`)
.join(', ');

if (details) {
message += `\nOperations applied: ${details}`;
}
}

return {
content: [{ type: "text", text: message }],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return createErrorResponse(errorMessage);
}
}
79 changes: 78 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
GetPromptsArgsSchema,
GetRecentToolCallsArgsSchema,
WritePdfArgsSchema,
WriteDocxArgsSchema,
} from './tools/schemas.js';
import { getConfig, setConfigValue } from './tools/config.js';
import { getUsageStats } from './tools/usage.js';
Expand Down Expand Up @@ -264,7 +265,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
name: "read_file",
description: `
Read contents from files and URLs.
Read PDF files and extract content as markdown and images.
Read PDF and DOCX files and extract content as markdown and images.

Prefer this over 'execute_command' with cat/type for viewing files.

Expand Down Expand Up @@ -301,6 +302,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
- PDF: Extracts text content as markdown with page structure
* offset/length work as page pagination (0-based)
* Includes embedded images when available
- DOCX (.docx): Extracts text content as markdown with formatting
* Preserves headings, lists, tables, bold, italic
* Includes embedded images as base64
* Extracts document metadata (title, author, dates)

${PATH_GUIDANCE}
${CMD_PREFIX_DESCRIPTION}`,
Expand Down Expand Up @@ -439,6 +444,74 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
openWorldHint: false,
},
},
{
name: "write_docx",
description: `
Create a new DOCX (Word) file or modify an existing one.

THIS IS THE TOOL FOR CREATING AND MODIFYING WORD DOCUMENTS (.docx).

MODES:
1. CREATE NEW DOCX:
- Pass a markdown string as 'content'.
- Markdown will be converted to a formatted Word document.
write_docx(path="new_doc.docx", content="# Title\\n\\nBody text...")

2. UPDATE/MODIFY EXISTING DOCX (DEFAULT: OVERWRITES ORIGINAL):
- Pass array of operations as 'content'.
- WITHOUT 'outputPath': UPDATES the original file (OVERWRITES IT)
- WITH 'outputPath': Creates a NEW file, preserving the original

// Update the original file (RECOMMENDED for edits):
write_docx(path="document.docx", content=[
{ type: "replaceText", search: "old", replace: "new" }
])

// Create a modified copy (use when you need to preserve original):
write_docx(path="document.docx", content=[
{ type: "appendMarkdown", markdown: "# New Section" }
], outputPath="document_updated.docx")

OPERATIONS:
- replaceText: Search and replace text in the document.
{ type: "replaceText", search: "old text", replace: "new text", matchCase: true, global: true }

- appendMarkdown: Add content at the end of the document.
{ type: "appendMarkdown", markdown: "# New Section\\n\\nContent here..." }

- insertTable: Add a table to the document.
{ type: "insertTable", rows: [["Header1", "Header2"], ["Row1Col1", "Row1Col2"]] }
or
{ type: "insertTable", markdownTable: "| Header1 | Header2 |\\n| --- | --- |\\n| Data1 | Data2 |" }

- insertImage: Add an image to the document.
{ type: "insertImage", imagePath: "/path/to/image.png", altText: "Description", width: 400, height: 300 }
Supports: local file paths, data URLs (data:image/png;base64,...)

SUPPORTED MARKDOWN FEATURES:
- Headings (# through ######)
- Paragraphs
- Tables (markdown table syntax)
- Images (![alt](path))
- Basic text formatting (bold, italic)

OPTIONS:
- baseDir: Base directory for resolving relative image paths
- includeImages: Extract/include images (default: true)
- preserveFormatting: Preserve text formatting (default: true)

Only works within allowed directories.

${PATH_GUIDANCE}
${CMD_PREFIX_DESCRIPTION}`,
inputSchema: zodToJsonSchema(WriteDocxArgsSchema),
annotations: {
title: "Write/Modify DOCX",
readOnlyHint: false,
destructiveHint: true,
openWorldHint: false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The write_docx tool is annotated with openWorldHint: false even though insertImage operations in the underlying implementation can reach out to HTTP/HTTPS URLs, so clients are misled into thinking the tool is strictly local, which is a security-relevant metadata bug; the hint should reflect that the tool may access the network. [security]

Severity Level: Major ⚠️
- ⚠️ ListTools metadata misleads clients about network access.
- ❌ write_docx image insertion may fetch remote images.
- ⚠️ Client UI policies may allow unsafe operations.
Suggested change
openWorldHint: false,
openWorldHint: true,
Steps of Reproduction ✅
1. Start the MCP server with the PR changes applied (see src/server.ts).

   - Inspect tool discovery by calling ListToolsRequestSchema handler which returns tool
   metadata including the write_docx tool. The write_docx tool's annotations block is
   present at src/server.ts:508-513 and shows openWorldHint: false.

2. Discover the tool in a client: call the MCP List Tools flow (ListToolsRequestSchema).

   - Client receives the write_docx tool metadata from src/server.ts and will interpret
   openWorldHint:false (no network access expected).

3. Invoke the tool via CallToolRequest with name "write_docx". The server routes this to
the handler at src/server.ts:1398 where the switch case calls
handlers.handleWriteDocx(args).

4. The handler path leads into the DOCX implementation that accepts insertImage operations
and intentionally does not validate HTTP(S) URLs. In src/tools/filesystem.ts (within the
writeDocx modification branch) the code only skips validation for paths starting with
'data:' or 'http' (so remote HTTP URLs are allowed to pass through), e.g. the insertImage
loop that checks:

   if (o.type === 'insertImage' && o.imagePath) {

       if (!o.imagePath.startsWith('data:') && !o.imagePath.startsWith('http')) {

           o.imagePath = await validatePath(o.imagePath);

       }

   }

   This means a client calling write_docx with an insertImage
   imagePath="http://example.com/img.png" will cause the system to accept the remote URL
   and the DOCX creation/update pipeline may fetch it.

Note: The existing annotations value openWorldHint:false appears to be an oversight
relative to the filesystem behavior above (the code intentionally treats http URLs as
external and doesn't validate them as local). The suggestion is not frivolous: it aligns
metadata with the actual runtime behavior.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/server.ts
**Line:** 512:512
**Comment:**
	*Security: The write_docx tool is annotated with openWorldHint: false even though insertImage operations in the underlying implementation can reach out to HTTP/HTTPS URLs, so clients are misled into thinking the tool is strictly local, which is a security-relevant metadata bug; the hint should reflect that the tool may access the network.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

},
},
{
name: "create_directory",
description: `
Expand Down Expand Up @@ -1322,6 +1395,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest)
result = await handlers.handleWritePdf(args);
break;

case "write_docx":
result = await handlers.handleWriteDocx(args);
break;

case "create_directory":
result = await handlers.handleCreateDirectory(args);
break;
Expand Down
7 changes: 7 additions & 0 deletions src/tools/docx/builders/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* DOCX Builders
* Centralized exports for all building utilities
*/

export * from './markdown-builder.js';

Loading