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
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 22 additions & 38 deletions mcproc/src/cli/commands/mcp/tools/grep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,78 +88,62 @@ impl ToolHandler for GrepTool {
let grep_response = response.into_inner();

if grep_response.matches.is_empty() {
return Ok(json!({
"process": params.name,
"pattern": params.pattern,
"total_matches": 0,
"message": format!("No matches found for pattern '{}' in process '{}'", params.pattern, params.name)
}));
let output = format!(
"SEARCH LOGS\n\nProcess: {}\nPattern: {}\n\nNo matches found.",
params.name, params.pattern
);
return Ok(json!({ "content": [{ "type": "text", "text": output }] }));
}

// Format timestamp helper uses the common local time formatter
let mut output = String::from("SEARCH LOGS\n\n");
output.push_str(&format!("Process: {}\n", params.name));
output.push_str(&format!("Pattern: {}\n", params.pattern));
output.push_str(&format!("Total matches: {}\n\n", grep_response.matches.len()));

let mut matches = Vec::new();

for grep_match in grep_response.matches {
let mut lines = Vec::new();
for (idx, grep_match) in grep_response.matches.iter().enumerate() {
if idx > 0 {
output.push_str("\n---\n\n");
}

// Context before
for entry in &grep_match.context_before {
let content =
String::from_utf8_lossy(&strip(entry.content.as_bytes())).to_string();
lines.push(format!(
"{:>6}: {} {}",
output.push_str(&format!(
"{:>6}: {} {}\n",
entry.line_number,
format_timestamp_local(entry.timestamp.as_ref()),
content
));
}

// Matched line (highlighted)
let matched_line_info = if let Some(matched_line) = &grep_match.matched_line {
if let Some(matched_line) = &grep_match.matched_line {
let content =
String::from_utf8_lossy(&strip(matched_line.content.as_bytes()))
.to_string();
lines.push(format!(
"{:>6}: {} {} <<< MATCH",
output.push_str(&format!(
"{:>6}: {} {} <<< MATCH\n",
matched_line.line_number,
format_timestamp_local(matched_line.timestamp.as_ref()),
content
));

Some(json!({
"line_number": matched_line.line_number,
"timestamp": format_timestamp_local(matched_line.timestamp.as_ref()),
"content": content
}))
} else {
None
};
}

// Context after
for entry in &grep_match.context_after {
let content =
String::from_utf8_lossy(&strip(entry.content.as_bytes())).to_string();
lines.push(format!(
"{:>6}: {} {}",
output.push_str(&format!(
"{:>6}: {} {}\n",
entry.line_number,
format_timestamp_local(entry.timestamp.as_ref()),
content
));
}

matches.push(json!({
"match_info": matched_line_info,
"context": lines.join("\n")
}));
}

Ok(json!({
"process": params.name,
"pattern": params.pattern,
"total_matches": matches.len(),
"matches": matches
}))
Ok(json!({ "content": [{ "type": "text", "text": output }] }))
}
Err(e) => {
if e.code() == tonic::Code::NotFound {
Expand Down
17 changes: 13 additions & 4 deletions mcproc/src/cli/commands/mcp/tools/logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,18 @@ impl ToolHandler for LogsTool {
}
}

// Return as an array for better MCP display
Ok(json!({
"logs": all_logs
}))
if all_logs.is_empty() {
return Ok(json!({
"content": [{
"type": "text",
"text": format!("No logs found for process '{}'.", params.name)
}]
}));
}

let mut output = format!("LOGS FOR PROCESS: {}\n\n", params.name);
output.push_str(&all_logs.join("\n"));

Ok(json!({ "content": [{ "type": "text", "text": output }] }))
}
}
61 changes: 43 additions & 18 deletions mcproc/src/cli/commands/mcp/tools/ps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,24 +47,49 @@ impl ToolHandler for PsTool {
.map_err(|e| McpError::Internal(e.to_string()))?
.into_inner();

let processes: Vec<Value> = response.processes.into_iter().map(|p| {
json!({
"id": p.id,
"project": p.project,
"name": p.name,
"pid": p.pid,
"status": format_status(p.status),
"cmd": p.cmd,
"log_file": p.log_file,
"start_time": p.start_time.map(|t| {
let ts = chrono::DateTime::<chrono::Utc>::from_timestamp(t.seconds, t.nanos as u32)
.unwrap_or_else(chrono::Utc::now);
ts.to_rfc3339()
}),
"ports": p.ports,
})
}).collect();
if response.processes.is_empty() {
return Ok(json!({ "content": [{ "type": "text", "text": "No processes found." }] }));
}

let mut output = String::from("PROCESSES\n\n");

for (idx, p) in response.processes.iter().enumerate() {
if idx > 0 {
output.push_str("\n---\n\n");
}

output.push_str(&format!("Process: {}\n", p.name));
output.push_str(&format!(" ID: {}\n", p.id));
output.push_str(&format!(" Project: {}\n", p.project));
output.push_str(&format!(" Status: {}\n", format_status(p.status)));

if let Some(pid) = p.pid {
output.push_str(&format!(" PID: {}\n", pid));
}

output.push_str(&format!(" Command: {}\n", p.cmd));
output.push_str(&format!(" Log file: {}\n", p.log_file));

if let Some(start_time) = &p.start_time {
let ts = chrono::DateTime::<chrono::Utc>::from_timestamp(
start_time.seconds,
start_time.nanos as u32,
)
.unwrap_or_else(chrono::Utc::now);
output.push_str(&format!(" Started: {}\n", ts.to_rfc3339()));
}

if !p.ports.is_empty() {
let ports_str = p
.ports
.iter()
.map(|port| port.to_string())
.collect::<Vec<_>>()
.join(", ");
output.push_str(&format!(" Ports: {}\n", ports_str));
}
}

Ok(json!({ "processes": processes }))
Ok(json!({ "content": [{ "type": "text", "text": output }] }))
}
}
83 changes: 48 additions & 35 deletions mcproc/src/cli/commands/mcp/tools/restart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,65 +102,78 @@ impl ToolHandler for RestartTool {

let process = process_info
.ok_or_else(|| McpError::Internal("No process info returned".to_string()))?;
let mut response = json!({
"id": process.id,
"project": process.project,
"name": process.name,
"pid": process.pid,
"status": format_status(process.status),
"log_file": process.log_file,
"start_time": process.start_time.map(|t| {
let ts = chrono::DateTime::<chrono::Utc>::from_timestamp(t.seconds, t.nanos as u32)
.unwrap_or_else(chrono::Utc::now);
ts.to_rfc3339()
}),
"ports": process.ports,
});

let mut output = String::from("RESTART PROCESS\n\n");
output.push_str(&format!("Process: {}\n", process.name));
output.push_str(&format!(" ID: {}\n", process.id));
output.push_str(&format!(" Project: {}\n", process.project));
output.push_str(&format!(" Status: {}\n", format_status(process.status)));

if let Some(pid) = process.pid {
output.push_str(&format!(" PID: {}\n", pid));
}

output.push_str(&format!(" Log file: {}\n", process.log_file));

if let Some(start_time) = process.start_time {
let ts = chrono::DateTime::<chrono::Utc>::from_timestamp(
start_time.seconds,
start_time.nanos as u32,
)
.unwrap_or_else(chrono::Utc::now);
output.push_str(&format!(" Started: {}\n", ts.to_rfc3339()));
}

if !process.ports.is_empty() {
let ports_str = process
.ports
.iter()
.map(|port| port.to_string())
.collect::<Vec<_>>()
.join(", ");
output.push_str(&format!(" Ports: {}\n", ports_str));
}

// Add exit information if process failed
if process.status == proto::ProcessStatus::Failed as i32 {
output.push_str("\nProcess failed:\n");
if let Some(exit_code) = process.exit_code {
response["exit_code"] = json!(exit_code);
output.push_str(&format!(" Exit code: {}\n", exit_code));
}
if let Some(exit_reason) = process.exit_reason {
response["exit_reason"] = json!(exit_reason);
if let Some(exit_reason) = &process.exit_reason {
output.push_str(&format!(" Reason: {}\n", exit_reason));
}
if let Some(stderr_tail) = process.stderr_tail {
response["stderr_tail"] = json!(stderr_tail);
if let Some(stderr_tail) = &process.stderr_tail {
output.push_str(&format!(" Stderr (last lines):\n{}\n", stderr_tail));
}
}

// Add wait pattern match info if process has wait_for_log configured (strip ANSI codes)
if !process.log_context.is_empty() {
let cleaned_context: Vec<String> = process
.log_context
.iter()
.map(|line| String::from_utf8_lossy(&strip(line.as_bytes())).to_string())
.collect();
response["log_context"] = json!(cleaned_context);
output.push_str("\nLog context:\n");
for line in &process.log_context {
let cleaned_line =
String::from_utf8_lossy(&strip(line.as_bytes())).to_string();
output.push_str(&format!(" {}\n", cleaned_line));
}
}

if let Some(matched_line) = process.matched_line {
if let Some(matched_line) = &process.matched_line {
let cleaned_line =
String::from_utf8_lossy(&strip(matched_line.as_bytes())).to_string();
response["matched_line"] = json!(cleaned_line);
output.push_str(&format!("\nMatched line: {}\n", cleaned_line));
}

// Add timeout information if available
if let Some(timeout_occurred) = process.wait_timeout_occurred {
if timeout_occurred {
response["wait_timeout_occurred"] = json!(true);
response["message"] = json!(
"Process restarted but wait_for_log pattern was not found within timeout"
);
output.push_str("\nNote: Process restarted but wait_for_log pattern was not found within timeout.\n");
} else {
response["pattern_matched"] = json!(true);
response["message"] =
json!("Process restarted successfully. Pattern matched in logs.");
output.push_str("\n✓ Process restarted successfully. Pattern matched in logs.\n");
}
}

Ok(response)
Ok(json!({ "content": [{ "type": "text", "text": output }] }))
}
Err(e) => Err(McpError::Internal(e.message().to_string())),
}
Expand Down
Loading
Loading