mdship 0.0.0__py3-none-any.whl
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.
- mdship/__init__.py +3 -0
- mdship/cli.py +106 -0
- mdship/markdown.py +179 -0
- mdship/mcp_server.py +133 -0
- mdship-0.0.0.dist-info/METADATA +11 -0
- mdship-0.0.0.dist-info/RECORD +8 -0
- mdship-0.0.0.dist-info/WHEEL +4 -0
- mdship-0.0.0.dist-info/entry_points.txt +2 -0
mdship/__init__.py
ADDED
mdship/cli.py
ADDED
|
@@ -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()
|
mdship/markdown.py
ADDED
|
@@ -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
|
mdship/mcp_server.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP server for mdship.
|
|
3
|
+
|
|
4
|
+
Exposes markdown manipulation tools over stdio. Start with:
|
|
5
|
+
mdship mcp
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from mcp.server import Server
|
|
15
|
+
from mcp.types import TextContent, Tool
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main() -> None:
|
|
19
|
+
"""Run the MCP server on stdio."""
|
|
20
|
+
server = Server("mdship")
|
|
21
|
+
|
|
22
|
+
@server.list_tools()
|
|
23
|
+
async def list_tools() -> list[Tool]:
|
|
24
|
+
return [
|
|
25
|
+
Tool(
|
|
26
|
+
name="fix_headings",
|
|
27
|
+
description="Fix heading levels to ensure consistent hierarchy",
|
|
28
|
+
inputSchema={
|
|
29
|
+
"type": "object",
|
|
30
|
+
"properties": {
|
|
31
|
+
"content": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "Markdown content to process",
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"required": ["content"],
|
|
37
|
+
},
|
|
38
|
+
),
|
|
39
|
+
Tool(
|
|
40
|
+
name="shift_headings",
|
|
41
|
+
description="Shift all headings by the specified number of levels",
|
|
42
|
+
inputSchema={
|
|
43
|
+
"type": "object",
|
|
44
|
+
"properties": {
|
|
45
|
+
"content": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"description": "Markdown content to process",
|
|
48
|
+
},
|
|
49
|
+
"levels": {
|
|
50
|
+
"type": "integer",
|
|
51
|
+
"description": "Number of levels to shift (positive=lower, negative=higher)",
|
|
52
|
+
"default": 1,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
"required": ["content"],
|
|
56
|
+
},
|
|
57
|
+
),
|
|
58
|
+
Tool(
|
|
59
|
+
name="add_checksum",
|
|
60
|
+
description="Add or update checksum in front-matter",
|
|
61
|
+
inputSchema={
|
|
62
|
+
"type": "object",
|
|
63
|
+
"properties": {
|
|
64
|
+
"content": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"description": "Markdown content to process",
|
|
67
|
+
},
|
|
68
|
+
"algorithm": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"description": "Hash algorithm (md5, sha256, sha1)",
|
|
71
|
+
"default": "sha256",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
"required": ["content"],
|
|
75
|
+
},
|
|
76
|
+
),
|
|
77
|
+
Tool(
|
|
78
|
+
name="reflow",
|
|
79
|
+
description="Reflow paragraphs to specified width or one sentence per line",
|
|
80
|
+
inputSchema={
|
|
81
|
+
"type": "object",
|
|
82
|
+
"properties": {
|
|
83
|
+
"content": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"description": "Markdown content to process",
|
|
86
|
+
},
|
|
87
|
+
"width": {
|
|
88
|
+
"type": "integer",
|
|
89
|
+
"description": "Line width (0 or null for one sentence per line)",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
"required": ["content"],
|
|
93
|
+
},
|
|
94
|
+
),
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
@server.call_tool()
|
|
98
|
+
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
99
|
+
from mdship.markdown import (
|
|
100
|
+
add_content_checksum,
|
|
101
|
+
fix_heading_levels,
|
|
102
|
+
reflow_paragraphs,
|
|
103
|
+
shift_heading_levels,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
if name == "fix_headings":
|
|
108
|
+
content = arguments["content"]
|
|
109
|
+
result = fix_heading_levels(content)
|
|
110
|
+
elif name == "shift_headings":
|
|
111
|
+
content = arguments["content"]
|
|
112
|
+
levels = arguments.get("levels", 1)
|
|
113
|
+
result = shift_heading_levels(content, levels)
|
|
114
|
+
elif name == "add_checksum":
|
|
115
|
+
content = arguments["content"]
|
|
116
|
+
algorithm = arguments.get("algorithm", "sha256")
|
|
117
|
+
result = add_content_checksum(content, algorithm)
|
|
118
|
+
elif name == "reflow":
|
|
119
|
+
content = arguments["content"]
|
|
120
|
+
width = arguments.get("width")
|
|
121
|
+
result = reflow_paragraphs(content, width)
|
|
122
|
+
else:
|
|
123
|
+
return [TextContent(type="text", text=f"Unknown tool: {name}")]
|
|
124
|
+
|
|
125
|
+
return [TextContent(type="text", text=result)]
|
|
126
|
+
except Exception as e:
|
|
127
|
+
return [TextContent(type="text", text=f"Error: {str(e)}")]
|
|
128
|
+
|
|
129
|
+
async def run():
|
|
130
|
+
async with server:
|
|
131
|
+
await server.wait_for_shutdown()
|
|
132
|
+
|
|
133
|
+
asyncio.run(run())
|
|
@@ -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'
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
mdship/__init__.py,sha256=kWJ5Ylr_DHKHbd2myITrih0VfZdWVAKqfgj5C0nf66A,90
|
|
2
|
+
mdship/cli.py,sha256=QVeTombjQe_dvZk58ExEW_HBi5cmoxLVS36f_26Rh_4,3157
|
|
3
|
+
mdship/markdown.py,sha256=HOw2B_NHurpB0s2NtsPKiZLSTJMd7uvZVLc-ygCN8p8,5732
|
|
4
|
+
mdship/mcp_server.py,sha256=xio11hxEw3f5MqQtebNfdPXI0G3nvHCOHPmxy3Mh-Qw,4667
|
|
5
|
+
mdship-0.0.0.dist-info/METADATA,sha256=R4JsKdLR417je5CJwxWwBvOOfIPpw0EcR52W02U1PJA,309
|
|
6
|
+
mdship-0.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
7
|
+
mdship-0.0.0.dist-info/entry_points.txt,sha256=7U-iGlLgwBr3_ccktLo4MOp_9Su38pXErzoun1opP24,42
|
|
8
|
+
mdship-0.0.0.dist-info/RECORD,,
|