Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

### Added

- `markkit mcp`: Model Context Protocol server over stdio exposing the `markkit` tool
(`tools/list`, `tools/call`). URL fetching keeps the SSRF guard; `--no-url` keeps it offline,
the `extract` limit flags and `--max-output-chars` bound each call.
- Plain text input (`text/plain`, `.txt`), always on: paragraphs and hard breaks through the IR,
UTF-8/UTF-16 BOM decoding, control characters dropped, `invalid_utf8` warning on lossy decode.
- Markdown input (`markdown` feature, in `all_formats`; `text/markdown`, `.md`): re-parsed with
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,13 @@ markkit extract --file page.html --base-url https://example.com/blog/

# From stdin
cat page.html | markkit extract -

# MCP server (stdio) exposing the `markkit` tool to agents
markkit mcp # add --no-url to keep it offline
```

MCP client config: `{"mcpServers": {"markkit": {"command": "markkit", "args": ["mcp"]}}}`.

### Python

```python
Expand Down
4 changes: 2 additions & 2 deletions crates/markkit-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ spreadsheet = ["markkit/spreadsheet"]
markdown = ["markkit/markdown"]

[dependencies]
markkit = { path = "../markkit", version = "0.1.1", features = ["http_client"] }
tokio = { workspace = true, features = ["macros", "net", "rt", "rt-multi-thread", "time", "fs"] }
markkit = { path = "../markkit", version = "0.1.1", features = ["http_client", "tool"] }
tokio = { workspace = true, features = ["macros", "net", "rt", "rt-multi-thread", "time", "fs", "io-std", "io-util"] }
clap.workspace = true
anyhow.workspace = true
serde_json.workspace = true
Expand Down
45 changes: 44 additions & 1 deletion crates/markkit-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use std::time::Duration;
use clap::{Args, Parser, Subcommand};
use markkit::{Limits, Markkit};

mod mcp;

#[derive(Parser)]
#[command(
name = "markkit",
Expand Down Expand Up @@ -53,6 +55,27 @@ enum Commands {
#[arg(long)]
main_content: bool,

#[command(flatten)]
limits: LimitArgs,
},
/// Serve the `markkit` tool over MCP (JSON-RPC on stdin/stdout)
Mcp {
/// Do not offer the `url` source (no network access)
#[arg(long)]
no_url: bool,

/// Maximum markdown characters returned per call
#[arg(long, default_value_t = markkit::tool::DEFAULT_MAX_OUTPUT_CHARS)]
max_output_chars: usize,

/// Keep only the detected main content (article body) of HTML pages
#[arg(long)]
main_content: bool,

/// Locale for tool descriptions and errors (en-US, uk-UA)
#[arg(long, default_value = "en-US")]
locale: String,

#[command(flatten)]
limits: LimitArgs,
},
Expand Down Expand Up @@ -133,6 +156,24 @@ async fn main() -> Result<()> {
let cli = Cli::parse();

match cli.command {
Commands::Mcp {
no_url,
max_output_chars,
main_content,
locale,
limits,
} => {
let limits = limits.apply(Limits::default());
let max_message = mcp::max_message_bytes(limits.max_input_size);
let tool = markkit::tool::MarkkitTool::builder()
.locale(&locale)
.config(Markkit::builder().limits(limits).main_content(main_content))
.allow_url(!no_url)
.max_output_chars(max_output_chars)
.build();
let stdin = tokio::io::BufReader::new(tokio::io::stdin());
mcp::serve(&tool, stdin, tokio::io::stdout(), max_message).await?;
}
Commands::Extract {
source,
file,
Expand Down Expand Up @@ -215,7 +256,9 @@ mod tests {

fn limits_from(args: &[&str]) -> Limits {
let cli = Cli::try_parse_from(["markkit", "extract", "x"].iter().chain(args)).unwrap();
let Commands::Extract { limits, .. } = cli.command;
let Commands::Extract { limits, .. } = cli.command else {
panic!("expected extract");
};
limits.apply(Limits::default())
}

Expand Down
Loading
Loading