edit_docx_paragraph
Replace text in specific paragraphs of a docx file while preserving formatting. Provide paragraph index and search/replace pairs to make targeted edits and receive a diff of changes.
Instructions
Make text replacements in specified paragraphs of a docx file. Accepts a list of edits with paragraph index and search/replace pairs. Each edit operates on a single paragraph and preserves the formatting of the first run. Returns a git-style diff showing the changes made. Only works within allowed directories.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | Absolute path to file to edit. It should be under your current working directory. | |
| edits | Yes | Sequence of edits to apply to specific paragraphs. |
Implementation Reference
- mcp_server_office/office.py:228-356 (handler)The core handler function that performs the actual editing of DOCX paragraphs or tables by replacing specified text while preserving formatting, generates a unified diff of changes, and saves the modified document.async def edit_docx_paragraph(path: str, edits: list[Dict[str, str | int]]) -> str: """Edit docx file by replacing text. Args: path: path to target docx file edits: list of dictionaries containing search and replace text, and paragraph_index [{'search': 'text to find', 'replace': 'text to replace with', 'paragraph_index': 0}, ...] paragraph_index: 0-based index of the paragraph to edit (required) search: text to find replace: text to replace with Returns: str: A git-style diff showing the changes made """ if not await validate_path(path): raise ValueError(f"Not a docx file: {path}") doc = Document(path) original = await read_docx(path) not_found = [] # パラグラフとテーブルを順番に収集 elements = [] paragraph_count = 0 table_count = 0 for element in doc._body._body: if element.tag == W_P: elements.append(('p', doc.paragraphs[paragraph_count])) paragraph_count += 1 elif element.tag == W_TBL: elements.append(('tbl', doc.tables[table_count])) table_count += 1 for edit in edits: search = edit["search"] replace = edit["replace"] if "paragraph_index" not in edit: raise ValueError("paragraph_index is required") paragraph_index = edit["paragraph_index"] if paragraph_index >= len(elements): raise ValueError(f"Paragraph index out of range: {paragraph_index}") element_type, element = elements[paragraph_index] if element_type == 'p': paragraph = element if search not in paragraph.text: not_found.append(f"'{search}' in paragraph {paragraph_index}") continue # Store original XML element and get first run's properties original_element = paragraph._element first_run_props = None runs = original_element.findall(f'.//w:r', WORDML_NS) if runs: first_run = runs[0] if hasattr(first_run, 'rPr'): first_run_props = first_run.rPr # Create new paragraph with the entire text new_paragraph = doc.add_paragraph() # Copy paragraph properties if original_element.pPr is not None: new_paragraph._p.append(original_element.pPr) # Replace text and create a single run with first run's properties new_text = process_track_changes(paragraph._element).replace(search, replace, 1) new_run = new_paragraph.add_run(new_text) if first_run_props is not None: new_run._element.append(first_run_props) # Replace original paragraph with new one original_element.getparent().replace(original_element, new_paragraph._element) elif element_type == 'tbl': # tableの場合、複数行に渡る書換では、特に行列が増減する場合、書式を保つことが困難なため、とりあえず0,0の書式を適用することとする。要検討。 table = element table_paragraph = table._element.getprevious() table_text = extract_table_text(table) if search in table_text: # 既存tableを削除(親要素の参照を保持して安全に削除) parent = table._element.getparent() if parent is not None: parent.remove(table._element) else: # テーブルが文書のルート要素である場合(先頭の場合などにおそらく必要) doc.element.body.remove(table._element) # Get first run's properties from the first cell first_run_props = None for paragraph in table.rows[0].cells[0].paragraphs: for run in paragraph.runs: if run._element.rPr is not None: first_run_props = run._element.rPr break new_text = table_text.replace(search, replace, 1) new_table = create_table_from_text(new_text, first_run_props) elements[paragraph_index] = ("tbl", new_table) # これがないと複数編集時に、あとの編集でtableがみつからなくなる if table_paragraph is not None: table_paragraph.addnext(new_table._element) else: # Noneの場合はtableの前がない、つまり先頭を意味する doc.element.body.insert(0, new_table._element) else: not_found.append(f"'{search}' in table at paragraph {paragraph_index}") if not_found: raise ValueError(f"Search text not found: {', '.join(not_found)}") doc.save(path) # Read modified content and create diff modified = await read_docx(path) # 差分の生成 diff_lines = [] original_lines = [line for line in original.split("\n") if line.strip()] modified_lines = [line for line in modified.split("\n") if line.strip()] for line in difflib.unified_diff(original_lines, modified_lines, n=0): if line.startswith('---') or line.startswith('+++'): continue if line.startswith('-') or line.startswith('+'): diff_lines.append(line) return "\n".join(diff_lines) if diff_lines else ""
- mcp_server_office/tools.py:90-139 (schema)The Tool object definition including name, description, and inputSchema for validating the tool's input parameters: path (string) and edits (array of objects with paragraph_index, search, replace).EDIT_DOCX_PARAGRAPH = types.Tool( name="edit_docx_paragraph", description=( "Make text replacements in specified paragraphs of a docx file. " "Accepts a list of edits with paragraph index and search/replace pairs. " "Each edit operates on a single paragraph and preserves the formatting of the first run. " "Returns a git-style diff showing the changes made. Only works within allowed directories." ), inputSchema={ "type": "object", "properties": { "path": { "type": "string", "description": "Absolute path to file to edit. It should be under your current working directory." }, "edits": { "type": "array", "description": "Sequence of edits to apply to specific paragraphs.", "items": { "type": "object", "properties": { "paragraph_index": { "type": "integer", "description": "0-based index of the paragraph to edit. tips: whole table is count as one paragraph." }, "search": { "type": "string", "description": ( "Text to find within the specified paragraph. " "The search is performed only within the target paragraph. " "Escape line break when you input multiple lines." ) }, "replace": { "type": "string", "description": ( "Text to replace the search string with. " "The formatting of the first run in the paragraph will be applied to the entire replacement text. " "Empty string represents deletion. " "Escape line break when you input multiple lines." ) } }, "required": ["paragraph_index", "search", "replace"] } } }, "required": ["path", "edits"] } )
- mcp_server_office/office.py:358-360 (registration)Registers the edit_docx_paragraph tool by including EDIT_DOCX_PARAGRAPH in the list returned by list_tools().@server.list_tools() async def list_tools() -> list[types.Tool]: return [READ_DOCX, EDIT_DOCX_PARAGRAPH, WRITE_DOCX, EDIT_DOCX_INSERT]
- mcp_server_office/office.py:373-375 (registration)Dispatches calls to the edit_docx_paragraph handler within the call_tool function.elif name == "edit_docx_paragraph": result = await edit_docx_paragraph(arguments["path"], arguments["edits"]) return [types.TextContent(type="text", text=result)]