weavatrix-git 0.3.0

Fast, bounded, evidence-carrying Git reader with an optional read-only MCP server
Documentation
//! Bounded, read-only MCP surface for local Git evidence.

mod arguments;
mod output;
mod tools;

use std::{path::Path, sync::Arc, time::Duration};

use mcport::{ConcurrentMcpServer, FlushPolicy, RuntimeConfig, TransportLimits, json};

use crate::{Limits, Repository, Result};

/// Maximum request body accepted by the stdio transport.
pub const MAX_REQUEST_BYTES: usize = 256 * 1024;
/// Maximum complete response emitted by the stdio transport.
pub const MAX_RESPONSE_BYTES: usize = 1024 * 1024;

/// Returns the production defaults for the controlled MCP runtime.
#[must_use]
pub fn runtime_config() -> RuntimeConfig {
    RuntimeConfig {
        transport: TransportLimits::new(MAX_REQUEST_BYTES, MAX_RESPONSE_BYTES),
        max_in_flight: 4,
        queue_depth: 32,
        output_queue_depth: 32,
        output_flush_policy: FlushPolicy::PerMessage,
        handler_deadline: Some(Duration::from_secs(30)),
    }
}

/// Opens one repository and builds its read-only MCP tool catalog.
pub fn open_server(path: impl AsRef<Path>) -> Result<ConcurrentMcpServer> {
    let repository = Arc::new(Repository::open_with_limits(path, mcp_limits())?);
    let location = repository
        .work_dir()
        .unwrap_or_else(|| repository.git_dir())
        .display()
        .to_string();
    let instructions = format!(
        "Read-only Git evidence for {location}. No Git subprocesses, hooks, \
         network access, checkout, or repository mutation."
    );

    let head_repository = Arc::clone(&repository);
    let history_repository = Arc::clone(&repository);
    let diff_repository = Arc::clone(&repository);
    let status_repository = Arc::clone(&repository);
    let snapshot_repository = Arc::clone(&repository);

    Ok(
        ConcurrentMcpServer::new("weavatrix-git", env!("CARGO_PKG_VERSION"))
            .instructions(instructions)
            .tool(
                "git_head",
                "Resolve HEAD and report repository identity without launching Git.",
                json!({
                    "type": "object",
                    "properties": {},
                    "additionalProperties": false
                }),
                move |context, arguments| tools::head(context, &head_repository, &arguments),
            )
            .tool(
                "git_history",
                "Read bounded commit history with exact object IDs and pagination.",
                json!({
                    "type": "object",
                    "properties": {
                        "revision": {"type": "string", "default": "HEAD", "maxLength": 256},
                        "limit": {"type": "integer", "minimum": 1, "maximum": 200, "default": 50},
                        "cursor": {"type": "integer", "minimum": 0, "maximum": 9999, "default": 0},
                        "firstParent": {"type": "boolean", "default": false},
                        "since": {"type": "integer"},
                        "until": {"type": "integer"},
                        "includeMessage": {"type": "boolean", "default": false}
                    },
                    "additionalProperties": false
                }),
                move |context, arguments| tools::history(context, &history_repository, &arguments),
            )
            .tool(
                "git_diff",
                "Compare two commits and return bounded, byte-evidenced tree changes.",
                json!({
                    "type": "object",
                    "required": ["old"],
                    "properties": {
                        "old": {"type": "string", "maxLength": 256},
                        "new": {"type": "string", "default": "HEAD", "maxLength": 256},
                        "limit": {"type": "integer", "minimum": 1, "maximum": 1000, "default": 200},
                        "cursor": {"type": "integer", "minimum": 0, "default": 0}
                    },
                    "additionalProperties": false
                }),
                move |context, arguments| tools::diff(context, &diff_repository, &arguments),
            )
            .tool(
                "git_status",
                "Read tracked index/worktree status; untracked files are intentionally excluded.",
                json!({
                    "type": "object",
                    "properties": {
                        "limit": {"type": "integer", "minimum": 1, "maximum": 1000, "default": 200},
                        "cursor": {"type": "integer", "minimum": 0, "default": 0}
                    },
                    "additionalProperties": false
                }),
                move |context, arguments| tools::status(context, &status_repository, &arguments),
            )
            .tool(
                "git_snapshot",
                "Read a canonical immutable manifest for one revision with pagination.",
                json!({
                    "type": "object",
                    "properties": {
                        "revision": {"type": "string", "default": "HEAD", "maxLength": 256},
                        "limit": {"type": "integer", "minimum": 1, "maximum": 1000, "default": 200},
                        "cursor": {"type": "integer", "minimum": 0, "default": 0}
                    },
                    "additionalProperties": false
                }),
                move |context, arguments| {
                    tools::snapshot(context, &snapshot_repository, &arguments)
                },
            ),
    )
}

fn mcp_limits() -> Limits {
    Limits {
        max_object_bytes: 64 * 1024 * 1024,
        max_tree_entries: 500_000,
        max_history_commits: 10_000,
        max_bitmap_objects: 2_000_000,
        max_reflog_entries: 100_000,
        max_index_entries: 500_000,
        ..Limits::default()
    }
}