mdship 0.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ .Python
6
+ build/
7
+ develop-eggs/
8
+ dist/
9
+ downloads/
10
+ eggs/
11
+ .eggs/
12
+ lib/
13
+ lib64/
14
+ parts/
15
+ sdist/
16
+ var/
17
+ wheels/
18
+ *.egg-info/
19
+ .installed.cfg
20
+ *.egg
21
+
22
+ .pytest_cache/
23
+ .ruff_cache/
24
+ .venv/
25
+ venv/
26
+ env/
27
+ .DS_Store
@@ -0,0 +1,3 @@
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="Encoding" addBOMForNewFiles="with NO BOM" />
4
+ </project>
@@ -0,0 +1,13 @@
1
+ <component name="InspectionProjectProfileManager">
2
+ <profile version="1.0">
3
+ <option name="myName" value="Project Default" />
4
+ <inspection_tool class="PyStubPackagesAdvertiser" enabled="true" level="WARNING" enabled_by_default="true">
5
+ <option name="ignoredPackages">
6
+ <list>
7
+ <option value="pyspark-stubs==3.0.0.dev8" />
8
+ </list>
9
+ </option>
10
+ </inspection_tool>
11
+ <inspection_tool class="ReassignedToPlainText" enabled="false" level="WARNING" enabled_by_default="false" />
12
+ </profile>
13
+ </component>
@@ -0,0 +1,6 @@
1
+ <component name="InspectionProjectProfileManager">
2
+ <settings>
3
+ <option name="USE_PROJECT_PROFILE" value="false" />
4
+ <version value="1.0" />
5
+ </settings>
6
+ </component>
@@ -0,0 +1,14 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="PYTHON_MODULE" version="4">
3
+ <component name="NewModuleRootManager">
4
+ <content url="file://$MODULE_DIR$">
5
+ <excludeFolder url="file://$MODULE_DIR$/.venv" />
6
+ </content>
7
+ <orderEntry type="jdk" jdkName="uv (mdship)" jdkType="Python SDK" />
8
+ <orderEntry type="sourceFolder" forTests="false" />
9
+ </component>
10
+ <component name="PyDocumentationSettings">
11
+ <option name="format" value="PLAIN" />
12
+ <option name="myDocStringFormat" value="Plain" />
13
+ </component>
14
+ </module>
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="Black">
4
+ <option name="sdkName" value="uv (mdship)" />
5
+ </component>
6
+ <component name="ProjectRootManager" version="2" project-jdk-name="uv (mdship)" project-jdk-type="Python SDK" />
7
+ </project>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/mdship.iml" filepath="$PROJECT_DIR$/.idea/mdship.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
@@ -0,0 +1,47 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="AutoImportSettings">
4
+ <option name="autoReloadType" value="SELECTIVE" />
5
+ </component>
6
+ <component name="ChangeListManager">
7
+ <list default="true" id="57e4e07a-8725-42a0-b41e-c0333c53c14c" name="Changes" comment="" />
8
+ <option name="SHOW_DIALOG" value="false" />
9
+ <option name="HIGHLIGHT_CONFLICTS" value="true" />
10
+ <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
11
+ <option name="LAST_RESOLUTION" value="IGNORE" />
12
+ </component>
13
+ <component name="ProjectColorInfo"><![CDATA[{
14
+ "associatedIndex": 4
15
+ }]]></component>
16
+ <component name="ProjectId" id="3EgA9ad6RYxNiPjwWPDgNaiD5hf" />
17
+ <component name="ProjectViewState">
18
+ <option name="hideEmptyMiddlePackages" value="true" />
19
+ <option name="showLibraryContents" value="true" />
20
+ </component>
21
+ <component name="PropertiesComponent"><![CDATA[{
22
+ "keyToString": {
23
+ "ModuleVcsDetector.initialDetectionPerformed": "true",
24
+ "RunOnceActivity.ShowReadmeOnStart": "true",
25
+ "junie.onboarding.icon.badge.shown": "true",
26
+ "last_opened_file_path": "/Users/verhasp/github/mdship",
27
+ "settings.editor.selected.configurable": "preferences.pluginManager"
28
+ }
29
+ }]]></component>
30
+ <component name="SharedIndexes">
31
+ <attachedChunks>
32
+ <set>
33
+ <option value="bundled-python-sdk-9f8e2b94138c-36ea0e71a18c-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-251.26094.141" />
34
+ </set>
35
+ </attachedChunks>
36
+ </component>
37
+ <component name="TaskManager">
38
+ <task active="true" id="Default" summary="Default task">
39
+ <changelist id="57e4e07a-8725-42a0-b41e-c0333c53c14c" name="Changes" comment="" />
40
+ <created>1780589687264</created>
41
+ <option name="number" value="Default" />
42
+ <option name="presentableId" value="Default" />
43
+ <updated>1780589687264</updated>
44
+ </task>
45
+ <servers />
46
+ </component>
47
+ </project>
mdship-0.0.0/.mcp.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "mcpServers": {
3
+ "mdship": {
4
+ "command": "mdship",
5
+ "args": [
6
+ "mcp"
7
+ ]
8
+ }
9
+ }
10
+ }
mdship-0.0.0/CLAUDE.md ADDED
@@ -0,0 +1,116 @@
1
+ # mdship — Markdown Manipulation Tool
2
+
3
+ ## Project Overview
4
+
5
+ **mdship** is a command-line tool and MCP (Model Context Protocol) server for manipulating markdown files. It provides utilities for fixing heading hierarchies, shifting heading levels, adding content checksums, and reflowing paragraphs.
6
+
7
+ The tool runs locally with no external dependencies (except for the base Python environment) and can be invoked either as a CLI command or as an MCP server for use with Claude.
8
+
9
+ ---
10
+
11
+ ## Repository Structure
12
+
13
+ ```
14
+ mdship/
15
+ ├── CLAUDE.md # This file
16
+ ├── README.md # User-facing documentation
17
+ ├── pyproject.toml # Project definition and dependencies
18
+ ├── .mcp.json # MCP server configuration
19
+ ├── .gitignore
20
+
21
+ ├── mdship/ # Main package
22
+ │ ├── __init__.py
23
+ │ ├── cli.py # CLI command dispatcher (typer app)
24
+ │ ├── markdown.py # Core markdown manipulation functions
25
+ │ └── mcp_server.py # MCP server implementation
26
+
27
+ └── tests/ # Test suite
28
+ ├── __init__.py
29
+ └── test_markdown.py # Unit tests for markdown functions
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Core Functions
35
+
36
+ ### `fix_heading_levels(content: str) -> str`
37
+
38
+ Ensures proper heading hierarchy. Should prevent skipping levels (e.g., h1 → h3).
39
+
40
+ **Status**: Skeleton only, needs implementation
41
+
42
+ ### `shift_heading_levels(content: str, levels: int) -> str`
43
+
44
+ Shifts all headings by N levels. Positive = lower (h1 → h2), negative = raise (h2 → h1).
45
+
46
+ **Status**: Basic implementation complete
47
+
48
+ ### `add_content_checksum(content: str, algorithm: str) -> str`
49
+
50
+ Adds or updates a checksum in YAML front-matter. Supports md5, sha1, sha256.
51
+
52
+ **Status**: Basic implementation complete
53
+
54
+ ### `reflow_paragraphs(content: str, width: Optional[int]) -> str`
55
+
56
+ Reflows paragraphs to a specified width, or one sentence per line if width=0.
57
+
58
+ **Status**: Basic implementation complete
59
+
60
+ ---
61
+
62
+ ## CLI Interface
63
+
64
+ Commands are dispatched via `typer.Typer` in `cli.py`. Each command:
65
+ - Takes a markdown file as an argument
66
+ - Outputs the result to stdout
67
+ - Exits with code 1 on error
68
+
69
+ ```bash
70
+ mdship fix-headings file.md
71
+ mdship shift-headings file.md --levels 1
72
+ mdship add-checksum file.md --algorithm sha256
73
+ mdship reflow file.md --width 80
74
+ mdship mcp # Start MCP server on stdio
75
+ ```
76
+
77
+ ---
78
+
79
+ ## MCP Integration
80
+
81
+ The `mcp_server.py` module implements a stdio-based MCP server that exposes the same markdown functions as async tools. The server:
82
+
83
+ - Runs on stdin/stdout only (no network)
84
+ - Exposes tools: `fix_headings`, `shift_headings`, `add_checksum`, `reflow`
85
+ - Handles errors gracefully and returns error messages as text content
86
+
87
+ Configure in Claude's MCP settings:
88
+
89
+ ```json
90
+ {
91
+ "mcpServers": {
92
+ "mdship": {
93
+ "command": "mdship",
94
+ "args": ["mcp"]
95
+ }
96
+ }
97
+ }
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Development Notes
103
+
104
+ - Uses `typer` for CLI, `mcp` Python SDK for server
105
+ - No external markdown parsing library yet; uses regex for headings
106
+ - Front-matter assumed to be YAML between `---` delimiters
107
+ - Code blocks (triple backticks) are preserved during reflow
108
+ - Tests use `pytest`; use `pytest -v` for detailed output
109
+
110
+ ### Next Steps
111
+
112
+ 1. Enhance `fix_heading_levels` to properly normalize hierarchies
113
+ 2. Add more comprehensive markdown parsing (consider `markdown-it-py` or similar)
114
+ 3. Add support for different markdown flavors (GFM, CommonMark, etc.)
115
+ 4. Add recursive directory processing option
116
+ 5. Add dry-run mode to preview changes
mdship-0.0.0/PKG-INFO ADDED
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: mdship
3
+ Version: 0.0.0
4
+ Summary: CLI and MCP tool for manipulating markdown files
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: mcp>=1.0
7
+ Requires-Dist: rich>=13
8
+ Requires-Dist: typer>=0.12
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest>=8; extra == 'dev'
11
+ Requires-Dist: ruff>=0.4; extra == 'dev'
mdship-0.0.0/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # mdship
2
+
3
+ A command-line and MCP tool for manipulating markdown files.
4
+
5
+ ## Features
6
+
7
+ - **Fix heading levels**: Ensure consistent heading hierarchy
8
+ - **Shift headings**: Move all headings up or down by N levels
9
+ - **Add checksums**: Insert content checksums into front-matter
10
+ - **Reflow paragraphs**: Reflow text to a specific width or one sentence per line
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ uv sync
16
+ uv run pip install -e .
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### Command Line
22
+
23
+ ```bash
24
+ mdship fix-headings file.md
25
+ mdship shift-headings file.md --levels 1
26
+ mdship add-checksum file.md --algorithm sha256
27
+ mdship reflow file.md --width 80
28
+ ```
29
+
30
+ ### MCP Server
31
+
32
+ Configure in your Claude settings:
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "mdship": {
38
+ "command": "mdship",
39
+ "args": ["mcp"]
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ Then use the available tools in Claude with markdown content.
46
+
47
+ ## Development
48
+
49
+ Install dependencies:
50
+
51
+ ```bash
52
+ uv sync
53
+ ```
54
+
55
+ Run tests:
56
+
57
+ ```bash
58
+ pytest
59
+ ```
60
+
61
+ Format code:
62
+
63
+ ```bash
64
+ ruff check --fix
65
+ ```
mdship-0.0.0/build.sh ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+ cd "$SCRIPT_DIR"
6
+
7
+ echo "=== Syncing dependencies ==="
8
+ uv sync --all-extras --python /opt/homebrew/bin/python3
9
+
10
+ echo "=== Running tests ==="
11
+ uv run pytest tests/ -v
12
+
13
+ echo "=== Building distribution ==="
14
+ uv build
15
+
16
+ VERSION=$(uv run python -c "from importlib.metadata import version; print(version('mdship'))")
17
+ echo "=== Built mdship $VERSION ==="
@@ -0,0 +1,3 @@
1
+ """mdship — CLI and MCP tool for manipulating markdown files."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,106 @@
1
+ import sys
2
+ from importlib.metadata import version as _pkg_version
3
+ from pathlib import Path
4
+ from typing import Annotated
5
+
6
+ import typer
7
+ from rich.console import Console
8
+
9
+ _VERSION = _pkg_version("mdship")
10
+
11
+ app = typer.Typer(
12
+ help=f"mdship — markdown manipulation tool (version {_VERSION})",
13
+ context_settings={"help_option_names": ["-h", "--help"]},
14
+ )
15
+ err = Console(stderr=True)
16
+
17
+
18
+ def _version_callback(value: bool) -> None:
19
+ if value:
20
+ print(f"mdship {_VERSION}")
21
+ raise typer.Exit()
22
+
23
+
24
+ @app.callback()
25
+ def _main(
26
+ _: Annotated[
27
+ bool | None,
28
+ typer.Option("--version", "-V", callback=_version_callback, is_eager=True, help="Show version and exit."),
29
+ ] = None,
30
+ ) -> None:
31
+ pass
32
+
33
+
34
+ @app.command()
35
+ def fix_headings(
36
+ file: Annotated[Path, typer.Argument(help="Markdown file to process")],
37
+ ) -> None:
38
+ """Fix heading levels to ensure consistent hierarchy."""
39
+ if not file.exists():
40
+ err.print(f"[red]Error:[/red] file not found: {file}")
41
+ raise typer.Exit(1)
42
+
43
+ from mdship.markdown import fix_heading_levels
44
+
45
+ content = file.read_text()
46
+ fixed_content = fix_heading_levels(content)
47
+ print(fixed_content, end="")
48
+
49
+
50
+ @app.command()
51
+ def shift_headings(
52
+ file: Annotated[Path, typer.Argument(help="Markdown file to process")],
53
+ levels: Annotated[int, typer.Option("--levels", "-l", help="Number of levels to shift (positive=lower, negative=higher)")] = 1,
54
+ ) -> None:
55
+ """Shift all headings by the specified number of levels."""
56
+ if not file.exists():
57
+ err.print(f"[red]Error:[/red] file not found: {file}")
58
+ raise typer.Exit(1)
59
+
60
+ from mdship.markdown import shift_heading_levels
61
+
62
+ content = file.read_text()
63
+ shifted_content = shift_heading_levels(content, levels)
64
+ print(shifted_content, end="")
65
+
66
+
67
+ @app.command()
68
+ def add_checksum(
69
+ file: Annotated[Path, typer.Argument(help="Markdown file to process")],
70
+ algorithm: Annotated[str, typer.Option("--algorithm", "-a", help="Hash algorithm (md5, sha256, sha1)")] = "sha256",
71
+ ) -> None:
72
+ """Add or update checksum in front-matter."""
73
+ if not file.exists():
74
+ err.print(f"[red]Error:[/red] file not found: {file}")
75
+ raise typer.Exit(1)
76
+
77
+ from mdship.markdown import add_content_checksum
78
+
79
+ content = file.read_text()
80
+ updated_content = add_content_checksum(content, algorithm)
81
+ print(updated_content, end="")
82
+
83
+
84
+ @app.command()
85
+ def reflow(
86
+ file: Annotated[Path, typer.Argument(help="Markdown file to process")],
87
+ width: Annotated[int | None, typer.Option("--width", "-w", help="Line width (0 for one sentence per line)")] = None,
88
+ ) -> None:
89
+ """Reflow paragraphs to specified width or one sentence per line."""
90
+ if not file.exists():
91
+ err.print(f"[red]Error:[/red] file not found: {file}")
92
+ raise typer.Exit(1)
93
+
94
+ from mdship.markdown import reflow_paragraphs
95
+
96
+ content = file.read_text()
97
+ reflowed_content = reflow_paragraphs(content, width)
98
+ print(reflowed_content, end="")
99
+
100
+
101
+ @app.command()
102
+ def mcp() -> None:
103
+ """Start mdship as an MCP server on stdio."""
104
+ from mdship.mcp_server import main as mcp_main
105
+
106
+ mcp_main()
@@ -0,0 +1,179 @@
1
+ """Core markdown manipulation functions."""
2
+
3
+ import hashlib
4
+ import re
5
+ from typing import Optional
6
+
7
+
8
+ def fix_heading_levels(content: str) -> str:
9
+ """Fix heading levels to ensure consistent hierarchy.
10
+
11
+ Ensures headings follow proper nesting (no skipping from h1 to h3, etc).
12
+ """
13
+ lines = content.split("\n")
14
+ result = []
15
+ min_level = None
16
+
17
+ for line in lines:
18
+ match = re.match(r"^(#{1,6})\s+(.+)$", line)
19
+ if match:
20
+ level = len(match.group(1))
21
+ text = match.group(2)
22
+
23
+ if min_level is None:
24
+ min_level = level
25
+ else:
26
+ # TODO: Implement logic to adjust heading levels
27
+ pass
28
+
29
+ result.append(line)
30
+ else:
31
+ result.append(line)
32
+
33
+ return "\n".join(result)
34
+
35
+
36
+ def shift_heading_levels(content: str, levels: int) -> str:
37
+ """Shift all headings by the specified number of levels.
38
+
39
+ Positive numbers lower the headings (h1 -> h2), negative numbers raise them (h2 -> h1).
40
+ """
41
+ lines = content.split("\n")
42
+ result = []
43
+
44
+ for line in lines:
45
+ match = re.match(r"^(#{1,6})\s+(.+)$", line)
46
+ if match:
47
+ current_level = len(match.group(1))
48
+ text = match.group(2)
49
+ new_level = max(1, min(6, current_level + levels))
50
+ result.append("#" * new_level + " " + text)
51
+ else:
52
+ result.append(line)
53
+
54
+ return "\n".join(result)
55
+
56
+
57
+ def add_content_checksum(content: str, algorithm: str = "sha256") -> str:
58
+ """Add or update checksum in front-matter.
59
+
60
+ Supports md5, sha1, and sha256 algorithms.
61
+ """
62
+ lines = content.split("\n")
63
+
64
+ if not lines or lines[0] != "---":
65
+ # No YAML front-matter, prepend it
66
+ hash_obj = hashlib.new(algorithm)
67
+ hash_obj.update(content.encode())
68
+ checksum = hash_obj.hexdigest()
69
+ front_matter = f"---\nchecksum: {checksum}\nchecksum_algorithm: {algorithm}\n---\n"
70
+ return front_matter + content
71
+
72
+ # Find the closing --- of front-matter
73
+ end_idx = None
74
+ for i in range(1, len(lines)):
75
+ if lines[i] == "---":
76
+ end_idx = i
77
+ break
78
+
79
+ if end_idx is None:
80
+ # Malformed front-matter, just add at the beginning
81
+ hash_obj = hashlib.new(algorithm)
82
+ hash_obj.update(content.encode())
83
+ checksum = hash_obj.hexdigest()
84
+ front_matter = f"---\nchecksum: {checksum}\nchecksum_algorithm: {algorithm}\n---\n"
85
+ return front_matter + content
86
+
87
+ # Calculate checksum of the content (excluding front-matter)
88
+ content_without_fm = "\n".join(lines[end_idx + 1 :])
89
+ hash_obj = hashlib.new(algorithm)
90
+ hash_obj.update(content_without_fm.encode())
91
+ checksum = hash_obj.hexdigest()
92
+
93
+ # Update or add checksum fields in front-matter
94
+ fm_lines = lines[1:end_idx]
95
+ checksum_line = f"checksum: {checksum}"
96
+ algorithm_line = f"checksum_algorithm: {algorithm}"
97
+
98
+ # Remove existing checksum lines
99
+ fm_lines = [
100
+ line
101
+ for line in fm_lines
102
+ if not line.startswith("checksum:") and not line.startswith("checksum_algorithm:")
103
+ ]
104
+
105
+ # Add new checksum lines
106
+ fm_lines.append(checksum_line)
107
+ fm_lines.append(algorithm_line)
108
+
109
+ result = ["---"] + fm_lines + ["---"] + lines[end_idx + 1 :]
110
+ return "\n".join(result)
111
+
112
+
113
+ def reflow_paragraphs(content: str, width: Optional[int] = None) -> str:
114
+ """Reflow paragraphs to specified width or one sentence per line.
115
+
116
+ If width is None or 0, splits into one sentence per line.
117
+ Otherwise, reflows to the specified width.
118
+ """
119
+ lines = content.split("\n")
120
+ result = []
121
+ in_code_block = False
122
+ current_paragraph = []
123
+
124
+ for line in lines:
125
+ # Track code blocks
126
+ if line.startswith("```"):
127
+ in_code_block = not in_code_block
128
+
129
+ if in_code_block:
130
+ if current_paragraph:
131
+ result.extend(_reflow_paragraph(current_paragraph, width))
132
+ current_paragraph = []
133
+ result.append(line)
134
+ elif line.strip() == "":
135
+ # Empty line marks end of paragraph
136
+ if current_paragraph:
137
+ result.extend(_reflow_paragraph(current_paragraph, width))
138
+ current_paragraph = []
139
+ result.append("")
140
+ elif re.match(r"^[#\-\*]", line) or line.startswith(">"):
141
+ # Heading, list, or blockquote
142
+ if current_paragraph:
143
+ result.extend(_reflow_paragraph(current_paragraph, width))
144
+ current_paragraph = []
145
+ result.append(line)
146
+ else:
147
+ # Regular paragraph line
148
+ current_paragraph.append(line)
149
+
150
+ # Handle remaining paragraph
151
+ if current_paragraph:
152
+ result.extend(_reflow_paragraph(current_paragraph, width))
153
+
154
+ return "\n".join(result)
155
+
156
+
157
+ def _reflow_paragraph(lines: list[str], width: Optional[int] = None) -> list[str]:
158
+ """Reflow a paragraph (list of lines) to the specified width or sentence per line."""
159
+ text = " ".join(line.strip() for line in lines if line.strip())
160
+
161
+ if width is None or width == 0:
162
+ # One sentence per line
163
+ sentences = re.split(r"(?<=[.!?])\s+", text)
164
+ return [s.strip() for s in sentences if s.strip()]
165
+ else:
166
+ # Reflow to width
167
+ result = []
168
+ current_line = ""
169
+ for word in text.split():
170
+ if not current_line:
171
+ current_line = word
172
+ elif len(current_line) + 1 + len(word) <= width:
173
+ current_line += " " + word
174
+ else:
175
+ result.append(current_line)
176
+ current_line = word
177
+ if current_line:
178
+ result.append(current_line)
179
+ return result