Skip to main content
Glama

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
NameRequiredDescriptionDefault
pathYesAbsolute path to file to edit. It should be under your current working directory.
editsYesSequence of edits to apply to specific paragraphs.

Implementation Reference

  • 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 ""
  • 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"] } )
  • 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]
  • 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)]

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/famano/mcp-server-office'

If you have feedback or need assistance with the MCP directory API, please join our Discord server