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, "&#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
}
```