Skip to main content
Glama

Peekaboo MCP

by steipete
TachikomaAgent.swiftโ€ข30.3 kB
import Foundation import ArgumentParser import Tachikoma import SharedExampleUtils /// Demonstrate AI agent patterns with function calling using Tachikoma @main struct TachikomaAgent: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "tachikoma-agent", abstract: "๐Ÿค– Build AI agents with custom tools and function calling", discussion: """ This example showcases Tachikoma's function calling capabilities, demonstrating how to build AI agents that can use custom tools to accomplish complex tasks. The agent can call functions for weather, calculations, file operations, and more. Examples: tachikoma-agent "What's the weather in Tokyo?" tachikoma-agent --tools calculator "Calculate 15% tip for $67.50" tachikoma-agent --tools weather,file_reader "Check weather and save to file" tachikoma-agent --conversation "Start multi-turn conversation" """ ) @Argument(help: "The task for the AI agent to perform") var task: String? @Option(name: .shortAndLong, help: "Comma-separated list of tools to enable (weather, calculator, file_reader, web_search, all)") var tools: String? @Option(name: .shortAndLong, help: "Specific provider to use for the agent") var provider: String? @Flag(name: .shortAndLong, help: "Start conversation mode for multi-turn interactions") var conversation: Bool = false @Flag(name: .shortAndLong, help: "Show verbose function call details") var verbose: Bool = false @Flag(name: .long, help: "List available tools and exit") var listTools: Bool = false @Option(help: "Maximum number of function calls per request") var maxFunctionCalls: Int = 5 func run() async throws { TerminalOutput.header("๐Ÿค– Tachikoma Agent Demo") if listTools { listAvailableTools() return } let modelProvider = try ConfigurationHelper.createProviderWithAvailableModels() let availableModels = modelProvider.availableModels() if availableModels.isEmpty { TerminalOutput.print("โŒ No AI providers configured! Please set up API keys.", color: .red) ConfigurationHelper.printSetupInstructions() return } // Select tools to enable let enabledTools = selectTools() if conversation { try await runConversationMode(modelProvider: modelProvider, availableModels: availableModels, tools: enabledTools) } else { guard let task = task else { TerminalOutput.print("โŒ Please provide a task or use --conversation mode", color: .red) return } try await runSingleTask(task: task, modelProvider: modelProvider, availableModels: availableModels, tools: enabledTools) } } /// List available tools private func listAvailableTools() { TerminalOutput.print("๐Ÿ”ง Available Agent Tools:", color: .cyan) TerminalOutput.separator("โ”€") for (name, description) in ExampleContent.sampleTools { TerminalOutput.print("โ€ข \(name): \(description)", color: .white) } TerminalOutput.separator("โ”€") TerminalOutput.print("๐Ÿ’ก Use --tools weather,calculator to enable specific tools", color: .yellow) TerminalOutput.print("๐Ÿ’ก Use --tools all to enable all available tools", color: .yellow) } /// Select which tools to enable for the agent private func selectTools() -> [ToolDefinition] { let allTools = createAllTools() if let toolsString = tools { if toolsString.lowercased() == "all" { return allTools } // Parse comma-separated tool names let requestedTools = toolsString.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces).lowercased() } return allTools.filter { tool in requestedTools.contains(tool.function.name.lowercased()) } } // Default: enable basic tools for demonstration return allTools.filter { ["weather", "calculator"].contains($0.function.name) } } /// Run a single task private func runSingleTask(task: String, modelProvider: AIModelProvider, availableModels: [String], tools: [ToolDefinition]) async throws { let selectedModel = try selectModel(from: availableModels) let model = try modelProvider.getModel(selectedModel) let providerName = getProviderName(from: selectedModel) TerminalOutput.print("๐ŸŽฏ Agent Provider: \(providerName)", color: .cyan) TerminalOutput.print("๐Ÿ”ง Enabled Tools: \(tools.map { $0.function.name }.joined(separator: ", "))", color: .dim) TerminalOutput.print("๐Ÿ’ญ Task: \(task)", color: .yellow) TerminalOutput.separator("โ”€") let agent = AgentRunner(model: model, tools: tools, verbose: verbose, maxFunctionCalls: maxFunctionCalls) try await agent.executeTask(task) } /// Run conversation mode private func runConversationMode(modelProvider: AIModelProvider, availableModels: [String], tools: [ToolDefinition]) async throws { let selectedModel = try selectModel(from: availableModels) let model = try modelProvider.getModel(selectedModel) let providerName = getProviderName(from: selectedModel) TerminalOutput.print("๐ŸŽญ Starting conversation with \(providerName) agent", color: .cyan) TerminalOutput.print("๐Ÿ”ง Available tools: \(tools.map { $0.function.name }.joined(separator: ", "))", color: .dim) TerminalOutput.print("Type 'quit' or 'exit' to end the conversation.", color: .dim) TerminalOutput.separator("โ”€") let agent = AgentRunner(model: model, tools: tools, verbose: verbose, maxFunctionCalls: maxFunctionCalls) while true { TerminalOutput.print("\n๐Ÿ—ฃ๏ธ You: ", color: .magenta) guard let input = readLine()?.trimmingCharacters(in: .whitespaces) else { continue } if input.lowercased() == "quit" || input.lowercased() == "exit" { TerminalOutput.print("๐Ÿ‘‹ Goodbye!", color: .green) break } if input.isEmpty { continue } try await agent.continueConversation(input) } } /// Select a model that supports function calling private func selectModel(from availableModels: [String]) throws -> String { if let requestedProvider = provider { let recommended = ProviderDetector.recommendedModels() if let recommendedModel = recommended[requestedProvider.capitalized], availableModels.contains(recommendedModel) { return recommendedModel } } // Prefer models with good function calling support let functionCallingPreferred = ["gpt-4.1", "claude-opus-4-20250514", "grok-4", "llama3.3"] for preferred in functionCallingPreferred { if availableModels.contains(preferred) { return preferred } } return availableModels.first! } /// Extract provider name from model name private func getProviderName(from modelName: String) -> String { switch modelName.lowercased() { case let m where m.contains("gpt") || m.contains("o3") || m.contains("o4"): return "OpenAI" case let m where m.contains("claude"): return "Anthropic" case let m where m.contains("llama") || m.contains("llava"): return "Ollama" case let m where m.contains("grok"): return "Grok" default: return "Unknown" } } /// Create all available tools private func createAllTools() -> [ToolDefinition] { return [ createWeatherTool(), createCalculatorTool(), createFileReaderTool(), createWebSearchTool(), createTimeTool(), createRandomTool() ] } /// Weather lookup tool private func createWeatherTool() -> ToolDefinition { return ToolDefinition( function: FunctionDefinition( name: "weather", description: "Get current weather information for a specific location", parameters: ToolParameters.object(properties: [ "location": .string(description: "The city and country/state, e.g. 'Tokyo, Japan' or 'San Francisco, CA'"), "units": .string(description: "Temperature units: 'celsius' or 'fahrenheit'") ], required: ["location"]) ) ) } /// Calculator tool private func createCalculatorTool() -> ToolDefinition { return ToolDefinition( function: FunctionDefinition( name: "calculator", description: "Perform mathematical calculations including basic math, percentages, and conversions", parameters: ToolParameters.object(properties: [ "expression": .string(description: "Mathematical expression to evaluate, e.g. '15 * 0.15' or '67.50 * 1.15'"), "operation": .enumeration(["basic", "percentage", "tip", "conversion"], description: "Type of calculation") ], required: ["expression"]) ) ) } /// File reader tool private func createFileReaderTool() -> ToolDefinition { return ToolDefinition( function: FunctionDefinition( name: "file_reader", description: "Read contents of text files from the local filesystem", parameters: ToolParameters.object(properties: [ "file_path": .string(description: "Path to the file to read"), "encoding": .enumeration(["utf8", "ascii"], description: "Text encoding") ], required: ["file_path"]) ) ) } /// Web search tool private func createWebSearchTool() -> ToolDefinition { return ToolDefinition( function: FunctionDefinition( name: "web_search", description: "Search the web for current information (simulated for demo)", parameters: ToolParameters.object(properties: [ "query": .string(description: "Search query"), "num_results": .integer(description: "Number of results to return (1-10)") ], required: ["query"]) ) ) } /// Time/date tool private func createTimeTool() -> ToolDefinition { return ToolDefinition( function: FunctionDefinition( name: "time", description: "Get current time, date, or timezone information", parameters: ToolParameters.object(properties: [ "timezone": .string(description: "Timezone identifier, e.g. 'America/New_York' or 'UTC'"), "format": .enumeration(["iso8601", "human", "timestamp"], description: "Output format") ]) ) ) } /// Random number/choice tool private func createRandomTool() -> ToolDefinition { return ToolDefinition( function: FunctionDefinition( name: "random", description: "Generate random numbers or make random choices", parameters: ToolParameters.object(properties: [ "type": .enumeration(["number", "choice", "dice"], description: "Type of random generation"), "min": .integer(description: "Minimum value for number generation"), "max": .integer(description: "Maximum value for number generation"), "choices": .array(of: .string(), description: "List of choices to pick from"), "sides": .integer(description: "Number of sides for dice roll") ], required: ["type"]) ) ) } } // MARK: - Agent Runner /// Handles the execution of agent tasks with function calling class AgentRunner { private let model: ModelInterface private let tools: [ToolDefinition] private let verbose: Bool private let maxFunctionCalls: Int private var conversationHistory: [Message] = [] init(model: ModelInterface, tools: [ToolDefinition], verbose: Bool, maxFunctionCalls: Int) { self.model = model self.tools = tools self.verbose = verbose self.maxFunctionCalls = maxFunctionCalls } /// Execute a single task func executeTask(_ task: String) async throws { conversationHistory = [ Message.system(content: createSystemPrompt()), Message.user(content: .text(task)) ] try await processConversation() } /// Continue an ongoing conversation func continueConversation(_ userInput: String) async throws { conversationHistory.append(Message.user(content: .text(userInput))) try await processConversation() } /// Process the conversation with function calling /// This demonstrates the core agent loop: request -> response -> function calls -> repeat private func processConversation() async throws { var functionCallCount = 0 let startTime = Date() // Track total execution time var totalTokens = 0 // Track total tokens used across all requests while functionCallCount < maxFunctionCalls { // Create request with conversation history and available tools let request = ModelRequest( messages: conversationHistory, tools: tools.isEmpty ? nil : tools, // Include tools for function calling settings: ModelSettings(maxTokens: 1000) ) if verbose { TerminalOutput.print("๐Ÿ“ก Sending request to model... (Function calls: \(functionCallCount)/\(maxFunctionCalls))", color: .yellow) } let response = try await model.getResponse(request: request) // Extract text content and tool calls from response // AssistantContent can contain both text and function calls let textContent = response.content.compactMap { item in if case let .outputText(text) = item { return text } return nil }.joined() // Track token usage for performance metrics totalTokens += PerformanceMeasurement.estimateTokenCount(textContent) let toolCalls = response.content.compactMap { item in if case let .toolCall(call) = item { return call } return nil } // Add assistant message to conversation history conversationHistory.append(Message.assistant(content: response.content)) // Check if the model wants to call functions if !toolCalls.isEmpty { if verbose { TerminalOutput.print("๐Ÿ”ง Model requesting \(toolCalls.count) function call(s)", color: .cyan) } var functionResults: [Message] = [] // Execute each function call the model requested for toolCall in toolCalls { if verbose { TerminalOutput.print(" ๐Ÿ“ž Calling function: \(toolCall.function.name)", color: .yellow) TerminalOutput.print(" Arguments: \(toolCall.function.arguments)", color: .dim) } // Execute the function and get the result let result = try await executeFunction(toolCall.function.name, arguments: toolCall.function.arguments) // Create a tool result message to send back to the model let resultMessage = Message.tool(toolCallId: toolCall.id, content: result) functionResults.append(resultMessage) if verbose { TerminalOutput.print(" Result: \(result)", color: .green) } } // Add all function results to conversation history conversationHistory.append(contentsOf: functionResults) functionCallCount += toolCalls.count } else { // No function calls, display the response and exit let emoji = getProviderEmoji() TerminalOutput.print("\n\(emoji) Agent: ", color: .bold) if !textContent.isEmpty { TerminalOutput.print(textContent, color: .white) } else { TerminalOutput.print("(No response content)", color: .dim) } break } } if functionCallCount >= maxFunctionCalls { TerminalOutput.print("\nโš ๏ธ Reached maximum function call limit (\(maxFunctionCalls))", color: .yellow) } // Display performance metrics after agent task completion let endTime = Date() let totalDuration = endTime.timeIntervalSince(startTime) displayAgentPerformance(duration: totalDuration, totalTokens: totalTokens, functionCalls: functionCallCount) } /// Execute a function call and return the result private func executeFunction(_ functionName: String, arguments: String) async throws -> String { switch functionName { case "weather": return try executeWeatherFunction(arguments) case "calculator": return try executeCalculatorFunction(arguments) case "file_reader": return try executeFileReaderFunction(arguments) case "web_search": return try executeWebSearchFunction(arguments) case "time": return try executeTimeFunction(arguments) case "random": return try executeRandomFunction(arguments) default: return "Error: Unknown function '\(functionName)'" } } /// Execute weather function (simulated) private func executeWeatherFunction(_ arguments: String) throws -> String { // Parse JSON arguments let data = arguments.data(using: .utf8) ?? Data() let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] let args = parsed ?? [:] guard let location = args["location"] as? String else { return "Error: Missing location parameter" } let units = args["units"] as? String ?? "celsius" // Simulate weather data let weatherData = [ "Tokyo, Japan": ("Partly cloudy", 22, 18), "San Francisco, CA": ("Foggy", 15, 12), "New York, NY": ("Sunny", 25, 20), "London, UK": ("Rainy", 12, 8), "Sydney, Australia": ("Clear", 28, 24) ] if let (condition, highC, lowC) = weatherData[location] { if units == "fahrenheit" { let highF = Int(Double(highC) * 9/5 + 32) let lowF = Int(Double(lowC) * 9/5 + 32) return "Weather in \(location): \(condition), High: \(highF)ยฐF, Low: \(lowF)ยฐF" } else { return "Weather in \(location): \(condition), High: \(highC)ยฐC, Low: \(lowC)ยฐC" } } else { return "Weather information not available for \(location). Try major cities like Tokyo, San Francisco, New York, London, or Sydney." } } /// Execute calculator function private func executeCalculatorFunction(_ arguments: String) throws -> String { // Parse JSON arguments let data = arguments.data(using: .utf8) ?? Data() let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] let args = parsed ?? [:] guard let expression = args["expression"] as? String else { return "Error: Missing expression parameter" } // Simple expression evaluator (in real implementation, use a proper math parser) let result = try evaluateExpression(expression) let operation = args["operation"] as? String ?? "basic" switch operation { case "tip": let tipAmount = result let total = extractNumberFromExpression(expression) + tipAmount return String(format: "Tip: $%.2f, Total: $%.2f", tipAmount, total) case "percentage": return String(format: "%.2f%%", result) default: return String(format: "%.2f", result) } } /// Execute file reader function private func executeFileReaderFunction(_ arguments: String) throws -> String { // Parse JSON arguments let data = arguments.data(using: .utf8) ?? Data() let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] let args = parsed ?? [:] guard let filePath = args["file_path"] as? String else { return "Error: Missing file_path parameter" } do { let content = try String(contentsOfFile: filePath, encoding: .utf8) return "File content (\(content.count) characters):\n\(content)" } catch { return "Error reading file '\(filePath)': \(error.localizedDescription)" } } /// Execute web search function (simulated) private func executeWebSearchFunction(_ arguments: String) throws -> String { // Parse JSON arguments let data = arguments.data(using: .utf8) ?? Data() let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] let args = parsed ?? [:] guard let query = args["query"] as? String else { return "Error: Missing query parameter" } let numResults = args["num_results"] as? Int ?? 3 // Simulate search results return """ Search results for "\(query)" (\(numResults) results): 1. Example.com - Comprehensive guide to \(query) Summary: Detailed information about \(query) with practical examples... 2. Reference.org - \(query) documentation Summary: Official documentation and API reference for \(query)... 3. Tutorial.net - Learn \(query) step by step Summary: Beginner-friendly tutorial covering the basics of \(query)... Note: This is a simulated search. In a real implementation, this would use a web search API. """ } /// Execute time function private func executeTimeFunction(_ arguments: String) throws -> String { // Parse JSON arguments let data = arguments.data(using: .utf8) ?? Data() let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] let args = parsed ?? [:] let timezone = args["timezone"] as? String ?? "UTC" let format = args["format"] as? String ?? "human" let now = Date() let formatter = DateFormatter() // Set timezone if let tz = TimeZone(identifier: timezone) { formatter.timeZone = tz } switch format { case "iso8601": formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" return formatter.string(from: now) case "timestamp": return String(Int(now.timeIntervalSince1970)) default: // human formatter.dateStyle = .full formatter.timeStyle = .full return formatter.string(from: now) } } /// Execute random function private func executeRandomFunction(_ arguments: String) throws -> String { // Parse JSON arguments let data = arguments.data(using: .utf8) ?? Data() let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] let args = parsed ?? [:] guard let type = args["type"] as? String else { return "Error: Missing type parameter" } switch type { case "number": let min = args["min"] as? Int ?? 1 let max = args["max"] as? Int ?? 100 let result = Int.random(in: min...max) return "Random number between \(min) and \(max): \(result)" case "choice": guard let choicesArray = args["choices"] as? [String], !choicesArray.isEmpty else { return "Error: No choices provided" } let result = choicesArray.randomElement()! return "Random choice from [\(choicesArray.joined(separator: ", "))]: \(result)" case "dice": let sides = args["sides"] as? Int ?? 6 let result = Int.random(in: 1...sides) return "Rolled \(sides)-sided die: \(result)" default: return "Error: Unknown random type '\(type)'" } } /// Simple expression evaluator private func evaluateExpression(_ expression: String) throws -> Double { // This is a very basic evaluator - in a real implementation, use NSExpression or a proper parser let cleanExpression = expression.replacingOccurrences(of: " ", with: "") // Handle simple operations if cleanExpression.contains("*") { let parts = cleanExpression.split(separator: "*") if parts.count == 2, let left = Double(parts[0]), let right = Double(parts[1]) { return left * right } } if cleanExpression.contains("/") { let parts = cleanExpression.split(separator: "/") if parts.count == 2, let left = Double(parts[0]), let right = Double(parts[1]) { return left / right } } if cleanExpression.contains("+") { let parts = cleanExpression.split(separator: "+") if parts.count == 2, let left = Double(parts[0]), let right = Double(parts[1]) { return left + right } } if cleanExpression.contains("-") { let parts = cleanExpression.split(separator: "-") if parts.count == 2, let left = Double(parts[0]), let right = Double(parts[1]) { return left - right } } // Try to parse as a single number if let number = Double(cleanExpression) { return number } throw NSError(domain: "Calculator", code: 1, userInfo: [ NSLocalizedDescriptionKey: "Unable to evaluate expression: \(expression)" ]) } /// Extract base number from expression for tip calculations private func extractNumberFromExpression(_ expression: String) -> Double { let components = expression.split(whereSeparator: { "+-*/".contains($0) }) if let first = components.first, let number = Double(first) { return number } return 0 } /// Create system prompt for the agent private func createSystemPrompt() -> String { let toolNames = tools.map { $0.function.name }.joined(separator: ", ") return """ You are a helpful AI agent with access to the following tools: \(toolNames). Use these tools to help the user accomplish their tasks. Always: 1. Use appropriate tools when the task requires external information or computation 2. Provide clear, helpful responses 3. Explain what you're doing when calling functions 4. Be conversational and friendly Available tools: \(tools.map { "- \($0.function.name): \($0.function.description)" }.joined(separator: "\n")) When a user asks for something that can be accomplished with your tools, use them! """ } /// Get provider emoji for display private func getProviderEmoji() -> String { // This is a simple implementation - in practice, you'd detect from the model return "๐Ÿค–" } /// Display agent performance metrics after task completion private func displayAgentPerformance(duration: TimeInterval, totalTokens: Int, functionCalls: Int) { TerminalOutput.separator("โ”€") TerminalOutput.print("๐Ÿ“Š Agent Performance Summary:", color: .bold) let stats = [ "โฑ๏ธ Total time: \(String(format: "%.2fs", duration))", "๐Ÿ”ค Tokens used: ~\(totalTokens)", "๐Ÿ”ง Function calls: \(functionCalls)" ] TerminalOutput.print(stats.joined(separator: " | "), color: .dim) // Performance assessment if duration < 10 { TerminalOutput.print("๐Ÿš€ Performance: Fast", color: .green) } else if duration < 30 { TerminalOutput.print("โšก Performance: Good", color: .yellow) } else { TerminalOutput.print("๐ŸŒ Performance: Slow (complex task or model latency)", color: .yellow) } TerminalOutput.separator("โ”€") } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/steipete/Peekaboo'

If you have feedback or need assistance with the MCP directory API, please join our Discord server