top of page

Building AI Travel Planner with Ollama and MCP - Part 3

Updated: Jul 7

Prerequisite: This is a continuation of the blog Part 2: A Complete Guide to Creating a Multi-Agent Book Writing System



🧰 Tool Definitions

Let’s now define all our MCP tools, one by one.

📅 Get Full Itinerary

@mcp.tool()
async def get_full_itinerary(city: str, days: int = 5) -> str:
    """Get a complete travel itinerary with direct Ollama AI using file-based data.
    
    Args:
        city: Name of the city to visit
        days: Number of days for the itinerary (default: 5)
    
    Returns:
        A formatted markdown itinerary with detailed daily plans
    """
    global travel_agents
    
    if not travel_agents:
        return "Error: Travel agents not initialized. Please wait for server startup."
    
    mode = "Direct Ollama AI" if travel_agents.use_ollama else "Rule-Based"
    session_id = int(time.time() * 1000) % 10000
    log(f"Creating {days}-day {city} itinerary using {mode} with file data (Session {session_id})...")
    
    try:
        # Research
        log("Researching destination from file data...")
        research_data = await travel_agents.research_destination(city)
        
        # Plan
        log("Creating itinerary...")
        itinerary = await travel_agents.create_itinerary(city, days, research_data)
        
        # Format
        log("Formatting output...")
        markdown_output = await travel_agents.format_itinerary(city, itinerary, research_data)
        
        log("File-based itinerary complete!", "SUCCESS")
        return markdown_output
        
    except Exception as e:
        log(f"Error creating itinerary: {e}", "ERROR")
        return f"Error creating itinerary: {str(e)}"

This function is the flagship tool of RAG + Ollama system — the one responsible for generating an entire multi-day itinerary for a given city using either:

  • AI-generated plans via Ollama, or

  • Traditional rule-based scheduling using local data files.


This is our "I want the works!" service counter. A customer walks in, says "Plan my entire Paris trip," and this function:

  1. Assigns the best available team (AI or rule-based)

  2. Conducts full research

  3. Creates a complete itinerary

  4. Formats it beautifully

  5. Delivers a professional travel guide


Even if things go wrong, we get a helpful error message instead of a system crash - like having customer service training!


Let’s break down how this tool works.


🛠️ Tool Declaration
@mcp.tool()
async def get_full_itinerary(city: str, days: int = 5) -> str:

We decorate this function with @mcp.tool() to register it as an exposable endpoint in the MCP server. This makes it callable by other agents, clients, or workflows.

  • city: the destination.

  • days: the number of days to plan (default is 5).

  • Returns: a markdown-formatted multi-day itinerary.


🧠 Ensure Agent Availability
global travel_agents

if not travel_agents:
    return "Error: Travel agents not initialized. Please wait for server startup."

We’re using the travel_agents instance created during server startup. If it hasn’t been initialized yet, we return a helpful error message — preventing null reference exceptions.


🎛️ Mode and Session Setup
mode = "Direct Ollama AI" if travel_agents.use_ollama else "Rule-Based"
session_id = int(time.time() * 1000) % 10000

This determines how the itinerary will be generated:

  • If use_ollama is True → LLM will be used to generate the plan.

  • Else → The fallback rule-based method (e.g., sentence summarization from local files) is used.

We also create a session_id to help log and track user interactions for debugging or tracing.


📝 Log the Start of Planning
log(f"Creating {days}-day {city} itinerary using {mode} with file data (Session {session_id})...")

Every important action should be logged — just like tracking breadcrumbs during a hike. This makes errors easier to debug and also informs users about system activity.


🔍 Research the Destination
log("Researching destination from file data...")
research_data = await travel_agents.research_destination(city)

Before we can generate a good itinerary, we must understand the destination.

This line calls a research method (which likely retrieves attractions, dining options, events, etc.) from the file-based RAG system. This contextual research is then used to guide the planning phase.


🗓️ Create the Day-by-Day Plan
log("Creating itinerary...")
itinerary = await travel_agents.create_itinerary(city, days, research_data)

Now that we have our research, this function builds a schedule over the given number of days.


It likely:

  • Distributes categories across days (e.g., sightseeing in the morning, dining in the evening)

  • Adjusts content density depending on the day count

  • Ensures there's no repetition of entries


🎨 Format Itinerary as Markdown
log("Formatting output...")
markdown_output = await travel_agents.format_itinerary(city, itinerary, research_data)

Once the raw itinerary structure is generated, this method:

  • Converts it into a clean, readable markdown format

  • Adds headings, bullet points, possibly emojis or categories

  • Makes it presentable for users or client interfaces


✅ Final Success Log
log("File-based itinerary complete!", "SUCCESS")
return markdown_output

We log a success message and return the final, ready-to-render itinerary.


⚠️ Exception Handling
except Exception as e:
    log(f"Error creating itinerary: {e}", "ERROR")
    return f"Error creating itinerary: {str(e)}"

If anything goes wrong (bad file, research failure, formatting bug), we catch the error, log it properly, and return a meaningful message to the user.


🕵️ Research City

@mcp.tool()
async def research_city(city: str, interests: str = "attractions,dining,activities") -> str:
    """Research a specific city for travel planning using file-based data.
    
    Args:
        city: Name of the city to research
        interests: Comma-separated list of interests (attractions, dining, activities, etc.)
    Returns:
        Detailed research information about the city from local data files
    """
    global travel_agents
    if not travel_agents:
        return "Error: Travel agents not initialized. Please wait for server startup."
    try:
        interest_list = [i.strip() for i in interests.split(",")]
        research_data = await travel_agents.research_destination(city, interest_list)
        # Format research results
        result = f"# Research Results for {city.title()}\n\n"
        result += f"*Based on {len(rag_system.documents)} local data files*\n\n"
        for category, data in research_data.items():
            result += f"## {category.title()}\n\n"
            if isinstance(data, dict) and "ai_analysis" in data:
                result += f"**AI Analysis** (Style: {data.get('style', 'standard')}):\n"
                result += f"{data['ai_analysis']}\n\n"
                if "rag_data" in data:
                    result += f"**Source Data from Files**:\n"
                    for item in data["rag_data"]:
                        result += f"- **{item.get('title', 'Unknown')}** (from {item.get('source_file', 'unknown file')}): {item.get('content', '')[:200]}...\n"
                    result += "\n"   
            elif isinstance(data, list):
                result += f"**Raw Data from Files**:\n"
                for item in data:
                    result += f"- **{item.get('title', 'Unknown')}** (from {item.get('source_file', 'unknown file')}): {item.get('content', '')}\n"
                result += "\n"
        return result
    except Exception as e:
        log(f"Error researching city: {e}", "ERROR")
        return f"Error researching city: {str(e)}"

In the journey of building a travel planning system, the research_city tool functions as a dedicated AI research assistant. It reads from structured local files, filters the content based on user interests, and generates an informative markdown report that combines AI analysis with traceable source data.


This is for customers who want to "browse before buying." They're not ready for a full itinerary yet - they just want to know what's available. It provides:

  • AI analysis (if available)

  • Raw data from files

  • Source information

  • Organized by interest categories


Customers can specify exactly what they're interested in - like having specialized brochure racks for different types of travelers!


🛠️ MCP Tool Declaration
@mcp.tool()
async def research_city(city: str, interests: str = "attractions,dining,activities") -> str:

This decorator registers the function as a callable tool within the MCP server.

  • city: The destination city for which the user wants information.

  • interests: A comma-separated string that represents specific interest areas, such as attractions, dining, or local events.

  • Returns: A markdown-formatted research summary.


🌍 System Readiness Check
global travel_agents
if not travel_agents:
    return "Error: Travel agents not initialized. Please wait for server startup."

The function uses a global reference to the travel_agents object, which is responsible for performing AI-driven or rule-based research. If the system is not yet initialized, it exits gracefully with an appropriate message.


🧠 Think of this as confirming that your research assistant is awake and ready before you start asking questions.


🧹 Parse the Interests
interest_list = [i.strip() for i in interests.split(",")]

This line takes the input string of interests and converts it into a clean list by removing any leading or trailing spaces.

For example:

"attractions, dining ,activities" → ["attractions", "dining", "activities"]

This list will be used to filter research content based on user preferences.


🔎 Research via Travel Agent
research_data = await travel_agents.research_destination(city, interest_list)

This line triggers the research phase. The travel agent queries the local file database and, depending on the configuration, either performs semantic search using RAG or generates AI summaries using Ollama.


The output is a structured dictionary with results for each category of interest.


🧾 Begin Formatting Markdown Output
result = f"# Research Results for {city.title()}\n\n"
result += f"*Based on {len(rag_system.documents)} local data files*\n\n"

Here, the system begins generating the output using markdown syntax. It provides a heading that reflects the city name and informs the user how many documents were used to create the report.


This builds user trust by demonstrating data coverage and source quantity.


🗂️ Process Each Category
for category, data in research_data.items():
    result += f"## {category.title()}\n\n"

Each category of interest (such as attractions or dining) is processed and gets its own subheading in the output.


🤖 AI-Generated Analysis
if isinstance(data, dict) and "ai_analysis" in data:
    result += f"**AI Analysis** (Style: {data.get('style', 'standard')}):\n"
    result += f"{data['ai_analysis']}\n\n"

If the data is a dictionary and contains an "ai_analysis" key, this means an AI model (such as Ollama) has summarized the findings.


The system also includes the analysis style, such as narrative, bullet points, or informative, depending on how the agent was instructed.


📁 Add Source Data from Files
if "rag_data" in data:
    result += f"**Source Data from Files**:\n"
    for item in data["rag_data"]:
        result += f"- **{item.get('title', 'Unknown')}** (from {item.get('source_file', 'unknown file')}): {item.get('content', '')[:200]}...\n"
    result += "\n"

To ensure transparency and traceability, the system provides previews of the original file-based content used by the AI. Each entry includes:

  • Title of the source

  • File name

  • First 200 characters of the content


🧠 This is similar to showing footnotes and references in a research paper.


📄 Fallback to Raw Data
elif isinstance(data, list):
    result += f"**Raw Data from Files**:\n"
    for item in data:
        result += f"- **{item.get('title', 'Unknown')}** (from {item.get('source_file', 'unknown file')}): {item.get('content', '')}\n"
    result += "\n"

If the data is not processed by AI but still available as a raw list, the system prints the entire content for each item, ensuring that the user still receives complete results even without AI enhancement.


This acts as a graceful fallback when only raw data is available.


⚠️ Handle Exceptions
except Exception as e:
    log(f"Error researching city: {e}", "ERROR")
    return f"Error researching city: {str(e)}"

If anything goes wrong during the operation (for example, a missing file or corrupted data), the system logs the error and returns a clean message to the user.


🗂️ Explore What You Already Have

@mcp.tool()
async def list_available_data() -> str:
    """List all available cities and categories in the data files.
    
    Returns:
        Information about available travel data
    """
    global rag_system
    
    if not rag_system:
        return "Error: RAG system not initialized. Please wait for server startup."
    
    try:
        cities = rag_system.get_cities()
        categories = rag_system.get_categories()
        
        result = f"# Available Travel Data\n\n"
        result += f"**Total Documents**: {len(rag_system.documents)}\n"
        result += f"**Data Directory**: {rag_system.data_path}\n\n"
        
        result += f"## Available Cities ({len(cities)})\n"
        for city in cities:
            city_docs = rag_system.get_documents_by_city(city)
            result += f"- **{city.title()}**: {len(city_docs)} documents\n"
        
        result += f"\n## Available Categories ({len(categories)})\n"
        for category in categories:
            category_docs = rag_system.get_documents_by_category(category)
            result += f"- **{category.title()}**: {len(category_docs)} documents\n"
        
        result += f"\n## Document Details\n"
        for doc in rag_system.documents:
            result += f"- **{doc['title']}** ({doc['city']}, {doc['category']}) - from {doc['source_file']}\n"
        
        return result
        
    except Exception as e:
        log(f"Error listing data: {e}", "ERROR")
        return f"Error listing available data: {str(e)}"

This is an MCP tool function that provides users with an overview of what data is already loaded in the system.


This is the big board at the front of our travel agency showing all available destinations and services. Perfect for customers who want to see what's on the menu before deciding.


✅ Ensure System Initialization
global rag_system

if not rag_system:
    return "Error: RAG system not initialized. Please wait for server startup."

Before anything is processed, the system checks whether the RAG system is initialized. If not, it returns an error message to the user.


🏙️ Get Cities and Categories
cities = rag_system.get_cities()
categories = rag_system.get_categories()

These lines extract the unique list of cities and categories present in the current travel dataset.


📄 Begin Formatting Markdown Output
result = f"# Available Travel Data\n\n"
result += f"**Total Documents**: {len(rag_system.documents)}\n"
result += f"**Data Directory**: {rag_system.data_path}\n\n"

The output begins with a markdown-formatted header. It includes the total number of documents and the path where those documents reside.


🗂️ Process Each City
result += f"## Available Cities ({len(cities)})\n"
for city in cities:
    city_docs = rag_system.get_documents_by_city(city)
    result += f"- **{city.title()}**: {len(city_docs)} documents\n"

A new section lists each city and counts how many documents are associated with it. The city names are formatted with title case.


🧭 Process Each Category
result += f"\n## Available Categories ({len(categories)})\n"
for category in categories:
    category_docs = rag_system.get_documents_by_category(category)
    result += f"- **{category.title()}**: {len(category_docs)} documents\n"

Similarly, the function lists the categories (such as attractions or dining) and the number of documents linked to each one.


📋 Show Document Details
result += f"\n## Document Details\n"
for doc in rag_system.documents:
    result += f"- **{doc['title']}** ({doc['city']}, {doc['category']}) - from {doc['source_file']}\n"

This final section lists all documents in detail, showing the title, associated city and category, and the file from which the data was loaded.

return result

Returns the fully formatted markdown summary to the caller.


⚠️ Handle Exceptions
except Exception as e:
    log(f"Error listing data: {e}", "ERROR")
    return f"Error listing available data: {str(e)}"

If anything goes wrong—such as a corrupted file or missing method—an error is logged and a user-friendly message is returned.


➕ Feed the System with New Knowledge

@mcp.tool()
async def add_travel_data(filename: str, title: str, city: str, category: str, content: str) -> str:
    """Add new travel data to the system by creating a text file.
    
    Args:
        filename: Name for the new text file (will add .txt extension)
        title: Title for the travel information
        city: City name (lowercase)
        category: Category (attractions, dining, activities, etc.)
        content: Detailed content about the travel information
    
    Returns:
        Confirmation message
    """
    global rag_system, travel_agents
    
    if not rag_system:
        return "Error: RAG system not initialized. Please wait for server startup."
    
    try:
        # Ensure filename has .txt extension
        if not filename.endswith('.txt'):
            filename = f"{filename}.txt"
        
        # Create file content in the correct format
        file_content = f"{title}\n{city.lower()}\n{category.lower()}\n{content}"
        
        # Write to file
        file_path = rag_system.data_path / filename
        with open(file_path, 'w', encoding='utf-8') as f:
            f.write(file_content)
        
        # Reload the RAG system to include new data
        log(f"Reloading RAG system with new file: {filename}")
        await rag_system.load_travel_data_from_files()
        await rag_system.build_index()
        
        log(f"Added new travel data: {filename}", "SUCCESS")
        return f"Successfully added travel data file: {filename}\nTitle: {title}\nCity: {city}\nCategory: {category}\nTotal documents now: {len(rag_system.documents)}"
        
    except Exception as e:
        log(f"Error adding travel data: {e}", "ERROR")
        return f"Error adding travel data: {str(e)}"

This tool enables dynamic updates by allowing users to create and add a new document directly through a function call.


🔒 Ensure System is Initialized
global rag_system, travel_agents

if not rag_system:
    return "Error: RAG system not initialized. Please wait for server startup."

The system first checks if rag_system is active. Without it, adding files would serve no purpose because indexing would fail.


This is like having a suggestion box that magically implements suggestions immediately! A customer can share insider knowledge about a great restaurant, and it instantly becomes part of our agency's knowledge base.


Our Smart Parts:

  • Automatically adds .txt extension (like autocorrecting)

  • Follows the standard format (like providing a template)

  • Immediately reloads the system (like instant inventory update)

  • Provides confirmation (like a receipt)


🧾 Ensure Proper File Extension
# Ensure filename has .txt extension
if not filename.endswith('.txt'):
    filename = f"{filename}.txt"

This ensures consistency in file extensions, avoiding malformed or missing .txt suffixes.


🏗️ Build the File Content
# Create file content in the correct format
file_content = f"{title}\n{city.lower()}\n{category.lower()}\n{content}"

All new file content is structured in four lines:

  1. Title

  2. City (lowercase)

  3. Category (lowercase)

  4. Detailed content

This format is crucial for parsing the file later.


📁 Write to File
# Write to file
file_path = rag_system.data_path / filename
with open(file_path, 'w', encoding='utf-8') as f:
    f.write(file_content)

The formatted string is then written to a file inside the data directory, using UTF-8 encoding to preserve non-ASCII characters.


🔄 Reload and Rebuild Index

# Reload the RAG system to include new data
log(f"Reloading RAG system with new file: {filename}")
await rag_system.load_travel_data_from_files()
await rag_system.build_index()

Once the file is written, the RAG system reloads all text files and rebuilds the FAISS index so that the new document is included in future searches and analyses.


✅ Return Confirmation
log(f"Added new travel data: {filename}", "SUCCESS")
return f"Successfully added travel data file: {filename}\nTitle: {title}\nCity: {city}\nCategory: {category}\nTotal documents now: {len(rag_system.documents)}"

A success log is recorded, and the function returns a summary of the added file and updated document count.


⚠️ Catch and Log Errors
except Exception as e:
    log(f"Error adding travel data: {e}", "ERROR")
    return f"Error adding travel data: {str(e)}"

Any exception that occurs during writing or reloading is caught and logged, and a formatted error message is returned.


🚀 One-Time Initialization of the Travel Planner

# Startup function
async def startup():
    """Initialize the travel system components"""
    global rag_system, travel_agents
    
    log("Initializing Direct Ollama Travel Planner with File-Based Data...")
    
    # Initialize RAG system
    rag_system = TravelRAGSystem()
    await rag_system.initialize()
    log("RAG system initialized with file-based data", "SUCCESS")
    
    # Initialize agents with direct Ollama
    travel_agents = DirectOllamaAgents(rag_system)
    
    if travel_agents.use_ollama:
        log("Direct Ollama AI agents active with file data!", "SUCCESS")
    else:
        log("Rule-based agents active with file data")
    
    # Show data summary
    cities = rag_system.get_cities()
    categories = rag_system.get_categories()
    log(f"Loaded data for cities: {', '.join(cities)}")
    log(f"Available categories: {', '.join(categories)}")
    
    log("Travel Planner ready to serve tools with file-based data!", "SUCCESS")

This is opening day at our travel agency! It coordinates everything:

  1. Initializes our research department (RAG system)

  2. Hires and trains our consultants (AI agents)

  3. Displays our welcome banner (showing available services)

  4. Unlocks our doors (makes tools available)


Like having a opening day coordinator who announces each milestone as systems come online!


🧠 Instantiate Core Systems
global rag_system, travel_agents

Marks both rag_system and travel_agents as global, so that the initialized objects can be reused across tools.

log("Initializing Direct Ollama Travel Planner with File-Based Data...")

Logs the beginning of the setup process to the console or dashboard.

# Initialize RAG system
rag_system = TravelRAGSystem()
await rag_system.initialize()
log("RAG system initialized with file-based data", "SUCCESS")

Initializes the document processing and retrieval system. It loads the text files and prepares everything required for semantic search.


🤖 Connect Travel Agents

# Initialize agents with direct Ollama
travel_agents = DirectOllamaAgents(rag_system)

Wraps the rag_system with AI or rule-based agents that can conduct research, planning, and formatting.

if travel_agents.use_ollama:
    log("Direct Ollama AI agents active with file data!", "SUCCESS")
else:
    log("Rule-based agents active with file data")

Depending on the configuration, the system either uses AI agents (such as those powered by Ollama) or a rule-based fallback. This line documents that choice.


🧾 Summarize Loaded Data
# Show data summary
cities = rag_system.get_cities()
categories = rag_system.get_categories()
log(f"Loaded data for cities: {', '.join(cities)}")
log(f"Available categories: {', '.join(categories)}")

As a final verification step, the system logs which cities and categories have been successfully loaded into memory.


✅ Declare System Ready
log("Travel Planner ready to serve tools with file-based data!", "SUCCESS")

The system confirms that it is ready to handle user queries and actions.

Let me know if you want these explanations added directly to your documentation site or integrated into the code as comments.


🔧 Running the Server

if __name__ == "__main__":
    # Run startup
    print("🚀 MCP Travel Planner with File-Based Data", file=sys.stderr)
    print("This version uses text files in the 'data' folder", file=sys.stderr)
    print("🗂️ File Format: title\\ncity\\ncategory\\ncontent", file=sys.stderr)
    print("=" * 50, file=sys.stderr)
    asyncio.run(startup())
    # Start the MCP server
    log("Starting MCP server with file-based data...", "SUCCESS")
    mcp.run()

When the script runs directly:

  • It prints usage instructions

  • Calls startup() using asyncio.run()

  • Finally, starts the MCP server via mcp.run()


The console also clarifies:

  • File structure: title\ncity\ncategory\ncontent

  • Data folder usage

  • Current session info


Example Console Output

🚀 MCP Travel Planner with File-Based Data
This version uses text files in the 'data' folder
🗂️ File Format: title\ncity\ncategory\ncontent
==================================================

🧪 test_mcp.py — Standalone MCP Tool Testing Script

This script is used to test the MCP tools independently from the FastMCP or full application environment. It manually initializes the system, runs an itinerary generation command, and checks if the output is as expected.


Begin Testing Environment

import asyncio
import time
import glob

We import the required modules for asynchronous execution, timing, and file management.


🛠️ Verify the MCP Itinerary Generator

sync def test_mcp_tool_only():
    """Test the MCP tool directly without other components"""
    print("🛠️ Testing MCP Tool Only")
    print("="*50)
    
    try:
        # Import the MCP tool function and required components
        from direct_ollama_server_3 import get_full_itinerary, rag_system, travel_agents
        
        # Import and set up the globals (normally done by FastMCP)
        import direct_ollama_server_3
        from direct_ollama_server_3 import TravelRAGSystem, DirectOllamaAgents
        
        # Initialize if not already done
        if direct_ollama_server_3.rag_system is None:
            print("🔧 Initializing global systems...")
            rag = TravelRAGSystem()
            await rag.initialize()
            agents = DirectOllamaAgents(rag)
            
            direct_ollama_server_3.rag_system = rag
            direct_ollama_server_3.travel_agents = agents
            print("   ✅ Systems initialized")
        else:
            print("✅ Systems already initialized")
        
        print("\n🎯 Testing MCP tool: get_full_itinerary...")
        
        # Test parameters
        destination = "paris"
        days = 5
        
        print(f"   📍 Destination: {destination}")
        print(f"   📅 Duration: {days} days")
        
        # Call the actual MCP tool
        print("   🚀 Generating itinerary...")
        start_time = time.time()
        
        result = await get_full_itinerary(destination, days)
        
        end_time = time.time()
        duration = end_time - start_time
        
        print(f"   ⏱️ Generation took {duration:.2f} seconds")
        
        # Validate result
        if result and "Paris" in result and "Day" in result:
            print("✅ MCP tool working correctly!")
            print(f"📊 Generated {len(result)} characters")
            
            # Save with timestamp
            timestamp = int(time.time())
            filename = f"mcp_only_test_{timestamp}.md"
            
            with open(filename, 'w', encoding='utf-8') as f:
                f.write(result)
            
            print(f"📁 Result saved to {filename}")
            
            # Show preview
            lines = result.split('\n')[:15]
            print("\n📋 Preview:")
            for line in lines:
                if line.strip():
                    print(f"   {line}")
            
            return True
            
        else:
            print("❌ MCP tool returned unexpected result")
            print(f"Result preview: {str(result)[:200]}...")
            return False
        
    except ImportError as e:
        print(f"❌ Import error: {e}")
        print("   Make sure direct_ollama_server_3.py is in the same directory")
        return False
        
    except Exception as e:
        print(f"❌ MCP tool test failed: {e}")
        import traceback
        traceback.print_exc()
        return False

This asynchronous function tests only the get_full_itinerary tool without needing a full server setup.


📥 Import and Inject Dependencies
from direct_ollama_server_3 import get_full_itinerary, rag_system, travel_agents

import direct_ollama_server_3
from direct_ollama_server_3 import TravelRAGSystem, DirectOllamaAgents

We import the MCP tool function and the global variables from the source script. These globals are normally injected by FastMCP, so we simulate that behavior manually here.


🔧 Initialize Core Systems
if direct_ollama_server_3.rag_system is None:
    print("🔧 Initializing global systems...")
    rag = TravelRAGSystem()
    await rag.initialize()
    agents = DirectOllamaAgents(rag)
    
    direct_ollama_server_3.rag_system = rag
    direct_ollama_server_3.travel_agents = agents
    print("   ✅ Systems initialized")
else:
    print("✅ Systems already initialized")

If the system is not already initialized, we manually create the rag_system and the travel_agents. This mimics startup behavior outside of FastMCP.


🎯 Define Test Parameters
destination = "paris"
days = 5

print(f"   📍 Destination: {destination}")
print(f"   📅 Duration: {days} days")

The test focuses on generating a five-day itinerary for Paris. This acts as our baseline case.


🚀 Call the MCP Tool
start_time = time.time()
result = await get_full_itinerary(destination, days)
end_time = time.time()
duration = end_time - start_time

We time how long it takes to generate the full itinerary and await the result asynchronously.


✅ Validate Output Format
if result and "Paris" in result and "Day" in result:

We check if the result contains expected keywords like “Paris” and “Day,” which suggests a valid travel itinerary was returned.


📝 Save and Preview Results
timestamp = int(time.time())
filename = f"mcp_only_test_{timestamp}.md"

with open(filename, 'w', encoding='utf-8') as f:
    f.write(result)

If the result is valid, we write it into a Markdown file with a timestamped filename for version tracking.


📋 Preview First 15 Lines
lines = result.split('\n')[:15]
for line in lines:
    if line.strip():
        print(f"   {line}")

We give a sneak peek into the generated output by printing the first few non-empty lines.


❌ Handle Unexpected Output
else:
    print("❌ MCP tool returned unexpected result")
    print(f"Result preview: {str(result)[:200]}...")

If the result does not meet the validation criteria, we print a short preview to help debug the issue.


⚠️ Catch Errors and Print Tracebacks
except ImportError as e:
    print(f"❌ Import error: {e}")
    print("   Make sure direct_ollama_server_3.py is in the same directory")

Common import errors are caught and explained to the user. This makes it easier to troubleshoot setup issues.


except Exception as e:
    print(f"❌ MCP tool test failed: {e}")
    import traceback
    traceback.print_exc()

All other exceptions are caught and logged using Python's traceback module for full visibility into what went wrong.


🌍 Try Multiple Locations

async def test_multiple_destinations():
    """Test MCP tool with multiple destinations for variety"""
    print("\n" + "="*50)
    print("🌍 Testing Multiple Destinations")
    print("="*50)
    
    destinations = ["tokyo", "london", "rome"]
    
    for dest in destinations:
        print(f"\n📍 Testing {dest.title()}...")
        try:
            from direct_ollama_server_3 import get_full_itinerary
            
            result = await get_full_itinerary(dest, 3)
            
            if result and dest.title() in result:
                print(f"   ✅ {dest.title()} itinerary generated")
                
                # Save each one
                timestamp = int(time.time())
                filename = f"mcp_{dest}_{timestamp}.md"
                with open(filename, 'w', encoding='utf-8') as f:
                    f.write(result)
                print(f"   📁 Saved to {filename}")
                
            else:
                print(f"   ❌ Failed to generate {dest} itinerary")
                
        except Exception as e:
            print(f"   ❌ Error with {dest}: {e}")

This asynchronous test ensures that the MCP itinerary generator functions correctly across different cities. It runs multiple travel queries sequentially and saves the output as Markdown files for each city.


🔄 Start the Batch Testing Loop

async def test_multiple_destinations():
    """Test MCP tool with multiple destinations for variety"""
    print("\n" + "="*50)
    print("🌍 Testing Multiple Destinations")
    print("="*50)

The function starts by displaying a formatted banner to indicate the beginning of multi-destination testing. This helps with visual clarity in the command-line output.


🌐 Define the List of Destinations

    destinations = ["tokyo", "london", "rome"]

A list of city names is defined. Each will be used to generate a three-day itinerary using the MCP system.


🔁 Iterate Over Each Destination

    for dest in destinations:
        print(f"\n📍 Testing {dest.title()}...")

The function loops through the list of cities. For each destination, it prints a headline to indicate the current test.


🚀 Call the MCP Tool

        try:
            from direct_ollama_server_3 import get_full_itinerary
            
            result = await get_full_itinerary(dest, 3)

Inside the loop, the script imports the get_full_itinerary function and calls it with the destination and a fixed duration of three days. This simulates the behavior of a user planning a short trip.


✅ Check Result and Save Markdown File

            if result and dest.title() in result:
                print(f"   ✅ {dest.title()} itinerary generated")
                
                timestamp = int(time.time())
                filename = f"mcp_{dest}_{timestamp}.md"
                with open(filename, 'w', encoding='utf-8') as f:
                    f.write(result)
                print(f"   📁 Saved to {filename}")

If the result contains the destination name (a basic check for output validity), the script acknowledges success and saves the itinerary to a uniquely named Markdown file. The filename includes the destination and a timestamp for easy reference.


❌ Handle Failures Gracefully

            else:
                print(f"   ❌ Failed to generate {dest} itinerary")

If the result is empty or does not include the expected city name, an error message is shown for that specific test case.


⚠️ Catch and Report Exceptions

        except Exception as e:
            print(f"   ❌ Error with {dest}: {e}")

Any unexpected error during the generation process (such as API failures, I/O errors, or import issues) is caught and printed, allowing the script to continue testing the remaining destinations.


Script Entry Point — Run the Tests

if __name__ == "__main__":

When run as a standalone script, this block executes the tests.


▶️ Run MCP Tool Test

success = asyncio.run(test_mcp_tool_only())

This runs the primary test and returns whether it succeeded.


🌐 Offer More Testing

choice = input("🌍 Test multiple destinations for variety? (y/n): ").strip().lower()
if choice == 'y':
    asyncio.run(test_multiple_destinations())

If the first test passes, the user can opt-in to run further tests for additional destinations.


🎉 End of Test Summary

print("\n🎉 MCP testing complete!")
print("📁 Check the generated .md files to see results")

Once all tests complete, we guide the user to the saved result files.


Bringing It All Together

This is the full code for server:


This is the full code for test code:



Output

**Parisian Delights: A 5-Day Journey Through the City of Light**

As you step into the world's most romantic city, prepare to be enchanted by the City of Light's charms. From the Seine River's gentle whispers to the Eiffel Tower's majestic grandeur, Paris is a treasure trove of experiences waiting to be discovered. In this 5-day itinerary, we'll guide you through a curated selection of must-see attractions, hidden gems, and local favorites that will leave you with unforgettable memories.

**Summary Table:**

| Day | Morning | Afternoon | Evening |
| --- | --- | --- | --- |
| 1 | Seine River Cruise | Le Grand Colbert | Le Comptoir du Relais' Secret Menu |
| 2 | Père Lachaise Cemetery | Canal Saint-Martin Boat Ride | Septime La Cave's Wine Pairings |
| 3 | L'Atelier de Joël Robuchon | Montmartre's Secret Gardens | Le Barav's Romantic Dinner |
| 4 | Café Constant & Artisanal Coffee | L'As du Fallafel's Unique Falafel | Pierre Hermé's Patisserie Adventure |
| 5 | Le ComptoirG+ (TBD) | - | - |

**Day-by-Day Itinerary:**

### Day 1

Start your Parisian adventure with a scenic Seine River Cruise at sunrise, watching the city come alive. Then, visit Le Grand Colbert for refined French cuisine and an Art Nouveau atmosphere in the afternoon. End your day with dinner at Le Comptoir du Relais' cozy corner, indulging in their hidden 'secret' menu.

### Day 2

Explore Père Lachaise Cemetery's lesser-known sections, discovering Oscar Wilde's tombstone and the city's industrial past in the morning. Take a leisurely boat ride along Canal Saint-Martin, passing through historic locks and bridges, in the afternoon. Dine at Septime La Cave for expertly curated wine pairings with artisanal cheeses in the evening.

### Day 3

Visit L'Atelier de Joël Robuchon for counter seating around an open kitchen, watching chefs prepare small plates in the morning. Wander through Montmartre's Secret Gardens, enjoying the tranquil oasis and city views, in the afternoon. Enjoy a romantic dinner at Le Barav, savoring seasonal French cuisine and natural wines, in the evening.

We have used the LLaMA 3 model in this. It will produce better results if we use a model with more parameters, such as ChatGPT, Claude, Gemini, and others.


To test it with Claude Desktop, use the following configuration in the configuration file claude_desktop_config.json.


To do this, first open Claude Desktop and follow this path: Menu → File → Settings → Developer → Edit Config → claude_desktop_config.json.


After opening the claude_desktop_config.json, add this configuration.

{
  "mcpServers": {
    "travel-planner_2": {
      "command": "E:/workspace/python/mcp/travel-planner-4/venv/Scripts/python.exe",
      "args": [
        "E:/workspace/python/mcp/travel-planner-4/direct_ollama_server_3.py"
      ],
      "cwd": "E:/workspace/python/mcp/travel-planner-4",
      "env": {
        "PYTHONPATH": "E:/workspace/python/mcp/travel-planner-4/venv/Lib/site-packages"
      }
    }
  }
}

After adding this, close the Claude Desktop app and terminate Claude from the Task Manager. Once it has been terminated, reopen the Claude Desktop app, and it will appear as running in the Developer settings.

Note: Claude has a timeout limit for generating results. If the results are not generated within that time (3 to 4 minutes), Claude will produce the output by itself instead of using the result from the Ollama model.


In the Search and Tools section, we can find our server along with all the tools.



Conclusion: We've Built Something Extraordinary!

Take a moment to appreciate what we have just accomplished! 🎉 We have constructed an AI-powered travel planning system that seamlessly blends cutting-edge technology with practical real-world applications. Let us recap the incredible journey we have taken together:


🧠 AI Architecture Mastery:

  • Multi-agent systems: We understand how specialized AI agents can collaborate - our DirectOllamaAgents class demonstrates sophisticated agent orchestration

  • RAG implementation: We have built a robust system (TravelRAGSystem) that grounds AI responses in real travel documents stored as text files

  • Vector search optimization: We leverage FAISS indexing with sentence transformers for lightning-fast semantic search

  • Hybrid intelligence: Our system gracefully falls back from AI-powered responses to rule-based alternatives, ensuring reliability


🛠️ Production-Ready Engineering Skills:

  • Error handling: Our system gracefully handles Ollama connection failures, malformed JSON responses, and missing data files

  • Quality assurance: Built-in validation with multiple JSON extraction strategies and fallback mechanisms

  • Audit trails: Complete traceability with session IDs, timing logs

  • Performance monitoring: Comprehensive logging system tracks every operation from data loading to itinerary generation


📚 Real-World Travel Applications:

We have built something that can genuinely transform:

  • Personalized trip planning: AI-generated itineraries based on local knowledge and user preferences

  • Travel research automation: Intelligent synthesis of attractions, dining, and activities from curated data sources

  • Dynamic content generation: Each itinerary is unique with randomized themes, styles, and session-specific variations

  • Knowledge management: Extensible system that allows adding new destinations and categories through simple text files


The Technical Marvel We Have Created:

System Capabilities:

  • File-based data management: Processes unlimited travel document collections stored as structured text files

  • Intelligent itinerary generation: Creates professional-quality travel plans in seconds using direct Ollama integration

  • Semantic search: FAISS-powered vector similarity search across travel content

  • MCP protocol integration: Seamless tool integration with Claude and other AI assistants

  • Extensible architecture: Easy addition of new cities, categories, and travel information


Performance Characteristics:

  • Direct Ollama Mode: Real-time AI-powered research and itinerary creation with thematic variations

  • Rule-based Fallback: Reliable operation even without AI connectivity using intelligent content recombination

  • Memory Efficient: Lightweight sentence transformer models with optimized vector storage

  • Scalable: Handles travel document collections from dozens to thousands of entries


Real-World Impact Stories:

Travel Industry Transformation:

Imagine a travel agency that can instantly generate personalized itineraries by feeding the system local expert knowledge, creating unique experiences for every client while maintaining authentic local insights.


Educational Travel Programs:

Picture educators who can automatically generate detailed study-abroad programs by combining academic requirements with cultural exploration, creating immersive learning experiences.


Personal Travel Revolution:

Think about travelers who can access expert local knowledge instantly, with AI-curated recommendations that feel like having a knowledgeable local friend in every destination.


The Bigger Picture

What we have built is not just a travel planner – it is a demonstration of the future of knowledge-augmented AI systems. We have shown that the real power of AI lies not in replacing human expertise, but in making it more accessible and actionable.


The Paradigm Shift:

Instead of generic travel recommendations, we have created a system where:

  • Humans provide curation (selecting quality travel information, organizing local knowledge)

  • AI handles synthesis (research analysis, itinerary optimization, personalized formatting)

  • The result amplifies local expertise rather than replacing it with generic content


Technical Innovation Patterns:

The architectural patterns we have implemented represent cutting-edge AI system design:

  • Hybrid intelligence: Seamless switching between AI and rule-based processing

  • Vector-augmented generation: Combining semantic search with large language models

  • Modular agent architecture: Specialized components for research, planning, and formatting

  • Fault-tolerant design: Multiple fallback strategies ensuring system reliability



Transform Your AI Workflows with Codersarts

Whether you're building intelligent systems with MCP, implementing RAG for smart information retrieval, or developing robust multi-agent architectures, the experts at Codersarts are here to support your vision. From academic prototypes to enterprise-grade solutions, we provide:


  • Custom RAG Implementation: Build retrieval-augmented generation systems tailored to your domain

  • MCP-Based Agent Systems: Design and deploy modular, coordinated AI agents with FastMCP

  • Semantic Search with FAISS: Implement efficient vector search for meaningful content discovery

  • End-to-End AI Development: From setup and orchestration to deployment and optimization


Do not let architectural complexity or tooling challenges slow down your progress. Partner with Codersarts and bring your next-generation AI systems to life.


Ready to get started? Visit Codersarts.com or connect with our team to discuss your MCP or RAG-based project.The future of modular, intelligent automation is here – let’s build it together!



Comments


bottom of page