Skip to content
Open
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `agit commit` - Create a neural commit with linked context
- `agit log` - View commit history with summaries
- `agit show` - Show full context for a commit
- `agit server` - Start the MCP server
- `agit serve` - Start the MCP server for AI editors
- MCP (Model Context Protocol) server:
- `agit_log_step` - Log conversation steps from AI editors
- `agit_read_roadmap` - Read project roadmap
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ your-project/
| `agit log` | View commit history with summaries |
| `agit show [hash]` | Show full context for a commit |
| `agit search <query>` | Search past reasoning |
| `agit server` | Start the MCP server |
| `agit serve` | Start the MCP server for AI editors |

## Philosophy

Expand Down
8 changes: 4 additions & 4 deletions src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ pub enum Commands {
/// Migrate storage from V1 (file-based) to V2 (Git-native)
Migrate(MigrateArgs),

/// Start the MCP server
Server(ServerArgs),
/// Start the MCP server for AI editors
Serve(ServeArgs),

/// Search-related commands (rebuild index, query)
Search(SearchArgs),
Expand Down Expand Up @@ -185,9 +185,9 @@ pub struct CommitArgs {
pub journal: bool,
}

/// Arguments for the `server` command
/// Arguments for the `serve` command
#[derive(Parser, Debug)]
pub struct ServerArgs {
pub struct ServeArgs {
/// Port to listen on (for HTTP mode, not implemented yet)
#[arg(short, long)]
pub port: Option<u16>,
Expand Down
79 changes: 65 additions & 14 deletions src/cli/commands/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,25 @@ pub fn execute(args: InitArgs) -> Result<()> {
return Err(AgitError::NotGitRepository);
}

// Handle --update mode: only update template files, don't reinitialize
// Handle --update mode: update template files and MCP configs
if args.update {
if !agit_dir.exists() {
println!("⚠️ AGIT is not initialized. Run `agit init` first.");
return Ok(());
}

// Update template files (CLAUDE.md, .cursorrules, etc.)
let result = update_template_files(&cwd)?;
if result.updated_count > 0 {
println!("\nRestart your AI assistant to apply the updated AGIT memory protocol.");

// Update MCP configs (server -> serve)
let mcp_updated = update_mcp_configs(&cwd)?;

if result.updated_count > 0 || mcp_updated > 0 {
println!("\nRestart your AI assistant to apply the updates.");
} else if result.up_to_date_count > 0 {
println!("Already up to date.");
} else {
println!("No template files were updated. Ensure CLAUDE.md or .cursorrules exists.");
println!("No files were updated.");
}
return Ok(());
}
Expand Down Expand Up @@ -242,7 +247,8 @@ fn update_template_files(project_dir: &Path) -> Result<UpdateResult> {
if let Some(version_start) = protocol_block.find("version=\"") {
let version_str_start = version_start + 9; // len of 'version="'
if let Some(version_end) = protocol_block[version_str_start..].find('"') {
let existing_version = &protocol_block[version_str_start..version_str_start + version_end];
let existing_version =
&protocol_block[version_str_start..version_str_start + version_end];
if existing_version == AGIT_VERSION {
result.up_to_date_count += 1;
continue;
Expand Down Expand Up @@ -284,7 +290,7 @@ fn generate_mcp_config(agit_path: &str) -> String {
"mcpServers": {{
"agit": {{
"command": "{}",
"args": ["server"]
"args": ["serve"]
}}
}}
}}
Expand All @@ -303,7 +309,7 @@ fn generate_vscode_mcp_config(agit_path: &str) -> String {
"servers": {{
"agit": {{
"command": "{}",
"args": ["server"]
"args": ["serve"]
}}
}}
}}
Expand Down Expand Up @@ -352,6 +358,33 @@ fn generate_mcp_configs(project_dir: &Path) -> Result<()> {
Ok(())
}

/// Update existing MCP configuration files to use the new "serve" command.
/// Returns the number of files updated.
fn update_mcp_configs(project_dir: &Path) -> Result<usize> {
let mcp_paths = [
project_dir.join(".mcp.json"),
project_dir.join(".cursor/mcp.json"),
project_dir.join(".vscode/mcp.json"),
];

let mut updated_count = 0;

for path in &mcp_paths {
if path.exists() {
let content = fs::read_to_string(path)?;
// Check if it has the old "server" command
if content.contains(r#""args": ["server"]"#) {
let new_content = content.replace(r#""args": ["server"]"#, r#""args": ["serve"]"#);
fs::write(path, new_content)?;
updated_count += 1;
println!(" - Updated {}", path.display());
}
}
}

Ok(updated_count)
}

/// Update .gitignore with AGIT entries.
/// Uses V2 (Git-native) entries by default.
fn update_gitignore(project_dir: &Path) -> Result<()> {
Expand Down Expand Up @@ -474,7 +507,7 @@ mod tests {
let mcp_content = fs::read_to_string(temp.path().join(".mcp.json")).unwrap();
assert!(mcp_content.contains("mcpServers"));
assert!(mcp_content.contains("agit"));
assert!(mcp_content.contains("server"));
assert!(mcp_content.contains("serve"));

// Verify VS Code uses "servers" key (not "mcpServers")
let vscode_content = fs::read_to_string(temp.path().join(".vscode/mcp.json")).unwrap();
Expand Down Expand Up @@ -556,7 +589,10 @@ Additional custom content below.

// Run the update
let result = update_template_files(temp.path()).unwrap();
assert!(result.updated_count > 0, "Should have updated at least one file");
assert!(
result.updated_count > 0,
"Should have updated at least one file"
);

// Verify the result
let new_content = fs::read_to_string(temp.path().join("CLAUDE.md")).unwrap();
Expand Down Expand Up @@ -617,8 +653,14 @@ Additional custom content below.

// Run the update - should not update anything
let result = update_template_files(temp.path()).unwrap();
assert_eq!(result.updated_count, 0, "Should not update when no protocol block found");
assert_eq!(result.up_to_date_count, 0, "Should not be up-to-date when no protocol block found");
assert_eq!(
result.updated_count, 0,
"Should not update when no protocol block found"
);
assert_eq!(
result.up_to_date_count, 0,
"Should not be up-to-date when no protocol block found"
);

// Content should remain unchanged
let after_content = fs::read_to_string(temp.path().join("CLAUDE.md")).unwrap();
Expand All @@ -631,8 +673,14 @@ Additional custom content below.

// No template files exist
let result = update_template_files(temp.path()).unwrap();
assert_eq!(result.updated_count, 0, "Should not update when no template files exist");
assert_eq!(result.up_to_date_count, 0, "Should not be up-to-date when no template files exist");
assert_eq!(
result.updated_count, 0,
"Should not update when no template files exist"
);
assert_eq!(
result.up_to_date_count, 0,
"Should not be up-to-date when no template files exist"
);
}

#[test]
Expand All @@ -659,7 +707,10 @@ Additional custom content below.

// Run the update - should detect as already up-to-date
let result = update_template_files(temp.path()).unwrap();
assert_eq!(result.updated_count, 0, "Should not update when already current version");
assert_eq!(
result.updated_count, 0,
"Should not update when already current version"
);
assert_eq!(result.up_to_date_count, 1, "Should count as up-to-date");

// Content should remain unchanged
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub mod push;
pub mod record;
pub mod reset;
pub mod search;
pub mod server;
pub mod serve;
pub mod show;
pub mod status;
pub mod sync;
8 changes: 4 additions & 4 deletions src/cli/commands/server.rs → src/cli/commands/serve.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
//! Implementation of the `agit server` command.
//! Implementation of the `agit serve` command.
//!
//! This starts the MCP (Model Context Protocol) server that AI assistants
//! can connect to for logging thoughts and reading context.

use crate::cli::args::ServerArgs;
use crate::cli::args::ServeArgs;
use crate::error::{AgitError, Result};
use crate::mcp::McpServer;

/// Execute the `server` command.
pub fn execute(args: ServerArgs) -> Result<()> {
/// Execute the `serve` command.
pub fn execute(args: ServeArgs) -> Result<()> {
let cwd = std::env::current_dir()?;
let agit_dir = cwd.join(".agit");

Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ fn run() -> Result<()> {
Commands::Push(args) => agit::cli::commands::push::execute(args),
Commands::Pull(args) => agit::cli::commands::pull::execute(args),
Commands::Migrate(args) => agit::cli::commands::migrate::execute(args),
Commands::Server(args) => agit::cli::commands::server::execute(args),
Commands::Serve(args) => agit::cli::commands::serve::execute(args),
Commands::Search(args) => agit::cli::commands::search::execute(args),
Commands::Reset(args) => agit::cli::commands::reset::execute(args),
Commands::Sync(args) => agit::cli::commands::sync::execute(args),
Expand Down
6 changes: 6 additions & 0 deletions src/mcp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,21 @@
//! The MCP server uses JSON-RPC 2.0 over stdio:
//! - AI editors connect to the server via stdin/stdout
//! - They can call tools like `agit_log_step` to record context
//! - They can read resources like `agit://history/recent`
//! - The server responds with JSON-RPC responses
//!
//! # Available Tools
//!
//! - `agit_log_step` - Log a step (intent/reasoning) in the conversation
//! - `agit_read_roadmap` - Read the project roadmap
//! - `agit_get_context` - Get context for a specific git commit
//!
//! # Available Resources
//!
//! - `agit://history/recent` - Recent commit summaries

pub mod protocol;
pub mod resources;
pub mod server;
pub mod tools;

Expand Down
81 changes: 81 additions & 0 deletions src/mcp/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ pub struct InitializeResult {
#[derive(Debug, Serialize)]
pub struct ServerCapabilities {
pub tools: ToolsCapability,
#[serde(skip_serializing_if = "Option::is_none")]
pub resources: Option<ResourcesCapability>,
}

/// Tools capability.
Expand Down Expand Up @@ -193,6 +195,85 @@ impl ToolCallResult {
}
}

// ============================================================
// RESOURCE TYPES
// ============================================================

/// Resources capability for server capabilities.
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesCapability {
/// Whether the server emits list_changed notifications.
#[serde(skip_serializing_if = "Option::is_none")]
pub list_changed: Option<bool>,
/// Whether the server supports subscriptions.
#[serde(skip_serializing_if = "Option::is_none")]
pub subscribe: Option<bool>,
}

/// Resource definition exposed by the server.
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ResourceDefinition {
/// Unique URI identifying this resource (e.g., "agit://history/recent")
pub uri: String,
/// Human-readable name of the resource
pub name: String,
/// Optional description of what this resource provides
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Optional MIME type of the resource content
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
}

/// Response for resources/list request.
#[derive(Debug, Serialize)]
pub struct ResourcesListResult {
pub resources: Vec<ResourceDefinition>,
}

/// Parameters for resources/read request.
#[derive(Debug, Deserialize)]
pub struct ResourceReadParams {
pub uri: String,
}

/// Content item returned from resources/read.
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceContent {
/// URI of the resource
pub uri: String,
/// MIME type of the content
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
/// Text content (for text resources)
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
/// Base64-encoded binary content (for binary resources)
#[serde(skip_serializing_if = "Option::is_none")]
pub blob: Option<String>,
}

impl ResourceContent {
/// Create a text resource content.
pub fn text(uri: &str, content: &str, mime_type: Option<&str>) -> Self {
Self {
uri: uri.to_string(),
mime_type: mime_type.map(|s| s.to_string()),
text: Some(content.to_string()),
blob: None,
}
}
}

/// Response for resources/read request.
#[derive(Debug, Serialize)]
pub struct ResourceReadResult {
pub contents: Vec<ResourceContent>,
}

// Tool-specific parameter types

/// A code location for MCP input.
Expand Down
6 changes: 6 additions & 0 deletions src/mcp/resources/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//! MCP resource implementations.
//!
//! This module contains the implementations of MCP resources that
//! AI editors can read to get project context.

pub mod recent_history;
Loading
Loading