Skip to content
Draft
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
36 changes: 35 additions & 1 deletion docs/reference/command-line.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,15 @@ seconv list-rf-rules # list remove-formatting rule IDs (alias: list-remov
seconv dump-settings # print a full --settings JSON with libse defaults (alias: default-settings)
seconv info <file> # print format/encoding/duration/language for a file
seconv lint <pattern> # validate subtitle(s); exit 1 if issues found
seconv mcp # run as a Model Context Protocol server over stdio
seconv --help # show help (same text as -h, /? and /help)
seconv --help-json # print the whole command-line schema as JSON
seconv --version # print version and exit
```

### Machine-readable output

Every subcommand above accepts `--json`, and so does a conversion run. Scripts and agents should prefer it: the tables are hundreds of box-drawing lines, while the JSON gives you the exact tokens each option accepts.
Every ordinary CLI subcommand above except `mcp` accepts `--json`, and so does a conversion run. `mcp` instead reserves stdout for JSON-RPC frames. Scripts and agents using the CLI should prefer `--json`: the tables are hundreds of box-drawing lines, while the JSON gives you the exact tokens each option accepts.

```bash
seconv formats --json | jq -r '.formats[] | select(.inputOnly | not) | .id'
Expand Down Expand Up @@ -124,6 +125,39 @@ seconv lint *.srt # check overlaps, line lengths, tags, ...
seconv lint *.srt --json # CI-friendly: exit 1 on any issue
```

### MCP server

`seconv mcp` exposes the engine as a [Model Context Protocol](https://modelcontextprotocol.io) server over stdio.
Register it as a local stdio server in an MCP-capable client:

```json
{ "mcpServers": { "seconv": { "command": "seconv", "args": ["mcp"] } } }
```

The server exposes:

| Tool | What it does |
|---|---|
| `list_formats` | Lists readable/writable formats. Optional substring filter. |
| `subtitle_info` | Detects format, encoding, paragraph count, time range, duration and language. |
| `read_subtitle` | Reads paged subtitle paragraphs from supported text or binary formats. |
| `lint_subtitle` | Runs the same subtitle validation rules as `seconv lint`. |
| `convert_subtitle` | Converts one or more local subtitle inputs using a focused subset of CLI controls (format/output, encoding, timing, track selection, OCR and cleanup operations). Advanced translation/image-style/custom-format/settings controls remain CLI-only. |
| `list_fix_common_errors_rules` | Lists rule ids accepted by `fixCommonErrorsRules`. |
| `list_remove_formatting_rules` | Lists rule ids accepted by `removeFormattingRules`. |

The six inspection/list tools are advertised read-only and non-destructive. `convert_subtitle` is explicitly
advertised as write-capable and destructive-capable because `overwrite=true` may replace an existing output file.
All tools are closed-world local-file operations. Client cancellation is propagated cooperatively into conversion;
legacy synchronous/native stages that do not accept a cancellation token are stopped at the surrounding checkpoints.

The MCP server does not add a filesystem sandbox. Paths are resolved as local paths under the operating-system permissions of the `seconv` process; clients should apply their normal tool-approval and sandbox policies.

A failed or cancelled multi-file call can have completed earlier output files before the later failure or cancellation. Inspect the returned per-file conversion data and existing outputs before retrying; a tool error is not a transaction rollback.

Stdout is reserved exclusively for MCP JSON-RPC traffic. Logs and diagnostics go to stderr; start the server with
`seconv mcp --verbose` for debug logging.

## Options

### File / I/O
Expand Down
1 change: 1 addition & 0 deletions src/seconv/Helpers/CliSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ public static string ToJson()
new { name = "dump-settings", json = true, description = "Print a full --settings JSON with libse defaults. Always JSON; redirect to a file." },
new { name = "info", json = true, description = "Print format / encoding / duration / language for one file. Usage: seconv info <file>" },
new { name = "lint", json = true, description = "Validate subtitles; exit 1 if any issues found. Usage: seconv lint <pattern>..." },
new { name = "mcp", json = false, description = "Run seconv as a Model Context Protocol server over stdio. Tools: list_formats, subtitle_info, read_subtitle, lint_subtitle, convert_subtitle, list_fix_common_errors_rules, list_remove_formatting_rules. Usage: seconv mcp [--verbose]" },
new { name = "--help-json", json = true, description = "Print this schema." },
new { name = "--version", json = false, description = "Print the seconv version and exit." },
},
Expand Down
1 change: 1 addition & 0 deletions src/seconv/Helpers/HelpDisplay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ private static void ShowHelp(IAnsiConsole console)
ShowParameter(console, "dump-settings", "Print a full --settings JSON with libse defaults (redirect to a file)");
ShowParameter(console, "info <file>", "Print format / encoding / duration / language info");
ShowParameter(console, "lint <pattern>", "Validate subtitle(s); exit 1 if any issues found");
ShowParameter(console, "mcp", "Run as a Model Context Protocol server over stdio (Claude, Cursor, ...)");

console.WriteLine();
ShowSection(console, "Examples", null);
Expand Down
77 changes: 77 additions & 0 deletions src/seconv/Mcp/McpServerHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using SeConv.Helpers;

namespace SeConv.Mcp;

/// <summary>
/// <c>seconv mcp</c>: runs seconv as a Model Context Protocol server over stdio so an MCP client
/// (Claude Desktop, Claude Code, Cursor, ...) can call the tools in <see cref="SubtitleTools"/>
/// without a shell. The JSON-RPC transport owns stdout; logs and any incidental console output
/// from libse or Spectre go to stderr so they can never corrupt a protocol frame.
/// </summary>
internal static class McpServerHost
{
private const string Instructions =
"seconv exposes Subtitle Edit's conversion engine for hundreds of subtitle formats. " +
"Start with subtitle_info to detect a file's format, encoding and duration; use read_subtitle to see " +
"its paragraphs (any format, paged); lint_subtitle to find timing/line-length problems; " +
"convert_subtitle to write a new file in another format, optionally shifting times, changing " +
"frame rate or applying operations such as FixCommonErrors. Paths are local file-system paths. " +
"The server does not add a filesystem sandbox; access is limited by the operating-system permissions of the seconv process.";

public static async Task<int> RunAsync(string[] args)
{
if (!TryParseArguments(args, out var verbose, out var error))
{
Console.Error.WriteLine(error);
return 1;
}

// Stdout is the protocol channel. The stdio transport writes to the raw standard output
// stream, so redirecting Console.Out only affects incidental writers (libse notices,
// Spectre markup from a non-quiet code path) - they land on stderr instead of inside a frame.
Console.SetOut(Console.Error);

var builder = Host.CreateApplicationBuilder(Array.Empty<string>());
builder.Logging.ClearProviders();
builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace);
builder.Logging.SetMinimumLevel(verbose ? LogLevel.Debug : LogLevel.Warning);

builder.Services
.AddMcpServer(options =>
{
options.ServerInfo = new Implementation { Name = "seconv", Version = CliSchema.Version };
options.ServerInstructions = Instructions;
})
.WithStdioServerTransport()
.WithTools<SubtitleTools>();

await builder.Build().RunAsync();
return 0;
}

internal static bool TryParseArguments(string[] args, out bool verbose, out string? error)
{
verbose = false;
error = null;

foreach (var arg in args)
{
if (arg.Equals("--verbose", StringComparison.OrdinalIgnoreCase) ||
arg.Equals("-v", StringComparison.OrdinalIgnoreCase))
{
verbose = true;
continue;
}

error = $"Unknown mcp option '{arg}'. Usage: seconv mcp [--verbose]";
return false;
}

return true;
}
}
Loading
Loading