#
tokens: 49727/50000 42/44 files (page 1/2)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 of 2. Use http://codebase.md/athapong/aio-mcp?page={x} to view the full context.

# Directory Structure

```
├── .env
├── .github
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── workflows
│       ├── release.yaml
│       └── scan.yaml
├── .gitignore
├── .vscode
│   └── settings.json
├── Dockerfile
├── docs
│   └── google_maps_tools.md
├── go.mod
├── go.sum
├── justfile
├── main.go
├── pkg
│   └── adf
│       ├── convert.go
│       └── types.go
├── prompts
│   └── code.go
├── README.md
├── resources
│   └── jira.go
├── scripts
│   ├── docs
│   │   └── update-doc.go
│   └── google-token
│       ├── main.go
│       └── README.md
├── services
│   ├── atlassian.go
│   ├── deepseek.go
│   ├── gchat.go
│   ├── google.go
│   ├── httpclient.go
│   └── openai.go
├── smithery.yaml
├── tools
│   ├── calendar.go
│   ├── confluence.go
│   ├── deepseek.go
│   ├── fetch.go
│   ├── gchat.go
│   ├── gemini.go
│   ├── gitlab.go
│   ├── gmail.go
│   ├── googlemaps_tools.go
│   ├── jira.go
│   ├── rag.go
│   ├── screenshot.go
│   ├── script.go
│   ├── search.go
│   ├── sequentialthinking.go
│   ├── tool_manager.go
│   ├── youtube_channel.go
│   └── youtube.go
└── util
    └── handler.go
```

# Files

--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------

```

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Binaries for programs and plugins
*.exe
*.dll
*.so
*.dylib

# Test binary
*.test

# Output of the go coverage tool
*.out

# Go module cache
/vendor/

# Temporary files
*.log
*.tmp

# IDE-specific files
.vscode/
.idea/

# Node modules (if applicable)
node_modules/

# Environment variables
.env
```

--------------------------------------------------------------------------------
/scripts/google-token/README.md:
--------------------------------------------------------------------------------

```markdown
Here's the English translation of the README.md:

# Guide to Getting token.json from Google API

You can follow the steps below or just ask Claude how to do it, I prefer the last one.

## 1. Create a project on Google Cloud Platform

1. Access Google Cloud Console (https://console.cloud.google.com/)
2. Sign in with your Google account
3. Create a new project:
   - Click on the dropdown menu in the top left corner (next to "Google Cloud")
   - Click "New Project"
   - Enter project name
   - Select organization (if applicable)
   - Click "Create"

4. Enable Gmail API:
   - From the left menu, select "APIs & Services" > "Library"
   - Search for "Gmail API"
   - Click on Gmail API
   - Click "Enable"

5. Configure OAuth consent screen:
   - From "APIs & Services" menu, select "OAuth consent screen"
   - Choose User Type as "External" (or "Internal" if you use Google Workspace)
   - Click "Create"
   - Fill in required information:
     + App name: Your application name
     + User support email: Contact email
     + Developer contact information: Contact email
   - Click "Save and Continue"
   - In the Scopes section, click "Add or Remove Scopes"
   - Add necessary scopes (e.g., Gmail API scopes)
   - Click "Save and Continue"
   - Add test users if needed
   - Click "Save and Continue"

## 2. Get credentials.json

1. Access Google Cloud Console
2. Create a new project or select an existing one
3. In the menu, select "APIs & Services" > "Credentials"
4. Click "Create Credentials" > "OAuth client ID"
5. Choose Application type as "Desktop app"
6. Name your credential and click "Create"
7. Download credentials.json and place it in the same directory as main.go

## 3. Run the program

1. Open terminal and cd to the directory containing main.go
2. Run the command:
```bash
go run main.go -credentials=/path/to/google-credentials.json -token=/path/to/google-token.json
```

Remember the paths, because you will need them in the next step.

## 4. Authenticate and get token

1. The program will display a URL. Copy this URL
2. Open the URL in your browser
3. Sign in to your Google account
4. Accept the requested access permissions
5. Google will provide an authorization code
6. Copy this authorization code
7. Return to terminal and paste the authorization code
8. Press Enter

## 5. Results

- The program will automatically create `token.json` file in the current directory
- This token.json contains access token and refresh token
- You can use this token for subsequent API access
- The program will display the list of labels in your Gmail

## 6. Configure the MCP

Set the path of two file as following key in your claude config file:

```
{
   ...
   "GOOGLE_CREDENTIALS_FILE": "/path/to/google-credentials.json",
   "GOOGLE_TOKEN_FILE": "/path/to/google-token.json",
   ...
}
```

## Notes

- token.json contains sensitive information, don't share it
- Tokens have expiration dates but will auto-refresh
- If you change scopes in the code, you need to delete the old token.json and create a new one

## Common Error Handling

1. If unable to read credentials.json:
   - Check if the file exists in the directory
   - Verify the filename is exactly "credentials.json"

2. If authorization code is invalid:
   - Ensure you copied the code correctly and completely
   - Try generating a new code

3. If access is denied:
   - Check scopes in the code
   - Confirm Gmail API is enabled in Google Cloud Console

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# AIO-MCP Server

A powerful Model Context Protocol (MCP) server implementation with integrations for GitLab, Jira, Confluence, YouTube, and more. This server provides AI-powered search capabilities and various utility tools for development workflows.

## Prerequisites

- Go 1.23.2 or higher
- Various API keys and tokens for the services you want to use

## Installation

### Installing via Smithery

To install AIO-MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@athapong/aio-mcp) (will guide you through interactive CLI setup):

```bash
npx -y @smithery/cli install @athapong/aio-mcp --client claude
```

*Note: Smithery will interactively prompt you for required configuration values and handle environment setup automatically*

### Installing via Go

To set the `go install` command to install into the Go bin path, you need to ensure your Go environment variables are correctly configured. Here's how to do it:

1. First, ensure you have a properly set `GOPATH` environment variable, which by default is `$HOME/go` on Unix-like systems or `%USERPROFILE%\go` on Windows.

2. The `go install` command places binaries in `$GOPATH/bin` by default. Make sure this directory is in your system's `PATH` environment variable.

Here's how to set this up on different operating systems:

### Linux/macOS:
```bash
# Add these to your ~/.bashrc, ~/.zshrc, or equivalent shell config file
export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bin
```

After adding these lines, reload your shell configuration:
```bash
source ~/.bashrc  # or ~/.zshrc
```

### Windows (PowerShell):
```powershell
# Set environment variables
[Environment]::SetEnvironmentVariable("GOPATH", "$env:USERPROFILE\go", "User")
[Environment]::SetEnvironmentVariable("PATH", "$env:PATH;$env:USERPROFILE\go\bin", "User")
```

### Windows (Command Prompt):
```cmd
setx GOPATH "%USERPROFILE%\go"
setx PATH "%PATH%;%USERPROFILE%\go\bin"
```

After setting these variables, you can verify they're working correctly with:
```bash
go env GOPATH
echo $PATH  # On Unix/Linux/macOS
echo %PATH%  # On Windows CMD
$env:PATH  # On Windows PowerShell
```

Now when you run `go install`, the binaries will be installed to your `$GOPATH/bin` directory, which is in your PATH, so you can run them from anywhere.

Finally, install the server:
```bash
go install github.com/athapong/aio-mcp@latest
```

2. **Manual setup required** - Create a `.env` file with your configuration:
```env
ENABLE_TOOLS=
QDRANT_HOST=
ATLASSIAN_HOST=
ATLASSIAN_EMAIL=
GITLAB_HOST=
GITLAB_TOKEN=
BRAVE_API_KEY=
ATLASSIAN_TOKEN=
GOOGLE_AI_API_KEY=
PROXY_URL=
OPENAI_API_KEY=
OPENAI_EMBEDDING_MODEL=
DEEPSEEK_API_KEY=
QDRANT_PORT=
GOOGLE_TOKEN_FILE=
GOOGLE_CREDENTIALS_FILE=
QDRANT_API_KEY=
USE_OLLAMA_DEEPSEEK=
ENABLE_SSE=
SSE_ADDR=
SSE_BASE_PATH=
```

3. Config your claude's config:

```json{claude_desktop_config.json}
{
  "mcpServers": {
    "aio-mcp": {
      "command": "aio-mcp",
      "args": ["-env", "/path/to/.env", "-sse", "-sse-addr", ":8080", "-sse-base-path", "/mcp"],

    }
  }
}
```
### or override environment values as
```json
{
  "mcpServers": {
    "aio-mcp": {
      "command": "aio-mcp",
      "env": {
        "ENABLE_TOOLS": "",
        "OPENAI_BASE_URL": "",
        "GOOGLE_AI_API_KEY": "",
        "GITLAB_TOKEN": "",
        "GITLAB_HOST": "",
        "QDRANT_HOST": "",
        "QDRANT_API_KEY": "",

        "PROXY_URL": "",
        "OPENAI_API_KEY": "",
        "GOOGLE_TOKEN_FILE": "",
        "GOOGLE_CREDENTIALS_FILE": "",

        "ATLASSIAN_TOKEN": "",
        "BRAVE_API_KEY": "",
        "QDRANT_PORT": "",
        "ATLASSIAN_HOST": "",
        "ATLASSIAN_EMAIL": "",

        "USE_OPENROUTER": "", // "true" if you want to use openrouter for AI to help with reasoning on `tool_use_plan`, default is false
        "DEEPSEEK_API_KEY": "", // specify the deepseek api key if you want to use deepseek for AI to help with reasoning on `tool_use_plan`
        "OPENROUTER_API_KEY": "", // specify the openrouter api key if you want to use openrouter for AI to help with reasoning on `tool_use_plan`
        "DEEPSEEK_API_BASE": "", // specify the deepseek api key if you want to use deepseek for AI to help with reasoning on `tool_use_plan`
        "USE_OLLAMA_DEEPSEEK": "", // "true" if you want to use deepseek with local ollama, default is false
        "OLLAMA_URL": "" // default with http://localhost:11434
      }
    }
  }
}
```

## Server Modes

AIO-MCP Server supports two modes of operation:

1. **Stdio Mode (Default)**: The server communicates via standard input/output, which is the default mode used by Claude Desktop and other MCP clients.

2. **SSE (Server-Sent Events) Mode**: The server runs as an HTTP server that supports Server-Sent Events for real-time communication. This is useful for web-based clients or when you need to access the MCP server over a network.

### Enabling SSE Mode

You can enable SSE mode in one of two ways:

1. **Command-line flags**:
   ```bash
   aio-mcp -sse -sse-addr ":8080" -sse-base-path "/mcp"
   ```

2. **Environment variables** (in your `.env` file):
   ```
   ENABLE_SSE=true
   SSE_ADDR=:8080
   SSE_BASE_PATH=/mcp
   ```

When SSE mode is enabled, the server will start an HTTP server that listens on the specified address. The server provides two endpoints:
- SSE endpoint: `{SSE_BASE_PATH}/sse` (default: `/mcp/sse`)
- Message endpoint: `{SSE_BASE_PATH}/message` (default: `/mcp/message`)

Clients can connect to the SSE endpoint to receive server events and send messages to the message endpoint.

## Enable Tools

There is a hidden variable `ENABLE_TOOLS` in the environment variable. It is a comma separated list of tools group to enable. If not set, all tools will be enabled. Leave it empty to enable all tools.


Here is the list of tools group:

- `gemini`: Gemini-powered search
- `fetch`: Fetch tools
- `brave_search`: Brave Search tools
- `google_maps`: Google Maps tools
- `confluence`: Confluence tools
- `youtube`: YouTube tools
- `jira`: Jira tools
- `gitlab`: GitLab tools
- `script`: Script tools
- `rag`: RAG tools
- `deepseek`: Deepseek AI tools, including reasoning and advanced search if 'USE_OLLAMA_DEEPSEEK' is set to true, default ollama endpoint is http://localhost:11434 with model deepseek-r1:8b

## Available Tools

### calendar_create_event

Create a new event in Google Calendar

Arguments:

- `summary` (String) (Required): Title of the event
- `description` (String): Description of the event
- `start_time` (String) (Required): Start time of the event in RFC3339 format (e.g., 2023-12-25T09:00:00Z)
- `end_time` (String) (Required): End time of the event in RFC3339 format
- `attendees` (String): Comma-separated list of attendee email addresses

### calendar_list_events

List upcoming events in Google Calendar

Arguments:

- `time_min` (String): Start time for the search in RFC3339 format (default: now)
- `time_max` (String): End time for the search in RFC3339 format (default: 1 week from now)
- `max_results` (Number): Maximum number of events to return (default: 10)

### calendar_update_event

Update an existing event in Google Calendar

Arguments:

- `event_id` (String) (Required): ID of the event to update
- `summary` (String): New title of the event
- `description` (String): New description of the event
- `start_time` (String): New start time of the event in RFC3339 format
- `end_time` (String): New end time of the event in RFC3339 format
- `attendees` (String): Comma-separated list of new attendee email addresses

### calendar_respond_to_event

Respond to an event invitation in Google Calendar

Arguments:

- `event_id` (String) (Required): ID of the event to respond to
- `response` (String) (Required): Your response (accepted, declined, or tentative)

### confluence_search

Search Confluence

Arguments:

- `query` (String) (Required): Atlassian Confluence Query Language (CQL)

### confluence_get_page

Get Confluence page content

Arguments:

- `page_id` (String) (Required): Confluence page ID

### confluence_create_page

Create a new Confluence page

Arguments:

- `space_key` (String) (Required): The key of the space where the page will be created
- `title` (String) (Required): Title of the page
- `content` (String) (Required): Content of the page in storage format (XHTML)
- `parent_id` (String): ID of the parent page (optional)

### confluence_update_page

Update an existing Confluence page

Arguments:

- `page_id` (String) (Required): ID of the page to update
- `title` (String): New title of the page (optional)
- `content` (String): New content of the page in storage format (XHTML)
- `version_number` (String): Version number for optimistic locking (optional)

### confluence_compare_versions

Compare two versions of a Confluence page

Arguments:

- `page_id` (String) (Required): Confluence page ID
- `source_version` (String) (Required): Source version number
- `target_version` (String) (Required): Target version number

### deepseek_reasoning

advanced reasoning engine using Deepseek's AI capabilities for multi-step problem solving, critical analysis, and strategic decision support

Arguments:

- `question` (String) (Required): The structured query or problem statement requiring deep analysis and reasoning
- `context` (String) (Required): Defines the operational context and purpose of the query within the MCP ecosystem
- `knowledge` (String): Provides relevant chat history, knowledge base entries, and structured data context for MCP-aware reasoning

### get_web_content

Fetches content from a given HTTP/HTTPS URL. This tool allows you to retrieve text content from web pages, APIs, or any accessible HTTP endpoints. Returns the raw content as text.

Arguments:

- `url` (String) (Required): The complete HTTP/HTTPS URL to fetch content from (e.g., https://example.com)

### gchat_list_spaces

List all available Google Chat spaces/rooms

### gchat_send_message

Send a message to a Google Chat space or direct message

Arguments:

- `space_name` (String) (Required): Name of the space to send the message to
- `message` (String) (Required): Text message to send

### ai_web_search

search the web by using Google AI Search. Best tool to update realtime information

Arguments:

- `question` (String) (Required): The question to ask. Should be a question
- `context` (String) (Required): Context/purpose of the question, helps Gemini to understand the question better

### gitlab_list_projects

List GitLab projects

Arguments:

- `group_id` (String) (Required): gitlab group ID
- `search` (String): Multiple terms can be provided, separated by an escaped space, either + or %20, and will be ANDed together. Example: one+two will match substrings one and two (in any order).

### gitlab_get_project

Get GitLab project details

Arguments:

- `project_path` (String) (Required): Project/repo path

### gitlab_list_mrs

List merge requests

Arguments:

- `project_path` (String) (Required): Project/repo path
- `state` (String) (Default: all): MR state (opened/closed/merged)

### gitlab_get_mr_details

Get merge request details

Arguments:

- `project_path` (String) (Required): Project/repo path
- `mr_iid` (String) (Required): Merge request IID

### gitlab_create_MR_note

Create a note on a merge request

Arguments:

- `project_path` (String) (Required): Project/repo path
- `mr_iid` (String) (Required): Merge request IID
- `comment` (String) (Required): Comment text

### gitlab_get_file_content

Get file content from a GitLab repository

Arguments:

- `project_path` (String) (Required): Project/repo path
- `file_path` (String) (Required): Path to the file in the repository
- `ref` (String) (Required): Branch name, tag, or commit SHA

### gitlab_list_pipelines

List pipelines for a GitLab project

Arguments:

- `project_path` (String) (Required): Project/repo path
- `status` (String) (Default: all): Pipeline status (running/pending/success/failed/canceled/skipped/all)

### gitlab_list_commits

List commits in a GitLab project within a date range

Arguments:

- `project_path` (String) (Required): Project/repo path
- `since` (String) (Required): Start date (YYYY-MM-DD)
- `until` (String): End date (YYYY-MM-DD). If not provided, defaults to current date
- `ref` (String) (Required): Branch name, tag, or commit SHA

### gitlab_get_commit_details

Get details of a commit

Arguments:

- `project_path` (String) (Required): Project/repo path
- `commit_sha` (String) (Required): Commit SHA

### gitlab_list_user_events

List GitLab user events within a date range

Arguments:

- `username` (String) (Required): GitLab username
- `since` (String) (Required): Start date (YYYY-MM-DD)
- `until` (String): End date (YYYY-MM-DD). If not provided, defaults to current date

### gitlab_list_group_users

List all users in a GitLab group

Arguments:

- `group_id` (String) (Required): GitLab group ID

### gitlab_create_mr

Create a new merge request

Arguments:

- `project_path` (String) (Required): Project/repo path
- `source_branch` (String) (Required): Source branch name
- `target_branch` (String) (Required): Target branch name
- `title` (String) (Required): Merge request title
- `description` (String): Merge request description

### gitlab_clone_repo

Clone or update a GitLab repository locally

Arguments:

- `project_path` (String) (Required): Project/repo path
- `ref` (String): Branch name or tag (optional, defaults to project's default branch)

### gmail_search

Search emails in Gmail using Gmail's search syntax

Arguments:

- `query` (String) (Required): Gmail search query. Follow Gmail's search syntax

### gmail_move_to_spam

Move specific emails to spam folder in Gmail by message IDs

Arguments:

- `message_ids` (String) (Required): Comma-separated list of message IDs to move to spam

### gmail_create_filter

Create a Gmail filter with specified criteria and actions

Arguments:

- `from` (String): Filter emails from this sender
- `to` (String): Filter emails to this recipient
- `subject` (String): Filter emails with this subject
- `query` (String): Additional search query criteria
- `add_label` (Boolean): Add label to matching messages
- `label_name` (String): Name of the label to add (required if add_label is true)
- `mark_important` (Boolean): Mark matching messages as important
- `mark_read` (Boolean): Mark matching messages as read
- `archive` (Boolean): Archive matching messages

### gmail_list_filters

List all Gmail filters in the account

### gmail_list_labels

List all Gmail labels in the account

### gmail_delete_filter

Delete a Gmail filter by its ID

Arguments:

- `filter_id` (String) (Required): The ID of the filter to delete

### gmail_delete_label

Delete a Gmail label by its ID

Arguments:

- `label_id` (String) (Required): The ID of the label to delete

### jira_get_issue

Retrieve detailed information about a specific Jira issue including its status, assignee, description, subtasks, and available transitions

Arguments:

- `issue_key` (String) (Required): The unique identifier of the Jira issue (e.g., KP-2, PROJ-123)

### jira_search_issue

Search for Jira issues using JQL (Jira Query Language). Returns key details like summary, status, assignee, and priority for matching issues

Arguments:

- `jql` (String) (Required): JQL query string (e.g., 'project = KP AND status = \"In Progress\"')

### jira_list_sprints

List all active and future sprints for a specific Jira board, including sprint IDs, names, states, and dates

Arguments:

- `board_id` (String) (Required): Numeric ID of the Jira board (can be found in board URL)

### jira_create_issue

Create a new Jira issue with specified details. Returns the created issue's key, ID, and URL

Arguments:

- `project_key` (String) (Required): Project identifier where the issue will be created (e.g., KP, PROJ)
- `summary` (String) (Required): Brief title or headline of the issue
- `description` (String) (Required): Detailed explanation of the issue
- `issue_type` (String) (Required): Type of issue to create (common types: Bug, Task, Story, Epic)

### jira_update_issue

Modify an existing Jira issue's details. Supports partial updates - only specified fields will be changed

Arguments:

- `issue_key` (String) (Required): The unique identifier of the issue to update (e.g., KP-2)
- `summary` (String): New title for the issue (optional)
- `description` (String): New description for the issue (optional)

### jira_list_statuses

Retrieve all available issue status IDs and their names for a specific Jira project

Arguments:

- `project_key` (String) (Required): Project identifier (e.g., KP, PROJ)

### jira_transition_issue

Transition an issue through its workflow using a valid transition ID. Get available transitions from jira_get_issue

Arguments:

- `issue_key` (String) (Required): The issue to transition (e.g., KP-123)
- `transition_id` (String) (Required): Transition ID from available transitions list
- `comment` (String): Optional comment to add with transition

### RAG_memory_index_content

Index a content into memory, can be inserted or updated

Arguments:

- `collection` (String) (Required): Memory collection name
- `filePath` (String) (Required): content file path
- `payload` (String) (Required): Plain text payload
- `model` (String): Embedding model to use (default: text-embedding-3-large)

### RAG_memory_index_file

Index a local file into memory

Arguments:

- `collection` (String) (Required): Memory collection name
- `filePath` (String) (Required): Path to the local file to be indexed

### RAG_memory_create_collection

Create a new vector collection in memory

Arguments:

- `collection` (String) (Required): Memory collection name
- `model` (String): Embedding model to use (default: text-embedding-3-large)

### RAG_memory_delete_collection

Delete a vector collection in memory

Arguments:

- `collection` (String) (Required): Memory collection name

### RAG_memory_list_collections

List all vector collections in memory

### RAG_memory_search

Search for memory in a collection based on a query

Arguments:

- `collection` (String) (Required): Memory collection name
- `query` (String) (Required): search query, should be a keyword
- `model` (String): Embedding model to use (default: text-embedding-3-large)

### RAG_memory_delete_index_by_filepath

Delete a vector index by filePath

Arguments:

- `collection` (String) (Required): Memory collection name
- `filePath` (String) (Required): Path to the local file to be deleted

### execute_comand_line_script

Safely execute command line scripts on the user's system with security restrictions. Features sandboxed execution, timeout protection, and output capture. Supports cross-platform scripting with automatic environment detection.

Arguments:

- `content` (String) (Required):
- `interpreter` (String) (Default: /bin/sh): Path to interpreter binary (e.g. /bin/sh, /bin/bash, /usr/bin/python, cmd.exe). Validated against allowed list for security
- `working_dir` (String): Execution directory path (default: user home). Validated to prevent unauthorized access to system locations

### web_search

Search the web using Brave Search API

Arguments:

- `query` (String) (Required): Query to search for (max 400 chars, 50 words)
- `count` (Number) (Default: 5): Number of results (1-20, default 5)
- `country` (String) (Default: ALL): Country code

### sequentialthinking

`A detailed tool for dynamic and reflective problem-solving through thoughts.
This tool helps analyze problems through a flexible thinking process that can adapt and evolve.
Each thought can build on, question, or revise previous insights as understanding deepens.

When to use this tool:
- Breaking down complex problems into steps
- Planning and design with room for revision
- Analysis that might need course correction
- Problems where the full scope might not be clear initially
- Problems that require a multi-step solution
- Tasks that need to maintain context over multiple steps
- Situations where irrelevant information needs to be filtered out

Key features:
- You can adjust total_thoughts up or down as you progress
- You can question or revise previous thoughts
- You can add more thoughts even after reaching what seemed like the end
- You can express uncertainty and explore alternative approaches
- Not every thought needs to build linearly - you can branch or backtrack
- Generates a solution hypothesis
- Verifies the hypothesis based on the Chain of Thought steps
- Repeats the process until satisfied
- Provides a correct answer

Parameters explained:
- thought: Your current thinking step, which can include:
* Regular analytical steps
* Revisions of previous thoughts
* Questions about previous decisions
* Realizations about needing more analysis
* Changes in approach
* Hypothesis generation
* Hypothesis verification
- next_thought_needed: True if you need more thinking, even if at what seemed like the end
- thought_number: Current number in sequence (can go beyond initial total if needed)
- total_thoughts: Current estimate of thoughts needed (can be adjusted up/down)
- is_revision: A boolean indicating if this thought revises previous thinking
- revises_thought: If is_revision is true, which thought number is being reconsidered
- branch_from_thought: If branching, which thought number is the branching point
- branch_id: Identifier for the current branch (if any)
- needs_more_thoughts: If reaching end but realizing more thoughts needed

You should:
1. Start with an initial estimate of needed thoughts, but be ready to adjust
2. Feel free to question or revise previous thoughts
3. Don't hesitate to add more thoughts if needed, even at the "end"
4. Express uncertainty when present
5. Mark thoughts that revise previous thinking or branch into new paths
6. Ignore information that is irrelevant to the current step
7. Generate a solution hypothesis when appropriate
8. Verify the hypothesis based on the Chain of Thought steps
9. Repeat the process until satisfied with the solution
10. Provide a single, ideally correct answer as the final output
11. Only set next_thought_needed to false when truly done and a satisfactory answer is reached`

Arguments:

- `thought` (String) (Required): Your current thinking step
- `nextThoughtNeeded` (Boolean) (Required): Whether another thought step is needed
- `thoughtNumber` (Number) (Required): Current thought number
- `totalThoughts` (Number) (Required): Estimated total thoughts needed
- `isRevision` (Boolean): Whether this revises previous thinking
- `revisesThought` (Number): Which thought is being reconsidered
- `branchFromThought` (Number): Branching point thought number
- `branchId` (String): Branch identifier
- `needsMoreThoughts` (Boolean): If more thoughts are needed
- `result` (String): Final result or conclusion from this thought
- `summary` (String): Brief summary of the thought's key points

### sequentialthinking_history

Retrieve the thought history for the current thinking process

Arguments:

- `branchId` (String): Optional branch ID to get history for

### tool_manager

Manage MCP tools - enable or disable tools

Arguments:

- `action` (String) (Required): Action to perform: list, enable, disable
- `tool_name` (String): Tool name to enable/disable

### tool_use_plan

Create a plan using available tools to solve the request

Arguments:

- `request` (String) (Required): Request to plan for
- `context` (String) (Required): Context related to the request

### youtube_transcript

Get YouTube video transcript

Arguments:

- `video_id` (String) (Required): YouTube video ID

### youtube_update_video

Update a video's title and description on YouTube

Arguments:

- `video_id` (String) (Required): ID of the video to update
- `title` (String) (Required): New title of the video
- `description` (String) (Required): New description of the video
- `keywords` (String) (Required): Comma-separated list of keywords for the video
- `category` (String) (Required): Category ID for the video. See https://developers.google.com/youtube/v3/docs/videoCategories/list for more information.

### youtube_get_video_details

Get details (title, description, ...) for a specific video

Arguments:

- `video_id` (String) (Required): ID of the video

### youtube_list_videos

List YouTube videos managed by the user

Arguments:

- `channel_id` (String) (Required): ID of the channel to list videos for
- `max_results` (Number) (Required): Maximum number of videos to return

```

--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------

```json
{
    "files.watcherExclude": {
        "**/target": true
    }
}
```

--------------------------------------------------------------------------------
/.github/workflows/scan.yaml:
--------------------------------------------------------------------------------

```yaml
name: Security and Licence Scan

on:
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      with:
        fetch-depth: 0
    - name: Secret Scanning
      uses: trufflesecurity/trufflehog@main
      with:
        extra_args: --results=verified,unknown
```

--------------------------------------------------------------------------------
/services/openai.go:
--------------------------------------------------------------------------------

```go
package services

import (
	"os"
	"sync"

	"github.com/sashabaranov/go-openai"
)

var DefaultOpenAIClient = sync.OnceValue(func() *openai.Client {
	apiKey := os.Getenv("OPENAI_API_KEY")
	if apiKey == "" {
		panic("OPENAI_API_KEY is not set, please set it in MCP Config")
	}

	baseURL := os.Getenv("OPENAI_BASE_URL")
	config := openai.DefaultConfig(apiKey)

	if baseURL != "" {
		config.BaseURL = baseURL
	}

	return openai.NewClientWithConfig(config)
})

```

--------------------------------------------------------------------------------
/pkg/adf/types.go:
--------------------------------------------------------------------------------

```go
package adf

// Node represents an ADF node
type Node struct {
	Type    string                 `json:"type"`
	Text    string                 `json:"text,omitempty"`
	Attrs   map[string]interface{} `json:"attrs,omitempty"`
	Marks   []*Mark                `json:"marks,omitempty"`
	Content []*Node                `json:"content,omitempty"`
}

// Mark represents formatting marks in ADF
type Mark struct {
	Type  string                 `json:"type"`
	Attrs map[string]interface{} `json:"attrs,omitempty"`
}

```

--------------------------------------------------------------------------------
/services/httpclient.go:
--------------------------------------------------------------------------------

```go
package services

import (
	"crypto/tls"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"sync"
)

var DefaultHttpClient = sync.OnceValue(func() *http.Client {
	transport := &http.Transport{}

	proxyURL := os.Getenv("PROXY_URL")
	if proxyURL != "" {
		proxy, err := url.Parse(proxyURL)
		if err != nil {
			panic(fmt.Sprintf("Failed to parse PROXY_URL: %v", err))
		}
		transport.Proxy = http.ProxyURL(proxy)
		transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
	}


	return &http.Client{Transport: transport}
})

```

--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------

```markdown
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.

```

--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------

```markdown
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Desktop (please complete the following information):**
 - OS: [e.g. iOS]
 - Browser [e.g. chrome, safari]
 - Version [e.g. 22]

**Smartphone (please complete the following information):**
 - Device: [e.g. iPhone6]
 - OS: [e.g. iOS8.1]
 - Browser [e.g. stock browser, safari]
 - Version [e.g. 22]

**Additional context**
Add any other context about the problem here.

```

--------------------------------------------------------------------------------
/prompts/code.go:
--------------------------------------------------------------------------------

```go
package prompts

import (
	"context"
	"fmt"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
)

func RegisterCodeTools(s *server.MCPServer) {
	tool := mcp.NewPrompt("code_review",
		mcp.WithPromptDescription("Review code and provide feedback"),
		mcp.WithArgument("developer_name", mcp.ArgumentDescription("The name of the developer who wrote the code")),
	)
	s.AddPrompt(tool, codeReviewHandler)
}

func codeReviewHandler(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
	developerName := request.Params.Arguments["developer_name"]

	return &mcp.GetPromptResult{
		Description: fmt.Sprintf("Code reviewed by %s", developerName),
		Messages: []mcp.PromptMessage{
			{
				Role: mcp.RoleUser,
				Content: mcp.TextContent{
					Type: "text",
					Text: fmt.Sprintf("Use gitlab tools to review code written by %s; convert name to username if needed", developerName),
				},
			},
		},
	}, nil
}

```

--------------------------------------------------------------------------------
/services/gchat.go:
--------------------------------------------------------------------------------

```go
package services

import (
	"context"
	"fmt"
	"sync"

	"google.golang.org/api/chat/v1"
	"google.golang.org/api/option"
)

// NewGChatService creates and initializes a new Google Chat service
func NewGChatService() (*chat.Service, error) {
	ctx := context.Background()

	// Initialize Google Chat API service with default credentials and required scopes
	srv, err := chat.NewService(ctx, option.WithScopes(
		chat.ChatAdminSpacesScope,
		chat.ChatSpacesScope,
		chat.ChatAdminMembershipsScope,
		chat.ChatAdminMembershipsReadonlyScope,
		chat.ChatAppMembershipsScope,
		chat.ChatAppSpacesScope,
		chat.ChatAppSpacesCreateScope,
		chat.ChatMessagesScope,
		chat.ChatMessagesCreateScope,
	))
	if err != nil {
		return nil, fmt.Errorf("failed to create chat service: %v", err)
	}

	return srv, nil
}

var DefaultGChatService = sync.OnceValue(func() *chat.Service {
	srv, err := NewGChatService()
	if err != nil {
		panic(fmt.Sprintf("failed to create chat service: %v", err))
	}
	return srv
})

```

--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------

```yaml
name: Release Please and GoReleaser

on:
  push:
    branches:
      - main

permissions:
  contents: write
  pull-requests: write

jobs:

  release-please:
    runs-on: ubuntu-latest
    outputs:
      release_created: ${{ steps.release.outputs.release_created }}
      tag_name: ${{ steps.release.outputs.tag_name }}
    steps:
      - uses: googleapis/release-please-action@v4
        id: release
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          release-type: go

  goreleaser:
    needs: release-please
    if: ${{ needs.release-please.outputs.release_created }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Set up Go
        uses: actions/setup-go@v5
      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@v6
        with:
          distribution: goreleaser
          version: latest
          args: release --clean
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
# Start from the official Go image for building
FROM golang:1.23.2-alpine AS builder

# Set the working directory inside the container
WORKDIR /app

# Copy go.mod and go.sum files for dependency installation
COPY go.mod ./
COPY go.sum ./

# Download all necessary Go modules
RUN go mod download

# Copy the entire project into the container
COPY . .

# Build the Go application
RUN go build -o all-in-one-model-context-protocol main.go

# Start a new stage from scratch
FROM alpine:latest

# Install certificates to make HTTPS requests
RUN apk --no-cache add ca-certificates

# Set the working directory inside the container
WORKDIR /root/

# Copy the binary from the builder stage
COPY --from=builder /app/all-in-one-model-context-protocol .

# Copy the .env file if needed (uncomment the following line if needed)
# COPY --from=builder /app/.env .

# Set the entrypoint command
ENTRYPOINT ["./all-in-one-model-context-protocol", "-env", "/path/to/.env"]

```

--------------------------------------------------------------------------------
/services/deepseek.go:
--------------------------------------------------------------------------------

```go
package services

import (
	"os"
	"sync"

	"github.com/sashabaranov/go-openai"
)

var (
	deepseekClient *openai.Client
	deepseekOnce   sync.Once
)

// DefaultDeepseekClient returns a singleton instance of the Deepseek OpenAI client
func DefaultDeepseekClient() *openai.Client {
	deepseekOnce.Do(func() {
		useOllama := os.Getenv("USE_OLLAMA_DEEPSEEK") == "true"
		useOpenRouter := os.Getenv("USE_OPENROUTER") == "true"

		if useOllama {
			config := openai.DefaultConfig("not-needed")
			config.BaseURL = "http://localhost:11434/v1"
			deepseekClient = openai.NewClientWithConfig(config)
			return
		}

		if useOpenRouter {
			apiKey := os.Getenv("OPENROUTER_API_KEY")
			if apiKey == "" {
				panic("OPENROUTER_API_KEY environment variable is not set")
			}

			config := openai.DefaultConfig(apiKey)
			config.BaseURL = "https://openrouter.ai/api/v1"
			config.OrgID = "openrouter"
			deepseekClient = openai.NewClientWithConfig(config)
			return
		}

		apiKey := os.Getenv("DEEPSEEK_API_KEY")
		if apiKey == "" {
			panic("DEEPSEEK_API_KEY environment variable is not set")
		}

		baseURL := os.Getenv("DEEPSEEK_API_BASE")
		if baseURL == "" {
			baseURL = "https://api.deepseek.com/v1"
		}

		config := openai.DefaultConfig(apiKey)
		config.BaseURL = baseURL

		deepseekClient = openai.NewClientWithConfig(config)
	})
	return deepseekClient
}

```

--------------------------------------------------------------------------------
/util/handler.go:
--------------------------------------------------------------------------------

```go
package util

import (
	"context"
	"fmt"
	"runtime"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
)

// HandleError is a wrapper function that wraps the handler function with error handling
// Deprecated: Use ErrorGuard instead
func HandleError(handler server.ToolHandlerFunc) server.ToolHandlerFunc {
	return ErrorGuard(handler)
}

// LegacyHandlerAdapter adapts a legacy handler function to the new signature
type LegacyHandlerFunc func(arguments map[string]interface{}) (*mcp.CallToolResult, error)

// AdaptLegacyHandler adapts a legacy handler function to the new signature
func AdaptLegacyHandler(legacyHandler LegacyHandlerFunc) server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		return legacyHandler(request.Params.Arguments)
	}
}

func ErrorGuard(handler server.ToolHandlerFunc) server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (result *mcp.CallToolResult, err error) {
		defer func() {
			if r := recover(); r != nil {
				// Get stack trace
				buf := make([]byte, 4096)
				n := runtime.Stack(buf, true)
				stackTrace := string(buf[:n])

				result = mcp.NewToolResultError(fmt.Sprintf("Panic: %v\nStack trace:\n%s", r, stackTrace))
			}
		}()
		result, err = handler(ctx, request)
		if err != nil {
			return mcp.NewToolResultError(fmt.Sprintf("Error: %v", err)), nil
		}
		return result, nil
	}
}

```

--------------------------------------------------------------------------------
/tools/screenshot.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"fmt"
	"image/png"
	"os"
	"time"

	"github.com/athapong/aio-mcp/util"
	"github.com/kbinani/screenshot"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
)

// RegisterScreenshotTool registers the screenshot capturing tool with the MCP server
func RegisterScreenshotTool(s *server.MCPServer) {
	tool := mcp.NewTool("capture_screenshot",
		mcp.WithDescription("Capture a screenshot of the entire screen"),
	)
	s.AddTool(tool, util.ErrorGuard(util.AdaptLegacyHandler(screenshotHandler)))
}

func screenshotHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	n := screenshot.NumActiveDisplays()
	if n <= 0 {
		return mcp.NewToolResultError("No active displays found"), nil
	}

	// Capture the screenshot of the first display
	bounds := screenshot.GetDisplayBounds(0)
	img, err := screenshot.CaptureRect(bounds)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to capture screenshot: %v", err)), nil
	}

	// Save the screenshot to a file
	fileName := fmt.Sprintf("screenshot_%d.png", time.Now().Unix())
	file, err := os.Create(fileName)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to create file: %v", err)), nil
	}
	defer file.Close()

	err = png.Encode(file, img)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to encode image: %v", err)), nil
	}

	return mcp.NewToolResultText(fmt.Sprintf("Screenshot saved to %s", fileName)), nil
}

```

--------------------------------------------------------------------------------
/services/google.go:
--------------------------------------------------------------------------------

```go
package services

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"

	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"google.golang.org/api/calendar/v3"
	"google.golang.org/api/gmail/v1"
	"google.golang.org/api/youtube/v3"
)

// Retrieves a token from a local file.
func tokenFromFile(file string) (*oauth2.Token, error) {
	f, err := os.Open(file)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	tok := &oauth2.Token{}
	err = json.NewDecoder(f).Decode(tok)
	return tok, err
}

func ListGoogleScopes() []string {
	return []string{
		gmail.GmailLabelsScope,
		gmail.GmailModifyScope,
		gmail.MailGoogleComScope,
		gmail.GmailSettingsBasicScope,
		calendar.CalendarScope,
		calendar.CalendarEventsScope,
		youtube.YoutubeScope,
		youtube.YoutubeUploadScope,
		youtube.YoutubepartnerChannelAuditScope,
		youtube.YoutubepartnerScope,
		youtube.YoutubeReadonlyScope,
	}
}

func GoogleHttpClient(tokenFile string, credentialsFile string) *http.Client {
	
	tok, err := tokenFromFile(tokenFile)
	if err != nil {
		panic(fmt.Sprintf("failed to read token file: %v", err))
	}

	ctx := context.Background()
	b, err := os.ReadFile(credentialsFile)
	if err != nil {
		log.Fatalf("Unable to read client secret file: %v", err)
	}

	// If modifying these scopes, delete your previously saved token.json.
	config, err := google.ConfigFromJSON(b, ListGoogleScopes()...)
	if err != nil {
		log.Fatalf("Unable to parse client secret file to config: %v", err)
	}

	return config.Client(ctx, tok)
}
```

--------------------------------------------------------------------------------
/tools/fetch.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"context"
	"fmt"
	"io"

	htmltomarkdownnnn "github.com/JohannesKaufmann/html-to-markdown/v2"

	"github.com/athapong/aio-mcp/services"
	"github.com/athapong/aio-mcp/util"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
)

func RegisterFetchTool(s *server.MCPServer) {
	tool := mcp.NewTool("get_web_content",
		mcp.WithDescription("Fetches content from a given HTTP/HTTPS URL. This tool allows you to retrieve text content from web pages, APIs, or any accessible HTTP endpoints. Returns the raw content as text."),
		mcp.WithString("url",
			mcp.Required(),
			mcp.Description("The complete HTTP/HTTPS URL to fetch content from (e.g., https://example.com)"),
		),
	)

	s.AddTool(tool, util.ErrorGuard(fetchHandler))
}

func fetchHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	arguments := request.Params.Arguments
	url, ok := arguments["url"].(string)
	if !ok {
		return mcp.NewToolResultError("url must be a string"), nil
	}

	resp, err := services.DefaultHttpClient().Get(url)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to fetch URL: %s", err)), nil
	}

	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to read response body: %s", err)), nil
	}

	// Convert HTML content to Markdown
	mdContent, err := htmltomarkdownnnn.ConvertString(string(body))
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to convert HTML to Markdown: %v", err)), nil
	}

	return mcp.NewToolResultText(mdContent), nil
}

```

--------------------------------------------------------------------------------
/services/atlassian.go:
--------------------------------------------------------------------------------

```go
package services

import (
	"log"
	"os"
	"sync"

	"github.com/ctreminiom/go-atlassian/confluence/v2"
	"github.com/ctreminiom/go-atlassian/jira/agile"
	jira "github.com/ctreminiom/go-atlassian/jira/v2"
	"github.com/pkg/errors"
)

func loadAtlassianCredentials() (host, mail, token string) {
	host = os.Getenv("ATLASSIAN_HOST")
	mail = os.Getenv("ATLASSIAN_EMAIL")
	token = os.Getenv("ATLASSIAN_TOKEN")

	if host == "" || mail == "" || token == "" {
		log.Fatal("ATLASSIAN_HOST, ATLASSIAN_EMAIL, ATLASSIAN_TOKEN are required, please set it in MCP Config")
	}

	return host, mail, token
}

var ConfluenceClient = sync.OnceValue(func() *confluence.Client {
	host, mail, token := loadAtlassianCredentials()

	instance, err := confluence.New(nil, host)
	if err != nil {
		log.Fatal(errors.WithMessage(err, "failed to create confluence client"))
	}

	instance.Auth.SetBasicAuth(mail, token)

	return instance
})

var JiraClient = sync.OnceValue(func() *jira.Client {
	host, mail, token := loadAtlassianCredentials()

	if host == "" || mail == "" || token == "" {
		log.Fatal("ATLASSIAN_HOST, ATLASSIAN_EMAIL, ATLASSIAN_TOKEN are required")
	}

	instance, err := jira.New(nil, host)
	if err != nil {
		log.Fatal(errors.WithMessage(err, "failed to create jira client"))
	}

	instance.Auth.SetBasicAuth(mail, token)

	return instance
})

var AgileClient = sync.OnceValue(func() *agile.Client {
	host, mail, token := loadAtlassianCredentials()

	instance, err := agile.New(nil, host)
	if err != nil {
		log.Fatal(errors.WithMessage(err, "failed to create agile client"))
	}

	instance.Auth.SetBasicAuth(mail, token)

	return instance
})

```

--------------------------------------------------------------------------------
/docs/google_maps_tools.md:
--------------------------------------------------------------------------------

```markdown
# Google Maps Tools for MCP

This document describes the Google Maps integration tools available in the Model Context Protocol (MCP) server.

## Setup

1. Obtain a Google Maps API key from the [Google Cloud Console](https://console.cloud.google.com/google/maps-apis)
2. Set the API key as an environment variable:
   ```
   export GOOGLE_MAPS_API_KEY=your_api_key_here
   ```
3. Enable the Google Maps tools using the tool_manager:
   ```
   maps_location_search
   maps_geocoding
   maps_place_details
   ```

## Available Tools

### 1. Location Search (`maps_location_search`)

Search for places and locations using Google Maps.

**Parameters:**
- `query` (string, required): Location or place to search for
- `limit` (integer, optional): Maximum number of results to return (default: 5)

**Example:**
```json
{
  "query": "coffee shops in San Francisco"
}
```

### 2. Geocoding (`maps_geocoding`)

Convert addresses to geographic coordinates (geocoding) or coordinates to addresses (reverse geocoding).

**Parameters for Geocoding:**
- `address` (string): Address to geocode

**Parameters for Reverse Geocoding:**
- `lat` (float): Latitude coordinate
- `lng` (float): Longitude coordinate

**Example - Geocoding:**
```json
{
  "address": "1600 Amphitheatre Parkway, Mountain View, CA"
}
```

**Example - Reverse Geocoding:**
```json
{
  "lat": 37.4224764,
  "lng": -122.0842499
}
```

### 3. Place Details (`maps_place_details`)

Get detailed information about a specific place using its place_id.

**Parameters:**
- `place_id` (string, required): Google Maps place ID

**Example:**
```json
{
  "place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4"
}
```

## Error Handling

All tools will return proper error messages if:
- Required parameters are missing
- The Google Maps API key is not set
- There's an error from the Google Maps API service

```

--------------------------------------------------------------------------------
/tools/gchat.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/athapong/aio-mcp/services"
	"github.com/athapong/aio-mcp/util"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
	"google.golang.org/api/chat/v1"
)

func RegisterGChatTool(s *server.MCPServer) {
	// List spaces tool
	listSpacesTool := mcp.NewTool("gchat_list_spaces",
		mcp.WithDescription("List all available Google Chat spaces/rooms"),
	)

	// Send message tool
	sendMessageTool := mcp.NewTool("gchat_send_message",
		mcp.WithDescription("Send a message to a Google Chat space or direct message"),
		mcp.WithString("space_name", mcp.Required(), mcp.Description("Name of the space to send the message to")),
		mcp.WithString("message", mcp.Required(), mcp.Description("Text message to send")),
	)

	s.AddTool(listSpacesTool, util.ErrorGuard(gChatListSpacesHandler))
	s.AddTool(sendMessageTool, util.ErrorGuard(gChatSendMessageHandler))
}

func gChatListSpacesHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	// No arguments needed for this handler
	spaces, err := services.DefaultGChatService().Spaces.List().Do()
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to list spaces: %v", err)), nil
	}

	result := make([]map[string]interface{}, 0)
	for _, space := range spaces.Spaces {
		spaceInfo := map[string]interface{}{
			"name":        space.Name,
			"displayName": space.DisplayName,
			"type":        space.Type,
		}
		result = append(result, spaceInfo)
	}

	jsonResult, err := json.MarshalIndent(result, "", "  ")
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to marshal spaces: %v", err)), nil
	}

	return mcp.NewToolResultText(string(jsonResult)), nil
}

func gChatSendMessageHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	arguments := request.Params.Arguments
	spaceName := arguments["space_name"].(string)
	message := arguments["message"].(string)

	msg := &chat.Message{
		Text: message,
	}

	resp, err := services.DefaultGChatService().Spaces.Messages.Create(spaceName, msg).Do()
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to send message: %v", err)), nil
	}

	return mcp.NewToolResultText(fmt.Sprintf("Message sent successfully. Message ID: %s", resp.Name)), nil
}

```

--------------------------------------------------------------------------------
/resources/jira.go:
--------------------------------------------------------------------------------

```go
package resources

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/athapong/aio-mcp/services"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
)

func RegisterJiraResource(s *server.MCPServer) {
	template := mcp.NewResourceTemplate(
		"jira://{id}",
		"Jira Issue",
		mcp.WithTemplateDescription("Returns details of a Jira issue"),
		mcp.WithTemplateMIMEType("text/markdown"),
		mcp.WithTemplateAnnotations([]mcp.Role{mcp.RoleAssistant, mcp.RoleUser}, 0.5),
	)

	// Add resource with its handler
	s.AddResourceTemplate(template, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {

		requestURI := request.Params.URI
		issueKey := strings.TrimPrefix(requestURI, "jira://")
		client := services.JiraClient()

		ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
		defer cancel()

		issue, response, err := client.Issue.Get(ctx, issueKey, nil, []string{"transitions"})
		if err != nil {
			if response != nil {
				return nil, fmt.Errorf("failed to get issue: %s (endpoint: %s)", response.Bytes.String(), response.Endpoint)
			}
			return nil, fmt.Errorf("failed to get issue: %v", err)
		}

		// Build subtasks string if they exist
		var subtasks string
		if issue.Fields.Subtasks != nil {
			subtasks = "\nSubtasks:\n"
			for _, subTask := range issue.Fields.Subtasks {
				subtasks += fmt.Sprintf("- %s: %s\n", subTask.Key, subTask.Fields.Summary)
			}
		}

		// Build transitions string
		var transitions string
		for _, transition := range issue.Transitions {
			transitions += fmt.Sprintf("- %s (ID: %s)\n", transition.Name, transition.ID)
		}

		// Get reporter name, handling nil case
		reporterName := "Unassigned"
		if issue.Fields.Reporter != nil {
			reporterName = issue.Fields.Reporter.DisplayName
		}

		// Get assignee name, handling nil case
		assigneeName := "Unassigned"
		if issue.Fields.Assignee != nil {
			assigneeName = issue.Fields.Assignee.DisplayName
		}

		// Get priority name, handling nil case
		priorityName := "None"
		if issue.Fields.Priority != nil {
			priorityName = issue.Fields.Priority.Name
		}

		result := fmt.Sprintf(`
Key: %s
Summary: %s
Status: %s
Reporter: %s
Assignee: %s
Created: %s
Updated: %s
Priority: %s
Description:
%s
%s
Available Transitions:
%s`,
			issue.Key,
			issue.Fields.Summary,
			issue.Fields.Status.Name,
			reporterName,
			assigneeName,
			issue.Fields.Created,
			issue.Fields.Updated,
			priorityName,
			issue.Fields.Description,
			subtasks,
			transitions,
		)

		return []mcp.ResourceContents{
			mcp.TextResourceContents{
				URI:      "jira://" + issueKey,
				MIMEType: "text/markdown",
				Text:     string(result),
			},
		}, nil
	})
}

```

--------------------------------------------------------------------------------
/tools/gemini.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"context"
	"fmt"
	"os"
	"strings"
	"sync"

	"github.com/athapong/aio-mcp/util"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
	"google.golang.org/genai"
)

func RegisterGeminiTool(s *server.MCPServer) {
	searchTool := mcp.NewTool("ai_web_search",
		mcp.WithDescription("search the web by using Google AI Search. Best tool to update realtime information"),
		mcp.WithString("question", mcp.Required(), mcp.Description("The question to ask. Should be a question")),
		// context
		mcp.WithString("context", mcp.Required(), mcp.Description("Context/purpose of the question, helps Gemini to understand the question better")),
	)

	s.AddTool(searchTool, util.ErrorGuard(aiWebSearchHandler))
}

var genAiClient = sync.OnceValue(func() *genai.Client {
	apiKey := os.Getenv("GOOGLE_AI_API_KEY")
	if apiKey == "" {
		panic("GOOGLE_AI_API_KEY environment variable must be set")
	}

	cfg := &genai.ClientConfig{
		APIKey:  apiKey,
		Backend: genai.BackendGoogleAI,
	}

	client, err := genai.NewClient(context.Background(), cfg)
	if err != nil {
		panic(fmt.Sprintf("failed to create Gemini client: %s", err))
	}

	return client
})

func aiWebSearchHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	arguments := request.Params.Arguments
	question, ok := arguments["question"].(string)
	if !ok {
		return mcp.NewToolResultError("question must be a string"), nil
	}

	systemInstruction := "You are a search engine. You will search the web for the answer to the question. You will then provide the answer to the question. Always try to search the web for the answer first before providing the answer. writing style: short, concise, direct, and to the point."

	questionContext, ok := arguments["context"].(string)
	if !ok {
		systemInstruction += "\n\nContext: " + questionContext
	}

	resp, err := genAiClient().Models.GenerateContent(ctx,
		"gemini-2.0-pro-exp-02-05", //gemini-2.0-flash
		genai.PartSlice{
			genai.Text(question),
		},
		&genai.GenerateContentConfig{
			SystemInstruction: genai.Text(systemInstruction).ToContent(),
			Tools: []*genai.Tool{
				{GoogleSearch: &genai.GoogleSearch{}},
			},
		},
	)

	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to generate content: %s", err)), nil
	}

	if len(resp.Candidates) == 0 {
		return mcp.NewToolResultError("no response from Gemini"), nil
	}

	candidate := resp.Candidates[0]

	var textBuilder strings.Builder
	for _, part := range candidate.Content.Parts {
		textBuilder.WriteString(part.Text)
	}

	if candidate.CitationMetadata != nil {
		for _, citation := range candidate.CitationMetadata.Citations {
			textBuilder.WriteString("\n\nSource: ")
			textBuilder.WriteString(citation.URI)
		}
	}

	if candidate.GroundingMetadata != nil {
		textBuilder.WriteString("\n\nSources: ")
		for _, chunk := range candidate.GroundingMetadata.GroundingChunks {
			if chunk.RetrievedContext != nil {
				textBuilder.WriteString("\n")
				textBuilder.WriteString(chunk.RetrievedContext.Text)
				textBuilder.WriteString(": ")
				textBuilder.WriteString(chunk.RetrievedContext.URI)
			}

			if chunk.Web != nil {
				textBuilder.WriteString("\n")
				textBuilder.WriteString(chunk.Web.Title)
				textBuilder.WriteString(": ")
				textBuilder.WriteString(chunk.Web.URI)
			}
		}
	}

	return mcp.NewToolResultText(textBuilder.String()), nil
}

```

--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------

```go
package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"os"
	"os/signal"
	"slices"
	"strings"
	"syscall"
	"time"

	"github.com/athapong/aio-mcp/prompts"
	"github.com/athapong/aio-mcp/resources"
	"github.com/athapong/aio-mcp/tools"
	"github.com/joho/godotenv"
	"github.com/mark3labs/mcp-go/server"
)

func main() {
	envFile := flag.String("env", ".env", "Path to environment file")
	enableSSE := flag.Bool("sse", false, "Enable SSE server")
	sseAddr := flag.String("sse-addr", ":8080", "Address for SSE server to listen on")
	sseBasePath := flag.String("sse-base-path", "/mcp", "Base path for SSE endpoints")
	flag.Parse()

	if err := godotenv.Load(*envFile); err != nil {
		log.Printf("Warning: Error loading env file %s: %v\n", *envFile, err)
	}
	// Create MCP server
	mcpServer := server.NewMCPServer(
		"aio-mcp",
		"1.0.0",
		server.WithLogging(),
		server.WithPromptCapabilities(true),
		server.WithResourceCapabilities(true, true),
	)

	tools.RegisterToolManagerTool(mcpServer)

	enableTools := strings.Split(os.Getenv("ENABLE_TOOLS"), ",")
	allToolsEnabled := len(enableTools) == 1 && enableTools[0] == ""

	isEnabled := func(toolName string) bool {
		return allToolsEnabled || slices.Contains(enableTools, toolName)
	}

	if isEnabled("gemini") {
		tools.RegisterGeminiTool(mcpServer)
	}

	if isEnabled("deepseek") {
		tools.RegisterDeepseekTool(mcpServer)
	}

	if isEnabled("fetch") {
		tools.RegisterFetchTool(mcpServer)
	}

	if isEnabled("brave_search") {
		tools.RegisterWebSearchTool(mcpServer)
	}

	if isEnabled("confluence") {
		tools.RegisterConfluenceTool(mcpServer)
	}

	if isEnabled("youtube") {
		tools.RegisterYouTubeTool(mcpServer)
	}

	if isEnabled("jira") {
		tools.RegisterJiraTool(mcpServer)
		resources.RegisterJiraResource(mcpServer)
	}

	if isEnabled("gitlab") {
		tools.RegisterGitLabTool(mcpServer)
	}

	if isEnabled("script") {
		tools.RegisterScriptTool(mcpServer)
	}

	if isEnabled("rag") {
		tools.RegisterRagTools(mcpServer)
	}

	if isEnabled("gmail") {
		tools.RegisterGmailTools(mcpServer)
	}

	if isEnabled("calendar") {
		tools.RegisterCalendarTools(mcpServer)
	}

	if isEnabled("youtube_channel") {
		tools.RegisterYouTubeChannelTools(mcpServer)
	}

	if isEnabled("sequential_thinking") {
		tools.RegisterSequentialThinkingTool(mcpServer)
		tools.RegisterSequentialThinkingHistoryTool(mcpServer)
	}

	if isEnabled("gchat") {
		tools.RegisterGChatTool(mcpServer)
	}

	tools.RegisterScreenshotTool(mcpServer)

	prompts.RegisterCodeTools(mcpServer)

	if isEnabled("google_maps") {
		tools.RegisterGoogleMapTools(mcpServer)
	}

	// Check if SSE server should be enabled
	if *enableSSE || os.Getenv("ENABLE_SSE") == "true" {
		// Create SSE server
		sseServer := server.NewSSEServer(
			mcpServer,
			server.WithBasePath(*sseBasePath),
			server.WithKeepAlive(true),
		)

		// Start SSE server in a goroutine
		go func() {
			log.Printf("Starting SSE server on %s with base path %s", *sseAddr, *sseBasePath)
			if err := sseServer.Start(*sseAddr); err != nil {
				log.Fatalf("Failed to start SSE server: %v", err)
			}
		}()

		// Set up signal handling for graceful shutdown
		sigCh := make(chan os.Signal, 1)
		signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)

		// Wait for termination signal
		sig := <-sigCh
		log.Printf("Received signal %v, shutting down...", sig)

		// Gracefully shutdown the SSE server
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		if err := sseServer.Shutdown(ctx); err != nil {
			log.Printf("Error during SSE server shutdown: %v", err)
		}
		log.Println("SSE server shutdown complete")
	} else {
		// Use stdio server as before
		if err := server.ServeStdio(mcpServer); err != nil {
			panic(fmt.Sprintf("Server error: %v", err))
		}
	}
}

```

--------------------------------------------------------------------------------
/tools/script.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"bytes"
	"context"
	"fmt"
	"os"
	"os/exec"
	"os/user"
	"runtime"
	"strings"
	"time"

	"github.com/athapong/aio-mcp/util"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
)

// RegisterScriptTool registers the script execution tool with the MCP server
func RegisterScriptTool(s *server.MCPServer) {
	currentUser, err := user.Current()
	if err != nil {
		currentUser = &user.User{HomeDir: "unknown"}
	}

	tool := mcp.NewTool("execute_comand_line_script",
		mcp.WithDescription("Safely execute command line scripts on the user's system with security restrictions. Features sandboxed execution, timeout protection, and output capture. Supports cross-platform scripting with automatic environment detection."),
		mcp.WithString("content", mcp.Required(), mcp.Description("Full script content to execute. Auto-detected environment: "+runtime.GOOS+" OS, current user: "+currentUser.Username+". Scripts are validated for basic security constraints")),
		mcp.WithString("interpreter", mcp.DefaultString("/bin/sh"), mcp.Description("Path to interpreter binary (e.g. /bin/sh, /bin/bash, /usr/bin/python, cmd.exe). Validated against allowed list for security")),
		mcp.WithString("working_dir", mcp.DefaultString(currentUser.HomeDir), mcp.Description("Execution directory path (default: user home). Validated to prevent unauthorized access to system locations")),
	)

	s.AddTool(tool, util.ErrorGuard(util.AdaptLegacyHandler(scriptExecuteHandler)))
}

func scriptExecuteHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	// Get script content
	contentElement, ok := arguments["content"]
	if !ok {
		return mcp.NewToolResultError("content must be provided"), nil
	}
	content, ok := contentElement.(string)
	if !ok {
		return mcp.NewToolResultError("content must be a string"), nil
	}

	// Get interpreter
	interpreter := "/bin/sh"
	if interpreterElement, ok := arguments["interpreter"]; ok {
		interpreter = interpreterElement.(string)
	}

	// Get working directory
	workingDir := ""
	if workingDirElement, ok := arguments["working_dir"]; ok {
		workingDir = workingDirElement.(string)
	}

	// Create temporary script file
	tmpFile, err := os.CreateTemp("", "script-*.sh")
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to create temporary file: %v", err)), nil
	}
	defer os.Remove(tmpFile.Name()) // Clean up

	// Write content to temporary file
	if _, err := tmpFile.WriteString(content); err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to write to temporary file: %v", err)), nil
	}
	if err := tmpFile.Close(); err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to close temporary file: %v", err)), nil
	}

	// Make the script executable
	if err := os.Chmod(tmpFile.Name(), 0700); err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to make script executable: %v", err)), nil
	}

	// Create command with context for timeout
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	cmd := exec.CommandContext(ctx, interpreter, tmpFile.Name())

	// Set working directory if specified
	if workingDir != "" {
		cmd.Dir = workingDir
	}

	// Inject environment variables from the OS
	cmd.Env = os.Environ()

	// Create buffers for stdout and stderr
	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	// Execute script
	err = cmd.Run()

	// Check if the error was due to timeout
	if ctx.Err() == context.DeadlineExceeded {
		return mcp.NewToolResultError("Script execution timed out after 30 seconds"), nil
	}

	// Build result
	var result strings.Builder
	if stdout.Len() > 0 {
		result.WriteString("Output:\n")
		result.WriteString(stdout.String())
		result.WriteString("\n")
	}

	if stderr.Len() > 0 {
		result.WriteString("Errors:\n")
		result.WriteString(stderr.String())
		result.WriteString("\n")
	}

	if err != nil {
		result.WriteString(fmt.Sprintf("\nExecution error: %v", err))
	}

	return mcp.NewToolResultText(result.String()), nil
}

```

--------------------------------------------------------------------------------
/tools/search.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"

	htmltomarkdownnnn "github.com/JohannesKaufmann/html-to-markdown/v2"
	"github.com/athapong/aio-mcp/services"
	"github.com/athapong/aio-mcp/util"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
	"github.com/tidwall/gjson"
)

func RegisterWebSearchTool(s *server.MCPServer) {
	tool := mcp.NewTool("web_search",
		mcp.WithDescription("Search the web using Brave Search API"),
		mcp.WithString("query", mcp.Required(), mcp.Description("Query to search for (max 400 chars, 50 words)")),
		mcp.WithNumber("count", mcp.DefaultNumber(5), mcp.Description("Number of results (1-20, default 5)")),
		mcp.WithString("country", mcp.DefaultString("ALL"), mcp.Description("Country code")),
	)

	s.AddTool(tool, util.ErrorGuard(util.AdaptLegacyHandler(webSearchHandler)))
}

type SearchResult struct {
	Title       string `json:"title"`
	URL         string `json:"url"`
	Description string `json:"description"`
	Type        string `json:"type"`
	Age         string `json:"age"`
}

func webSearchHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	query, ok := arguments["query"].(string)
	if !ok {
		return mcp.NewToolResultError("query must be a string"), nil
	}

	count := 10
	if countArg, ok := arguments["count"].(float64); ok {
		count = int(countArg)
		if count < 1 {
			count = 1
		} else if count > 20 {
			count = 20
		}
	}

	country := "ALL"
	if countryArg, ok := arguments["country"].(string); ok {
		country = countryArg
	}

	apiKey := os.Getenv("BRAVE_API_KEY")
	if apiKey == "" {
		return mcp.NewToolResultError("BRAVE_API_KEY environment variable is required"), nil
	}

	baseURL := "https://api.search.brave.com/res/v1/web/search"
	params := url.Values{}
	params.Add("q", query)
	params.Add("count", fmt.Sprintf("%d", count))
	params.Add("country", country)

	req, err := http.NewRequest("GET", baseURL+"?"+params.Encode(), nil)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to create request: %v", err)), nil
	}

	req.Header.Set("Accept", "application/json")
	req.Header.Set("X-Subscription-Token", apiKey)

	resp, err := services.DefaultHttpClient().Do(req)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to perform search: %v", err)), nil
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to read response: %v", err)), nil
	}

	if resp.StatusCode != http.StatusOK {
		return mcp.NewToolResultError(fmt.Sprintf("API request failed: %s", string(body))), nil
	}

	var results []*SearchResult

	gbody := gjson.ParseBytes(body)

	videoResults := gbody.Get("videos.results")
	for _, video := range videoResults.Array() {

		mdContent, err := htmltomarkdownnnn.ConvertString(video.Get("description").String())
		if err != nil {
			return nil, fmt.Errorf("failed to convert HTML to Markdown: %v", err)
		}

		results = append(results, &SearchResult{
			Title:       video.Get("title").String(),
			URL:         video.Get("url").String(),
			Description: mdContent,
			Type:        "video",
			Age:         video.Get("age").String(),
		})
	}

	webResults := gbody.Get("web.results")
	for _, web := range webResults.Array() {

		mdContent, err := htmltomarkdownnnn.ConvertString(web.Get("description").String())
		if err != nil {
			return nil, fmt.Errorf("failed to convert HTML to Markdown: %v", err)
		}

		results = append(results, &SearchResult{
			Title:       web.Get("title").String(),
			URL:         web.Get("url").String(),
			Description: mdContent,
			Type:        "web",
			Age:         web.Get("age").String(),
		})
	}

	if len(results) == 0 {
		return mcp.NewToolResultError("No results found, pls try again with a different query"), nil
	}

	responseText := ""
	for _, result := range results {
		responseText += fmt.Sprintf("Title: %s\nURL: %s\nDescription: %s\nType: %s\nAge: %s\n\n",
			result.Title, result.URL, result.Description, result.Type, result.Age)
	}

	return mcp.NewToolResultText(responseText), nil
}

```

--------------------------------------------------------------------------------
/tools/deepseek.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"os"

	"github.com/athapong/aio-mcp/services"
	"github.com/athapong/aio-mcp/util"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
	"github.com/sashabaranov/go-openai"
)

type OllamaRequest struct {
	Model    string    `json:"model"`
	Messages []Message `json:"messages"`
}

type Message struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}

type OllamaResponse struct {
	Message Message `json:"message"`
}

func RegisterDeepseekTool(s *server.MCPServer) {
	reasoningTool := mcp.NewTool("deepseek_reasoning",
		mcp.WithDescription("advanced reasoning engine using Deepseek's AI capabilities for multi-step problem solving, critical analysis, and strategic decision support"),
		mcp.WithString("question", mcp.Required(), mcp.Description("The structured query or problem statement requiring deep analysis and reasoning")),
		mcp.WithString("context", mcp.Required(), mcp.Description("Defines the operational context and purpose of the query within the MCP ecosystem")),
		mcp.WithString("knowledge", mcp.Description("Provides relevant chat history, knowledge base entries, and structured data context for MCP-aware reasoning")),
	)

	s.AddTool(reasoningTool, util.ErrorGuard(deepseekReasoningHandler))
}

func deepseekReasoningHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	arguments := request.Params.Arguments
	systemPrompt, question, _ := buildMessages(arguments)

	// Check if we should use Ollama
	if useOllama := os.Getenv("USE_OLLAMA_DEEPSEEK"); useOllama == "true" {
		ollamaMessages := []Message{
			{
				Role:    "system",
				Content: systemPrompt,
			},
			{
				Role:    "user",
				Content: question,
			},
		}

		ollamaReq := OllamaRequest{
			Model:    "deepseek-r1:1.5b",
			Messages: ollamaMessages,
		}

		return callOllamaDeepseek(ollamaReq)
	}

	// Using Deepseek API
	messages := []openai.ChatCompletionMessage{
		{
			Role:    openai.ChatMessageRoleSystem,
			Content: systemPrompt,
		},
		{
			Role:    openai.ChatMessageRoleUser,
			Content: question,
		},
	}

	return callDeepseekAPI(messages)
}

func buildMessages(arguments map[string]interface{}) (string, string, string) {
	question, _ := arguments["question"].(string)
	contextArgument, _ := arguments["context"].(string)
	chatContext, _ := arguments["chat_context"].(string)

	systemPrompt := "Context:\n" + contextArgument
	if chatContext != "" {
		systemPrompt += "\n\nAdditional Context:\n" + chatContext
	}

	return systemPrompt, question, chatContext
}

func callDeepseekAPI(messages []openai.ChatCompletionMessage) (*mcp.CallToolResult, error) {
	ctx := context.Background()
	client := services.DefaultDeepseekClient()
	if client == nil {
		return mcp.NewToolResultError("Deepseek client not properly initialized"), nil
	}

	resp, err := client.CreateChatCompletion(
		ctx,
		openai.ChatCompletionRequest{
			Model:       "deepseek-reasoner",
			Messages:    messages,
			Temperature: 0.7,
		},
	)

	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to generate content: %s", err)), nil
	}

	if len(resp.Choices) == 0 {
		return mcp.NewToolResultError("no response from Deepseek"), nil
	}

	return mcp.NewToolResultText(resp.Choices[0].Message.Content), nil
}

func callOllamaDeepseek(req OllamaRequest) (*mcp.CallToolResult, error) {
	jsonData, err := json.Marshal(req)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to marshal Ollama request: %s", err)), nil
	}

	ollamaURL := os.Getenv("OLLAMA_URL")
	if ollamaURL == "" {
		ollamaURL = "http://localhost:11434"
	}

	resp, err := http.Post(ollamaURL+"/api/chat", "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to call Ollama: %s", err)), nil
	}
	defer resp.Body.Close()

	var ollamaResp OllamaResponse
	if err := json.NewDecoder(resp.Body).Decode(&ollamaResp); err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to decode Ollama response: %s", err)), nil
	}

	return mcp.NewToolResultText(ollamaResp.Message.Content), nil
}

```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml

startCommand:
  type: stdio
  configSchema:
    # JSON Schema defining the configuration options for the MCP.
    type: object
    required:
    properties:
      enableTools:
        type: string
        description: Comma separated list of tools group to enable. Leave empty to
          enable all tools.
      qdrantHost:
        type: string
        description: Qdrant host URL
      atlassianHost:
        type: string
        description: Atlassian host URL
      atlassianEmail:
        type: string
        description: Email for Atlassian
      gitlabHost:
        type: string
        description: GitLab host URL
      gitlabToken:
        type: string
        description: Token for GitLab access
      braveApiKey:
        type: string
        description: API key for Brave
      atlassianToken:
        type: string
        description: Token for Atlassian access
      googleAiApiKey:
        type: string
        description: API key for Google AI
      proxyUrl:
        type: string
        description: Proxy URL if required
      openaiApiKey:
        type: string
        description: API key for OpenAI
      qdrantPort:
        type: string
        description: Port for Qdrant service
      googleTokenFile:
        type: string
        description: Path to Google token file
      googleCredentialsFile:
        type: string
        description: Path to Google credentials file
      qdrantApiKey:
        type: string
        description: API key for Qdrant
      openaiBaseUrl:
      type: string
      description: Base URL for OpenAI API
    openaiEmbeddingModel:
      type: string
      description: Model name for OpenAI embeddings
    googleMapsApiKey:
      type: string
      description: API key for Google Maps
    deepseekApiKey:
      type: string
      description: API key for Deepseek
    useOllamaDeepseek:
      type: string
      description: Flag to use Ollama Deepseek
    useOpenrouter:
      type: string
      description: Flag to use OpenRouter
    openrouterApiKey:
      type: string
      description: API key for OpenRouter
    enableSse:
      type: string
      description: Flag to enable SSE server (true/false)
    sseAddr:
      type: string
      description: Address for SSE server to listen on (e.g., :8080)
    sseBasePath:
      type: string
      description: Base path for SSE endpoints (e.g., /mcp)
  commandFunction:
    # A function that produces the CLI command to start the MCP on stdio.
    |-
    (config) => {
      const args = ['-env', '/path/to/.env'];

      // Add SSE-related args if SSE is enabled
      if (config.enableSse === 'true') {
        args.push('-sse');
        if (config.sseAddr) {
          args.push('-sse-addr', config.sseAddr);
        }
        if (config.sseBasePath) {
          args.push('-sse-base-path', config.sseBasePath);
        }
      }

      return {
        command: './all-in-one-model-context-protocol',
        args: args,
        env: {
          ENABLE_TOOLS: config.enableTools,
          QDRANT_HOST: config.qdrantHost,
          QDRANT_PORT: config.qdrantPort,
          QDRANT_API_KEY: config.qdrantApiKey,
          ATLASSIAN_HOST: config.atlassianHost,
          ATLASSIAN_EMAIL: config.atlassianEmail,
          ATLASSIAN_TOKEN: config.atlassianToken,
          GITLAB_HOST: config.gitlabHost,
          GITLAB_TOKEN: config.gitlabToken,
          BRAVE_API_KEY: config.braveApiKey,
          GOOGLE_AI_API_KEY: config.googleAiApiKey,
          PROXY_URL: config.proxyUrl,
          OPENAI_API_KEY: config.openaiApiKey,
          OPENAI_BASE_URL: config.openaiBaseUrl,
          OPENAI_EMBEDDING_MODEL: config.openaiEmbeddingModel,
          GOOGLE_TOKEN_FILE: config.googleTokenFile,
          GOOGLE_CREDENTIALS_FILE: config.googleCredentialsFile,
          GOOGLE_MAPS_API_KEY: config.googleMapsApiKey,
          DEEPSEEK_API_KEY: config.deepseekApiKey,
          USE_OLLAMA_DEEPSEEK: config.useOllamaDeepseek,
          USE_OPENROUTER: config.useOpenrouter,
          OPENROUTER_API_KEY: config.openrouterApiKey,
          ENABLE_SSE: config.enableSse,
          SSE_ADDR: config.sseAddr,
          SSE_BASE_PATH: config.sseBasePath
        }
      };
    }


```

--------------------------------------------------------------------------------
/scripts/google-token/main.go:
--------------------------------------------------------------------------------

```go
package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"

	"github.com/athapong/aio-mcp/services"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"google.golang.org/api/gmail/v1"
	"google.golang.org/api/option"
)

func main() {
	// Define command line flags
	credentialsPath := flag.String("credentials", "", "Path to Google credentials JSON file")
	tokenPath := flag.String("token", "", "Path to save/load Google token JSON file")
	flag.Parse()

	// Validate required flags
	if *credentialsPath == "" || *tokenPath == "" {
		flag.PrintDefaults()
		log.Fatal("Both -credentials and -token flags are required")
	}

	// try to delete the token file if it exists
	if _, err := os.Stat(*tokenPath); err == nil {
		os.Remove(*tokenPath)
	}

	ctx := context.Background()
	b, err := os.ReadFile(*credentialsPath)
	if err != nil {
		log.Fatalf("Unable to read client secret file: %v", err)
	}

	config, err := google.ConfigFromJSON(b, services.ListGoogleScopes()...)
	if err != nil {
		log.Fatalf("Unable to parse client secret file to config: %v", err)
	}

	client := getClient(config, *tokenPath)

	srv, err := gmail.NewService(ctx, option.WithHTTPClient(client))
	if err != nil {
		log.Fatalf("Unable to retrieve Gmail client: %v", err)
	}

	// Test the connection
	user := "me"
	_, err = srv.Users.Labels.List(user).Do()
	if err != nil {
		log.Fatalf("Unable to retrieve labels: %v", err)
	}

	tokenFileAbsPath, err := filepath.Abs(*tokenPath)
	if err != nil {
		log.Fatalf("Unable to get absolute path of token.json: %v", err)
	}

	fmt.Println("It works! Token file is saved at:")
	fmt.Println(tokenFileAbsPath)
}

// Update getClient to accept tokenPath parameter
func getClient(config *oauth2.Config, tokenPath string) *http.Client {
	tok, err := tokenFromFile(tokenPath)
	if err != nil {
		tok = getTokenFromWeb(config)
		saveToken(tokenPath, tok)
	}
	return config.Client(context.Background(), tok)
}

// Request a token from the web, then returns the retrieved token.
func getTokenFromWeb(config *oauth2.Config) *oauth2.Token {
	// Create a channel to receive the authorization code
	codeChan := make(chan string)

	// Start a local HTTP server to handle the redirect
	http.HandleFunc("/oauth2/callback", func(w http.ResponseWriter, r *http.Request) {
		err := r.ParseForm()
		if err != nil {
			http.Error(w, "Failed to parse form", http.StatusBadRequest)
			return
		}

		code := r.Form.Get("code")
		if code == "" {
			http.Error(w, "Authorization code not found", http.StatusBadRequest)
			return
		}

		// Send the code to the channel
		codeChan <- code

		// Inform the user that the process is complete
		fmt.Fprintln(w, "<h1>Authentication successful!</h1><p>You can close this window.</p>")
	})

	// Determine the port for the local server
	port := "8080"
	redirectURL := fmt.Sprintf("http://localhost:%s/oauth2/callback", port)

	// Update the configuration with the redirect URL
	config.RedirectURL = redirectURL

	authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)

	// Open the URL in the default browser
	err := openBrowser(authURL)
	if err != nil {
		log.Printf("Could not open browser automatically: %v", err)
		fmt.Printf("Please open the following URL in your browser:\n\n%v\n\n", authURL)
	} else {
		fmt.Println("Opening your browser to authenticate...")
	}

	// Start the HTTP server in a goroutine
	server := &http.Server{Addr: ":" + port}
	go func() {
		if err := server.ListenAndServe(); err != http.ErrServerClosed {
			log.Fatalf("Failed to start server: %v", err)
		}
	}()

	// Wait for the authorization code
	code := <-codeChan

	// Shut down the server
	if err := server.Shutdown(context.Background()); err != nil {
		log.Printf("Failed to shut down server: %v", err)
	}

	tok, err := config.Exchange(context.TODO(), code)
	if err != nil {
		log.Fatalf("Unable to retrieve token from web: %v", err)
	}
	return tok
}

// Retrieves a token from a local file.
func tokenFromFile(file string) (*oauth2.Token, error) {
	f, err := os.Open(file)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	tok := &oauth2.Token{}
	err = json.NewDecoder(f).Decode(tok)
	return tok, err
}

// Saves a token to a file path.
func saveToken(path string, token *oauth2.Token) {
	fmt.Printf("Saving credential file to: %s\n", path)
	f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
	if err != nil {
		log.Fatalf("Unable to cache oauth token: %v", err)
	}
	defer f.Close()
	json.NewEncoder(f).Encode(token)
}

func openBrowser(url string) error {
	var err error

	switch runtime.GOOS {
	case "linux":
		err = exec.Command("xdg-open", url).Start()
	case "windows":
		err = exec.Command("cmd", "/c", "start", url).Start()
	case "darwin":
		err = exec.Command("open", url).Start()
	default:
		err = fmt.Errorf("unsupported platform")
	}

	return err
}

```

--------------------------------------------------------------------------------
/pkg/adf/convert.go:
--------------------------------------------------------------------------------

```go
package adf

import (
	"fmt"
	"strings"
)

// Convert converts an ADF node to Markdown
func Convert(node *Node) string {
	if node == nil {
		return ""
	}

	var result strings.Builder
	convertNode(node, &result, 0)
	return result.String()
}

func convertNode(node *Node, result *strings.Builder, depth int) {
	switch node.Type {
	case "doc":
		convertDoc(node, result, depth)
	case "paragraph":
		convertParagraph(node, result, depth)
	case "heading":
		convertHeading(node, result, depth)
	case "text":
		convertText(node, result)
	case "hardBreak":
		result.WriteString("\n")
	case "bulletList":
		convertBulletList(node, result, depth)
	case "orderedList":
		convertOrderedList(node, result, depth)
	case "listItem":
		convertListItem(node, result, depth)
	case "codeBlock":
		convertCodeBlock(node, result)
	case "blockquote":
		convertBlockquote(node, result, depth)
	case "rule":
		result.WriteString("---\n")
	case "table":
		convertTable(node, result)
	default:
		convertChildren(node, result, depth)
	}
}

func convertDoc(node *Node, result *strings.Builder, depth int) {
	convertChildren(node, result, depth)
}

func convertParagraph(node *Node, result *strings.Builder, depth int) {
	if depth > 0 {
		result.WriteString(strings.Repeat("  ", depth))
	}
	convertChildren(node, result, depth)
	result.WriteString("\n\n")
}

func convertHeading(node *Node, result *strings.Builder, depth int) {
	level := 1
	if l, ok := node.Attrs["level"].(float64); ok {
		level = int(l)
	}
	result.WriteString(strings.Repeat("#", level) + " ")
	convertChildren(node, result, depth)
	result.WriteString("\n\n")
}

func convertText(node *Node, result *strings.Builder) {
	text := node.Text
	if node.Marks != nil {
		for _, mark := range node.Marks {
			switch mark.Type {
			case "strong":
				text = "**" + text + "**"
			case "em":
				text = "_" + text + "_"
			case "code":
				text = "`" + text + "`"
			case "strike":
				text = "~~" + text + "~~"
			case "link":
				if href, ok := mark.Attrs["href"].(string); ok {
					text = fmt.Sprintf("[%s](%s)", text, href)
				}
			}
		}
	}
	result.WriteString(text)
}

func convertBulletList(node *Node, result *strings.Builder, depth int) {
	for _, child := range node.Content {
		result.WriteString(strings.Repeat("  ", depth) + "* ")
		convertChildren(child, result, depth+1)
		result.WriteString("\n")
	}
	result.WriteString("\n")
}

func convertOrderedList(node *Node, result *strings.Builder, depth int) {
	for i, child := range node.Content {
		result.WriteString(fmt.Sprintf("%s%d. ", strings.Repeat("  ", depth), i+1))
		convertChildren(child, result, depth+1)
		result.WriteString("\n")
	}
	result.WriteString("\n")
}

func convertListItem(node *Node, result *strings.Builder, depth int) {
	convertChildren(node, result, depth)
}

func convertCodeBlock(node *Node, result *strings.Builder) {
	language := ""
	if lang, ok := node.Attrs["language"].(string); ok {
		language = lang
	}
	result.WriteString("```" + language + "\n")
	convertChildren(node, result, 0)
	result.WriteString("```\n\n")
}

func convertBlockquote(node *Node, result *strings.Builder, depth int) {
	for _, child := range node.Content {
		result.WriteString("> ")
		convertChildren(child, result, depth+1)
	}
	result.WriteString("\n")
}

func convertTable(node *Node, result *strings.Builder) {
	if len(node.Content) == 0 {
		return
	}

	// Extract headers and calculate column widths
	columnWidths := make([]int, 0)
	rows := make([][]string, 0)

	// Process header row
	if len(node.Content) > 0 && len(node.Content[0].Content) > 0 {
		headerRow := make([]string, 0)
		for _, cell := range node.Content[0].Content {
			var cellContent strings.Builder
			convertChildren(cell, &cellContent, 0)
			content := strings.TrimSpace(cellContent.String())
			headerRow = append(headerRow, content)
			columnWidths = append(columnWidths, len(content))
		}
		rows = append(rows, headerRow)
	}

	// Process data rows and update column widths
	for i := 1; i < len(node.Content); i++ {
		row := make([]string, 0)
		for j, cell := range node.Content[i].Content {
			var cellContent strings.Builder
			convertChildren(cell, &cellContent, 0)
			content := strings.TrimSpace(cellContent.String())
			row = append(row, content)
			if j < len(columnWidths) && len(content) > columnWidths[j] {
				columnWidths[j] = len(content)
			}
		}
		rows = append(rows, row)
	}

	// Write table
	for i, row := range rows {
		result.WriteString("|")
		for j, cell := range row {
			if j < len(columnWidths) {
				padding := columnWidths[j] - len(cell)
				result.WriteString(" " + cell + strings.Repeat(" ", padding) + " |")
			}
		}
		result.WriteString("\n")

		// Write separator after header
		if i == 0 {
			result.WriteString("|")
			for _, width := range columnWidths {
				result.WriteString(strings.Repeat("-", width+2) + "|")
			}
			result.WriteString("\n")
		}
	}
	result.WriteString("\n")
}

func convertChildren(node *Node, result *strings.Builder, depth int) {
	if node.Content != nil {
		for _, child := range node.Content {
			convertNode(child, result, depth)
		}
	}
}

```

--------------------------------------------------------------------------------
/tools/tool_manager.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"context"
	"fmt"
	"os"
	"strings"

	"github.com/athapong/aio-mcp/services"
	"github.com/athapong/aio-mcp/util"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
	"github.com/sashabaranov/go-openai"
)

func RegisterToolManagerTool(s *server.MCPServer) {
	tool := mcp.NewTool("tool_manager",
		mcp.WithDescription("Manage MCP tools - enable or disable tools"),
		mcp.WithString("action", mcp.Required(), mcp.Description("Action to perform: list, enable, disable")),
		mcp.WithString("tool_name", mcp.Description("Tool name to enable/disable")),
	)

	s.AddTool(tool, util.ErrorGuard(util.AdaptLegacyHandler(toolManagerHandler)))

	planTool := mcp.NewTool("tool_use_plan",
		mcp.WithDescription("Create a plan using available tools to solve the request"),
		mcp.WithString("request", mcp.Required(), mcp.Description("Request to plan for")),
		mcp.WithString("context", mcp.Required(), mcp.Description("Context related to the request")),
	)
	s.AddTool(planTool, util.ErrorGuard(util.AdaptLegacyHandler(toolUsePlanHandler)))
}

func toolManagerHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	action, ok := arguments["action"].(string)
	if !ok {
		return mcp.NewToolResultError("action must be a string"), nil
	}

	enableTools := os.Getenv("ENABLE_TOOLS")
	toolList := strings.Split(enableTools, ",")

	switch action {
	case "list":
		response := "Available tools:\n"
		allEnabled := enableTools == ""

		// List all available tools with status
		tools := []struct {
			name string
			desc string
		}{
			{"tool_manager", "Tool management"},
			{"gemini", "AI tools: web search"},
			{"fetch", "Web content fetching"},
			{"confluence", "Confluence integration"},
			{"youtube", "YouTube transcript"},
			{"jira", "Jira issue management"},
			{"gitlab", "GitLab integration"},
			{"script", "Script execution"},
			{"rag", "RAG memory tools"},
			{"gmail", "Gmail tools"},
			{"calendar", "Google Calendar tools"},
			{"youtube_channel", "YouTube channel tools"},
			{"sequential_thinking", "Sequential thinking tool"},
			{"deepseek", "Deepseek reasoning tool"},
			{"maps_location_search", "Google Maps location search"},
			{"maps_geocoding", "Google Maps geocoding and reverse geocoding"},
			{"maps_place_details", "Google Maps detailed place information"},
		}

		for _, t := range tools {
			status := "disabled"
			if allEnabled || contains(toolList, t.name) {
				status = "enabled"
			}
			response += fmt.Sprintf("- %s (%s) [%s]\n", t.name, t.desc, status)
		}
		response += "\n"

		// List enabled tools
		response += "Currently enabled tools:\n"
		if allEnabled {
			response += "All tools are enabled (ENABLE_TOOLS is empty)\n"
		} else {
			for _, tool := range toolList {
				if tool != "" {
					response += fmt.Sprintf("- %s\n", tool)
				}
			}
		}
		return mcp.NewToolResultText(response), nil

	case "enable", "disable":
		toolName, ok := arguments["tool_name"].(string)
		if !ok || toolName == "" {
			return mcp.NewToolResultError("tool_name is required for enable/disable actions"), nil
		}

		if enableTools == "" {
			toolList = []string{}
		}

		if action == "enable" {
			if !contains(toolList, toolName) {
				toolList = append(toolList, toolName)
			}
		} else {
			toolList = removeString(toolList, toolName)
		}

		newEnableTools := strings.Join(toolList, ",")
		os.Setenv("ENABLE_TOOLS", newEnableTools)

		return mcp.NewToolResultText(fmt.Sprintf("Successfully %sd tool: %s", action, toolName)), nil

	default:
		return mcp.NewToolResultError("Invalid action. Use 'list', 'enable', or 'disable'"), nil
	}
}

func contains(slice []string, item string) bool {
	for _, s := range slice {
		if s == item {
			return true
		}
	}
	return false
}

func removeString(slice []string, item string) []string {
	result := []string{}
	for _, s := range slice {
		if s != item {
			result = append(result, s)
		}
	}
	return result
}

func toolUsePlanHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	request, _ := arguments["request"].(string)
	contextString, _ := arguments["context"].(string)

	enabledTools := strings.Split(os.Getenv("ENABLE_TOOLS"), ",")
	if !contains(enabledTools, "deepseek") {
		return mcp.NewToolResultError("Deepseek tool must be enabled to generate plans"), nil
	}

	// Check for configuration
	useOllama := os.Getenv("USE_OLLAMA_DEEPSEEK") == "true"
	useOpenRouter := os.Getenv("USE_OPENROUTER") == "true"

	if !useOllama && !useOpenRouter && os.Getenv("DEEPSEEK_API_KEY") == "" {
		return mcp.NewToolResultError("Either USE_OLLAMA_DEEPSEEK, USE_OPENROUTER must be true, or DEEPSEEK_API_KEY must be set"), nil
	}

	systemPrompt := fmt.Sprintf(`You are a tool usage planning assistant. Create a detailed execution plan using the currently enabled tools: %s

Context: %s

Output format:
1. [Tool Name] - Purpose: ... (Expected result: ...)
2. [Tool Name] - Purpose: ... (Expected result: ...)
...`, strings.Join(enabledTools, ", "), contextString)

	messages := []openai.ChatCompletionMessage{
		{
			Role:    openai.ChatMessageRoleSystem,
			Content: systemPrompt,
		},
		{
			Role:    openai.ChatMessageRoleUser,
			Content: request,
		},
	}

	client := services.DefaultDeepseekClient()
	if client == nil {
		return mcp.NewToolResultError("Failed to initialize client"), nil
	}

	modelName := "deepseek-reasoner"
	if useOllama {
		modelName = "deepseek-r1:8b"
	} else if useOpenRouter {
		modelName = "deepseek/deepseek-r1-distill-qwen-32b" // or any other model available on OpenRouter
	}

	resp, err := client.CreateChatCompletion(
		context.Background(),
		openai.ChatCompletionRequest{
			Model:       modelName,
			Messages:    messages,
			Temperature: 0.3,
		},
	)

	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("API call failed: %v", err)), nil
	}

	if len(resp.Choices) == 0 {
		return mcp.NewToolResultError("No response from Deepseek"), nil
	}

	content := strings.TrimSpace(resp.Choices[0].Message.Content)
	return mcp.NewToolResultText("📝 **Execution Plan:**\n" + content), nil
}

```

--------------------------------------------------------------------------------
/tools/youtube.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"encoding/json"
	"fmt"
	"html"
	"io"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/athapong/aio-mcp/services"
	"github.com/athapong/aio-mcp/util"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
)

const (
	RE_YOUTUBE        = `(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})`
	USER_AGENT        = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36`
	RE_XML_TRANSCRIPT = `<text start="([^"]*)" dur="([^"]*)">([^<]*)<\/text>`
)

// RegisterYouTubeTool registers the YouTube transcript tool with the MCP server
func RegisterYouTubeTool(s *server.MCPServer) {
	tool := mcp.NewTool("youtube_transcript",
		mcp.WithDescription("Get YouTube video transcript"),
		mcp.WithString("video_id", mcp.Required(), mcp.Description("YouTube video ID")),
	)

	s.AddTool(tool, util.ErrorGuard(util.AdaptLegacyHandler(youtubeTranscriptHandler)))
}

func youtubeTranscriptHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	// Get URL from arguments
	videoID, ok := arguments["video_id"].(string)
	if !ok {
		return nil, fmt.Errorf("video_id argument is required")
	}

	// Fetch transcript
	transcripts, videoTitle, err := FetchTranscript(videoID)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch transcript: %v", err)
	}

	// Build result string
	var builder strings.Builder
	builder.WriteString(fmt.Sprintf("Title: %s\n\n", videoTitle))

	for _, transcript := range transcripts {
		// Decode HTML entities in the text
		decodedText := decodeHTML(transcript.Text)
		// Format timestamp in [HH:MM:SS] format
		timestamp := formatTimestamp(transcript.Offset)

		builder.WriteString(timestamp)
		builder.WriteString(decodedText)
		builder.WriteString("\n")
	}

	return mcp.NewToolResultText(builder.String()), nil
}

// Error types
type YoutubeTranscriptError struct {
	Message string
}

func (e *YoutubeTranscriptError) Error() string {
	return fmt.Sprintf("[YoutubeTranscript] 🚨 %s", e.Message)
}

type TranscriptResponse struct {
	Text     string
	Duration float64
	Offset   float64
	Lang     string
}

// FetchTranscript retrieves the transcript for a YouTube video
func FetchTranscript(videoId string) ([]TranscriptResponse, string, error) {
	identifier, err := retrieveVideoId(videoId)
	if err != nil {
		return nil, "", err
	}

	videoPageURL := fmt.Sprintf("https://www.youtube.com/watch?v=%s", identifier)

	videoPageResponse, err := services.DefaultHttpClient().Get(videoPageURL)
	if err != nil {
		return nil, "", err
	}
	defer videoPageResponse.Body.Close()

	videoPageBody, err := io.ReadAll(videoPageResponse.Body)
	if err != nil {
		return nil, "", err
	}

	// Extract video title
	titleRegex := regexp.MustCompile(`<title>(.+?) - YouTube</title>`)
	titleMatch := titleRegex.FindSubmatch(videoPageBody)
	var videoTitle string
	if len(titleMatch) > 1 {
		videoTitle = string(titleMatch[1])
		videoTitle = html.UnescapeString(videoTitle)
	}

	splittedHTML := strings.Split(string(videoPageBody), `"captions":`)
	if len(splittedHTML) <= 1 {
		if strings.Contains(string(videoPageBody), `class="g-recaptcha"`) {
			return nil, "", &YoutubeTranscriptError{Message: "YouTube is receiving too many requests from this IP and now requires solving a captcha to continue"}
		}
		if !strings.Contains(string(videoPageBody), `"playabilityStatus":`) {
			return nil, "", &YoutubeTranscriptError{Message: fmt.Sprintf("The video is no longer available (%s)", videoId)}
		}
		return nil, "", &YoutubeTranscriptError{Message: fmt.Sprintf("Transcript is disabled on this video (%s)", videoId)}
	}

	var captions struct {
		PlayerCaptionsTracklistRenderer struct {
			CaptionTracks []struct {
				BaseURL      string `json:"baseUrl"`
				LanguageCode string `json:"languageCode"`
			} `json:"captionTracks"`
		} `json:"playerCaptionsTracklistRenderer"`
	}

	captionsData := splittedHTML[1][:strings.Index(splittedHTML[1], ",\"videoDetails")]
	err = json.Unmarshal([]byte(captionsData), &captions)
	if err != nil {
		return nil, "", &YoutubeTranscriptError{Message: fmt.Sprintf("Transcript is disabled on this video (%s)", videoId)}
	}

	if len(captions.PlayerCaptionsTracklistRenderer.CaptionTracks) == 0 {
		return nil, "", &YoutubeTranscriptError{Message: fmt.Sprintf("No transcripts are available for this video (%s)", videoId)}
	}

	transcriptURL := captions.PlayerCaptionsTracklistRenderer.CaptionTracks[0].BaseURL

	transcriptResponse, err := services.DefaultHttpClient().Get(transcriptURL)
	if err != nil {
		return nil, "", &YoutubeTranscriptError{Message: fmt.Sprintf("No transcripts are available for this video (%s)", videoId)}
	}
	defer transcriptResponse.Body.Close()

	transcriptBody, err := io.ReadAll(transcriptResponse.Body)
	if err != nil {
		return nil, "", err
	}

	re := regexp.MustCompile(RE_XML_TRANSCRIPT)
	matches := re.FindAllStringSubmatch(string(transcriptBody), -1)
	var results []TranscriptResponse
	for _, match := range matches {
		duration, _ := strconv.ParseFloat(match[2], 64)
		offset, _ := strconv.ParseFloat(match[1], 64)
		results = append(results, TranscriptResponse{
			Text:     match[3],
			Duration: duration,
			Offset:   offset,
			Lang:     captions.PlayerCaptionsTracklistRenderer.CaptionTracks[0].LanguageCode,
		})
	}

	return results, videoTitle, nil
}

// Helper functions
func retrieveVideoId(videoId string) (string, error) {
	if len(videoId) == 11 {
		return videoId, nil
	}
	re := regexp.MustCompile(RE_YOUTUBE)
	match := re.FindStringSubmatch(videoId)
	if match != nil {
		return match[1], nil
	}
	return "", &YoutubeTranscriptError{Message: "Impossible to retrieve Youtube video ID."}
}

func decodeHTML(text string) string {
	text = strings.ReplaceAll(text, "&amp;#39;", "'")
	text = html.UnescapeString(text)
	return text
}

func formatTimestamp(offset float64) string {
	duration := time.Duration(offset * float64(time.Second))
	hours := duration / time.Hour
	duration -= hours * time.Hour
	minutes := duration / time.Minute
	duration -= minutes * time.Minute
	seconds := duration / time.Second
	return fmt.Sprintf("[%02d:%02d:%02d] ", hours, minutes, seconds)
}

```

--------------------------------------------------------------------------------
/tools/youtube_channel.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"context"
	"fmt"
	"os"
	"strings"
	"sync"

	"github.com/athapong/aio-mcp/services"
	"github.com/athapong/aio-mcp/util"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
	"google.golang.org/api/option"
	"google.golang.org/api/youtube/v3"
)

func RegisterYouTubeChannelTools(s *server.MCPServer) {
	// Update video tool
	updateVideoTool := mcp.NewTool("youtube_update_video",
		mcp.WithDescription("Update a video's title and description on YouTube"),
		mcp.WithString("video_id", mcp.Required(), mcp.Description("ID of the video to update")),
		mcp.WithString("title", mcp.Required(), mcp.Description("New title of the video")),
		mcp.WithString("description", mcp.Required(), mcp.Description("New description of the video")),
		mcp.WithString("keywords", mcp.Required(), mcp.Description("Comma-separated list of keywords for the video")),
		mcp.WithString("category", mcp.Required(), mcp.Description("Category ID for the video. See https://developers.google.com/youtube/v3/docs/videoCategories/list for more information.")),
	)
	s.AddTool(updateVideoTool, util.ErrorGuard(util.AdaptLegacyHandler(youtubeUpdateVideoHandler)))

	getVideoDetailsTool := mcp.NewTool("youtube_get_video_details",
		mcp.WithDescription("Get details (title, description, ...) for a specific video"),
		mcp.WithString("video_id", mcp.Required(), mcp.Description("ID of the video")),
	)
	s.AddTool(getVideoDetailsTool, util.ErrorGuard(util.AdaptLegacyHandler(youtubeGetVideoDetailsHandler)))

	// List my channels tool
	listMyChannelsTool := mcp.NewTool("youtube_list_videos",
		mcp.WithDescription("List YouTube videos managed by the user"),
		mcp.WithString("channel_id", mcp.Required(), mcp.Description("ID of the channel to list videos for")),
		mcp.WithNumber("max_results", mcp.Required(), mcp.Description("Maximum number of videos to return")),
	)
	s.AddTool(listMyChannelsTool, util.ErrorGuard(util.AdaptLegacyHandler(youtubeListVideosHandler)))

}

var youtubeService = sync.OnceValue(func() *youtube.Service {
	ctx := context.Background()

	tokenFile := os.Getenv("GOOGLE_TOKEN_FILE")
	if tokenFile == "" {
		panic("GOOGLE_TOKEN_FILE environment variable must be set")
	}

	credentialsFile := os.Getenv("GOOGLE_CREDENTIALS_FILE")
	if credentialsFile == "" {
		panic("GOOGLE_CREDENTIALS_FILE environment variable must be set")
	}

	client := services.GoogleHttpClient(tokenFile, credentialsFile)

	srv, err := youtube.NewService(ctx, option.WithHTTPClient(client))
	if err != nil {
		panic(fmt.Sprintf("failed to create YouTube service: %v", err))
	}

	return srv
})

func youtubeUpdateVideoHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	var videoID string
	if videoIDArg, ok := arguments["video_id"]; ok {
		videoID = videoIDArg.(string)
	} else {
		return mcp.NewToolResultError("video_id is required"), nil
	}

	var title string
	if titleArg, ok := arguments["title"]; ok {
		title = titleArg.(string)
	}

	var description string
	if descArg, ok := arguments["description"]; ok {
		description = descArg.(string)
	}

	var keywords string
	if keywordsArg, ok := arguments["keywords"]; ok {
		keywords = keywordsArg.(string)
	}

	var category string
	if categoryArg, ok := arguments["category"]; ok {
		category = categoryArg.(string)
	}

	updateCall := youtubeService().Videos.Update([]string{"snippet"}, &youtube.Video{
		Id: videoID,
		Snippet: &youtube.VideoSnippet{
			Title:       title,
			Description: description,
			Tags:        strings.Split(keywords, ","),
			CategoryId:  category,
		},
	})

	_, err := updateCall.Do()
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to update video: %v", err)), nil
	}

	return mcp.NewToolResultText(fmt.Sprintf("Successfully updated video with ID: %s", videoID)), nil
}

func youtubeGetVideoDetailsHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	videoID, ok := arguments["video_id"].(string)
	if !ok {
		return mcp.NewToolResultError("video_id is required"), nil
	}

	listCall := youtubeService().Videos.List([]string{"snippet", "contentDetails", "statistics"}).
		Id(videoID)
	listResponse, err := listCall.Do()
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to get video details: %v", err)), nil
	}

	if len(listResponse.Items) == 0 {
		return mcp.NewToolResultError(fmt.Sprintf("video with ID %s not found", videoID)), nil
	}

	video := listResponse.Items[0]
	result := fmt.Sprintf("Title: %s\n", video.Snippet.Title)
	result += fmt.Sprintf("Description: %s\n", video.Snippet.Description)
	result += fmt.Sprintf("Video ID: %s\n", video.Id)
	result += fmt.Sprintf("Duration: %s\n", video.ContentDetails.Duration)
	result += fmt.Sprintf("Views: %d\n", video.Statistics.ViewCount)
	result += fmt.Sprintf("Likes: %d\n", video.Statistics.LikeCount)
	result += fmt.Sprintf("Comments: %d\n", video.Statistics.CommentCount)

	return mcp.NewToolResultText(result), nil
}

func youtubeListVideosHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	var channelID string
	if channelIDArg, ok := arguments["channel_id"]; ok {
		channelID = channelIDArg.(string)
	} else {
		return mcp.NewToolResultError("channel_id is required"), nil
	}

	var maxResults int64
	if maxResultsArg, ok := arguments["max_results"].(float64); ok {
		maxResults = int64(maxResultsArg)
	} else {
		maxResults = 10
	}

	// Get the channel's uploads playlist ID
	channelsListCall := youtubeService().Channels.List([]string{"contentDetails"}).
		Id(channelID)
	channelsListResponse, err := channelsListCall.Do()
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to get channel details: %v", err)), nil
	}

	if len(channelsListResponse.Items) == 0 {
		return mcp.NewToolResultError("channel not found"), nil
	}

	uploadsPlaylistID := channelsListResponse.Items[0].ContentDetails.RelatedPlaylists.Uploads

	// List videos in the uploads playlist
	playlistItemsListCall := youtubeService().PlaylistItems.List([]string{"snippet"}).
		PlaylistId(uploadsPlaylistID).
		MaxResults(maxResults)
	playlistItemsListResponse, err := playlistItemsListCall.Do()
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to list videos: %v", err)), nil
	}

	var result string
	for _, playlistItem := range playlistItemsListResponse.Items {
		videoID := playlistItem.Snippet.ResourceId.VideoId
		videoDetailsCall := youtubeService().Videos.List([]string{"snippet", "statistics"}).
			Id(videoID)
		videoDetailsResponse, err := videoDetailsCall.Do()
		if err != nil {
			return mcp.NewToolResultError(fmt.Sprintf("failed to get video details: %v", err)), nil
		}

		if len(videoDetailsResponse.Items) > 0 {
			video := videoDetailsResponse.Items[0]
			result += fmt.Sprintf("Video ID: %s\n", video.Id)
			result += fmt.Sprintf("Published At: %s\n", video.Snippet.PublishedAt)
			result += fmt.Sprintf("View Count: %d\n", video.Statistics.ViewCount)
			result += fmt.Sprintf("Like Count: %d\n", video.Statistics.LikeCount)
			result += fmt.Sprintf("Comment Count: %d\n", video.Statistics.CommentCount)
			result += fmt.Sprintf("Title: %s\n", video.Snippet.Title)
			result += fmt.Sprintf("Description: %s\n", video.Snippet.Description)
			result += "-------------------\n"
		}
	}

	return mcp.NewToolResultText(result), nil
}

```

--------------------------------------------------------------------------------
/tools/calendar.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"context"
	"fmt"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/athapong/aio-mcp/services"
	"github.com/athapong/aio-mcp/util"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
	"google.golang.org/api/calendar/v3"
	"google.golang.org/api/option"
)

func RegisterCalendarTools(s *server.MCPServer) {
	// Create event tool
	createEventTool := mcp.NewTool("calendar_create_event",
		mcp.WithDescription("Create a new event in Google Calendar"),
		mcp.WithString("summary", mcp.Required(), mcp.Description("Title of the event")),
		mcp.WithString("description", mcp.Description("Description of the event")),
		mcp.WithString("start_time", mcp.Required(), mcp.Description("Start time of the event in RFC3339 format (e.g., 2023-12-25T09:00:00Z)")),
		mcp.WithString("end_time", mcp.Required(), mcp.Description("End time of the event in RFC3339 format")),
		mcp.WithString("attendees", mcp.Description("Comma-separated list of attendee email addresses")),
	)
	s.AddTool(createEventTool, util.ErrorGuard(calendarCreateEventHandler))

	// List events tool
	listEventsTool := mcp.NewTool("calendar_list_events",
		mcp.WithDescription("List upcoming events in Google Calendar"),
		mcp.WithString("time_min", mcp.Description("Start time for the search in RFC3339 format (default: now)")),
		mcp.WithString("time_max", mcp.Description("End time for the search in RFC3339 format (default: 1 week from now)")),
		mcp.WithNumber("max_results", mcp.Description("Maximum number of events to return (default: 10)")),
	)
	s.AddTool(listEventsTool, util.ErrorGuard(calendarListEventsHandler))

	// Update event tool
	updateEventTool := mcp.NewTool("calendar_update_event",
		mcp.WithDescription("Update an existing event in Google Calendar"),
		mcp.WithString("event_id", mcp.Required(), mcp.Description("ID of the event to update")),
		mcp.WithString("summary", mcp.Description("New title of the event")),
		mcp.WithString("description", mcp.Description("New description of the event")),
		mcp.WithString("start_time", mcp.Description("New start time of the event in RFC3339 format")),
		mcp.WithString("end_time", mcp.Description("New end time of the event in RFC3339 format")),
		mcp.WithString("attendees", mcp.Description("Comma-separated list of new attendee email addresses")),
	)
	s.AddTool(updateEventTool, util.ErrorGuard(calendarUpdateEventHandler))

	// Respond to event tool
	respondToEventTool := mcp.NewTool("calendar_respond_to_event",
		mcp.WithDescription("Respond to an event invitation in Google Calendar"),
		mcp.WithString("event_id", mcp.Required(), mcp.Description("ID of the event to respond to")),
		mcp.WithString("response", mcp.Required(), mcp.Description("Your response (accepted, declined, or tentative)")),
	)
	s.AddTool(respondToEventTool, util.ErrorGuard(calendarRespondToEventHandler))
}

var calendarService = sync.OnceValue(func() *calendar.Service {
	ctx := context.Background()

	tokenFile := os.Getenv("GOOGLE_TOKEN_FILE")
	if tokenFile == "" {
		panic("GOOGLE_TOKEN_FILE environment variable must be set")
	}

	credentialsFile := os.Getenv("GOOGLE_CREDENTIALS_FILE")
	if credentialsFile == "" {
		panic("GOOGLE_CREDENTIALS_FILE environment variable must be set")
	}

	client := services.GoogleHttpClient(tokenFile, credentialsFile)

	srv, err := calendar.NewService(ctx, option.WithHTTPClient(client))
	if err != nil {
		panic(fmt.Sprintf("failed to create Calendar service: %v", err))
	}

	return srv
})

func calendarCreateEventHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	arguments := request.Params.Arguments
	summary, _ := arguments["summary"].(string)
	description, _ := arguments["description"].(string)
	startTimeStr, _ := arguments["start_time"].(string)
	endTimeStr, _ := arguments["end_time"].(string)
	attendeesStr, _ := arguments["attendees"].(string)

	startTime, err := time.Parse(time.RFC3339, startTimeStr)
	if err != nil {
		return mcp.NewToolResultError("Invalid start_time format"), nil
	}
	endTime, err := time.Parse(time.RFC3339, endTimeStr)
	if err != nil {
		return mcp.NewToolResultError("Invalid end_time format"), nil
	}

	var attendees []*calendar.EventAttendee
	if attendeesStr != "" {
		for _, email := range strings.Split(attendeesStr, ",") {
			attendees = append(attendees, &calendar.EventAttendee{Email: email})
		}
	}

	event := &calendar.Event{
		Summary:     summary,
		Description: description,
		Start: &calendar.EventDateTime{
			DateTime: startTime.Format(time.RFC3339),
		},
		End: &calendar.EventDateTime{
			DateTime: endTime.Format(time.RFC3339),
		},
		Attendees: attendees,
	}

	createdEvent, err := calendarService().Events.Insert("primary", event).Do()
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to create event: %v", err)), nil
	}

	return mcp.NewToolResultText(fmt.Sprintf("Successfully created event with ID: %s", createdEvent.Id)), nil
}

func calendarListEventsHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	arguments := request.Params.Arguments
	timeMinStr, ok := arguments["time_min"].(string)
	if !ok || timeMinStr == "" {
		timeMinStr = time.Now().Format(time.RFC3339)
	}

	timeMaxStr, ok := arguments["time_max"].(string)
	if !ok || timeMaxStr == "" {
		timeMaxStr = time.Now().AddDate(0, 0, 7).Format(time.RFC3339) // 1 week from now
	}

	maxResults, ok := arguments["max_results"].(float64)
	if !ok {
		maxResults = 10
	}

	events, err := calendarService().Events.List("primary").
		ShowDeleted(false).
		SingleEvents(true).
		TimeMin(timeMinStr).
		TimeMax(timeMaxStr).
		MaxResults(int64(maxResults)).
		OrderBy("startTime").
		Do()
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to list events: %v", err)), nil
	}

	var result strings.Builder
	result.WriteString(fmt.Sprintf("Found %d upcoming events:\n\n", len(events.Items)))

	for _, item := range events.Items {
		start, _ := time.Parse(time.RFC3339, item.Start.DateTime)
		end, _ := time.Parse(time.RFC3339, item.End.DateTime)

		result.WriteString(fmt.Sprintf("Event: %s\n", item.Summary))
		result.WriteString(fmt.Sprintf("Start: %s\n", start.Format("2006-01-02 15:04")))
		result.WriteString(fmt.Sprintf("End: %s\n", end.Format("2006-01-02 15:04")))
		if item.Description != "" {
			result.WriteString(fmt.Sprintf("Description: %s\n", item.Description))
		}
		result.WriteString("-------------------\n")
	}

	return mcp.NewToolResultText(result.String()), nil
}

func calendarUpdateEventHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	arguments := request.Params.Arguments
	eventID, _ := arguments["event_id"].(string)
	summary, _ := arguments["summary"].(string)
	description, _ := arguments["description"].(string)
	startTimeStr, _ := arguments["start_time"].(string)
	endTimeStr, _ := arguments["end_time"].(string)
	attendeesStr, _ := arguments["attendees"].(string)

	event, err := calendarService().Events.Get("primary", eventID).Do()
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to get event: %v", err)), nil
	}

	if summary != "" {
		event.Summary = summary
	}
	if description != "" {
		event.Description = description
	}
	if startTimeStr != "" {
		startTime, err := time.Parse(time.RFC3339, startTimeStr)
		if err != nil {
			return mcp.NewToolResultError("Invalid start_time format"), nil
		}
		event.Start.DateTime = startTime.Format(time.RFC3339)
	}
	if endTimeStr != "" {
		endTime, err := time.Parse(time.RFC3339, endTimeStr)
		if err != nil {
			return mcp.NewToolResultError("Invalid end_time format"), nil
		}
		event.End.DateTime = endTime.Format(time.RFC3339)
	}
	if attendeesStr != "" {
		var attendees []*calendar.EventAttendee
		for _, email := range strings.Split(attendeesStr, ",") {
			attendees = append(attendees, &calendar.EventAttendee{Email: email})
		}
		event.Attendees = attendees
	}

	updatedEvent, err := calendarService().Events.Update("primary", eventID, event).Do()
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to update event: %v", err)), nil
	}

	return mcp.NewToolResultText(fmt.Sprintf("Successfully updated event with ID: %s", updatedEvent.Id)), nil
}

func calendarRespondToEventHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	arguments := request.Params.Arguments
	eventID, _ := arguments["event_id"].(string)
	response, _ := arguments["response"].(string)

	event, err := calendarService().Events.Get("primary", eventID).Do()
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to get event: %v", err)), nil
	}

	for _, attendee := range event.Attendees {
		if attendee.Self {
			attendee.ResponseStatus = response
			break
		}
	}

	_, err = calendarService().Events.Update("primary", eventID, event).Do()
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to update event response: %v", err)), nil
	}

	return mcp.NewToolResultText(fmt.Sprintf("Successfully responded '%s' to event with ID: %s", response, eventID)), nil
}

```

--------------------------------------------------------------------------------
/scripts/docs/update-doc.go:
--------------------------------------------------------------------------------

```go
package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"io/ioutil"
	"os"
	"path/filepath"
	"regexp"
	"strings"
)

type EnvVarInfo struct {
    comment string
}

type ToolInfo struct {
    Name        string
    Description string
    Args        []ArgInfo
}

type ArgInfo struct {
    Name        string
    Type        string
    Required    bool
    Description string
    Default     string
}
func updateReadmeConfig(envVars map[string]EnvVarInfo, tools []ToolInfo) error {
    // Read README.md
    content, err := ioutil.ReadFile("README.md")
    if err != nil {
        return fmt.Errorf("error reading README.md: %v", err)
    }

    // Convert to string
    readmeContent := string(content)

    // Update env vars section
    configRegex := regexp.MustCompile(`(?s)"env": \{[^}]*\}`)
    
    var envConfig strings.Builder
    envConfig.WriteString(`"env": {`)
    first := true
    for envVar, info := range envVars {
        if !first {
            envConfig.WriteString(",")
        }
        first = false
        envConfig.WriteString("\n        ")
        envConfig.WriteString(fmt.Sprintf(`"%s": "%s"`, envVar, info.comment))
    }
    envConfig.WriteString("\n      }")

    // Replace env config
    readmeContent = configRegex.ReplaceAllString(readmeContent, envConfig.String())

    // Generate tools section content
    var toolsSection strings.Builder
    toolsSection.WriteString("## Available Tools\n\n")
    for _, tool := range tools {
        toolsSection.WriteString(fmt.Sprintf("### %s\n\n", tool.Name))
        if tool.Description != "" {
            toolsSection.WriteString(fmt.Sprintf("%s\n\n", tool.Description))
        }
        if len(tool.Args) > 0 {
            toolsSection.WriteString("Arguments:\n\n")
            for _, arg := range tool.Args {
                toolsSection.WriteString(fmt.Sprintf("- `%s` (%s)", arg.Name, arg.Type))
                if arg.Required {
                    toolsSection.WriteString(" (Required)")
                }
                if arg.Default != "" {
                    toolsSection.WriteString(fmt.Sprintf(" (Default: %s)", arg.Default))
                }
                toolsSection.WriteString(fmt.Sprintf(": %s\n", arg.Description))
            }
            toolsSection.WriteString("\n")
        }
    }

    // Replace existing tools section
    // Look for the section between "## Available Tools" and the next section starting with "##"
    toolsSectionRegex := regexp.MustCompile(`(?s)## Available Tools.*?(\n## |$)`)
    
    if toolsSectionRegex.MatchString(readmeContent) {
        // Replace existing section
        readmeContent = toolsSectionRegex.ReplaceAllString(readmeContent, toolsSection.String())
    } else {
        // If section doesn't exist, add it before the end
        readmeContent += "\n\n" + toolsSection.String()
    }

    // Write back to README.md
    err = ioutil.WriteFile("README.md", []byte(readmeContent), 0644)
    if err != nil {
        return fmt.Errorf("error writing README.md: %v", err)
    }

    return nil
}

func extractToolInfo(node *ast.File) []ToolInfo {
    var tools []ToolInfo

    ast.Inspect(node, func(n ast.Node) bool {
        // Look for tool registrations
        callExpr, ok := n.(*ast.CallExpr)
        if !ok {
            return true
        }

        // Check if it's a NewTool call
        if sel, ok := callExpr.Fun.(*ast.SelectorExpr); ok {
            if sel.Sel.Name == "NewTool" {
                tool := ToolInfo{}
                
                // Extract tool name
                if len(callExpr.Args) > 0 {
                    if lit, ok := callExpr.Args[0].(*ast.BasicLit); ok {
                        tool.Name = strings.Trim(lit.Value, `"'`)
                    }
                }

                // Extract description and arguments from WithX calls
                for _, arg := range callExpr.Args[1:] {
                    if call, ok := arg.(*ast.CallExpr); ok {
                        if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
                            switch sel.Sel.Name {
                            case "WithDescription":
                                if len(call.Args) > 0 {
                                    if lit, ok := call.Args[0].(*ast.BasicLit); ok {
                                        tool.Description = strings.Trim(lit.Value, `"'`)
                                    }
                                }
                            case "WithString", "WithNumber", "WithBoolean":
                                if len(call.Args) >= 2 {
                                    arg := ArgInfo{
                                        Type: strings.TrimPrefix(sel.Sel.Name, "With"),
                                    }
                                    if lit, ok := call.Args[0].(*ast.BasicLit); ok {
                                        arg.Name = strings.Trim(lit.Value, `"'`)
                                    }
                                    for _, opt := range call.Args[1:] {
                                        if call, ok := opt.(*ast.CallExpr); ok {
                                            if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
                                                switch sel.Sel.Name {
                                                case "Required":
                                                    arg.Required = true
                                                case "Description":
                                                    if len(call.Args) > 0 {
                                                        if lit, ok := call.Args[0].(*ast.BasicLit); ok {
                                                            arg.Description = strings.Trim(lit.Value, `"'`)
                                                        }
                                                    }
                                                case "DefaultString", "DefaultNumber":
                                                    if len(call.Args) > 0 {
                                                        if lit, ok := call.Args[0].(*ast.BasicLit); ok {
                                                            arg.Default = strings.Trim(lit.Value, `"'`)
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                    tool.Args = append(tool.Args, arg)
                                }
                            }
                        }
                    }
                }

                if tool.Name != "" {
                    tools = append(tools, tool)
                }
            }
        }
        return true
    })

    return tools
}

func main() {
    envVars := make(map[string]EnvVarInfo)
    var allTools []ToolInfo

    // Walk through all .go files
    err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }

        if !strings.HasSuffix(path, ".go") {
            return nil
        }

        // Parse the Go file
        fset := token.NewFileSet()
        node, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
        if err != nil {
            return fmt.Errorf("error parsing %s: %v", path, err)
        }

        // Extract environment variables
        ast.Inspect(node, func(n ast.Node) bool {
            call, ok := n.(*ast.CallExpr)
            if !ok {
                return true
            }

            sel, ok := call.Fun.(*ast.SelectorExpr)
            if !ok {
                return true
            }

            if ident, ok := sel.X.(*ast.Ident); ok {
                if ident.Name == "os" && (sel.Sel.Name == "Getenv" || sel.Sel.Name == "LookupEnv") {
                    if len(call.Args) > 0 {
                        if strLit, ok := call.Args[0].(*ast.BasicLit); ok && strLit.Kind == token.STRING {
                            envName := strings.Trim(strLit.Value, `"'`)
                            
                            var comment string
                            for _, cg := range node.Comments {
                                if cg.End() < call.Pos() {
                                    lastComment := cg.List[len(cg.List)-1]
                                    if lastComment.End()+100 >= call.Pos() {
                                        comment = strings.TrimPrefix(lastComment.Text, "//")
                                        comment = strings.TrimSpace(comment)
                                    }
                                }
                            }

                            envVars[envName] = EnvVarInfo{comment: comment}
                        }
                    }
                }
            }
            return true
        })

        // Extract tool information if in tools directory
        if strings.HasPrefix(path, "tools/") {
            tools := extractToolInfo(node)
            allTools = append(allTools, tools...)
        }

        return nil
    })

    if err != nil {
        fmt.Fprintf(os.Stderr, "Error walking files: %v\n", err)
        os.Exit(1)
    }

    // Update README.md with both env vars and tools
    err = updateReadmeConfig(envVars, allTools)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error updating README.md: %v\n", err)
        os.Exit(1)
    }

    fmt.Println("Successfully updated README.md with environment variables and tools documentation")
}
```

--------------------------------------------------------------------------------
/tools/sequentialthinking.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"encoding/json"
	"fmt"

	"github.com/athapong/aio-mcp/util"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
)

type ThoughtData struct {
	Thought           string  `json:"thought"`
	ThoughtNumber     int     `json:"thoughtNumber"`
	TotalThoughts     int     `json:"totalThoughts"`
	IsRevision        *bool   `json:"isRevision,omitempty"`
	RevisesThought    *int    `json:"revisesThought,omitempty"`
	BranchFromThought *int    `json:"branchFromThought,omitempty"`
	BranchID          *string `json:"branchId,omitempty"`
	NeedsMoreThoughts *bool   `json:"needsMoreThoughts,omitempty"`
	NextThoughtNeeded bool    `json:"nextThoughtNeeded"`
	Result            *string `json:"result,omitempty"`
	Summary           *string `json:"summary,omitempty"`
}

type SequentialThinkingServer struct {
	thoughtHistory    []ThoughtData
	branches          map[string][]ThoughtData
	currentBranchID   string
	lastThoughtNumber int
}

func NewSequentialThinkingServer() *SequentialThinkingServer {
	return &SequentialThinkingServer{
		thoughtHistory: make([]ThoughtData, 0),
		branches:       make(map[string][]ThoughtData),
	}
}

func (s *SequentialThinkingServer) getThoughtHistory() []ThoughtData {
	if s.currentBranchID != "" && len(s.branches[s.currentBranchID]) > 0 {
		return s.branches[s.currentBranchID]
	}
	return s.thoughtHistory
}

func (s *SequentialThinkingServer) validateThoughtData(input map[string]interface{}) (*ThoughtData, error) {
	thought, ok := input["thought"].(string)
	if !ok || thought == "" {
		return nil, fmt.Errorf("invalid thought: must be a string")
	}

	thoughtNumber, ok := input["thoughtNumber"].(float64)
	if !ok {
		return nil, fmt.Errorf("invalid thoughtNumber: must be a number")
	}

	totalThoughts, ok := input["totalThoughts"].(float64)
	if !ok {
		return nil, fmt.Errorf("invalid totalThoughts: must be a number")
	}

	nextThoughtNeeded, ok := input["nextThoughtNeeded"].(bool)
	if !ok {
		return nil, fmt.Errorf("invalid nextThoughtNeeded: must be a boolean")
	}

	data := &ThoughtData{
		Thought:           thought,
		ThoughtNumber:     int(thoughtNumber),
		TotalThoughts:     int(totalThoughts),
		NextThoughtNeeded: nextThoughtNeeded,
	}

	// Optional fields
	if isRevision, ok := input["isRevision"].(bool); ok {
		data.IsRevision = &isRevision
	}
	if revisesThought, ok := input["revisesThought"].(float64); ok {
		rt := int(revisesThought)
		data.RevisesThought = &rt
	}
	if branchFromThought, ok := input["branchFromThought"].(float64); ok {
		bft := int(branchFromThought)
		data.BranchFromThought = &bft
	}
	if branchID, ok := input["branchId"].(string); ok {
		data.BranchID = &branchID
	}
	if needsMoreThoughts, ok := input["needsMoreThoughts"].(bool); ok {
		data.NeedsMoreThoughts = &needsMoreThoughts
	}
	if result, ok := input["result"].(string); ok {
		data.Result = &result
	}
	if summary, ok := input["summary"].(string); ok {
		data.Summary = &summary
	}

	return data, nil
}

func (s *SequentialThinkingServer) processThought(input map[string]interface{}) (*mcp.CallToolResult, error) {
	thoughtData, err := s.validateThoughtData(input)
	if err != nil {
		return mcp.NewToolResultError(err.Error()), nil
	}

	if thoughtData.ThoughtNumber > thoughtData.TotalThoughts {
		thoughtData.TotalThoughts = thoughtData.ThoughtNumber
	}

	// Update current branch ID
	if thoughtData.BranchID != nil {
		s.currentBranchID = *thoughtData.BranchID
	}

	// Track last thought number
	if thoughtData.ThoughtNumber > s.lastThoughtNumber {
		s.lastThoughtNumber = thoughtData.ThoughtNumber
	}

	// Store thought in appropriate collection
	if s.currentBranchID != "" {
		if _, exists := s.branches[s.currentBranchID]; !exists {
			s.branches[s.currentBranchID] = make([]ThoughtData, 0)
		}
		s.branches[s.currentBranchID] = append(s.branches[s.currentBranchID], *thoughtData)
	} else {
		s.thoughtHistory = append(s.thoughtHistory, *thoughtData)
	}

	branchKeys := make([]string, 0, len(s.branches))
	for k := range s.branches {
		branchKeys = append(branchKeys, k)
	}

	// Prepare response
	history := s.getThoughtHistory()
	response := map[string]interface{}{
		"thoughtNumber":     thoughtData.ThoughtNumber,
		"totalThoughts":     thoughtData.TotalThoughts,
		"nextThoughtNeeded": thoughtData.NextThoughtNeeded,
		"currentBranch":     s.currentBranchID,
		"branches":          s.getBranchSummary(),
		"history":           history,
		"lastThought":       s.lastThoughtNumber,
	}

	// Add result and summary if present
	if thoughtData.Result != nil {
		response["result"] = *thoughtData.Result
	}
	if thoughtData.Summary != nil {
		response["summary"] = *thoughtData.Summary
	}

	jsonResponse, err := json.MarshalIndent(response, "", "  ")
	if err != nil {
		return mcp.NewToolResultError(err.Error()), nil
	}

	return mcp.NewToolResultText(string(jsonResponse)), nil
}

func (s *SequentialThinkingServer) getBranchSummary() map[string]interface{} {
	summary := make(map[string]interface{})
	for branchID, thoughts := range s.branches {
		branchSummary := map[string]interface{}{
			"thoughtCount": len(thoughts),
			"lastThought":  thoughts[len(thoughts)-1].ThoughtNumber,
		}
		if lastThought := thoughts[len(thoughts)-1]; lastThought.Result != nil {
			branchSummary["result"] = *lastThought.Result
		}
		summary[branchID] = branchSummary
	}
	return summary
}

// Add package-level variable to share the server instance
var thinkingServer *SequentialThinkingServer

// Modify existing RegisterSequentialThinkingTool to remove history tool registration
func RegisterSequentialThinkingTool(s *server.MCPServer) {
	thinkingServer = NewSequentialThinkingServer() // Make thinkingServer package-level

	sequentialThinkingTool := mcp.NewTool("sequentialthinking",
		mcp.WithDescription(`A detailed tool for dynamic and reflective problem-solving through thoughts.
This tool helps analyze problems through a flexible thinking process that can adapt and evolve.
Each thought can build on, question, or revise previous insights as understanding deepens.

When to use this tool:
- Breaking down complex problems into steps
- Planning and design with room for revision
- Analysis that might need course correction
- Problems where the full scope might not be clear initially
- Problems that require a multi-step solution
- Tasks that need to maintain context over multiple steps
- Situations where irrelevant information needs to be filtered out

Key features:
- You can adjust total_thoughts up or down as you progress
- You can question or revise previous thoughts
- You can add more thoughts even after reaching what seemed like the end
- You can express uncertainty and explore alternative approaches
- Not every thought needs to build linearly - you can branch or backtrack
- Generates a solution hypothesis
- Verifies the hypothesis based on the Chain of Thought steps
- Repeats the process until satisfied
- Provides a correct answer

Parameters explained:
- thought: Your current thinking step, which can include:
* Regular analytical steps
* Revisions of previous thoughts
* Questions about previous decisions
* Realizations about needing more analysis
* Changes in approach
* Hypothesis generation
* Hypothesis verification
- next_thought_needed: True if you need more thinking, even if at what seemed like the end
- thought_number: Current number in sequence (can go beyond initial total if needed)
- total_thoughts: Current estimate of thoughts needed (can be adjusted up/down)
- is_revision: A boolean indicating if this thought revises previous thinking
- revises_thought: If is_revision is true, which thought number is being reconsidered
- branch_from_thought: If branching, which thought number is the branching point
- branch_id: Identifier for the current branch (if any)
- needs_more_thoughts: If reaching end but realizing more thoughts needed

You should:
1. Start with an initial estimate of needed thoughts, but be ready to adjust
2. Feel free to question or revise previous thoughts
3. Don't hesitate to add more thoughts if needed, even at the "end"
4. Express uncertainty when present
5. Mark thoughts that revise previous thinking or branch into new paths
6. Ignore information that is irrelevant to the current step
7. Generate a solution hypothesis when appropriate
8. Verify the hypothesis based on the Chain of Thought steps
9. Repeat the process until satisfied with the solution
10. Provide a single, ideally correct answer as the final output
11. Only set next_thought_needed to false when truly done and a satisfactory answer is reached`),
		mcp.WithString("thought", mcp.Required(), mcp.Description("Your current thinking step")),
		mcp.WithBoolean("nextThoughtNeeded", mcp.Required(), mcp.Description("Whether another thought step is needed")),
		mcp.WithNumber("thoughtNumber", mcp.Required(), mcp.Description("Current thought number")),
		mcp.WithNumber("totalThoughts", mcp.Required(), mcp.Description("Estimated total thoughts needed")),
		mcp.WithBoolean("isRevision", mcp.Description("Whether this revises previous thinking")),
		mcp.WithNumber("revisesThought", mcp.Description("Which thought is being reconsidered")),
		mcp.WithNumber("branchFromThought", mcp.Description("Branching point thought number")),
		mcp.WithString("branchId", mcp.Description("Branch identifier")),
		mcp.WithBoolean("needsMoreThoughts", mcp.Description("If more thoughts are needed")),
		mcp.WithString("result", mcp.Description("Final result or conclusion from this thought")),
		mcp.WithString("summary", mcp.Description("Brief summary of the thought's key points")),
	)

	s.AddTool(sequentialThinkingTool, util.ErrorGuard(util.AdaptLegacyHandler(func(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
		return thinkingServer.processThought(arguments)
	})))
}

// Move the history tool to its own registration function
func RegisterSequentialThinkingHistoryTool(s *server.MCPServer) {
	historyTool := mcp.NewTool("sequentialthinking_history",
		mcp.WithDescription("Retrieve the thought history for the current thinking process"),
		mcp.WithString("branchId", mcp.Description("Optional branch ID to get history for")),
	)

	s.AddTool(historyTool, util.ErrorGuard(util.AdaptLegacyHandler(func(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
		var history []ThoughtData
		if branchID, ok := arguments["branchId"].(string); ok && branchID != "" {
			if branch, exists := thinkingServer.branches[branchID]; exists {
				history = branch
			}
		} else {
			history = thinkingServer.thoughtHistory
		}

		jsonResponse, err := json.MarshalIndent(history, "", "  ")
		if err != nil {
			return mcp.NewToolResultError(err.Error()), nil
		}
		return mcp.NewToolResultText(string(jsonResponse)), nil
	})))
}

```

--------------------------------------------------------------------------------
/tools/gmail.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"context"
	"fmt"
	"log"
	"os"
	"strings"
	"sync"

	"github.com/athapong/aio-mcp/services"
	"github.com/athapong/aio-mcp/util"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
	"google.golang.org/api/gmail/v1"
	"google.golang.org/api/option"
)

func RegisterGmailTools(s *server.MCPServer) {
	// Search tool
	searchTool := mcp.NewTool("gmail_search",
		mcp.WithDescription("Search emails in Gmail using Gmail's search syntax"),
		mcp.WithString("query", mcp.Required(), mcp.Description("Gmail search query. Follow Gmail's search syntax")),
	)
	s.AddTool(searchTool, util.ErrorGuard(util.AdaptLegacyHandler(gmailSearchHandler)))

	// Move to spam tool
	spamTool := mcp.NewTool("gmail_move_to_spam",
		mcp.WithDescription("Move specific emails to spam folder in Gmail by message IDs"),
		mcp.WithString("message_ids", mcp.Required(), mcp.Description("Comma-separated list of message IDs to move to spam")),
	)
	s.AddTool(spamTool, util.ErrorGuard(util.AdaptLegacyHandler(gmailMoveToSpamHandler)))

	// Add create filter tool
	createFilterTool := mcp.NewTool("gmail_create_filter",
		mcp.WithDescription("Create a Gmail filter with specified criteria and actions"),
		mcp.WithString("from", mcp.Description("Filter emails from this sender")),
		mcp.WithString("to", mcp.Description("Filter emails to this recipient")),
		mcp.WithString("subject", mcp.Description("Filter emails with this subject")),
		mcp.WithString("query", mcp.Description("Additional search query criteria")),
		mcp.WithBoolean("add_label", mcp.Description("Add label to matching messages")),
		mcp.WithString("label_name", mcp.Description("Name of the label to add (required if add_label is true)")),
		mcp.WithBoolean("mark_important", mcp.Description("Mark matching messages as important")),
		mcp.WithBoolean("mark_read", mcp.Description("Mark matching messages as read")),
		mcp.WithBoolean("archive", mcp.Description("Archive matching messages")),
	)
	s.AddTool(createFilterTool, util.ErrorGuard(util.AdaptLegacyHandler(gmailCreateFilterHandler)))

	// List filters tool
	listFiltersTool := mcp.NewTool("gmail_list_filters",
		mcp.WithDescription("List all Gmail filters in the account"),
	)
	s.AddTool(listFiltersTool, util.ErrorGuard(util.AdaptLegacyHandler(gmailListFiltersHandler)))

	// List labels tool
	listLabelsTool := mcp.NewTool("gmail_list_labels",
		mcp.WithDescription("List all Gmail labels in the account"),
	)
	s.AddTool(listLabelsTool, util.ErrorGuard(util.AdaptLegacyHandler(gmailListLabelsHandler)))

	// Add delete filter tool
	deleteFilterTool := mcp.NewTool("gmail_delete_filter",
		mcp.WithDescription("Delete a Gmail filter by its ID"),
		mcp.WithString("filter_id", mcp.Required(), mcp.Description("The ID of the filter to delete")),
	)
	s.AddTool(deleteFilterTool, util.ErrorGuard(util.AdaptLegacyHandler(gmailDeleteFilterHandler)))

	// Add delete label tool
	deleteLabelTool := mcp.NewTool("gmail_delete_label",
		mcp.WithDescription("Delete a Gmail label by its ID"),
		mcp.WithString("label_id", mcp.Required(), mcp.Description("The ID of the label to delete")),
	)
	s.AddTool(deleteLabelTool, util.ErrorGuard(util.AdaptLegacyHandler(gmailDeleteLabelHandler)))
}

var gmailService = sync.OnceValue(func() *gmail.Service {
	ctx := context.Background()

	tokenFile := os.Getenv("GOOGLE_TOKEN_FILE")
	if tokenFile == "" {
		panic("GOOGLE_TOKEN_FILE environment variable must be set")
	}

	credentialsFile := os.Getenv("GOOGLE_CREDENTIALS_FILE")
	if credentialsFile == "" {
		panic("GOOGLE_CREDENTIALS_FILE environment variable must be set")
	}

	client := services.GoogleHttpClient(tokenFile, credentialsFile)

	srv, err := gmail.NewService(ctx, option.WithHTTPClient(client))
	if err != nil {
		panic(fmt.Sprintf("failed to create Gmail service: %v", err))
	}

	return srv
})

func gmailSearchHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	query, ok := arguments["query"].(string)
	if !ok {
		return mcp.NewToolResultError("query must be a string"), nil
	}

	user := "me"

	listCall := gmailService().Users.Messages.List(user).Q(query).MaxResults(10)

	resp, err := listCall.Do()
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to search emails: %v", err)), nil
	}

	var result strings.Builder
	result.WriteString(fmt.Sprintf("Found %d emails:\n\n", len(resp.Messages)))

	for _, msg := range resp.Messages {
		message, err := gmailService().Users.Messages.Get(user, msg.Id).Do()
		if err != nil {
			log.Printf("Failed to get message %s: %v", msg.Id, err)
			continue
		}

		details := make(map[string]string)
		for _, header := range message.Payload.Headers {
			switch header.Name {
			case "From":
				details["from"] = header.Value
			case "Subject":
				details["subject"] = header.Value
			case "Date":
				details["date"] = header.Value
			}
		}

		result.WriteString(fmt.Sprintf("Message ID: %s\n", msg.Id))
		result.WriteString(fmt.Sprintf("From: %s\n", details["from"]))
		result.WriteString(fmt.Sprintf("Subject: %s\n", details["subject"]))
		result.WriteString(fmt.Sprintf("Date: %s\n", details["date"]))
		result.WriteString(fmt.Sprintf("Snippet: %s\n", message.Snippet))
		result.WriteString("-------------------\n")
	}

	return mcp.NewToolResultText(result.String()), nil
}

func gmailMoveToSpamHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	messageIdsStr, ok := arguments["message_ids"].(string)
	if !ok {
		return mcp.NewToolResultError("message_ids must be a string"), nil
	}

	messageIds := strings.Split(messageIdsStr, ",")

	if len(messageIds) == 0 {
		return mcp.NewToolResultError("no message IDs provided"), nil
	}

	user := "me"

	for _, messageId := range messageIds {
		_, err := gmailService().Users.Messages.Modify(user, messageId, &gmail.ModifyMessageRequest{
			AddLabelIds: []string{"SPAM"},
		}).Do()
		if err != nil {
			return mcp.NewToolResultError(fmt.Sprintf("failed to move email %s to spam: %v", messageId, err)), nil
		}
	}

	return mcp.NewToolResultText(fmt.Sprintf("Successfully moved %d emails to spam.", len(messageIds))), nil
}

func gmailCreateFilterHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	// Create filter criteria
	criteria := &gmail.FilterCriteria{}

	if from, ok := arguments["from"].(string); ok && from != "" {
		criteria.From = from
	}
	if to, ok := arguments["to"].(string); ok && to != "" {
		criteria.To = to
	}
	if subject, ok := arguments["subject"].(string); ok && subject != "" {
		criteria.Subject = subject
	}
	if query, ok := arguments["query"].(string); ok && query != "" {
		criteria.Query = query
	}

	// Create filter action
	action := &gmail.FilterAction{}

	if addLabel, ok := arguments["add_label"].(bool); ok && addLabel {
		labelName, ok := arguments["label_name"].(string)
		if !ok || labelName == "" {
			return mcp.NewToolResultError("label_name is required when add_label is true"), nil
		}

		// First, create or get the label
		label, err := createOrGetLabel(labelName)
		if err != nil {
			return mcp.NewToolResultError(fmt.Sprintf("failed to create/get label: %v", err)), nil
		}
		action.AddLabelIds = []string{label.Id}
	}

	if markImportant, ok := arguments["mark_important"].(bool); ok && markImportant {
		action.AddLabelIds = append(action.AddLabelIds, "IMPORTANT")
	}

	if markRead, ok := arguments["mark_read"].(bool); ok && markRead {
		action.RemoveLabelIds = append(action.RemoveLabelIds, "UNREAD")
	}

	if archive, ok := arguments["archive"].(bool); ok && archive {
		action.RemoveLabelIds = append(action.RemoveLabelIds, "INBOX")
	}

	// Create the filter
	filter := &gmail.Filter{
		Criteria: criteria,
		Action:   action,
	}

	result, err := gmailService().Users.Settings.Filters.Create("me", filter).Do()
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to create filter: %v", err)), nil
	}

	return mcp.NewToolResultText(fmt.Sprintf("Successfully created filter with ID: %s", result.Id)), nil
}

func createOrGetLabel(name string) (*gmail.Label, error) {
	// First try to find existing label
	labels, err := gmailService().Users.Labels.List("me").Do()
	if err != nil {
		return nil, fmt.Errorf("failed to list labels: %v", err)
	}

	for _, label := range labels.Labels {
		if label.Name == name {
			return label, nil
		}
	}

	// If not found, create new label
	newLabel := &gmail.Label{
		Name:                  name,
		MessageListVisibility: "show",
		LabelListVisibility:   "labelShow",
	}

	label, err := gmailService().Users.Labels.Create("me", newLabel).Do()
	if err != nil {
		return nil, fmt.Errorf("failed to create label: %v", err)
	}

	return label, nil
}

func gmailListFiltersHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	filters, err := gmailService().Users.Settings.Filters.List("me").Do()
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to list filters: %v", err)), nil
	}

	var result strings.Builder
	result.WriteString(fmt.Sprintf("Found %d filters:\n\n", len(filters.Filter)))

	for _, filter := range filters.Filter {
		result.WriteString(fmt.Sprintf("Filter ID: %s\n", filter.Id))

		// Write criteria
		result.WriteString("Criteria:\n")
		if filter.Criteria.From != "" {
			result.WriteString(fmt.Sprintf("  From: %s\n", filter.Criteria.From))
		}
		if filter.Criteria.To != "" {
			result.WriteString(fmt.Sprintf("  To: %s\n", filter.Criteria.To))
		}
		if filter.Criteria.Subject != "" {
			result.WriteString(fmt.Sprintf("  Subject: %s\n", filter.Criteria.Subject))
		}
		if filter.Criteria.Query != "" {
			result.WriteString(fmt.Sprintf("  Query: %s\n", filter.Criteria.Query))
		}

		// Write actions
		result.WriteString("Actions:\n")
		if len(filter.Action.AddLabelIds) > 0 {
			result.WriteString(fmt.Sprintf("  Add Labels: %s\n", strings.Join(filter.Action.AddLabelIds, ", ")))
		}
		if len(filter.Action.RemoveLabelIds) > 0 {
			result.WriteString(fmt.Sprintf("  Remove Labels: %s\n", strings.Join(filter.Action.RemoveLabelIds, ", ")))
		}
		result.WriteString("-------------------\n")
	}

	return mcp.NewToolResultText(result.String()), nil
}

func gmailListLabelsHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	labels, err := gmailService().Users.Labels.List("me").Do()
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to list labels: %v", err)), nil
	}

	var result strings.Builder
	result.WriteString(fmt.Sprintf("Found %d labels:\n\n", len(labels.Labels)))

	// First list system labels
	result.WriteString("System Labels:\n")
	for _, label := range labels.Labels {
		if label.Type == "system" {
			result.WriteString(fmt.Sprintf("- %s (ID: %s)\n", label.Name, label.Id))
		}
	}

	// Then list user labels
	result.WriteString("\nUser Labels:\n")
	for _, label := range labels.Labels {
		if label.Type == "user" {
			result.WriteString(fmt.Sprintf("- %s (ID: %s)\n", label.Name, label.Id))
			if label.MessagesTotal > 0 {
				result.WriteString(fmt.Sprintf("  Messages: %d\n", label.MessagesTotal))
			}
		}
	}

	return mcp.NewToolResultText(result.String()), nil
}

func gmailDeleteFilterHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	filterID, ok := arguments["filter_id"].(string)
	if !ok {
		return mcp.NewToolResultError("filter_id must be a string"), nil
	}

	if filterID == "" {
		return mcp.NewToolResultError("filter_id cannot be empty"), nil
	}

	err := gmailService().Users.Settings.Filters.Delete("me", filterID).Do()
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to delete filter: %v", err)), nil
	}

	return mcp.NewToolResultText(fmt.Sprintf("Successfully deleted filter with ID: %s", filterID)), nil
}

func gmailDeleteLabelHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	labelID, ok := arguments["label_id"].(string)
	if !ok {
		return mcp.NewToolResultError("label_id must be a string"), nil
	}

	if labelID == "" {
		return mcp.NewToolResultError("label_id cannot be empty"), nil
	}

	err := gmailService().Users.Labels.Delete("me", labelID).Do()
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to delete label: %v", err)), nil
	}

	return mcp.NewToolResultText(fmt.Sprintf("Successfully deleted label with ID: %s", labelID)), nil
}

```

--------------------------------------------------------------------------------
/tools/googlemaps_tools.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"context"
	"encoding/json"
	"fmt"
	"math"
	"os"

	"github.com/athapong/aio-mcp/util"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
	"googlemaps.github.io/maps"
)

// RegisterGoogleMapTools registers all Google Maps related tools with the MCP server
func RegisterGoogleMapTools(s *server.MCPServer) {
	// Location search tool
	locationSearchTool := mcp.NewTool("maps_location_search",
		mcp.WithDescription("Search for locations using Google Maps"),
		mcp.WithString("query", mcp.Required(), mcp.Description("Location to search for")),
		mcp.WithNumber("limit", mcp.Description("Maximum number of results to return (default: 5)")),
	)
	s.AddTool(locationSearchTool, util.ErrorGuard(util.AdaptLegacyHandler(locationSearchHandler)))

	// Geocoding tool
	geocodingTool := mcp.NewTool("maps_geocoding",
		mcp.WithDescription("Convert addresses to coordinates and vice versa"),
		mcp.WithString("address", mcp.Description("Address to geocode (required if not using lat/lng)")),
		mcp.WithNumber("lat", mcp.Description("Latitude for reverse geocoding (required with lng if not using address)")),
		mcp.WithNumber("lng", mcp.Description("Longitude for reverse geocoding (required with lat if not using address)")),
	)
	s.AddTool(geocodingTool, util.ErrorGuard(util.AdaptLegacyHandler(geocodingHandler)))

	// Place details tool
	placeDetailsTool := mcp.NewTool("maps_place_details",
		mcp.WithDescription("Get detailed information about a specific place"),
		mcp.WithString("place_id", mcp.Required(), mcp.Description("Google Maps place ID")),
	)
	s.AddTool(placeDetailsTool, util.ErrorGuard(util.AdaptLegacyHandler(placeDetailsHandler)))

	// Directions tool
	directionsTool := mcp.NewTool("maps_directions",
		mcp.WithDescription("Get directions between locations"),
		mcp.WithString("origin", mcp.Required(), mcp.Description("Starting point (address, place ID, or lat,lng)")),
		mcp.WithString("destination", mcp.Required(), mcp.Description("Destination point (address, place ID, or lat,lng)")),
		mcp.WithString("mode", mcp.Description("Travel mode: driving (default), walking, bicycling, transit")),
		mcp.WithString("waypoints", mcp.Description("Optional waypoints separated by '|' (e.g. 'place_id:ChIJ...|place_id:ChIJ...')")),
		mcp.WithBoolean("alternatives", mcp.Description("Return alternative routes if available")),
	)
	s.AddTool(directionsTool, util.ErrorGuard(util.AdaptLegacyHandler(directionsHandler)))
}

// getGoogleMapsClient creates and returns a Google Maps client
func getGoogleMapsClient() (*maps.Client, error) {
	apiKey := os.Getenv("GOOGLE_MAPS_API_KEY")
	if apiKey == "" {
		return nil, fmt.Errorf("GOOGLE_MAPS_API_KEY environment variable not set")
	}

	return maps.NewClient(maps.WithAPIKey(apiKey))
}

// locationSearchHandler handles location search requests
func locationSearchHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	query, ok := arguments["query"].(string)
	if !ok || query == "" {
		return mcp.NewToolResultError("query is required and must be a string"), nil
	}

	limit := 5 // default limit
	if limitVal, ok := arguments["limit"].(float64); ok {
		limit = int(limitVal)
	}

	client, err := getGoogleMapsClient()
	if err != nil {
		return mcp.NewToolResultError(err.Error()), nil
	}

	req := &maps.TextSearchRequest{
		Query: query,
	}

	resp, err := client.TextSearch(context.Background(), req)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Google Maps API error: %v", err)), nil
	}

	if len(resp.Results) == 0 {
		return mcp.NewToolResultText("No locations found for query: " + query), nil
	}

	// Limit the number of results
	if len(resp.Results) > limit {
		resp.Results = resp.Results[:limit]
	}

	var results []map[string]interface{}
	for _, place := range resp.Results {
		results = append(results, map[string]interface{}{
			"name":     place.Name,
			"address":  place.FormattedAddress,
			"place_id": place.PlaceID,
			"location": map[string]float64{"lat": place.Geometry.Location.Lat, "lng": place.Geometry.Location.Lng},
			"rating":   place.Rating,
			"types":    place.Types,
		})
	}

	data := map[string]interface{}{
		"query":   query,
		"results": results,
	}

	jsonData, err := json.Marshal(data)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal JSON: %v", err)), nil
	}

	return mcp.NewToolResultText(string(jsonData)), nil
}

// geocodingHandler handles geocoding and reverse geocoding requests
func geocodingHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	client, err := getGoogleMapsClient()
	if err != nil {
		return mcp.NewToolResultError(err.Error()), nil
	}

	// Check if we're doing geocoding (address to coordinates)
	if address, ok := arguments["address"].(string); ok && address != "" {
		return handleGeocoding(client, address)
	}

	// Check if we're doing reverse geocoding (coordinates to address)
	lat, latOk := arguments["lat"].(float64)
	lng, lngOk := arguments["lng"].(float64)
	if latOk && lngOk {
		return handleReverseGeocoding(client, lat, lng)
	}

	return mcp.NewToolResultError("Please provide either an address for geocoding or lat/lng for reverse geocoding"), nil
}

// handleGeocoding processes an address to get coordinates
func handleGeocoding(client *maps.Client, address string) (*mcp.CallToolResult, error) {
	req := &maps.GeocodingRequest{
		Address: address,
	}

	resp, err := client.Geocode(context.Background(), req)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Google Maps API error: %v", err)), nil
	}

	if len(resp) == 0 {
		return mcp.NewToolResultText("No geocoding results found for address: " + address), nil
	}

	var results []map[string]interface{}
	for _, result := range resp {
		results = append(results, map[string]interface{}{
			"formatted_address": result.FormattedAddress,
			"place_id":          result.PlaceID,
			"location":          map[string]float64{"lat": result.Geometry.Location.Lat, "lng": result.Geometry.Location.Lng},
			"location_type":     result.Geometry.LocationType,
			"types":             result.Types,
		})
	}

	data := map[string]interface{}{
		"query":   address,
		"type":    "geocoding",
		"results": results,
	}

	jsonData, err := json.Marshal(data)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal JSON: %v", err)), nil
	}

	return mcp.NewToolResultText(string(jsonData)), nil
}

// handleReverseGeocoding processes coordinates to get an address
func handleReverseGeocoding(client *maps.Client, lat, lng float64) (*mcp.CallToolResult, error) {
	req := &maps.GeocodingRequest{
		LatLng: &maps.LatLng{Lat: lat, Lng: lng},
	}

	resp, err := client.Geocode(context.Background(), req)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Google Maps API error: %v", err)), nil
	}

	if len(resp) == 0 {
		return mcp.NewToolResultText(fmt.Sprintf("No reverse geocoding results found for coordinates: %f,%f", lat, lng)), nil
	}

	var results []map[string]interface{}
	for _, result := range resp {
		results = append(results, map[string]interface{}{
			"formatted_address": result.FormattedAddress,
			"place_id":          result.PlaceID,
			"types":             result.Types,
		})
	}

	data := map[string]interface{}{
		"coordinates": map[string]float64{"lat": lat, "lng": lng},
		"type":        "reverse_geocoding",
		"results":     results,
	}

	jsonData, err := json.Marshal(data)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal JSON: %v", err)), nil
	}

	return mcp.NewToolResultText(string(jsonData)), nil
}

// placeDetailsHandler handles requests for detailed place information
func placeDetailsHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	placeID, ok := arguments["place_id"].(string)
	if !ok || placeID == "" {
		return mcp.NewToolResultError("place_id is required and must be a string"), nil
	}

	client, err := getGoogleMapsClient()
	if err != nil {
		return mcp.NewToolResultError(err.Error()), nil
	}

	req := &maps.PlaceDetailsRequest{
		PlaceID: placeID,
		Fields: []maps.PlaceDetailsFieldMask{
			maps.PlaceDetailsFieldMaskName,
			maps.PlaceDetailsFieldMaskFormattedAddress,
			maps.PlaceDetailsFieldMaskGeometry,
			maps.PlaceDetailsFieldMaskTypes,
			maps.PlaceDetailsFieldMaskOpeningHours,
			maps.PlaceDetailsFieldMaskWebsite,
			maps.PlaceDetailsFieldMaskReviews,
			maps.PlaceDetailsFieldMaskPhotos,
		},
	}

	resp, err := client.PlaceDetails(context.Background(), req)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Google Maps API error: %v", err)), nil
	}

	details := map[string]interface{}{
		"name":               resp.Name,
		"formatted_address":  resp.FormattedAddress,
		"place_id":           resp.PlaceID,
		"location":           map[string]float64{"lat": resp.Geometry.Location.Lat, "lng": resp.Geometry.Location.Lng},
		"types":              resp.Types,
		"rating":             resp.Rating,
		"user_ratings_total": resp.UserRatingsTotal,
	}

	if resp.Website != "" {
		details["website"] = resp.Website
	}

	if resp.FormattedPhoneNumber != "" {
		details["phone_number"] = resp.FormattedPhoneNumber
	}

	if len(resp.OpeningHours.WeekdayText) > 0 {
		details["opening_hours"] = resp.OpeningHours.WeekdayText
	}

	jsonData, err := json.Marshal(details)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal JSON: %v", err)), nil
	}

	return mcp.NewToolResultText(string(jsonData)), nil
}

// directionsHandler handles requests for directions between two locations
func directionsHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	// Extract required parameters
	origin, ok := arguments["origin"].(string)
	if !ok || origin == "" {
		return mcp.NewToolResultError("origin is required and must be a string"), nil
	}

	destination, ok := arguments["destination"].(string)
	if !ok || destination == "" {
		return mcp.NewToolResultError("destination is required and must be a string"), nil
	}

	// Extract optional parameters
	mode := "driving" // default mode
	if modeVal, ok := arguments["mode"].(string); ok && modeVal != "" {
		switch modeVal {
		case "driving", "walking", "bicycling", "transit":
			mode = modeVal
		default:
			return mcp.NewToolResultError("Invalid mode. Must be one of: driving, walking, bicycling, transit"), nil
		}
	}

	// Create Google Maps client
	client, err := getGoogleMapsClient()
	if err != nil {
		return mcp.NewToolResultError(err.Error()), nil
	}

	// Build directions request
	req := &maps.DirectionsRequest{
		Origin:        origin,
		Destination:   destination,
		Mode:          maps.TravelModeDriving,
		DepartureTime: "now",
	}

	// Add waypoints if provided
	if waypoints, ok := arguments["waypoints"].(string); ok && waypoints != "" {
		req.Waypoints = []string{waypoints}
	}

	// Add alternatives if requested
	if alternatives, ok := arguments["alternatives"].(bool); ok {
		req.Alternatives = alternatives
	}

	// Call the Directions API
	routes, _, err := client.Directions(context.Background(), req)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Google Maps API error: %v", err)), nil
	}

	if len(routes) == 0 {
		return mcp.NewToolResultText(fmt.Sprintf("No directions found from %s to %s", origin, destination)), nil
	}

	// Format the response
	var formattedRoutes []map[string]interface{}
	for i, route := range routes {
		routeInfo := map[string]interface{}{
			"summary": route.Summary,
		}

		// Calculate total distance and duration
		var totalDistance int
		var totalDuration float64
		var steps []map[string]interface{}

		for _, leg := range route.Legs {
			totalDistance += leg.Distance.Meters
			totalDuration += leg.Duration.Seconds()

			for _, step := range leg.Steps {
				stepInfo := map[string]interface{}{
					"instruction":      step.HTMLInstructions,
					"distance":         map[string]interface{}{"meters": step.Distance.Meters, "text": step.Distance.HumanReadable},
					"duration":         map[string]interface{}{"seconds": step.Duration.Seconds(), "text": step.Duration.String()},
					"travel_mode":      step.TravelMode,
					"start_location":   map[string]float64{"lat": step.StartLocation.Lat, "lng": step.StartLocation.Lng},
					"end_location":     map[string]float64{"lat": step.EndLocation.Lat, "lng": step.EndLocation.Lng},
					"encoded_polyline": step.Polyline.Points,
				}
				steps = append(steps, stepInfo)
			}
		}
		// Format as hours and minutes for better readability
		hours := int(totalDuration / 3600)
		minutes := int(math.Mod(totalDuration, 3600) / 60)
		durationText := ""
		durationText = ""
		if hours > 0 {
			durationText = fmt.Sprintf("%d hours", hours)
			if minutes > 0 {
				durationText += fmt.Sprintf(" %d minutes", minutes)
			}
		} else {
			durationText = fmt.Sprintf("%d minutes", minutes)
		}

		// Add distance and duration info
		routeInfo["distance"] = map[string]interface{}{
			"meters": totalDistance,
			"text":   fmt.Sprintf("%.1f km", float64(totalDistance)/1000),
		}
		routeInfo["duration"] = map[string]interface{}{
			"seconds": totalDuration,
			"text":    durationText,
		}
		routeInfo["steps"] = steps
		routeInfo["encoded_overview_polyline"] = route.OverviewPolyline.Points
		routeInfo["warnings"] = route.Warnings
		routeInfo["route_index"] = i

		formattedRoutes = append(formattedRoutes, routeInfo)
	}

	data := map[string]interface{}{
		"origin":      origin,
		"destination": destination,
		"mode":        mode,
		"routes":      formattedRoutes,
	}

	jsonData, err := json.Marshal(data)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal JSON: %v", err)), nil
	}

	return mcp.NewToolResultText(string(jsonData)), nil
}

```

--------------------------------------------------------------------------------
/tools/jira.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"context"
	"encoding/json" // added for unmarshalling raw issue
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/athapong/aio-mcp/services"
	"github.com/athapong/aio-mcp/util"
	"github.com/ctreminiom/go-atlassian/pkg/infra/models"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
)

// RegisterJiraTool registers the Jira tools to the server
func RegisterJiraTool(s *server.MCPServer) {
	// Get issue details tool
	jiraGetIssueTool := mcp.NewTool("jira_get_issue",
		mcp.WithDescription("Retrieve detailed information about a specific Jira issue including its status, assignee, description, subtasks, and available transitions"),
		mcp.WithString("issue_key", mcp.Required(), mcp.Description("The unique identifier of the Jira issue (e.g., KP-2, PROJ-123)")),
	)
	s.AddTool(jiraGetIssueTool, util.ErrorGuard(util.AdaptLegacyHandler(jiraIssueHandler)))

	// Search issues tool
	jiraSearchTool := mcp.NewTool("jira_search_issue",
		mcp.WithDescription("Search for Jira issues using JQL (Jira Query Language). Returns key details like summary, status, assignee, and priority for matching issues"),
		mcp.WithString("jql", mcp.Required(), mcp.Description("JQL query string (e.g., 'project = KP AND status = \"In Progress\"')")),
	)

	// List sprints tool
	jiraListSprintTool := mcp.NewTool("jira_list_sprints",
		mcp.WithDescription("List all active and future sprints for a specific Jira board, including sprint IDs, names, states, and dates"),
		mcp.WithString("board_id", mcp.Required(), mcp.Description("Numeric ID of the Jira board (can be found in board URL)")),
	)

	// Create issue tool
	jiraCreateIssueTool := mcp.NewTool("jira_create_issue",
		mcp.WithDescription("Create a new Jira issue with specified details. Returns the created issue's key, ID, and URL"),
		mcp.WithString("project_key", mcp.Required(), mcp.Description("Project identifier where the issue will be created (e.g., KP, PROJ)")),
		mcp.WithString("summary", mcp.Required(), mcp.Description("Brief title or headline of the issue")),
		mcp.WithString("description", mcp.Required(), mcp.Description("Detailed explanation of the issue")),
		mcp.WithString("issue_type", mcp.Required(), mcp.Description("Type of issue to create (common types: Bug, Task, Story, Epic)")),
	)

	// Update issue tool
	jiraUpdateIssueTool := mcp.NewTool("jira_update_issue",
		mcp.WithDescription("Modify an existing Jira issue's details. Supports partial updates - only specified fields will be changed"),
		mcp.WithString("issue_key", mcp.Required(), mcp.Description("The unique identifier of the issue to update (e.g., KP-2)")),
		mcp.WithString("summary", mcp.Description("New title for the issue (optional)")),
		mcp.WithString("description", mcp.Description("New description for the issue (optional)")),
	)

	// Add status list tool
	jiraStatusListTool := mcp.NewTool("jira_list_statuses",
		mcp.WithDescription("Retrieve all available issue status IDs and their names for a specific Jira project"),
		mcp.WithString("project_key", mcp.Required(), mcp.Description("Project identifier (e.g., KP, PROJ)")),
	)

	// Add new tool definition in RegisterJiraTool function
	jiraTransitionTool := mcp.NewTool("jira_transition_issue",
		mcp.WithDescription("Transition an issue through its workflow using a valid transition ID. Get available transitions from jira_get_issue"),
		mcp.WithString("issue_key", mcp.Required(), mcp.Description("The issue to transition (e.g., KP-123)")),
		mcp.WithString("transition_id", mcp.Required(), mcp.Description("Transition ID from available transitions list")),
		mcp.WithString("comment", mcp.Description("Optional comment to add with transition")),
	)

	s.AddTool(jiraSearchTool, util.ErrorGuard(util.AdaptLegacyHandler(jiraSearchHandler)))
	s.AddTool(jiraListSprintTool, util.ErrorGuard(util.AdaptLegacyHandler(jiraListSprintHandler)))
	s.AddTool(jiraCreateIssueTool, util.ErrorGuard(util.AdaptLegacyHandler(jiraCreateIssueHandler)))
	s.AddTool(jiraUpdateIssueTool, util.ErrorGuard(util.AdaptLegacyHandler(jiraUpdateIssueHandler)))
	s.AddTool(jiraStatusListTool, util.ErrorGuard(util.AdaptLegacyHandler(jiraGetStatusesHandler)))
	s.AddTool(jiraTransitionTool, util.ErrorGuard(util.AdaptLegacyHandler(jiraTransitionIssueHandler)))
}

func jiraUpdateIssueHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	client := services.JiraClient()

	issueKey, ok := arguments["issue_key"].(string)
	if !ok {
		return nil, fmt.Errorf("issue_key argument is required")
	}

	// Create update payload
	payload := &models.IssueSchemeV2{
		Fields: &models.IssueFieldsSchemeV2{},
	}

	// Check and add optional fields if provided
	if summary, ok := arguments["summary"].(string); ok && summary != "" {
		payload.Fields.Summary = summary
	}

	if description, ok := arguments["description"].(string); ok && description != "" {
		payload.Fields.Description = description
	}

	ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
	defer cancel()

	response, err := client.Issue.Update(ctx, issueKey, true, payload, nil, nil)
	if err != nil {
		if response != nil {
			return nil, fmt.Errorf("failed to update issue: %s (endpoint: %s)", response.Bytes.String(), response.Endpoint)
		}
		return nil, fmt.Errorf("failed to update issue: %v", err)
	}

	return mcp.NewToolResultText("Issue updated successfully!"), nil
}

func jiraCreateIssueHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	client := services.JiraClient()

	projectKey, ok := arguments["project_key"].(string)
	if !ok {
		return nil, fmt.Errorf("project_key argument is required")
	}

	summary, ok := arguments["summary"].(string)
	if !ok {
		return nil, fmt.Errorf("summary argument is required")
	}

	description, ok := arguments["description"].(string)
	if !ok {
		return nil, fmt.Errorf("description argument is required")
	}

	issueType, ok := arguments["issue_type"].(string)
	if !ok {
		return nil, fmt.Errorf("issue_type argument is required")
	}

	ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
	defer cancel()

	var payload = models.IssueSchemeV2{
		Fields: &models.IssueFieldsSchemeV2{
			Summary:     summary,
			Project:     &models.ProjectScheme{Key: projectKey},
			Description: description,
			IssueType:   &models.IssueTypeScheme{Name: issueType},
		},
	}

	issue, response, err := client.Issue.Create(ctx, &payload, nil)
	if err != nil {
		if response != nil {
			return nil, fmt.Errorf("failed to create issue: %s (endpoint: %s)", response.Bytes.String(), response.Endpoint)
		}
		return nil, fmt.Errorf("failed to create issue: %v", err)
	}

	result := fmt.Sprintf("Issue created successfully!\nKey: %s\nID: %s\nURL: %s", issue.Key, issue.ID, issue.Self)
	return mcp.NewToolResultText(result), nil
}

func jiraListSprintHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	boardIDStr, ok := arguments["board_id"].(string)
	if !ok {
		return nil, fmt.Errorf("board_id argument is required")
	}

	boardID, err := strconv.Atoi(boardIDStr)
	if err != nil {
		return nil, fmt.Errorf("invalid board_id: %v", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
	defer cancel()

	sprints, response, err := services.AgileClient().Board.Sprints(ctx, boardID, 0, 50, []string{"active", "future"})
	if err != nil {
		if response != nil {
			return nil, fmt.Errorf("failed to get sprints: %s (endpoint: %s)", response.Bytes.String(), response.Endpoint)
		}
		return nil, fmt.Errorf("failed to get sprints: %v", err)
	}

	if len(sprints.Values) == 0 {
		return mcp.NewToolResultText("No sprints found for this board."), nil
	}

	var result string
	for _, sprint := range sprints.Values {
		result += fmt.Sprintf("ID: %d\nName: %s\nState: %s\nStartDate: %s\nEndDate: %s\n\n", sprint.ID, sprint.Name, sprint.State, sprint.StartDate, sprint.EndDate)
	}

	return mcp.NewToolResultText(result), nil
}

func jiraSearchHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	client := services.JiraClient()

	// Get search text from arguments
	jql, ok := arguments["jql"].(string)
	if !ok {
		return nil, fmt.Errorf("jql argument is required")
	}

	ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
	defer cancel()

	searchResult, response, err := client.Issue.Search.Get(ctx, jql, nil, nil, 0, 30, "")
	if err != nil {
		if response != nil {
			return nil, fmt.Errorf("failed to search issues: %s (endpoint: %s)", response.Bytes.String(), response.Endpoint)
		}
		return nil, fmt.Errorf("failed to search issues: %v", err)
	}

	if len(searchResult.Issues) == 0 {
		return mcp.NewToolResultText("No issues found matching the search criteria."), nil
	}

	var sb strings.Builder
	for _, issue := range searchResult.Issues {
		sb.WriteString(fmt.Sprintf("Key: %s\n", issue.Key))

		if issue.Fields.Summary != "" {
			sb.WriteString(fmt.Sprintf("Summary: %s\n", issue.Fields.Summary))
		}

		if issue.Fields.Status != nil && issue.Fields.Status.Name != "" {
			sb.WriteString(fmt.Sprintf("Status: %s\n", issue.Fields.Status.Name))
		}

		if issue.Fields.Created != "" {
			sb.WriteString(fmt.Sprintf("Created: %s\n", issue.Fields.Created))
		}

		if issue.Fields.Updated != "" {
			sb.WriteString(fmt.Sprintf("Updated: %s\n", issue.Fields.Updated))
		}

		if issue.Fields.Assignee != nil {
			sb.WriteString(fmt.Sprintf("Assignee: %s\n", issue.Fields.Assignee.DisplayName))
		} else {
			sb.WriteString("Assignee: Unassigned\n")
		}

		if issue.Fields.Priority != nil {
			sb.WriteString(fmt.Sprintf("Priority: %s\n", issue.Fields.Priority.Name))
		} else {
			sb.WriteString("Priority: Unset\n")
		}

		if issue.Fields.Resolutiondate != "" {
			sb.WriteString(fmt.Sprintf("Resolution date: %s\n", issue.Fields.Resolutiondate))
		}

		sb.WriteString("\n")
	}

	return mcp.NewToolResultText(sb.String()), nil
}

// Add a helper function to format custom field values
func formatCustomFieldValue(fieldName string, value interface{}) string {
	if value == nil {
		return "None"
	}
	if m, ok := value.(map[string]interface{}); ok {
		if dn, exists := m["displayName"]; exists {
			return fmt.Sprintf("%v", dn)
		}
		if dn, exists := m["value"]; exists {
			return fmt.Sprintf("%v", dn)
		}
		if dn, exists := m["name"]; exists {
			return fmt.Sprintf("%v", dn)
		}
	}
	switch v := value.(type) {
	case string:
		return v
	case float64:
		return fmt.Sprintf("%.2f", v)
	case []interface{}:
		var parts []string
		for _, item := range v {
			parts = append(parts, fmt.Sprintf("%v", item))
		}
		return strings.Join(parts, ", ")
	default:
		return fmt.Sprintf("%v", v)
	}
}

func jiraIssueHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	client := services.JiraClient()

	// Get issue key from arguments
	issueKey, ok := arguments["issue_key"].(string)
	if !ok {
		return nil, fmt.Errorf("issue_key argument is required")
	}

	ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
	defer cancel()

	// Request all fields including custom fields
	issue, response, err := client.Issue.Get(ctx, issueKey, []string{"*all"}, []string{"transitions"})
	if err != nil {
		if response != nil {
			return nil, fmt.Errorf("failed to get issue: %s (endpoint: %s)", response.Bytes.String(), response.Endpoint)
		}
		return nil, fmt.Errorf("failed to get issue: %v", err)
	}

	// Build subtasks string if they exist
	var subtasks string
	if issue.Fields.Subtasks != nil {
		subtasks = "\nSubtasks:\n"
		for _, subTask := range issue.Fields.Subtasks {
			subtasks += fmt.Sprintf("- %s: %s\n", subTask.Key, subTask.Fields.Summary)
		}
	}

	// Build transitions string
	var transitions string
	for _, transition := range issue.Transitions {
		transitions += fmt.Sprintf("- %s (ID: %s)\n", transition.Name, transition.ID)
	}

	// Get reporter, assignee, and priority names with nil checks
	reporterName := "Unassigned"
	if issue.Fields.Reporter != nil {
		reporterName = issue.Fields.Reporter.DisplayName
	}

	assigneeName := "Unassigned"
	if issue.Fields.Assignee != nil {
		assigneeName = issue.Fields.Assignee.DisplayName
	}

	priorityName := "None"
	if issue.Fields.Priority != nil {
		priorityName = issue.Fields.Priority.Name
	}

	// Extract custom fields by unmarshalling the raw JSON response
	var rawIssue map[string]interface{}
	err = json.Unmarshal([]byte(response.Bytes.String()), &rawIssue)
	if err != nil {
		return nil, fmt.Errorf("failed to unmarshal raw issue: %v", err)
	}
	fieldsData, ok := rawIssue["fields"].(map[string]interface{})
	if !ok {
		return nil, fmt.Errorf("raw issue fields not found")
	}

	// Retrieve field definitions for mapping custom field IDs to friendly names
	fieldsDef, resp2, err2 := client.Issue.Field.Gets(ctx)
	if err2 != nil {
		if resp2 != nil {
			return nil, fmt.Errorf("failed to get field definitions: %s (endpoint: %s)", resp2.Bytes.String(), resp2.Endpoint)
		}
		return nil, fmt.Errorf("failed to get field definitions: %v", err2)
	}
	// Define the custom field names to display
	desiredCustom := map[string]bool{
		"Development":          true,
		"Create branch":        true,
		"Create commit":        true,
		"Releases":             true,
		"Add feature flag":     true,
		"Labels":               true,
		"Squad":                true,
		"Story/Bug Type":       true,
		"Deployment Object ID": true,
		"Est. QA Effort":       true,
		"BE Story point":       true,
		"FE Story point":       true,
		"QA Story point":       true,
		"Developer":            true,
		"QA":                   true,
		"Story Points":         true,
		"Parent":               true,
		"Sprint":               true,
		"Fix versions":         true,
		"Original estimate":    true,
		"Time tracking":        true,
		"Components":           true,
		"Due date":             true,
	}

	var filteredCustomFields strings.Builder
	filteredCustomFields.WriteString("\nFiltered Custom Fields:\n")
	for _, fieldDef := range fieldsDef {
		if fieldDef.Custom && desiredCustom[fieldDef.Name] {
			if value, exists := fieldsData[fieldDef.ID]; exists {
				formatted := formatCustomFieldValue(fieldDef.Name, value)
				filteredCustomFields.WriteString(fmt.Sprintf("%s: %s\n", fieldDef.Name, formatted))
			}
		}
	}

	result := fmt.Sprintf(`
Key: %s
Summary: %s
Status: %s
Reporter: %s
Assignee: %s
Created: %s
Updated: %s
Priority: %s
Description:
%s
%s
Available Transitions:
%s`,
		issue.Key,
		issue.Fields.Summary,
		issue.Fields.Status.Name,
		reporterName,
		assigneeName,
		issue.Fields.Created,
		issue.Fields.Updated,
		priorityName,
		issue.Fields.Description,
		subtasks+filteredCustomFields.String(),
		transitions,
	)

	return mcp.NewToolResultText(result), nil
}

func jiraGetStatusesHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	client := services.JiraClient()

	projectKey, ok := arguments["project_key"].(string)
	if !ok {
		return nil, fmt.Errorf("project_key argument is required")
	}

	ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
	defer cancel()

	issueTypes, response, err := client.Project.Statuses(ctx, projectKey)
	if err != nil {
		if response != nil {
			return nil, fmt.Errorf("failed to get statuses: %s (endpoint: %s)", response.Bytes.String(), response.Endpoint)
		}
		return nil, fmt.Errorf("failed to get statuses: %v", err)
	}

	if len(issueTypes) == 0 {
		return mcp.NewToolResultText("No issue types found for this project."), nil
	}

	var result strings.Builder
	result.WriteString("Available Statuses:\n")
	for _, issueType := range issueTypes {
		result.WriteString(fmt.Sprintf("\nIssue Type: %s\n", issueType.Name))
		for _, status := range issueType.Statuses {
			result.WriteString(fmt.Sprintf("  - %s: %s\n", status.Name, status.ID))
		}
	}

	return mcp.NewToolResultText(result.String()), nil
}

func jiraTransitionIssueHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	client := services.JiraClient()

	issueKey, ok := arguments["issue_key"].(string)
	if !ok || issueKey == "" {
		return nil, fmt.Errorf("valid issue_key is required")
	}

	transitionID, ok := arguments["transition_id"].(string)
	if !ok || transitionID == "" {
		return nil, fmt.Errorf("valid transition_id is required")
	}

	var options *models.IssueMoveOptionsV2
	if comment, ok := arguments["comment"].(string); ok && comment != "" {
		options = &models.IssueMoveOptionsV2{
			Fields: &models.IssueSchemeV2{},
		}
	}

	ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
	defer cancel()

	response, err := client.Issue.Move(ctx, issueKey, transitionID, options)
	if err != nil {
		if response != nil {
			return nil, fmt.Errorf("transition failed: %s (endpoint: %s)",
				response.Bytes.String(),
				response.Endpoint)
		}
		return nil, fmt.Errorf("transition failed: %v", err)
	}

	return mcp.NewToolResultText("Issue transition completed successfully"), nil
}

```

--------------------------------------------------------------------------------
/tools/rag.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"context"
	"fmt"
	"os"
	"strconv"
	"sync"

	"github.com/athapong/aio-mcp/services"
	"github.com/athapong/aio-mcp/util"
	"github.com/google/uuid"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
	"github.com/pkoukk/tiktoken-go"
	"github.com/qdrant/go-client/qdrant"
	"github.com/sashabaranov/go-openai"
)

// Update model dimensions mapping to include commonly used compatible models
var embeddingModelDimensions = map[openai.EmbeddingModel]uint64{
	openai.AdaEmbeddingV2:  1536,
	openai.SmallEmbedding3: 512,
	openai.LargeEmbedding3: 2048,
	"baai/bge-base-en":     768,  // BGE base model
	"baai/bge-large-en":    1024, // BGE large model
	"codesmart.embedding":  1536, // CodeSmart embedding model
}

// Update validation function to work with EmbeddingModel
func validateEmbeddingModel(modelStr string) (openai.EmbeddingModel, uint64, error) {
	model := openai.EmbeddingModel(modelStr)
	if dimensions, ok := embeddingModelDimensions[model]; ok {
		return model, dimensions, nil
	}
	return "", 0, fmt.Errorf("unsupported embedding model: %s. Supported models: %s",
		modelStr,
		"text-embedding-ada-002, text-embedding-3-small, text-embedding-3-large, baai/bge-base-en, baai/bge-large-en, codesmart.embedding")
}

var qdrantClient = sync.OnceValue(func() *qdrant.Client {

	host := os.Getenv("QDRANT_HOST")
	port := os.Getenv("QDRANT_PORT")
	apiKey := os.Getenv("QDRANT_API_KEY")
	if host == "" || port == "" || apiKey == "" {
		panic("QDRANT_HOST, QDRANT_PORT, or QDRANT_API_KEY is not set, please set it in MCP Config")
	}

	portInt, err := strconv.Atoi(port)
	if err != nil {
		panic(fmt.Sprintf("failed to parse QDRANT_PORT: %v", err))
	}

	if apiKey == "" {
		panic("QDRANT_API_KEY is not set")
	}

	client, err := qdrant.NewClient(&qdrant.Config{
		Host:   host,
		Port:   portInt,
		APIKey: apiKey,
		UseTLS: true,
	})

	if err != nil {
		panic(fmt.Sprintf("failed to connect to Qdrant: %v", err))
	}

	return client
})

func RegisterRagTools(s *server.MCPServer) {
	indexContentTool := mcp.NewTool("RAG_memory_index_content",
		mcp.WithDescription("Index a content into memory, can be inserted or updated"),
		mcp.WithString("collection", mcp.Required(), mcp.Description("Memory collection name")),
		mcp.WithString("filePath", mcp.Required(), mcp.Description("content file path")),
		mcp.WithString("payload", mcp.Required(), mcp.Description("Plain text payload")),
		mcp.WithString("model", mcp.Description("Embedding model to use (default: text-embedding-3-large)")),
	)

	indexFileTool := mcp.NewTool("RAG_memory_index_file",
		mcp.WithDescription("Index a local file into memory"),
		mcp.WithString("collection", mcp.Required(), mcp.Description("Memory collection name")),
		mcp.WithString("filePath", mcp.Required(), mcp.Description("Path to the local file to be indexed")),
	)

	createCollectionTool := mcp.NewTool("RAG_memory_create_collection",
		mcp.WithDescription("Create a new vector collection in memory"),
		mcp.WithString("collection", mcp.Required(), mcp.Description("Memory collection name")),
		mcp.WithString("model", mcp.Description("Embedding model to use (default: text-embedding-3-large)")),
	)

	deleteCollectionTool := mcp.NewTool("RAG_memory_delete_collection",
		mcp.WithDescription("Delete a vector collection in memory"),
		mcp.WithString("collection", mcp.Required(), mcp.Description("Memory collection name")),
	)

	listCollectionTool := mcp.NewTool("RAG_memory_list_collections",
		mcp.WithDescription("List all vector collections in memory"),
	)

	searchTool := mcp.NewTool("RAG_memory_search",
		mcp.WithDescription("Search for memory in a collection based on a query"),
		mcp.WithString("collection", mcp.Required(), mcp.Description("Memory collection name")),
		mcp.WithString("query", mcp.Required(), mcp.Description("search query, should be a keyword")),
		mcp.WithString("model", mcp.Description("Embedding model to use (default: text-embedding-3-large)")),
	)

	deleteIndexByFilePathTool := mcp.NewTool("RAG_memory_delete_index_by_filepath",
		mcp.WithDescription("Delete a vector index by filePath"),
		mcp.WithString("collection", mcp.Required(), mcp.Description("Memory collection name")),
		mcp.WithString("filePath", mcp.Required(), mcp.Description("Path to the local file to be deleted")),
	)

	s.AddTool(createCollectionTool, util.ErrorGuard(util.AdaptLegacyHandler(createCollectionHandler)))
	s.AddTool(deleteCollectionTool, util.ErrorGuard(util.AdaptLegacyHandler(deleteCollectionHandler)))
	s.AddTool(listCollectionTool, util.ErrorGuard(util.AdaptLegacyHandler(listCollectionHandler)))
	s.AddTool(indexContentTool, util.ErrorGuard(util.AdaptLegacyHandler(indexContentHandler)))
	s.AddTool(searchTool, util.ErrorGuard(util.AdaptLegacyHandler(vectorSearchHandler)))
	s.AddTool(indexFileTool, util.ErrorGuard(util.AdaptLegacyHandler(indexFileHandler)))
	s.AddTool(deleteIndexByFilePathTool, util.ErrorGuard(util.AdaptLegacyHandler(deleteIndexByFilePathHandler)))
}

func deleteIndexByFilePathHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	collection := arguments["collection"].(string)
	filePath := arguments["filePath"].(string)
	ctx := context.Background()

	// Delete points by IDs using PointSelector
	pointsSelector := &qdrant.PointsSelector{
		PointsSelectorOneOf: &qdrant.PointsSelector_Filter{
			Filter: &qdrant.Filter{
				Must: []*qdrant.Condition{
					{
						ConditionOneOf: &qdrant.Condition_Field{
							Field: &qdrant.FieldCondition{
								Key: "filePath",
								Match: &qdrant.Match{
									MatchValue: &qdrant.Match_Text{
										Text: filePath,
									},
								},
							},
						},
					},
				},
			},
		},
	}

	deleteResp, err := qdrantClient().Delete(ctx, &qdrant.DeletePoints{
		CollectionName: collection,
		Points:         pointsSelector,
	})
	if err != nil {
		return nil, fmt.Errorf("failed to delete points for filePath %s: %v", filePath, err)
	}

	result := fmt.Sprintf("Successfully deleted points for filePath: %s\nOperation ID: %d\nStatus: %s", filePath, deleteResp.OperationId, deleteResp.Status)
	return mcp.NewToolResultText(result), nil
}

func indexFileHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	collection := arguments["collection"].(string)
	filePath := arguments["filePath"].(string)

	// Read the file content
	content, err := os.ReadFile(filePath)
	if err != nil {
		return nil, fmt.Errorf("failed to read file: %v", err)
	}

	// Prepare arguments for vectorUpsertHandler
	upsertArgs := map[string]interface{}{
		"collection": collection,
		"filePath":   filePath,
		"payload":    string(content), // Convert content to string
	}

	// Call vectorUpsertHandler
	return indexContentHandler(upsertArgs)
}

func listCollectionHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	ctx := context.Background()
	collections, err := qdrantClient().ListCollections(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to list collections: %w", err)
	}
	return mcp.NewToolResultText(fmt.Sprintf("Collections: %v", collections)), nil
}

// Update createCollectionHandler to always use codesmart.embedding
func createCollectionHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	collection := arguments["collection"].(string)
	modelStr := "codesmart.embedding" // Always use codesmart.embedding as default

	if modelArg, ok := arguments["model"].(string); ok && modelArg != "" {
		embModel, _, err := validateEmbeddingModel(modelArg)
		if err != nil {
			return nil, err
		}
		modelStr = string(embModel)
	}

	ctx := context.Background()

	// Check if collection already exists
	collectionInfo, err := qdrantClient().GetCollectionInfo(ctx, collection)
	if err == nil && collectionInfo != nil {
		return nil, fmt.Errorf("collection %s already exists", collection)
	}

	// Get dimensions for the model
	dimensions := embeddingModelDimensions[openai.EmbeddingModel(modelStr)]

	// Create collection with configuration for the selected model
	err = qdrantClient().CreateCollection(ctx, &qdrant.CreateCollection{
		CollectionName: collection,
		VectorsConfig: &qdrant.VectorsConfig{
			Config: &qdrant.VectorsConfig_Params{
				Params: &qdrant.VectorParams{
					Size:     dimensions,
					Distance: qdrant.Distance_Cosine,
				},
			},
		},
	})
	if err != nil {
		return nil, fmt.Errorf("failed to create collection: %v", err)
	}

	result := fmt.Sprintf("Successfully created collection: %s with model: %s", collection, modelStr)
	return mcp.NewToolResultText(result), nil
}

func deleteCollectionHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	collection := arguments["collection"].(string)
	ctx := context.Background()

	// Check if collection exists
	collectionInfo, err := qdrantClient().GetCollectionInfo(ctx, collection)
	if err != nil || collectionInfo == nil {
		return nil, fmt.Errorf("collection %s does not exist", collection)
	}

	// Delete collection
	err = qdrantClient().DeleteCollection(ctx, collection)
	if err != nil {
		return nil, fmt.Errorf("failed to delete collection: %v", err)
	}

	result := fmt.Sprintf("Successfully deleted collection: %s", collection)
	return mcp.NewToolResultText(result), nil
}

// Update indexContentHandler to use codesmart.embedding by default
func indexContentHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	collection := arguments["collection"].(string)
	filePath := arguments["filePath"].(string)
	payload := arguments["payload"].(string)

	// Always default to codesmart.embedding
	modelStr := "codesmart.embedding"
	if modelArg, ok := arguments["model"].(string); ok && modelArg != "" {
		embModel, _, err := validateEmbeddingModel(modelArg)
		if err != nil {
			return nil, err
		}
		modelStr = string(embModel)
	}

	// Split content into chunks
	chunks, err := splitIntoChunks(payload, filePath) // Implement chunking logic
	if err != nil {
		return nil, fmt.Errorf("failed to split into chunks: %v", err)
	}

	var points []*qdrant.PointStruct
	for i, chunk := range chunks {
		// Generate embeddings for each chunk using selected model
		resp, err := services.DefaultOpenAIClient().CreateEmbeddings(context.Background(), openai.EmbeddingRequest{
			Input: []string{chunk},
			Model: openai.EmbeddingModel(modelStr),
		})
		if err != nil {
			return nil, fmt.Errorf("failed to generate embeddings: %v", err)
		}

		// Create point for each chunk
		point := &qdrant.PointStruct{
			Id:      qdrant.NewIDUUID(uuid.NewSHA1(uuid.NameSpaceURL, []byte(filePath+strconv.Itoa(i))).String()),
			Vectors: qdrant.NewVectors(resp.Data[0].Embedding...),
			Payload: qdrant.NewValueMap(map[string]any{
				"filePath":   filePath,
				"content":    chunk,
				"chunkIndex": i,
				"model":      modelStr, // Store the model used for embedding
			}),
		}
		points = append(points, point)
	}

	ctx := context.Background()
	waitUpsert := true

	// Upsert all chunks
	upsertResp, err := qdrantClient().Upsert(ctx, &qdrant.UpsertPoints{
		CollectionName: collection,
		Wait:           &waitUpsert,
		Points:         points,
	})
	if err != nil {
		return nil, fmt.Errorf("failed to upsert points: %v", err)
	}

	result := fmt.Sprintf("Successfully upserted\nOperation ID: %d\nStatus: %s", upsertResp.OperationId, upsertResp.Status)

	return mcp.NewToolResultText(result), nil
}

func splitIntoChunks(content string, _ string) ([]string, error) {
	const (
		maxTokensPerChunk = 512
		overlapTokens     = 50
		model             = "text-embedding-3-large"
	)

	encoding, err := tiktoken.GetEncoding("cl100k_base")
	if err != nil {
		return nil, fmt.Errorf("failed to get encoding: %v", err)
	}

	tokens := encoding.Encode(content, nil, nil)

	var chunks []string
	var currentChunk []int

	// First pass: collect all chunks without context
	var rawChunks []string
	for i := 0; i < len(tokens); i++ {
		currentChunk = append(currentChunk, tokens[i])

		if len(currentChunk) >= maxTokensPerChunk {
			chunkText := encoding.Decode(currentChunk)
			rawChunks = append(rawChunks, chunkText)

			if len(currentChunk) > overlapTokens {
				currentChunk = currentChunk[len(currentChunk)-overlapTokens:]
			} else {
				currentChunk = []int{}
			}
		}
	}

	// Handle remaining tokens
	if len(currentChunk) > 0 {
		chunkText := encoding.Decode(currentChunk)
		rawChunks = append(rawChunks, chunkText)
	}

	// If there's only one chunk, return it without context
	if len(rawChunks) == 1 {
		return rawChunks, nil
	}

	// If there are multiple chunks, add context to each
	for _, chunkText := range rawChunks {
		contextualizedChunk, err := generateContext(content, chunkText)
		if err != nil {
			return nil, fmt.Errorf("failed to generate context: %v", err)
		}
		chunks = append(chunks, contextualizedChunk)
	}

	return chunks, nil
}

func generateContext(fullText, chunkText string) (string, error) {
	prompt := fmt.Sprintf(`
<document>%s</document>
Here is the chunk we want to situate within the whole document:
<chunk>%s</chunk>
Please give a short succinct context to situate this chunk within the overall document for the purposes of improving search retrieval of the chunk. Answer only with the succinct context and nothing else.
	`, fullText, chunkText)

	// Use codesmart model instead of GPT
	model := "codesmart"

	resp, err := services.DefaultOpenAIClient().CreateChatCompletion(
		context.Background(),
		openai.ChatCompletionRequest{
			Model: model,
			Messages: []openai.ChatCompletionMessage{
				{
					Role:    openai.ChatMessageRoleUser,
					Content: prompt,
				},
			},
		},
	)

	if err != nil {
		return "", fmt.Errorf("failed to generate context: %v", err)
	}

	context := resp.Choices[0].Message.Content
	return fmt.Sprintf("Context: \n%s;\n\nChunk: \n%s", context, chunkText), nil
}

// Update vectorSearchHandler to use codesmart.embedding by default
func vectorSearchHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
	collection := arguments["collection"].(string)
	query := arguments["query"].(string)

	ctx := context.Background()

	// Check if collection exists and get info
	collectionInfo, err := qdrantClient().GetCollectionInfo(ctx, collection)
	if err != nil {
		return nil, fmt.Errorf("failed to get collection info: %v", err)
	}

	// Always default to codesmart.embedding
	modelStr := "codesmart.embedding"
	if modelArg, ok := arguments["model"].(string); ok && modelArg != "" {
		embModel, _, err := validateEmbeddingModel(modelArg)
		if err != nil {
			return nil, err
		}
		modelStr = string(embModel)
	}

	// Generate embedding for the query using selected model
	resp, err := services.DefaultOpenAIClient().CreateEmbeddings(context.Background(), openai.EmbeddingRequest{
		Input: []string{query},
		Model: openai.EmbeddingModel(modelStr),
	})
	if err != nil {
		return nil, fmt.Errorf("failed to generate embeddings for query: %v", err)
	}

	// Lower score threshold and add limit
	scoreThreshold := float32(0.3) // Lower threshold to get more results
	limit := uint64(10)            // Limit results to 10

	// Search Qdrant with debug info
	searchResult, err := qdrantClient().Query(ctx, &qdrant.QueryPoints{
		CollectionName: collection,
		Query:          qdrant.NewQuery(resp.Data[0].Embedding...), // Use Query instead of Vector
		Limit:          &limit,
		ScoreThreshold: &scoreThreshold,
		WithPayload: &qdrant.WithPayloadSelector{
			SelectorOptions: &qdrant.WithPayloadSelector_Enable{
				Enable: true,
			},
		},
	})
	if err != nil {
		return nil, fmt.Errorf("failed to search in Qdrant: %v", err)
	}

	// Add debug info to results
	var resultText string
	resultText = fmt.Sprintf("Search Results for Collection: %s\nTotal points in collection: %d\nQuery: %s\nModel: %s\nScore threshold: %f\n\n",
		collection,
		collectionInfo.PointsCount,
		query,
		modelStr,
		scoreThreshold)

	if len(searchResult) == 0 {
		resultText += "No results found that match the query with the current threshold.\n"
	}

	for i, hit := range searchResult {
		content := hit.Payload["content"].GetStringValue()
		filePath := hit.Payload["filePath"].GetStringValue()
		usedModel := hit.Payload["model"].GetStringValue()

		// Extract additional payload fields if they exist
		component := hit.Payload["component"].GetStringValue()
		status := hit.Payload["status"].GetStringValue()
		testID := hit.Payload["test_id"].GetStringValue()
		priority := hit.Payload["priority"].GetStringValue()
		subfeature := hit.Payload["subfeature"].GetStringValue()
		feature := hit.Payload["feature"].GetStringValue()

		resultText += fmt.Sprintf("Result %d (Score: %.4f):\n"+
			"Model: %s\n"+
			"FilePath: %s\n"+
			"Component: %s\n"+
			"Status: %s\n"+
			"Test ID: %s\n"+
			"Priority: %s\n"+
			"Feature: %s\n"+
			"Subfeature: %s\n"+
			"Content: %s\n\n",
			i+1, hit.Score, usedModel, filePath,
			component, status, testID, priority,
			feature, subfeature, content)
	}

	return mcp.NewToolResultText(resultText), nil
}

```
Page 1/2FirstPrevNextLast