octomind 0.22.0

Session-based AI development assistant with conversational codebase interaction, multimodal vision support, built-in MCP tools, and multi-provider AI integration
Documentation
// Copyright 2025 Muvon Un Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Pure utility functions for MCP tool handling.

use serde::{Deserialize, Serialize};
use serde_json::Value;

use super::{McpToolCall, McpToolResult};

// Extract content from MCP-compliant result
pub fn extract_mcp_content(result: &Value) -> String {
	// MCP Standard: Extract from content array
	if let Some(content_array) = result.get("content") {
		if let Some(content_items) = content_array.as_array() {
			let main_content = content_items
				.iter()
				.filter_map(|item| {
					if item.get("type").and_then(|t| t.as_str()) == Some("text") {
						item.get("text").and_then(|t| t.as_str())
					} else {
						None
					}
				})
				.collect::<Vec<_>>()
				.join("\n");

			// For debug mode, also include metadata if available
			if let Some(metadata) = result.get("metadata") {
				if !metadata.is_null() {
					return format!(
						"{}\n\n[Metadata: {}]",
						main_content,
						serde_json::to_string_pretty(metadata).unwrap_or_default()
					);
				}
			}

			return main_content;
		}
	}

	// Fallback: Check for old "output" field for backward compatibility
	if let Some(output) = result.get("output") {
		if let Some(output_str) = output.as_str() {
			return output_str.to_string();
		}
	}

	// Last resort: serialize the whole result for debugging
	serde_json::to_string_pretty(result).unwrap_or_default()
}

// Guess the category of a tool based on its name
pub fn guess_tool_category(tool_name: &str) -> &'static str {
	match tool_name {
		"core" => "system",
		"text_editor" | "batch_edit" | "extract_lines" => "filesystem",
		"shell" | "ast_grep" | "workdir" | "view" | "list_files" => "filesystem",
		"plan" => "core",
		name if name.contains("file") || name.contains("editor") => "core",
		name if name.contains("search") || name.contains("find") => "search",
		name if name.contains("image") || name.contains("photo") => "media",
		name if name.contains("web") || name.contains("http") => "web",
		name if name.contains("db") || name.contains("database") => "database",
		name if name.contains("browser") => "browser",
		name if name.contains("terminal") => "terminal",
		name if name.contains("video") => "video",
		name if name.contains("audio") => "audio",
		name if name.contains("location") || name.contains("map") => "location",
		name if name.contains("google") => "google",
		name if name.contains("weather") => "weather",
		name if name.contains("calculator") || name.contains("math") => "math",
		name if name.contains("news") => "news",
		name if name.contains("email") => "email",
		name if name.contains("calendar") => "calendar",
		name if name.contains("translate") => "translation",
		name if name.contains("github") => "github",
		name if name.contains("git") => "git",
		_ => "external",
	}
}

// Parse a model's response to extract tool calls - kept for backward compatibility
pub fn parse_tool_calls(_content: &str) -> Vec<McpToolCall> {
	// This function is kept for backward compatibility but is no longer used directly
	// as we now prefer to pass tool calls directly as structs
	Vec::new()
}

// Structure to represent tool responses for OpenAI/Claude format
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResponseMessage {
	pub role: String,
	pub tool_call_id: String,
	pub name: String,
	pub content: String,
}

// Convert tool results to proper messages with global truncation
pub fn tool_results_to_messages(
	results: &[McpToolResult],
	config: &crate::config::Config,
) -> Vec<ToolResponseMessage> {
	let mut messages = Vec::new();

	for result in results {
		let content_str = serde_json::to_string(&result.result).unwrap_or_default();

		// Apply global MCP response truncation
		let (final_content, was_truncated) = crate::utils::truncation::truncate_mcp_response_global(
			&content_str,
			config.mcp_response_tokens_threshold,
		);
		if was_truncated {
			use colored::Colorize;
			eprintln!(
				"{}",
				format!(
					"⚠️  Tool '{}' response truncated to {} tokens (mcp_response_tokens_threshold)",
					result.tool_name, config.mcp_response_tokens_threshold
				)
				.bright_yellow()
			);
		}

		messages.push(ToolResponseMessage {
			role: "tool".to_string(),
			tool_call_id: result.tool_id.clone(),
			name: result.tool_name.clone(),
			content: final_content,
		});
	}

	messages
}

// Ensure tool calls have valid IDs
pub fn ensure_tool_call_ids(calls: &mut [McpToolCall]) {
	for call in calls.iter_mut() {
		if call.tool_id.is_empty() {
			call.tool_id = format!("tool_{}", uuid::Uuid::new_v4().simple());
		}
	}
}