Skip to main content

CLI and MCP tool for manipulating markdown files

Project description


last-updated: '2026-06-10T11:37:48.418276' mdship-log: | 2026-06-10 11:37:48 - update: processed all placeholders checksum: 0665df433ace74313eb1d1da1d241e748653b47f1388ec6bcedcdfd5509e5bba checksum_algorithm: sha256

1. mdship

A command-line and MCP tool for manipulating markdown files.

1.1. Features

  • Fix heading levels: Ensure consistent heading hierarchy
  • Shift headings: Move all headings up or down by N levels
  • Add checksums: Insert content checksums into front-matter
  • Check checksums: Verify checksums against content (useful in scripts)
  • Reflow paragraphs: Reflow text to a specific width
  • Semantic line breaks: Break lines at sentence/clause boundaries for better readability and diffs
  • Number headings: Add hierarchical numbering to headings (1. 1.1. 1.1.1.)
  • Remove numbering: Strip numbering from headings
  • Variables: Define and use variables throughout your document with hierarchical support
    • SET: Define variables with scalar or YAML values
    • IMPORT: Load data from JSON, YAML, TOML, or XML files
    • SLURP: Extract variable names and values from files using regex patterns
    • SIP: Extract predefined variables from files with simpler patterns
    • SUP: Extract a single value from the next line in the document
  • Hierarchical names: Support dot-notation variable naming (e.g., config.database.host)
  • Table of contents: Generate and insert a TOC with anchor links between markers
  • Include files: Embed code snippets and content from other files with flexible line selection
  • Render Mermaid diagrams: Generate SVG/PNG diagrams from Mermaid source code with variable substitution
  • Template placeholders: Insert dynamic content with $variable substitution or Jinja2 rendering
  • Pattern dictionary: Built-in patterns for common extraction tasks (@heading, @version) and support for custom patterns
  • Placeholder validation: Early detection of mistyped closing tags, unclosed placeholders, and duplicate AI placeholder names before processing
  • Managed content integrity: Hash-based protection against accidental manual edits inside managed blocks (TOC, INCLUDE, TEMPLATE, JINJA2, MERMAID)
  • Track changes: Automatically maintain last-updated timestamp and operation logs in front-matter (use --track or -t flag)
  • Validate links: Check for broken anchor references, missing file references, and unused anchors
  • AI placeholders: Embed generation prompts in markdown documents for Claude to fill or update
    • deps:: Declare file dependencies; mdship extracts content and computes checksums so Claude never reads source files directly
    • brief:: Shared writing instructions (style, tone, audience) applied to every generation run
    • ai-fix: Record content, prompt, brief, and dep checksums after generation so accidental edits are detectable
    • ai-check: Verify that all stored checksums still match — use in CI or pre-commit hooks

1.2. Installation

uv sync
uv run pip install -e .

1.3. Usage

1.3.1. Command Line

Most commands modify the file in place and create a backup with a .md.bak extension (use --no-bak to skip):

mdship fix-headings file.md              # Fix heading hierarchy
mdship shift-headings file.md --levels 1 # Shift all headings down 1 level
mdship sum file.md --algorithm sha256    # Add or update checksum
mdship verify file.md                    # Verify checksum
mdship validate file.md                  # Validate links and anchors
mdship reflow file.md --width 80         # Reflow paragraphs to 80 characters
mdship semantic-line-breaks file.md      # Break lines at sentence boundaries
mdship number file.md                    # Add hierarchical numbering to headings
mdship unnumber file.md                  # Remove numbering from headings
mdship update file.md                    # Update all placeholders (variables, includes, TOC, diagrams)
mdship update --force file.md            # Update and skip managed-content hash checks (-f for short)

1.3.2. Shift Validation

The shift-headings command validates that the shift won't create invalid heading levels:

mdship shift-headings file.md --levels 1    # OK: h1→h2, h2→h3, etc.
mdship shift-headings file.md --levels -1   # ERROR if any h1 (can't promote above h1)
mdship shift-headings file.md --levels 10   # ERROR if any h6 (can't demote below h6)

If validation fails, the file is not modified and an error is printed.

1.3.3. Shift Line Range

Use --lines START:END to only shift headings within a specific line range (1-based, inclusive):

mdship shift-headings file.md --levels 1 --lines 10:50    # Only lines 10-50
mdship shift-headings file.md --levels 1 --lines 10:      # From line 10 to end
mdship shift-headings file.md --levels 1 --lines :50      # From start to line 50

Headings outside the specified range are not modified.

1.3.4. Semantic Line Breaks

Split lines at sentence and clause boundaries for better readability and cleaner diffs:

mdship semantic-line-breaks file.md                  # Split entire document
mdship semantic-line-breaks file.md --lines 10:50   # Only lines 10-50
mdship semantic-line-breaks file.md --lines 10:     # From line 10 to end

Before:

This is a long paragraph with multiple sentences. Each sentence contains important information. 
The text flows together making diffs harder to read.

After:

This is a long paragraph with multiple sentences.
Each sentence contains important information.
The text flows together making diffs harder to read.

1.3.5. Number Headings

Add hierarchical numbering to your headings for automatic outline creation:

mdship number file.md                           # Default: 1. 1.1. 1.1.1.
mdship number file.md --style period            #  1.1. 1.1.1.
mdship number file.md --style space             #  1.1 1.1.1
mdship number file.md --style parenthesis       # ) 1.1) 1.1.1)
mdship number file.md --lines 10:50             # Only lines 10-50

Remove numbering with unnumber:

mdship unnumber file.md                # Remove all numbering
mdship unnumber file.md --lines 10:50  # Only lines 10-50

1.3.6. Variables

Define and use variables throughout your document. Variables can come from multiple sources: SET placeholders (inline definitions), IMPORT (load from files), SLURP (extract names and values from files), SIP (extract predefined variable names), and SUP (extract from document lines). All variables support hierarchical names using dot notation.

Define variables with SET placeholder:

<!--SET
appName: "MyApplication"
version: "1.0.0"
config:
  language: "Python"
  framework: "mdship"
  authors:
    - "Alice"
    - "Bob"
-->

Simple variable references (no spaces in value):

Application: <!--$appName-->placeholder
Version: <!--$version-->placeholder
Language: <!--$config.language-->placeholder

After mdship update:

Application: <!--$appName-->MyApplication
Version: <!--$version-->1.0.0
Language: <!--$config.language-->Python

Variable references with spaces (using markers):

Framework: <!--$config.framework<>-->old framework<!---->
Author: <!--$config.authors[0]<>-->Unknown<!---->

After mdship update:

Framework: <!--$config.framework<>-->mdship<!---->
Author: <!--$config.authors[0]<>-->Alice<!---->

Features:

  • Variables support nested access: $config.language, $config.nested.field
  • Array indexing: $items[0], $authors[2]
  • Both $variable and ${variable} syntax are supported
  • Front-matter YAML is automatically available as $fm
  • Code blocks and MERMAID diagrams are preserved (variables in code blocks are not replaced)
  • Backtick-wrapped values are preserved: `value` becomes `newValue`
  • Error messages include file path and line number for easy debugging

Hierarchical variable names:

Variables support hierarchical naming using dot notation. Intermediate dictionaries are created automatically:

<!--IMPORT
name: "app.database.config"
from: "settings.json"
-->

Host: <!--$app.database.config.host-->localhost<!---->
Port: <!--$app.database.config.port-->5432<!---->

Variable sources:

Variables can come from multiple sources, all processed together:

  • SET: Define inline with YAML values
  • IMPORT: Load complete data structures from JSON/YAML/TOML/XML files
  • SLURP: Extract variable names and values from files using regex patterns
  • SIP: Extract values using predefined variable names and regex patterns
  • SUP: Extract a single value from the next line in the document

1.3.6.1. IMPORT: Load from External Files

Import complete data structures from JSON, YAML, TOML, or XML files:

<!--IMPORT
name: "config"
from: "settings.json"
-->

Database: <!--$config.database.host-->unknown<!---->
Port: <!--$config.database.port-->0<!---->

Configuration:

  • name: Variable name to store data under (required, supports hierarchical names like app.db)
  • from: File path relative to the markdown file (required)
  • format: File format (json, yaml, toml, xml). Auto-detected from extension if omitted

Supported formats:

  • .json: JSON objects and arrays
  • .yaml / .yml: YAML structures
  • .toml: TOML configuration
  • .xml: XML with attribute support (use @attribute for attributes)

1.3.6.2. SLURP: Extract Names and Values from Files

Extract both variable names and values from files using regex patterns with 2 capturing groups:

<!--SLURP
name: "config"
from: "settings.txt"
strategy: "first"
rules:
  - '(\w+)=(.+)'
-->

Host: <!--$config.host-->unknown<!---->
Port: <!--$config.port-->0<!---->

Configuration:

  • from: File or directory path (required)
  • rules: List of regex patterns with exactly 2 capturing groups (required)
    • First group = variable name
    • Second group = variable value
    • Supports named groups: (?P<var>...) and (?P<val>...) for value-name ordering
  • name: Optional namespace for variables (supports hierarchical names)
  • include / exclude: Glob patterns (directory only)
  • recurse: Recurse subdirectories (default: false)
  • strategy: Handle multiple matches: fail, first, last, concatenate (default: fail)
  • separator: String separator for concatenate strategy (default: empty)

1.3.6.3. SIP: Extract Predefined Variables from Files

Extract values for predefined variables using regex patterns with 1 capturing group:

<!--SIP
name: "app"
from: "config.txt"
vars:
  version: 'version:\s+([0-9.]+)'
  author: 'author:\s+(\w+)'
-->

Version: <!--$app.version-->0.0.0<!---->
Author: <!--$app.author-->unknown<!---->

Configuration:

  • from: File or directory path (required)
  • vars: Dictionary of variable names and regex patterns (required)
    • Patterns must have exactly 1 capturing group (the value)
  • name: Optional namespace for variables (supports hierarchical names)
  • include / exclude: Glob patterns (directory only)
  • recurse: Recurse subdirectories (default: false)
  • strategy: Handle multiple matches: fail, first, last, concatenate (default: fail)
  • separator: String separator for concatenate strategy (default: empty)

1.3.6.4. SUP: Extract from Document Lines

Extract a single value from the next non-empty line in the document:

<!--SUP
name: "doc.title"
pattern: '^#+\s+(.*?)\s*$'
-->
# My Document Title

Title: <!--$doc.title-->placeholder<!---->

Configuration:

  • name: Variable name (required, supports hierarchical names)
  • pattern: Regex pattern with exactly 1 capturing group (required)

The pattern is matched against the first non-empty line following the placeholder, and the captured value is stored.

1.3.6.5. Pattern Dictionary

SUP and SIP support a built-in pattern dictionary for common extraction tasks. Pre-defined patterns are available:

  • @heading: Extract heading numbers (e.g., 1.5.8)
  • @version: Extract semantic versions (e.g., 1.2.3)

Using built-in patterns:

<!--SUP
name: "chapter_number"
pattern: "@heading"
-->
# 1.5.8. Advanced Topics

Chapter: <!--$chapter_number-->placeholder<!---->

Defining custom patterns:

You can define custom patterns using SET with a pattern dictionary:

<!--SET
pattern:
  snapshot: '(\d+\.\d+\.\d+)-SNAPSHOT'
  buildnum: 'build-(\d+)'
-->

<!--SUP
name: "release"
pattern: "@snapshot"
-->
v1.2.3-SNAPSHOT

Build: <!--$buildnum-->placeholder<!---->

Accessing patterns:

All patterns (built-in and custom) are stored in the $pattern variable and can be referenced elsewhere:

Available patterns: <!--$pattern<>-->
{heading: ..., version: ..., custom: ...}
<!---->

Configuration:

  • pattern: Dictionary of pattern name → regex pairs (inside SET)
    • Patterns must have exactly 1 capturing group for the value
    • Built-in patterns are automatically available
    • Custom patterns extend the built-in set (they don't replace it)

1.3.7. Template Placeholders

Insert dynamic content between opening and closing markers. TEMPLATE is useful for:

  • Code blocks with dynamic values
  • Multi-line formatted content with variables
  • Content that cannot contain HTML comments

Basic usage:

<​!--TEMPLATE
content: |
  ```python
  # Generated code with $variable
  app = "$appName"
  version = "$appVersion"

--> (old content gets replaced) <​!--/TEMPLATE-->


**Configuration:**

- `content`: The template content (multi-line YAML literal block, required)
  - Variables are substituted in the content
  - Supports all variable features: `$var`, `$nested.field`, `${var}`

**Example:**

```markdown
<​!--SET
appName: "MyApp"
config:
  debug: true
  port: 8000
-->

<​!--TEMPLATE
content: |
  Application Configuration
  =======================
  
  - Name: $appName
  - Debug: $config.debug
  - Port: $config.port
-->
old documentation
<​!--/TEMPLATE-->

After running mdship update:

  Application Configuration
  =======================
  
  - Name: MyApp
  - Debug: true
  - Port: 8000

Key features:

  • Content is inserted between the opening --> and closing <​!--/TEMPLATE-->
  • All variables are substituted before insertion
  • Idempotent: running update multiple times produces the same result
  • Perfect for code examples with dynamic values

1.3.7.1. Jinja2 Template Placeholders

Use JINJA2 when the generated content needs real template logic such as loops, conditionals, filters, or nested object access. It works like TEMPLATE: the rendered result is inserted between the opening and closing markers and protected as managed content.

<!--SET
appName: "MyApp"
authors:
  - "Alice"
  - "Bob"
-->

<!--JINJA2
content: |
  # {{ appName }} Authors

  {% for author in authors %}
  - {{ author }}
  {% endfor %}
-->
old documentation
<!--/JINJA2-->

After running mdship update:

# MyApp Authors

- Alice
- Bob

Configuration:

  • content: The Jinja2 template content (multi-line YAML literal block, required)
    • All mdship variables are available as Jinja2 template variables
    • Nested dictionaries can be accessed using Jinja2 dot syntax, such as {{ config.database.host }}
    • Lists can be iterated using normal Jinja2 syntax

Use TEMPLATE for simple $var substitution. Use JINJA2 when you need template control flow or richer formatting.

1.3.8. Placeholder Processing Order

The update command processes placeholders in a specific order to enable powerful workflows:

  1. Variable source placeholders (collected in order they appear)

    • : Define variables with scalar or YAML values (including custom patterns)
    • : Load data from JSON, YAML, TOML, or XML files
    • : Extract variable names and values from files using regex (2 capturing groups)
    • : Extract predefined variables from files using regex (1 capturing group)
    • : Extract a single value from the next line in the document (can use pattern references like @heading)
    • All variables become available to subsequent placeholders
    • Built-in patterns (@heading, @version) are automatically available
    • Front-matter YAML is automatically available as $fm
  2. placeholders

    • Embeds content from other files
    • Done before variable replacement so variables can be substituted in included content too
  3. Variable references

    • Replaces <​!--$variable-->value and <​!--$variable<MARKER>-->value<!--MARKER--> in the document
    • Variables are substituted in both original and included content
    • Variables are NOT replaced inside code blocks (between ``` markers) — they are safe for code that uses $var notation
    • Front-matter YAML is automatically available as $fm
  4. <!​--TEMPLATE--> and <!​--JINJA2--> placeholders

    • Substitute or render template content and insert between opening and closing markers
    • Useful for dynamic code blocks and formatted content with variables
  5. <!​--TOC--> placeholders

    • Generates table of contents from headings
    • Can include headings from both original document and included content
  6. Other placeholders (top-to-bottom)

    • <!​--MERMAID--> diagrams with variable substitution
    • Processed in document order

This order allows:

  • Variables to be defined early and used everywhere in the document
  • INCLUDE content to be embedded before variables are replaced (so included content benefits from variable substitution)
  • TEMPLATE and JINJA2 to use any variables including those in included content
  • TOC to include headings from included files
  • Subsequent placeholders to use variables defined anywhere in the document
  • Safe inclusion of code with $var notation since code blocks are skipped
  • Pattern references (@heading, @version) to work in SUP placeholders

1.3.9. Table of Contents

Generate and insert a table of contents between <!--TOC--> markers. Configuration is specified inside the marker using YAML. Also adds anchor links to headings:

mdship update file.md                  # Update all placeholders

Configuration inside markers:

You can specify TOC options directly in the marker using YAML format:

<!--TOC min-level: 2
max-level: 3
_terminate_: "CONTENTS"
-->
  • min-level: Minimum heading level to include (1-6, default: 1)
  • max-level: Maximum heading level to include (1-6, default: 6)
  • _terminate_: Custom closing marker (default: /TOC). When specified, marker closes with <!--/CUSTOM--> instead of <!--/TOC-->

How it works:

  1. Finds <!--TOC--> and <!--/TOC--> markers (or custom termination marker)
  2. Parses YAML configuration from the opening marker
  3. Generates a nested table of contents with anchor links
  4. Automatically adds anchors to headings if they don't have them
  5. Preserves existing anchors

Example with defaults:

# My Document

<!--TOC-->
<!--/TOC-->

## Introduction
## Getting Started
### Installation
### Configuration

Example with custom config:

# My Document

<!--TOC min-level: 2
max-level: 3
-->
<!--/TOC-->

## Introduction
## Getting Started
### Installation
### Configuration

After running mdship update file.md:

# My Document

<!--TOC min-level: 2
max-level: 3
-->
- [Introduction](#introduction)
- [Getting Started](#getting-started)
  - [Installation](#installation)
  - [Configuration](#configuration)
<!--/TOC-->

## Introduction
## Getting Started
### Installation
### Configuration

1.3.10. Including Files

Include content from other files between <​!--INCLUDE--> markers. Useful for embedding code examples, documentation snippets, or keeping content synchronized:

mdship update file.md                  # Update all placeholders

Configuration inside markers:

You can include content from other files with flexible line selection:

<​!--INCLUDE
from: "path/to/file.ext"
prefix: "```python"
postfix: "```"
range: "10..20"
-->
<​!--/INCLUDE-->

Configuration parameters:

  • from: File path relative to the markdown file (required)
  • prefix: String to insert before included content (e.g., "```python" for code fences)
  • postfix: String to insert after included content (e.g., "```")
  • range: "x..y": Include lines x through y (1-based, inclusive). Mutually exclusive with start/end/section.
  • start: "regex": Start including from line after first regex match. Mutually exclusive with range/section.
  • end: "regex": Stop including before first regex match (after start)
  • section: "Title": Include a markdown section by heading title (without numbering). Includes from the matching heading through the end of that section. Case-insensitive. Mutually exclusive with range/start/end.
  • margin: N: Indent all lines so the leftmost line has exactly N spaces (preserves relative indentation)
  • _terminate_: Custom closing marker (default: /INCLUDE). When specified, use <!--/CUSTOM--> instead of <!--/INCLUDE-->

Selection methods:

Range-based selection:

<!--INCLUDE
from: "script.py"
prefix: "```python"
postfix: "```"
range: "1..20"
-->
<!--/INCLUDE-->

Regex-based selection (string format):

<!--INCLUDE
from: "example.java"
prefix: "```java"
postfix: "```"
start: "// START_EXAMPLE"
end: "// END_EXAMPLE"
-->
<!--/INCLUDE-->

By default, start/end marker lines are excluded. Use structure format to control inclusion:

Regex-based selection (structure format with include control):

<!--INCLUDE
from: "test_markdown.py"
prefix: "```python"
postfix: "```"
start:
  pattern: 'class\s+MyClass'
  include: true
end:
  pattern: '^class '
  include: false
-->
<!--/INCLUDE-->

In this format:

  • pattern: Required. The regex pattern to match
  • include: Optional (default: false). If true, the line matching the pattern is included in the output

This is useful for including method definitions where you want to start from the def or class line itself rather than the line after it.

Section-based selection:

<!--INCLUDE
from: "reference.md"
section: "Configuration"
-->
<!--/INCLUDE-->

This selects the heading whose bare title matches "Configuration" (case-insensitive, numbering prefixes like 2.3. are ignored) and all lines following it until the next heading at the same or higher level, or end of file. Useful for pulling a named section from another markdown document without needing to track line numbers.

Example:

Source file hello.py:

def greet(name):
    print(f"Hello, {name}!")

# START_ADVANCED
def greet_advanced(name, greeting="Hello"):
    """Advanced greeting function."""
    return f"{greeting}, {name}!"
# END_ADVANCED

def farewell(name):
    print(f"Goodbye, {name}!")

Markdown with INCLUDE:

# Functions

## Basic Function

<!--INCLUDE
from: "hello.py"
prefix: "```python"
postfix: "```"
range: "1..2"
-->
<!--/INCLUDE-->

## Advanced Function

<!--INCLUDE
from: "hello.py"
prefix: "```python"
postfix: "```"
start: "START_ADVANCED"
end: "END_ADVANCED"
-->
<!--/INCLUDE-->

After running mdship update, the file contains:

# Functions

## Basic Function

[included code from lines 1-2 of hello.py]

## Advanced Function

[included code from lines 5-7 of hello.py]

1.3.11. Rendering Mermaid Diagrams

Generate Mermaid diagrams and embed them as images using <!--MERMAID--> markers. Diagrams are rendered to SVG or PNG files:

mdship update file.md                  # Update all placeholders

Configuration inside the marker:

<!--MERMAID
file: "_diagrams/architecture.svg"
diagram: |
  flowchart LR
    A[Client] --\> B[API Server]
    B --\> C[(Database)]
-->

No closing tag is needed. The single line immediately after --> is managed automatically by mdship update and holds the generated image reference.

Important: Use --\> instead of --> in your diagram source to prevent the HTML comment from closing prematurely. The --\> is automatically converted to --> during rendering.

Configuration parameters:

  • file: Output file path (required). Relative to the markdown file. Supports .svg or .png extensions
  • diagram: Mermaid diagram source code (required). Can be multiline YAML literal block
  • theme: Mermaid theme to use (optional). Supported themes: default, forest, dark, neutral

File creation:

  • Files are created relative to the markdown file's directory
  • Intermediate directories are created automatically (e.g., _diagrams/nested/diagram.svg)
  • Running update again with the same configuration is idempotent (produces identical results)

After running mdship update:

The line after the marker is replaced with the image reference:

<!--MERMAID
file: "architecture.svg"
diagram: |
  flowchart LR
    A[Client] --\> B[API]
_content_generated_: 28:md5:abc123
# danger zone: Do not edit the line below.
# danger zone: Delete _content_generated_ to override.
-->
![diagram](architecture.svg)

The diagram file architecture.svg is created in the same directory as the markdown file.

Backward compatibility: If a document still contains a <!--/MERMAID--> closing tag on the line after the managed image reference, mdship update leaves it in place — it is treated as ordinary documentation content beyond the managed line.

Arrow Syntax Escaping:

When your Mermaid diagram includes arrow syntax, you must escape the --> part:

Mermaid Arrow In HTML Comment Auto-converted to
A --> B A --\> B A --> B (rendered)
A -->> B A --\>> B A -->> B (rendered)
A <-- B A <-- B A <-- B (rendered, no escape needed)
A |--> B (classDiagram) A |--\> B A |--> B (rendered)

The escape sequence --\> tells the MERMAID processor to replace it with --> before rendering, preventing premature closure of the outer HTML comment.

Supported diagram types:

  • Flowchart: flowchart TD, flowchart LR
  • Sequence diagram: sequenceDiagram
  • Entity Relationship: erDiagram
  • Class diagram: classDiagram
  • State diagram: stateDiagram-v2
  • Timeline: timeline
  • Pie chart: pie title
  • And all other Mermaid diagram types

Example with nested paths:

<!--MERMAID
file: "_diagrams/architecture/system.svg"
diagram: |
  flowchart LR
    A[Client] --\> B[Server]
-->

This creates _diagrams/architecture/ automatically if it doesn't exist.

Example with theme:

<!--MERMAID
file: "dark-diagram.svg"
theme: "dark"
diagram: |
  flowchart TD
    A[Start] --\> B{Decision}
    B --\>|Yes| C[Success]
    B --\>|No| D[Retry]
-->

Supported themes are default, forest, dark, and neutral. The theme affects colors, fonts, and styling in the rendered diagram.

Note: PNG rendering requires the cairosvg library. SVG rendering works out of the box.

1.3.12. Checksums

Use sum to add or update checksums, and verify to check them:

mdship sum file.md --algorithm sha256     # Add or update checksum
mdship verify file.md                     # Verify checksum

The verify command is useful in shell scripts:

mdship verify document.md
# Prints "OK" and exits with 0 if valid
# Prints error message and exits with 1 if invalid

Example in a script:

if mdship verify document.md; then
  echo "Checksum is valid"
else
  echo "Checksum is invalid"
fi

1.3.13. Skipping Backups

To skip backup creation, use the --no-bak option:

mdship --no-bak fix-headings file.md
mdship --no-bak shift-headings file.md --levels 1

The --no-bak option can be used with any modifying command.

1.3.14. Tracking Changes

Use the --track (or -t) option to automatically track changes in the document's front-matter:

mdship --track update file.md
mdship -t fix-headings file.md
mdship --track shift-headings file.md --levels 1

What tracking does:

  1. Creates front-matter if missing - Ensures the document has YAML front-matter
  2. Adds last-updated timestamp - Records when the document was last modified (ISO format)
  3. Appends to mdship-log - Maintains a log of all operations with timestamps

Example output:

---
title: My Document
author: Jane Doe
last-updated: '2026-06-10T14:32:45.123456'
mdship-log: |
  2026-06-10 14:30:22 - fix-headings: fixed heading hierarchy
  2026-06-10 14:31:15 - number: added heading numbers with period style
  2026-06-10 14:32:45 - update: processed all placeholders
---

Features:

  • ✅ Works with all modifying commands
  • ✅ Preserves existing front-matter fields
  • ✅ Uses YAML literal block style (|) for clean formatting
  • ✅ No empty lines between log entries
  • ✅ Short form -t available as alternative to --track

Use cases:

  • Document version control and audit trails
  • Tracking when sections were last reviewed
  • Automation and workflow documentation
  • Compliance and record-keeping requirements

The --track option can be combined with --no-bak:

mdship --track --no-bak update file.md
mdship -t --no-bak number file.md --style period

1.3.15. Validating Links

Use the validate command to check for broken links and unused anchors in your markdown document:

mdship validate file.md

What it checks:

  1. Broken anchor references - Links like [text](#anchor) where the anchor doesn't exist
  2. Missing file references - Links like [text](file.md) where the file doesn't exist (only local files, no HTTP/HTTPS checks)
  3. Missing image files - Images like ![alt](image.png) where the image file doesn't exist (external URLs like https:// are ignored)
  4. Unused anchors - Headings that are not referenced by any internal links (warning only, they may be referenced from other documents)

Example output:

✓ All links and anchors are valid

Or with issues:

✗ Broken anchor references (2):
  Line 15: [learn more](#details) - anchor not found
  Line 42: [FAQ](#faq) - anchor not found

✗ Missing file references (1):
  Line 28: [guide](../docs/setup.md) - file not found

✗ Missing image files (1):
  Line 35: ![hero banner](images/hero.png) - image not found

⚠ Unused anchors (3) (may be referenced from other files):
  #introduction
  #legacy-section
  #api-reference

How anchors are generated:

Headings are automatically converted to anchor IDs using the same algorithm as GitHub:

  • # Getting Started!#getting-started
  • ## API Reference#api-reference
  • Special characters are removed, spaces become hyphens, everything is lowercase

Use cases:

  • Validate documentation before publishing
  • Check for broken internal links in README files
  • Ensure heading references in tables of contents are correct
  • Quality assurance checks in documentation workflows

1.3.16. Placeholder Validation

mdship automatically validates placeholder syntax before processing to prevent silent data corruption. All opening placeholders must have matching closing tags (where required).

Placeholders that require closing tags:

  • <!​--TEMPLATE ... -->...<!--/TEMPLATE-->
  • <!​--JINJA2 ... -->...<!--/JINJA2-->
  • <!​--INCLUDE ... -->...<!--/INCLUDE-->
  • <!​--TOC ... -->...<!--/TOC-->

Placeholders with a single managed line (no closing tag):

  • <!​--MERMAID ... --> — the next line is the generated image reference

Placeholders without closing tags:

  • <!​--SET ... -->
  • <!​--IMPORT ... -->
  • <!​--SLURP ... -->
  • <!​--SIP ... -->
  • <!​--SUP ... -->

Error Detection:

mdship catches common mistakes before any processing:

# ❌ Typo in closing tag
<​!--TEMPLATE
content: |
  Test
-->
content
<​!--/TEMPLATEE-->  ← Error: Should be <!--/TEMPLATE-->

Error message:

Line 6: Closing <​!--/TEMPLATEE--> does not match opening <​!--TEMPLATE--> at line 1.
Is there a typo in the closing tag? Expected <!--/TEMPLATE-->
# ❌ Unclosed TEMPLATE placeholder
<​!--TEMPLATE
content: |
  Test
-->
content
(missing closing tag)

Error message:

Line 1: Unclosed <!--TEMPLATE--> placeholder. Expected closing tag <!--/TEMPLATE-->
# ❌ Mismatched nested placeholders
<​!--TEMPLATE ... -->
  <​!--INCLUDE ... -->
  <​!--/TEMPLATE-->  ← Should be <!--/INCLUDE-->
<​!--/INCLUDE-->

Error message:

Line 3: Closing <!--/TEMPLATE--> does not match opening <!--INCLUDE--> at line 2.

Safety with backups:

Combined with the .md.bak backup files, you can always compare changes using diff:

mdship update myfile.md
diff myfile.md.bak myfile.md  # See exactly what changed

1.3.17. Managed Content Integrity

When mdship update writes content between placeholder markers (TOC, INCLUDE, MERMAID), it automatically records a hash of that content inside the opening marker. On every subsequent run it verifies the hash before overwriting the block. If the hash does not match — meaning the content was edited manually — mdship refuses to continue and prints an error.

This prevents accidentally losing hand-written edits that were made inside a managed block.

Applies to: <!--TOC-->, <!--INCLUDE-->, <!--MERMAID-->, <!--TEMPLATE-->, <!--JINJA2--> — all built-in placeholders that generate managed content. Each placeholder processor must explicitly wire in the hash check; it is not applied automatically to new placeholders.

What it looks like after the first run:

<!--TOC
_content_generated_: 312:md5:a3f1c8b2e94d7056...
# ⚠️ MANAGED CONTENT: Edits will be lost.
# danger zone: Delete _content_generated_ to override.
-->
- [Introduction](#introduction)
- [Getting Started](#getting-started)
<!--/TOC-->

The _content_generated_ line stores the character count and MD5 hash of the managed block. The two comment lines below it are a human-readable warning — they are plain YAML comments and are ignored by the parser.

Three scenarios:

  1. First run_content_generated_ is absent. mdship generates the content, computes the hash, and inserts the _content_generated_ line plus the warning comments into the opening marker.

  2. Subsequent runs_content_generated_ is present. mdship first checks that the closing tag is at exactly the stored character offset (catching insertions or deletions that shift the closing tag), then verifies the MD5 hash of the current content. If both checks pass, it regenerates the content and updates the hash.

  3. User override (manual) — delete the _content_generated_ line from the opening marker. The next run treats the placeholder as new (first-run behaviour) and overwrites whatever is between the markers without complaint.

  4. User override (command-line) — pass --force / -f to mdship update:

mdship update --force file.md
mdship update -f file.md

With --force, all hash checks are skipped. If the stored length is present and the closing tag is at the expected position, length-based positioning is still used (so resilience against closing-tag strings in generated content is preserved). If the closing tag is not at the expected position, mdship falls back to a regex scan to find it, just as if there were no _content_generated_ at all. Either way, the hash is recomputed and written back after the update.

Note: When the content length has changed, --force falls back to a regex scan to locate the closing tag. If the managed content itself contains a string that looks like a closing placeholder tag (e.g. <!--/INCLUDE-->), the regex scan will stop at that false match rather than the real closing tag, and the update will produce incorrect output. In that case, --force cannot help. The safe recovery is to delete the managed content (everything between the opening --> and the "real" closing tag) manually, leaving the markers in place, and then run mdship update with or without --force. With no content and no _content_generated_ key, mdship treats the placeholder as new and regenerates it cleanly.

Error messages:

If the closing tag is not at the expected position (content length changed):

ERROR: Placeholder TOC document integrity compromised.
Closing tag not found at expected position.
Delete _content_generated_ line to override and accept data loss.

If the closing tag is in the right place but the content hash differs (same-length edit):

ERROR: Placeholder TOC content was manually edited.
Hash mismatch detected.
Delete _content_generated_ line to override and accept data loss.

Notes:

  • The hash is computed on the exact bytes between the opening --> and the closing tag, including surrounding newlines.
  • The length-based check makes the parser resilient to generated content that itself contains a closing-tag-like string (e.g. an INCLUDE that pulls in a file mentioning <!--/INCLUDE-->).
  • You can freely edit the YAML configuration keys inside the opening marker (e.g. min-level, from, file) — those are outside the managed block and are never overwritten.

1.3.18. AI Placeholders

The <!--AI--> placeholder embeds a generation prompt directly in a markdown document. Claude reads the prompt, generates the content, and writes it between the markers. Unlike all other placeholders, AI placeholders are not processed by mdship update — they are handled by Claude through the /ai-placeholder skill.

Basic example:

<!--AI
name: "intro"
prompt: |
    Write a two-paragraph introduction explaining what this tool does.
    Keep it concise and aimed at developers.
-->

Content Claude generates goes here.

<!--/AI-->

Declaring file dependencies with deps::

<!--AI
name: "api-summary"
prompt: |
    Summarise the public API from the source files below.
deps:
  - path: src/api.py
  - path: src/models.py
    range: "1..80"
-->

<!--/AI-->

mdship extracts the file content and checksums each dep. Claude receives the content without reading the files itself. If a dep changes, the next ai_context check detects it automatically.

Shared writing instructions with brief::

<!--AI
name: "overview"
brief: docs/brief.md
prompt: |
    Write a high-level overview of this module.
-->

<!--/AI-->

The brief file is read once per generation run and provides standing style/tone/audience instructions that apply to every AI placeholder referencing it.

Protecting generated content:

After Claude writes content, run ai-fix to record checksums of the content, prompt, brief, and all deps:

mdship ai-fix file.md               # Record checksums for all AI placeholders
mdship ai-fix file.md --name intro  # Record for a specific placeholder

Verify that nothing was accidentally edited since the last generation:

mdship ai-check file.md             # Verify all AI placeholder checksums
mdship ai-check file.md --name api-summary

ai-check exits with code 0 on success and code 1 with error messages if any content, prompt, brief, or dep has changed.

Validate AI placeholder names:

The validate command now also checks AI placeholder name uniqueness:

mdship validate file.md

For full details on deps, brief, the MCP tools (ai_context, ai_update), and the Claude workflow see documentation/AI.md.

1.3.19. MCP Server

Configure in your Claude settings:

{
  "mcpServers": {
    "mdship": {
      "command": "mdship",
      "args": ["mcp"]
    }
  }
}

Available tools include all CLI operations plus four AI-specific tools:

Tool Description
ai_fix Record content, prompt, brief, and dep checksums after generation
ai_check Verify all stored checksums still match
ai_context Gate call: returns up_to_date, needs_update (with prompt, deps, brief, previous content), or error
ai_update Write new content and record all checksums atomically — agent never reads or writes the file directly

The /ai-placeholder skill uses ai_context → generate → ai_update so the full source document never enters the agent's context window.

1.4. Design

mdship uses markdown-it-py to parse markdown into an AST (Abstract Syntax Tree). This ensures:

  • Robust handling of complex markdown documents
  • Preservation of document structure (headings, lists, code blocks, etc.)
  • Proper handling of inline formatting (bold, italic, links, etc.)
  • Reliable reflow operations that respect markdown semantics

1.4.1. Dependencies

Production dependencies used by mdship:

Package Version License Purpose
typer >=0.12 BSD 3-Clause CLI framework for building command-line interfaces with type hints
rich >=13 MIT Rich text and beautiful formatting in the terminal
mcp >=1.0 MIT Model Context Protocol for connecting Claude with external tools
markdown-it-py >=3 MIT Markdown parser with AST support
pyyaml >=6 MIT YAML parser and emitter for configuration
merm >=0.1 WTFPL Mermaid diagram rendering to SVG/PNG

1.4.2. Development Dependencies

Testing and code quality tools (not included in distribution):

Package Version License Purpose
pytest >=8 MIT Testing framework
ruff >=0.4 MIT Python linter and formatter

All dependencies are pinned to minimum compatible versions for stability and compatibility. Most use permissive open-source licenses (MIT, BSD, WTFPL).

1.5. Development

Install dependencies:

uv sync

Run tests:

pytest

Format code:

ruff check --fix

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mdship-1.1.6.tar.gz (37.6 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mdship-1.1.6-py3-none-any.whl (77.6 kB view details)

Uploaded Python 3

File details

Details for the file mdship-1.1.6.tar.gz.

File metadata

  • Download URL: mdship-1.1.6.tar.gz
  • Upload date:
  • Size: 37.6 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.7.13

File hashes

Hashes for mdship-1.1.6.tar.gz
Algorithm Hash digest
SHA256 caae5a2f93fa4fb54f5156d811df2190d59a4d16938b1c071e084b4e92ac1e2c
MD5 b9318fc4141f2fef145943f034f6b644
BLAKE2b-256 3775ee9cded6b22d83217d31b35f647418a8d0cf95e1bd9839fb323b1da4f318

See more details on using hashes here.

File details

Details for the file mdship-1.1.6-py3-none-any.whl.

File metadata

  • Download URL: mdship-1.1.6-py3-none-any.whl
  • Upload date:
  • Size: 77.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.7.13

File hashes

Hashes for mdship-1.1.6-py3-none-any.whl
Algorithm Hash digest
SHA256 1fbf740517545c69f2226339f46379b02b42ac3e6ac0a63e3437ec15369f76be
MD5 54544c6794338a2a81e10d03f7925edf
BLAKE2b-256 2707dabb4ae37f286dcd325dbae466ed87842d9d7e183b110526f1d059a88254

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page