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};
pub const MAX_REQUEST_BYTES: usize = 256 * 1024;
pub const MAX_RESPONSE_BYTES: usize = 1024 * 1024;
#[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)),
}
}
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()
}
}