Commit 0bf769b
Changed files (44)
pkg
fetch
filesystem
git
gitlab
htmlprocessor
imap
maildir
mcp
memory
packages
semantic
signal
speech
thinking
time
test
cmd/bash/main.go
@@ -66,7 +66,7 @@ func main() {
}
// Create and start the server
- server, err := bash.NewBashServer(config)
+ server, err := bash.New(config)
if err != nil {
log.Fatalf("Failed to create bash server: %v", err)
}
cmd/gitlab/main.go
@@ -99,7 +99,7 @@ For more information, visit: https://github.com/xlgmokha/mcp
url = envURL
}
- server, err := gitlab.NewServer(url, token)
+ server, err := gitlab.New(url, token)
if err != nil {
log.Fatalf("Failed to create GitLab server: %v", err)
}
cmd/packages/main.go
@@ -1,114 +0,0 @@
-package main
-
-import (
- "context"
- "flag"
- "fmt"
- "log"
-
- "github.com/xlgmokha/mcp/pkg/packages"
-)
-
-func main() {
- var showHelp bool
- flag.BoolVar(&showHelp, "help", false, "Show help information")
- flag.Parse()
-
- if showHelp {
- showHelpText()
- return
- }
-
- server := packages.NewServer()
- if err := server.Run(context.Background()); err != nil {
- log.Fatalf("Server error: %v", err)
- }
-}
-
-func showHelpText() {
- fmt.Printf(`Package Manager MCP Server
-
-A Model Context Protocol server that provides package management tools for multiple
-ecosystems including Rust (Cargo) and macOS (Homebrew).
-
-USAGE:
- mcp-packages [OPTIONS]
-
-OPTIONS:
- --help Show this help message
-
-TOOLS PROVIDED:
-
-Cargo (Rust) Tools:
- cargo_build Build Rust projects with optional flags
- cargo_run Run Rust applications with arguments
- cargo_test Execute test suites with filtering
- cargo_add Add dependencies to Cargo.toml
- cargo_update Update dependencies to latest versions
- cargo_check Quick compile check without building
- cargo_clippy Run Rust linter with suggestions
-
-Homebrew Tools:
- brew_install Install packages or casks
- brew_uninstall Remove packages or casks
- brew_search Search for available packages
- brew_update Update Homebrew itself
- brew_upgrade Upgrade installed packages
- brew_doctor Check system health
- brew_list List installed packages
-
-Cross-Platform Tools:
- check_vulnerabilities Scan for security vulnerabilities
- outdated_packages Find packages needing updates
- package_info Get detailed package information
-
-EXAMPLES:
-
-Cargo Operations:
- # Build a Rust project in release mode
- {"name": "cargo_build", "arguments": {"directory": ".", "release": true}}
-
- # Run with arguments
- {"name": "cargo_run", "arguments": {"args": ["--version"]}}
-
- # Add a dependency
- {"name": "cargo_add", "arguments": {"packages": ["serde", "tokio"]}}
-
-Homebrew Operations:
- # Install a package
- {"name": "brew_install", "arguments": {"packages": ["git"]}}
-
- # Search for packages
- {"name": "brew_search", "arguments": {"query": "nodejs"}}
-
- # Check system health
- {"name": "brew_doctor", "arguments": {}}
-
-Security & Maintenance:
- # Check for vulnerabilities in current directory
- {"name": "check_vulnerabilities", "arguments": {"directory": "."}}
-
- # Find outdated packages
- {"name": "outdated_packages", "arguments": {"package_manager": "cargo"}}
-
-INTEGRATION:
-Add to your Claude Code configuration (~/.claude.json):
-
-{
- "mcpServers": {
- "packages": {
- "command": "mcp-packages"
- }
- }
-}
-
-ENVIRONMENT:
-The server auto-detects package managers based on project files:
-- Cargo.toml โ Rust/Cargo tools
-- package.json โ NPM tools
-- go.mod โ Go modules
-- Homebrew โ System-wide macOS packages
-
-For support or issues, see: https://github.com/xlgmokha/mcp
-`)
-}
cmd/semantic/main.go
@@ -36,10 +36,13 @@ func main() {
setupLogging(*logLevel)
// Create and configure the semantic server
- server := semantic.NewServer()
+ server, err := semantic.New()
+ if err != nil {
+ log.Fatalf("Failed to create semantic server: %v", err)
+ }
// Initialize project discovery
- if err := initializeProject(server, *projectRoot, *configFile); err != nil {
+ if err := initializeProject(*projectRoot, *configFile); err != nil {
log.Fatalf("Failed to initialize project: %v", err)
}
@@ -50,9 +53,8 @@ func main() {
go func() {
<-sigChan
log.Println("Shutting down semantic server...")
- if err := server.Shutdown(); err != nil {
- log.Printf("Error during shutdown: %v", err)
- }
+ // Note: Shutdown is no longer available in the new pattern
+ // LSP managers are handled internally
os.Exit(0)
}()
@@ -207,7 +209,7 @@ func setupLogging(level string) {
}
}
-func initializeProject(server *semantic.Server, projectRoot, configFile string) error {
+func initializeProject(projectRoot, configFile string) error {
// This is a placeholder for project initialization
// In the full implementation, this would:
// 1. Load configuration from file if provided
cmd/signal/main.go
@@ -68,7 +68,7 @@ For more information, visit: https://github.com/xlgmokha/mcp
return
}
- server, err := signal.NewServer(*signalPath)
+ server, err := signal.New(*signalPath)
if err != nil {
log.Fatalf("Failed to create Signal server: %v", err)
}
cmd/speech/main.go
@@ -19,7 +19,10 @@ func main() {
return
}
- server := speech.NewServer()
+ server, err := speech.New()
+ if err != nil {
+ log.Fatalf("Failed to create speech server: %v", err)
+ }
if err := server.Run(context.Background()); err != nil {
log.Fatalf("Server error: %v", err)
}
docs/mcp-server-specifications.md
@@ -1,890 +0,0 @@
-# MCP Server Feature Specifications for Go Implementation
-
-This document provides detailed specifications for implementing MCP servers in Go based on analysis of the Python reference implementations. Each server must implement the Model Context Protocol using JSON-RPC 2.0 over stdio.
-
-## Core MCP Protocol Requirements
-
-### Base Protocol
-- **Transport**: JSON-RPC 2.0 over stdio (stdin/stdout)
-- **Connection**: Stateful client-server connection
-- **Initialization**: Capability negotiation during handshake
-- **Message Format**: All messages must follow JSON-RPC 2.0 specification
-
-### Required MCP Methods
-All servers must implement these base protocol methods:
-- `initialize` - Capability negotiation and server info
-- `initialized` - Notification that initialization is complete
-- `shutdown` - Graceful shutdown request
-- `exit` - Immediate termination notification
-
-### Server Capabilities
-Servers can implement any combination of:
-- **Tools** - Functions the AI model can execute
-- **Resources** - Context and data access
-- **Prompts** - Templated messages and workflows
-
-## 1. Git Server (`mcp-server-git`)
-
-### Dependencies
-- Git CLI or go-git library
-- File system access for repository operations
-
-### Tools
-
-#### `git_status`
-- **Description**: Shows the working tree status
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "repo_path": {"type": "string", "description": "Path to Git repository"}
- },
- "required": ["repo_path"]
- }
- ```
-- **Implementation**: Execute `git status` or use go-git Status()
-- **Output**: TextContent with repository status
-
-#### `git_diff_unstaged`
-- **Description**: Shows changes in working directory not yet staged
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "repo_path": {"type": "string", "description": "Path to Git repository"}
- },
- "required": ["repo_path"]
- }
- ```
-- **Implementation**: Execute `git diff` or use go-git diff functionality
-- **Output**: TextContent with unstaged changes
-
-#### `git_diff_staged`
-- **Description**: Shows changes that are staged for commit
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "repo_path": {"type": "string", "description": "Path to Git repository"}
- },
- "required": ["repo_path"]
- }
- ```
-- **Implementation**: Execute `git diff --cached`
-- **Output**: TextContent with staged changes
-
-#### `git_diff`
-- **Description**: Shows differences between branches or commits
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "repo_path": {"type": "string", "description": "Path to Git repository"},
- "target": {"type": "string", "description": "Target branch or commit to compare with"}
- },
- "required": ["repo_path", "target"]
- }
- ```
-- **Implementation**: Execute `git diff <target>`
-- **Output**: TextContent with diff output
-
-#### `git_commit`
-- **Description**: Records changes to the repository
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "repo_path": {"type": "string", "description": "Path to Git repository"},
- "message": {"type": "string", "description": "Commit message"}
- },
- "required": ["repo_path", "message"]
- }
- ```
-- **Implementation**: Execute `git commit -m "message"`
-- **Output**: TextContent with commit confirmation and hash
-
-#### `git_add`
-- **Description**: Adds file contents to the staging area
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "repo_path": {"type": "string", "description": "Path to Git repository"},
- "files": {"type": "array", "items": {"type": "string"}, "description": "Array of file paths to stage"}
- },
- "required": ["repo_path", "files"]
- }
- ```
-- **Implementation**: Execute `git add <files...>`
-- **Output**: TextContent with staging confirmation
-
-#### `git_reset`
-- **Description**: Unstages all staged changes
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "repo_path": {"type": "string", "description": "Path to Git repository"}
- },
- "required": ["repo_path"]
- }
- ```
-- **Implementation**: Execute `git reset`
-- **Output**: TextContent with reset confirmation
-
-#### `git_log`
-- **Description**: Shows the commit logs
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "repo_path": {"type": "string", "description": "Path to Git repository"},
- "max_count": {"type": "integer", "description": "Maximum number of commits to show", "default": 10}
- },
- "required": ["repo_path"]
- }
- ```
-- **Implementation**: Execute `git log --max-count=<n>` or use go-git log iterator
-- **Output**: TextContent with formatted commit history
-
-#### `git_create_branch`
-- **Description**: Creates a new branch from an optional base branch
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "repo_path": {"type": "string", "description": "Path to Git repository"},
- "branch_name": {"type": "string", "description": "Name of the new branch"},
- "base_branch": {"type": "string", "description": "Starting point for the new branch", "default": null}
- },
- "required": ["repo_path", "branch_name"]
- }
- ```
-- **Implementation**: Execute `git checkout -b <branch_name> [base_branch]`
-- **Output**: TextContent with branch creation confirmation
-
-#### `git_checkout`
-- **Description**: Switches branches
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "repo_path": {"type": "string", "description": "Path to Git repository"},
- "branch_name": {"type": "string", "description": "Name of branch to checkout"}
- },
- "required": ["repo_path", "branch_name"]
- }
- ```
-- **Implementation**: Execute `git checkout <branch_name>`
-- **Output**: TextContent with checkout confirmation
-
-#### `git_show`
-- **Description**: Shows the contents of a commit
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "repo_path": {"type": "string", "description": "Path to Git repository"},
- "revision": {"type": "string", "description": "The revision (commit hash, branch name, tag) to show"}
- },
- "required": ["repo_path", "revision"]
- }
- ```
-- **Implementation**: Execute `git show <revision>` or use go-git commit details
-- **Output**: TextContent with commit details and diff
-
-#### `git_init`
-- **Description**: Initialize a new Git repository
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "repo_path": {"type": "string", "description": "Path to directory to initialize git repo"}
- },
- "required": ["repo_path"]
- }
- ```
-- **Implementation**: Execute `git init` or use go-git PlainInit()
-- **Output**: TextContent with initialization confirmation
-
-### Configuration
-- Command line argument: `--repository <path>` to specify default repository
-- Can also detect repositories from MCP roots capability
-
-## 2. Filesystem Server (`mcp-server-filesystem`)
-
-### Dependencies
-- File system access with configurable permissions
-- Pattern matching for file search
-- Text diff generation
-
-### Security Model
-- **Sandboxing**: Only allow operations within specified directories
-- **Validation**: All paths must be validated against allowed directories
-- **Error Handling**: Graceful handling of permission errors
-
-### Tools
-
-#### `read_file`
-- **Description**: Read complete contents of a file
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "path": {"type": "string", "description": "Absolute path to the file to read"}
- },
- "required": ["path"]
- }
- ```
-- **Implementation**: Read file with UTF-8 encoding, handle various encodings
-- **Output**: TextContent with file contents
-- **Security**: Validate path is within allowed directories
-
-#### `read_multiple_files`
-- **Description**: Read multiple files simultaneously
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "paths": {"type": "array", "items": {"type": "string"}, "description": "Array of file paths to read"}
- },
- "required": ["paths"]
- }
- ```
-- **Implementation**: Read multiple files, continue on individual failures
-- **Output**: TextContent with results for each file
-- **Security**: Validate all paths are within allowed directories
-
-#### `write_file`
-- **Description**: Create new file or overwrite existing
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "path": {"type": "string", "description": "File location"},
- "content": {"type": "string", "description": "File content"}
- },
- "required": ["path", "content"]
- }
- ```
-- **Implementation**: Write content to file with UTF-8 encoding
-- **Output**: TextContent with write confirmation
-- **Security**: Validate path is within allowed directories
-
-#### `edit_file`
-- **Description**: Make selective edits using pattern matching
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "path": {"type": "string", "description": "File to edit"},
- "edits": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "oldText": {"type": "string", "description": "Text to search for"},
- "newText": {"type": "string", "description": "Text to replace with"}
- },
- "required": ["oldText", "newText"]
- }
- },
- "dryRun": {"type": "boolean", "description": "Preview changes without applying", "default": false}
- },
- "required": ["path", "edits"]
- }
- ```
-- **Implementation**:
- - Line-based and multi-line content matching
- - Whitespace normalization with indentation preservation
- - Multiple simultaneous edits with correct positioning
- - Git-style diff output
-- **Output**: TextContent with diff information or edit confirmation
-- **Security**: Validate path is within allowed directories
-
-#### `create_directory`
-- **Description**: Create new directory or ensure it exists
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "path": {"type": "string", "description": "Directory path to create"}
- },
- "required": ["path"]
- }
- ```
-- **Implementation**: Create directory and parent directories if needed
-- **Output**: TextContent with creation confirmation
-- **Security**: Validate path is within allowed directories
-
-#### `list_directory`
-- **Description**: List directory contents with type indicators
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "path": {"type": "string", "description": "Directory path to list"}
- },
- "required": ["path"]
- }
- ```
-- **Implementation**: List files and directories with [FILE] or [DIR] prefixes
-- **Output**: TextContent with directory listing
-- **Security**: Validate path is within allowed directories
-
-#### `move_file`
-- **Description**: Move or rename files and directories
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "source": {"type": "string", "description": "Source path"},
- "destination": {"type": "string", "description": "Destination path"}
- },
- "required": ["source", "destination"]
- }
- ```
-- **Implementation**: Move/rename operation, fail if destination exists
-- **Output**: TextContent with move confirmation
-- **Security**: Validate both paths are within allowed directories
-
-#### `search_files`
-- **Description**: Recursively search for files/directories
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "path": {"type": "string", "description": "Starting directory"},
- "pattern": {"type": "string", "description": "Search pattern"},
- "excludePatterns": {"type": "array", "items": {"type": "string"}, "description": "Exclude patterns", "default": []}
- },
- "required": ["path", "pattern"]
- }
- ```
-- **Implementation**: Case-insensitive recursive search with glob patterns
-- **Output**: TextContent with matching file paths
-- **Security**: Validate path is within allowed directories
-
-#### `get_file_info`
-- **Description**: Get detailed file/directory metadata
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "path": {"type": "string", "description": "File/directory path"}
- },
- "required": ["path"]
- }
- ```
-- **Implementation**: Return size, timestamps, permissions, type
-- **Output**: TextContent with file metadata
-- **Security**: Validate path is within allowed directories
-
-#### `list_allowed_directories`
-- **Description**: List all directories the server is allowed to access
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {},
- "required": []
- }
- ```
-- **Implementation**: Return configured allowed directories
-- **Output**: TextContent with allowed directory list
-
-### Configuration
-- Command line arguments: List of allowed directories
-- Example: `mcp-server-filesystem /home/user/documents /home/user/projects`
-
-## 3. Fetch Server (`mcp-server-fetch`)
-
-### Dependencies
-- HTTP client for web requests
-- HTML to Markdown conversion
-- robots.txt parsing
-- URL validation and parsing
-
-### Security Considerations
-- **robots.txt Compliance**: Check robots.txt before autonomous fetching
-- **User Agent**: Different user agents for autonomous vs manual requests
-- **Proxy Support**: Optional proxy configuration
-- **Content Filtering**: Handle various content types appropriately
-
-### Tools
-
-#### `fetch`
-- **Description**: Fetches a URL from the internet and extracts its contents as markdown
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "url": {"type": "string", "format": "uri", "description": "URL to fetch"},
- "max_length": {"type": "integer", "description": "Maximum number of characters to return", "default": 5000, "minimum": 1, "maximum": 999999},
- "start_index": {"type": "integer", "description": "Start content from this character index", "default": 0, "minimum": 0},
- "raw": {"type": "boolean", "description": "Get raw content without markdown conversion", "default": false}
- },
- "required": ["url"]
- }
- ```
-- **Implementation**:
- - Check robots.txt compliance for autonomous requests
- - Fetch URL with appropriate user agent
- - Convert HTML to markdown unless raw requested
- - Handle content truncation and pagination
- - Support content type detection
-- **Output**: TextContent with fetched content
-- **Error Handling**: Detailed error messages for robots.txt violations, HTTP errors
-
-### Prompts
-
-#### `fetch`
-- **Description**: Fetch a URL and extract its contents as markdown
-- **Arguments**:
- ```json
- [
- {
- "name": "url",
- "description": "URL to fetch",
- "required": true
- }
- ]
- ```
-- **Implementation**: Manual fetch without robots.txt restrictions
-
-### Configuration Options
-- `--custom-user-agent`: Custom User-Agent string
-- `--ignore-robots-txt`: Ignore robots.txt restrictions
-- `--proxy-url`: Proxy URL for requests
-
-## 4. Memory Server (`mcp-server-memory`)
-
-### Dependencies
-- JSON file storage for persistence
-- Graph data structure for entities and relations
-- Text search capabilities
-
-### Data Model
-
-#### Entity
-```json
-{
- "name": "string (unique identifier)",
- "entityType": "string (e.g., 'person', 'organization', 'event')",
- "observations": ["array of strings"]
-}
-```
-
-#### Relation
-```json
-{
- "from": "string (source entity name)",
- "to": "string (target entity name)",
- "relationType": "string (relationship type in active voice)"
-}
-```
-
-### Tools
-
-#### `create_entities`
-- **Description**: Create multiple new entities in the knowledge graph
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "entities": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "name": {"type": "string", "description": "Entity identifier"},
- "entityType": {"type": "string", "description": "Type classification"},
- "observations": {"type": "array", "items": {"type": "string"}, "description": "Associated observations"}
- },
- "required": ["name", "entityType", "observations"]
- }
- }
- },
- "required": ["entities"]
- }
- ```
-- **Implementation**: Add entities to graph, ignore duplicates by name
-- **Output**: TextContent with creation results
-
-#### `create_relations`
-- **Description**: Create multiple new relations between entities
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "relations": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "from": {"type": "string", "description": "Source entity name"},
- "to": {"type": "string", "description": "Target entity name"},
- "relationType": {"type": "string", "description": "Relationship type"}
- },
- "required": ["from", "to", "relationType"]
- }
- }
- },
- "required": ["relations"]
- }
- ```
-- **Implementation**: Add relations to graph, skip duplicates
-- **Output**: TextContent with creation results
-
-#### `add_observations`
-- **Description**: Add new observations to existing entities
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "observations": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "entityName": {"type": "string", "description": "Target entity"},
- "contents": {"type": "array", "items": {"type": "string"}, "description": "New observations to add"}
- },
- "required": ["entityName", "contents"]
- }
- }
- },
- "required": ["observations"]
- }
- ```
-- **Implementation**: Add observations to existing entities
-- **Output**: TextContent with added observations per entity
-- **Error**: Fail if entity doesn't exist
-
-#### `delete_entities`
-- **Description**: Remove entities and their relations
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "entityNames": {"type": "array", "items": {"type": "string"}, "description": "Entity names to delete"}
- },
- "required": ["entityNames"]
- }
- ```
-- **Implementation**: Remove entities and cascade delete relations
-- **Output**: TextContent with deletion results
-
-#### `delete_observations`
-- **Description**: Remove specific observations from entities
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "deletions": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "entityName": {"type": "string", "description": "Target entity"},
- "observations": {"type": "array", "items": {"type": "string"}, "description": "Observations to remove"}
- },
- "required": ["entityName", "observations"]
- }
- }
- },
- "required": ["deletions"]
- }
- ```
-- **Implementation**: Remove specific observations from entities
-- **Output**: TextContent with deletion results
-
-#### `delete_relations`
-- **Description**: Remove specific relations from the graph
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "relations": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "from": {"type": "string", "description": "Source entity name"},
- "to": {"type": "string", "description": "Target entity name"},
- "relationType": {"type": "string", "description": "Relationship type"}
- },
- "required": ["from", "to", "relationType"]
- }
- }
- },
- "required": ["relations"]
- }
- ```
-- **Implementation**: Remove specific relations
-- **Output**: TextContent with deletion results
-
-#### `read_graph`
-- **Description**: Read the entire knowledge graph
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {},
- "required": []
- }
- ```
-- **Implementation**: Return complete graph structure
-- **Output**: TextContent with full graph data
-
-#### `search_nodes`
-- **Description**: Search for nodes based on query
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "query": {"type": "string", "description": "Search query"}
- },
- "required": ["query"]
- }
- ```
-- **Implementation**: Search entity names, types, and observation content
-- **Output**: TextContent with matching entities and relations
-
-#### `open_nodes`
-- **Description**: Retrieve specific nodes by name
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "names": {"type": "array", "items": {"type": "string"}, "description": "Entity names to retrieve"}
- },
- "required": ["names"]
- }
- ```
-- **Implementation**: Return requested entities and their relations
-- **Output**: TextContent with requested nodes
-
-### Configuration
-- Environment variable: `MEMORY_FILE_PATH` for custom storage location
-- Default: `memory.json` in server directory
-
-## 5. Sequential Thinking Server (`mcp-server-sequential-thinking`)
-
-### Dependencies
-- JSON file storage for session persistence
-- State management for thought sequences and branches
-- Thread-safe concurrent access controls
-
-### Configuration
-- Command line argument: `--session-file <path>` for persistent session storage
-
-### Tools
-
-#### `sequentialthinking`
-- **Description**: Dynamic problem-solving through structured thought sequences with session persistence
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "thought": {"type": "string", "description": "Your current thinking step"},
- "nextThoughtNeeded": {"type": "boolean", "description": "Whether another thought step is needed"},
- "thoughtNumber": {"type": "integer", "description": "Current thought number", "minimum": 1},
- "totalThoughts": {"type": "integer", "description": "Estimated total thoughts needed", "minimum": 1},
- "isRevision": {"type": "boolean", "description": "Whether this revises previous thinking"},
- "revisesThought": {"type": "integer", "description": "Which thought is being reconsidered", "minimum": 1},
- "branchFromThought": {"type": "integer", "description": "Branching point thought number", "minimum": 1},
- "branchId": {"type": "string", "description": "Branch identifier"},
- "needsMoreThoughts": {"type": "boolean", "description": "If more thoughts are needed"},
- "sessionId": {"type": "string", "description": "Session ID for thought continuity (auto-generated if not provided)"}
- },
- "required": ["thought", "nextThoughtNeeded", "thoughtNumber", "totalThoughts"]
- }
- ```
-- **Implementation**:
- - Track thought sequences with persistent sessions
- - Support branching for alternative reasoning paths
- - Support revision and refinement of previous thoughts
- - Thread-safe concurrent session access
- - JSON file persistence across server restarts
-- **Output**: TextContent with thought processing results and session context
-
-#### `get_session_history`
-- **Description**: Retrieve complete thought history for a thinking session
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "sessionId": {"type": "string", "description": "Session ID to get history for"}
- },
- "required": ["sessionId"]
- }
- ```
-- **Output**: JSON TextContent with complete session data and thoughts
-
-#### `list_sessions`
-- **Description**: List all active thinking sessions
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {}
- }
- ```
-- **Output**: JSON TextContent with array of session summaries
-
-#### `get_branch_history`
-- **Description**: Get thought history for a specific reasoning branch
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "branchId": {"type": "string", "description": "Branch ID to get history for"}
- },
- "required": ["branchId"]
- }
- ```
-- **Output**: JSON TextContent with complete branch data and thoughts
-
-#### `clear_session`
-- **Description**: Clear a thinking session and all its branches
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "sessionId": {"type": "string", "description": "Session ID to clear"}
- },
- "required": ["sessionId"]
- }
- ```
-- **Output**: JSON TextContent with confirmation message and details
-
-## 6. Time Server (`mcp-server-time`)
-
-### Dependencies
-- Timezone database (IANA timezones)
-- System timezone detection
-- Time parsing and formatting
-
-### Tools
-
-#### `get_current_time`
-- **Description**: Get current time in a specific timezone
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "timezone": {"type": "string", "description": "IANA timezone name (e.g., 'America/New_York', 'Europe/London')"}
- },
- "required": ["timezone"]
- }
- ```
-- **Implementation**: Get current time in specified timezone with DST information
-- **Output**: JSON TextContent with timezone, datetime (ISO format), is_dst
-
-#### `convert_time`
-- **Description**: Convert time between timezones
-- **Input Schema**:
- ```json
- {
- "type": "object",
- "properties": {
- "source_timezone": {"type": "string", "description": "Source IANA timezone name"},
- "time": {"type": "string", "description": "Time in 24-hour format (HH:MM)"},
- "target_timezone": {"type": "string", "description": "Target IANA timezone name"}
- },
- "required": ["source_timezone", "time", "target_timezone"]
- }
- ```
-- **Implementation**: Convert time between timezones, calculate time difference
-- **Output**: JSON TextContent with source and target time information plus difference
-
-### Configuration
-- Command line argument: `--local-timezone` to override system timezone detection
-
-## Common Go Implementation Requirements
-
-### JSON-RPC 2.0 Implementation
-- Message framing over stdio
-- Request/response correlation with IDs
-- Notification handling (no response expected)
-- Error response formatting
-
-### Shared Types
-```go
-type TextContent struct {
- Type string `json:"type"`
- Text string `json:"text"`
-}
-
-type Tool struct {
- Name string `json:"name"`
- Description string `json:"description"`
- InputSchema interface{} `json:"inputSchema"`
-}
-
-type ServerInfo struct {
- Name string `json:"name"`
- Version string `json:"version"`
-}
-```
-
-### Error Handling
-- Standard JSON-RPC error codes
-- Descriptive error messages
-- Graceful degradation
-- Input validation with clear error messages
-
-### Logging and Debugging
-- Structured logging support
-- Debug mode for development
-- Request/response tracing capability
-
-### Configuration
-- Command line argument parsing
-- Environment variable support
-- Configuration file support where appropriate
-- Sensible defaults
-
-### Testing Requirements
-- Unit tests for all tools
-- Integration tests with MCP clients
-- Error condition testing
-- Mock external dependencies where needed
-
-This specification provides the foundation for implementing full-featured MCP servers in Go with complete compatibility with the Python reference implementations.
\ No newline at end of file
pkg/bash/handlers.go
@@ -13,7 +13,7 @@ import (
// Core Execution Tools
// HandleBashExec handles the bash_exec tool
-func (bs *BashServer) HandleBashExec(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (bash *BashOperations) HandleBashExec(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
var options ExecutionOptions
// Parse command (required)
@@ -51,7 +51,7 @@ func (bs *BashServer) HandleBashExec(req mcp.CallToolRequest) (mcp.CallToolResul
}
// Execute command
- result, err := bs.executeCommand(options)
+ result, err := bash.executeCommand(options)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("Failed to execute command: %v", err)), nil
}
@@ -68,16 +68,16 @@ func (bs *BashServer) HandleBashExec(req mcp.CallToolRequest) (mcp.CallToolResul
}
// HandleBashExecStream handles the bash_exec_stream tool
-func (bs *BashServer) HandleBashExecStream(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (bash *BashOperations) HandleBashExecStream(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
// For now, this is the same as regular execution
// TODO: Implement actual streaming in future version
- return bs.HandleBashExec(req)
+ return bash.HandleBashExec(req)
}
// Documentation Tools
// HandleManPage handles the man_page tool
-func (bs *BashServer) HandleManPage(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (bash *BashOperations) HandleManPage(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
command, ok := req.Arguments["command"].(string)
if !ok || command == "" {
return mcp.NewToolError("command parameter is required"), nil
@@ -94,7 +94,7 @@ func (bs *BashServer) HandleManPage(req mcp.CallToolRequest) (mcp.CallToolResult
CaptureStderr: true,
}
- result, err := bs.executeCommand(options)
+ result, err := bash.executeCommand(options)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("Failed to get man page: %v", err)), nil
}
@@ -110,7 +110,7 @@ func (bs *BashServer) HandleManPage(req mcp.CallToolRequest) (mcp.CallToolResult
}
// HandleWhichCommand handles the which_command tool
-func (bs *BashServer) HandleWhichCommand(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (bash *BashOperations) HandleWhichCommand(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
command, ok := req.Arguments["command"].(string)
if !ok || command == "" {
return mcp.NewToolError("command parameter is required"), nil
@@ -128,7 +128,7 @@ func (bs *BashServer) HandleWhichCommand(req mcp.CallToolRequest) (mcp.CallToolR
CaptureStderr: true,
}
- result, err := bs.executeCommand(options)
+ result, err := bash.executeCommand(options)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("Failed to locate command: %v", err)), nil
}
@@ -144,7 +144,7 @@ func (bs *BashServer) HandleWhichCommand(req mcp.CallToolRequest) (mcp.CallToolR
}
// HandleCommandHelp handles the command_help tool
-func (bs *BashServer) HandleCommandHelp(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (bash *BashOperations) HandleCommandHelp(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
command, ok := req.Arguments["command"].(string)
if !ok || command == "" {
return mcp.NewToolError("command parameter is required"), nil
@@ -157,7 +157,7 @@ func (bs *BashServer) HandleCommandHelp(req mcp.CallToolRequest) (mcp.CallToolRe
CaptureStderr: true,
}
- result, err := bs.executeCommand(options)
+ result, err := bash.executeCommand(options)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("Failed to get command help: %v", err)), nil
}
@@ -181,7 +181,7 @@ func (bs *BashServer) HandleCommandHelp(req mcp.CallToolRequest) (mcp.CallToolRe
// Environment Management Tools
// HandleGetEnv handles the get_env tool
-func (bs *BashServer) HandleGetEnv(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (bash *BashOperations) HandleGetEnv(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
// Check if requesting all environment variables
if all, ok := req.Arguments["all"].(bool); ok && all {
envVars := make(map[string]string)
@@ -224,8 +224,8 @@ func (bs *BashServer) HandleGetEnv(req mcp.CallToolRequest) (mcp.CallToolResult,
}
// HandleGetWorkingDir handles the get_working_dir tool
-func (bs *BashServer) HandleGetWorkingDir(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- workingDir := bs.getWorkingDir()
+func (bash *BashOperations) HandleGetWorkingDir(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ workingDir := bash.getWorkingDir()
result := map[string]string{
"working_directory": workingDir,
@@ -242,18 +242,18 @@ func (bs *BashServer) HandleGetWorkingDir(req mcp.CallToolRequest) (mcp.CallTool
}
// HandleSetWorkingDir handles the set_working_dir tool
-func (bs *BashServer) HandleSetWorkingDir(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (bash *BashOperations) HandleSetWorkingDir(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
directory, ok := req.Arguments["directory"].(string)
if !ok || directory == "" {
return mcp.NewToolError("directory parameter is required"), nil
}
- if err := bs.setWorkingDir(directory); err != nil {
+ if err := bash.setWorkingDir(directory); err != nil {
return mcp.NewToolError(fmt.Sprintf("Failed to set working directory: %v", err)), nil
}
result := map[string]string{
- "working_directory": bs.getWorkingDir(),
+ "working_directory": bash.getWorkingDir(),
"message": "Working directory updated successfully",
}
@@ -270,8 +270,8 @@ func (bs *BashServer) HandleSetWorkingDir(req mcp.CallToolRequest) (mcp.CallTool
// System Information Tools
// HandleSystemInfo handles the system_info tool
-func (bs *BashServer) HandleSystemInfo(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- sysInfo, err := bs.getSystemInfo()
+func (bash *BashOperations) HandleSystemInfo(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ sysInfo, err := bash.getSystemInfo()
if err != nil {
return mcp.NewToolError(fmt.Sprintf("Failed to get system information: %v", err)), nil
}
@@ -287,7 +287,7 @@ func (bs *BashServer) HandleSystemInfo(req mcp.CallToolRequest) (mcp.CallToolRes
}
// HandleProcessInfo handles the process_info tool
-func (bs *BashServer) HandleProcessInfo(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (bash *BashOperations) HandleProcessInfo(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
// Build ps command
format := "aux" // default format
if f, ok := req.Arguments["format"].(string); ok && f != "" {
@@ -315,7 +315,7 @@ func (bs *BashServer) HandleProcessInfo(req mcp.CallToolRequest) (mcp.CallToolRe
CaptureStderr: true,
}
- result, err := bs.executeCommand(options)
+ result, err := bash.executeCommand(options)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("Failed to get process information: %v", err)), nil
}
@@ -333,8 +333,8 @@ func (bs *BashServer) HandleProcessInfo(req mcp.CallToolRequest) (mcp.CallToolRe
// Resource Handlers
// HandleSystemResource handles the system information resource
-func (bs *BashServer) HandleSystemResource(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
- sysInfo, err := bs.getSystemInfo()
+func (bash *BashOperations) HandleSystemResource(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
+ sysInfo, err := bash.getSystemInfo()
if err != nil {
return mcp.ReadResourceResult{}, fmt.Errorf("failed to get system information: %w", err)
}
@@ -350,8 +350,8 @@ func (bs *BashServer) HandleSystemResource(req mcp.ReadResourceRequest) (mcp.Rea
}
// HandleHistoryResource handles the command history resource
-func (bs *BashServer) HandleHistoryResource(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
- history := bs.getCommandHistory()
+func (bash *BashOperations) HandleHistoryResource(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
+ history := bash.getCommandHistory()
return mcp.ReadResourceResult{
Contents: []mcp.Content{
@@ -364,7 +364,7 @@ func (bs *BashServer) HandleHistoryResource(req mcp.ReadResourceRequest) (mcp.Re
}
// HandleEnvResource handles the environment variables resource
-func (bs *BashServer) HandleEnvResource(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
+func (bash *BashOperations) HandleEnvResource(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
envVars := make(map[string]string)
for _, env := range os.Environ() {
parts := strings.SplitN(env, "=", 2)
pkg/bash/server.go
@@ -16,9 +16,8 @@ import (
"github.com/xlgmokha/mcp/pkg/mcp"
)
-// BashServer implements the Bash MCP server
-type BashServer struct {
- *mcp.Server
+// BashOperations provides bash command execution operations
+type BashOperations struct {
workingDir string
commandHistory []CommandRecord
mu sync.RWMutex
@@ -74,8 +73,8 @@ type SystemInfo struct {
Path string `json:"path"`
}
-// NewBashServer creates a new Bash MCP server
-func NewBashServer(config *Config) (*BashServer, error) {
+// NewBashOperations creates a new BashOperations helper
+func NewBashOperations(config *Config) (*BashOperations, error) {
if config == nil {
config = DefaultConfig()
}
@@ -90,23 +89,200 @@ func NewBashServer(config *Config) (*BashServer, error) {
workingDir = cwd
}
- // Create base MCP server
- baseServer := mcp.NewServer("bash", "1.0.0", []mcp.Tool{}, []mcp.Resource{}, []mcp.Root{})
-
- server := &BashServer{
- Server: baseServer,
+ return &BashOperations{
workingDir: workingDir,
commandHistory: make([]CommandRecord, 0, config.HistorySize),
config: config,
- }
+ }, nil
+}
- // Register all tools
- server.registerTools()
+// New creates a new Bash MCP server
+func New(config *Config) (*mcp.Server, error) {
+ bash, err := NewBashOperations(config)
+ if err != nil {
+ return nil, err
+ }
- // Register resources
- server.registerResources()
-
- return server, nil
+ builder := mcp.NewServerBuilder("bash", "1.0.0")
+
+ // Add bash_exec tool
+ builder.AddTool(mcp.NewTool("bash_exec", "Execute a shell command and return output", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "command": map[string]interface{}{
+ "type": "string",
+ "description": "Shell command to execute",
+ },
+ "working_dir": map[string]interface{}{
+ "type": "string",
+ "description": "Working directory for command execution (optional)",
+ },
+ "timeout": map[string]interface{}{
+ "type": "number",
+ "description": "Timeout in seconds (default: 30, max: 300)",
+ },
+ "capture_stderr": map[string]interface{}{
+ "type": "boolean",
+ "description": "Include stderr in output (default: true)",
+ },
+ "env": map[string]interface{}{
+ "type": "object",
+ "description": "Additional environment variables",
+ },
+ },
+ "required": []string{"command"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return bash.HandleBashExec(req)
+ }))
+
+ // Add bash_exec_stream tool
+ builder.AddTool(mcp.NewTool("bash_exec_stream", "Execute command with real-time output streaming", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "command": map[string]interface{}{
+ "type": "string",
+ "description": "Shell command to execute",
+ },
+ "working_dir": map[string]interface{}{
+ "type": "string",
+ "description": "Working directory for command execution (optional)",
+ },
+ "timeout": map[string]interface{}{
+ "type": "number",
+ "description": "Timeout in seconds (default: 30, max: 300)",
+ },
+ "buffer_size": map[string]interface{}{
+ "type": "number",
+ "description": "Stream buffer size in bytes",
+ },
+ },
+ "required": []string{"command"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return bash.HandleBashExecStream(req)
+ }))
+
+ // Add man_page tool
+ builder.AddTool(mcp.NewTool("man_page", "Get manual page for a command", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "command": map[string]interface{}{
+ "type": "string",
+ "description": "Command to get manual for",
+ },
+ "section": map[string]interface{}{
+ "type": "string",
+ "description": "Manual section (1-8)",
+ },
+ },
+ "required": []string{"command"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return bash.HandleManPage(req)
+ }))
+
+ // Add which_command tool
+ builder.AddTool(mcp.NewTool("which_command", "Find the location of a command", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "command": map[string]interface{}{
+ "type": "string",
+ "description": "Command to locate",
+ },
+ },
+ "required": []string{"command"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return bash.HandleWhichCommand(req)
+ }))
+
+ // Add command_help tool
+ builder.AddTool(mcp.NewTool("command_help", "Get help text for a command (--help flag)", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "command": map[string]interface{}{
+ "type": "string",
+ "description": "Command to get help for",
+ },
+ },
+ "required": []string{"command"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return bash.HandleCommandHelp(req)
+ }))
+
+ // Add get_env tool
+ builder.AddTool(mcp.NewTool("get_env", "Get environment variable value", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "variable": map[string]interface{}{
+ "type": "string",
+ "description": "Environment variable name",
+ },
+ "all": map[string]interface{}{
+ "type": "boolean",
+ "description": "Return all environment variables",
+ },
+ },
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return bash.HandleGetEnv(req)
+ }))
+
+ // Add get_working_dir tool
+ builder.AddTool(mcp.NewTool("get_working_dir", "Get the current working directory", map[string]interface{}{
+ "type": "object",
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return bash.HandleGetWorkingDir(req)
+ }))
+
+ // Add set_working_dir tool
+ builder.AddTool(mcp.NewTool("set_working_dir", "Set working directory for future commands", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "directory": map[string]interface{}{
+ "type": "string",
+ "description": "Directory path to set as working directory",
+ },
+ },
+ "required": []string{"directory"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return bash.HandleSetWorkingDir(req)
+ }))
+
+ // Add system_info tool
+ builder.AddTool(mcp.NewTool("system_info", "Get basic system information", map[string]interface{}{
+ "type": "object",
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return bash.HandleSystemInfo(req)
+ }))
+
+ // Add process_info tool
+ builder.AddTool(mcp.NewTool("process_info", "Get information about running processes (ps command wrapper)", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "format": map[string]interface{}{
+ "type": "string",
+ "description": "ps format string (default: aux)",
+ },
+ "filter": map[string]interface{}{
+ "type": "string",
+ "description": "grep filter for processes",
+ },
+ },
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return bash.HandleProcessInfo(req)
+ }))
+
+ // Add resources
+ builder.AddResource(mcp.NewResource("bash://system/info", "System Information", "application/json", func(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
+ return bash.HandleSystemResource(req)
+ }))
+
+ builder.AddResource(mcp.NewResource("bash://history/recent", "Command History", "application/json", func(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
+ return bash.HandleHistoryResource(req)
+ }))
+
+ builder.AddResource(mcp.NewResource("bash://env/all", "Environment Variables", "application/json", func(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
+ return bash.HandleEnvResource(req)
+ }))
+
+ return builder.Build(), nil
}
// DefaultConfig returns default configuration
@@ -148,106 +324,42 @@ func ConfigFromEnv() *Config {
return config
}
-// registerTools registers all bash tools with the server
-func (bs *BashServer) registerTools() {
- // Get all tool definitions from ListTools method
- tools := bs.ListTools()
-
- // Register each tool with its proper definition
- for _, tool := range tools {
- var handler mcp.ToolHandler
- switch tool.Name {
- case "bash_exec":
- handler = bs.HandleBashExec
- case "bash_exec_stream":
- handler = bs.HandleBashExecStream
- case "man_page":
- handler = bs.HandleManPage
- case "which_command":
- handler = bs.HandleWhichCommand
- case "command_help":
- handler = bs.HandleCommandHelp
- case "get_env":
- handler = bs.HandleGetEnv
- case "get_working_dir":
- handler = bs.HandleGetWorkingDir
- case "set_working_dir":
- handler = bs.HandleSetWorkingDir
- case "system_info":
- handler = bs.HandleSystemInfo
- case "process_info":
- handler = bs.HandleProcessInfo
- default:
- continue
- }
- bs.RegisterToolWithDefinition(tool, handler)
- }
-}
-// registerResources registers MCP resources
-func (bs *BashServer) registerResources() {
- // System information resource
- systemResource := mcp.Resource{
- URI: "bash://system/info",
- Name: "System Information",
- Description: "Live system information and environment state",
- MimeType: "application/json",
- }
- bs.Server.RegisterResourceWithDefinition(systemResource, bs.HandleSystemResource)
-
- // Command history resource
- historyResource := mcp.Resource{
- URI: "bash://history/recent",
- Name: "Command History",
- Description: "Recent command execution history",
- MimeType: "application/json",
- }
- bs.Server.RegisterResourceWithDefinition(historyResource, bs.HandleHistoryResource)
-
- // Environment variables resource
- envResource := mcp.Resource{
- URI: "bash://env/all",
- Name: "Environment Variables",
- Description: "Complete environment variables",
- MimeType: "application/json",
- }
- bs.Server.RegisterResourceWithDefinition(envResource, bs.HandleEnvResource)
-}
// addCommandRecord adds a command record to history
-func (bs *BashServer) addCommandRecord(record CommandRecord) {
- bs.mu.Lock()
- defer bs.mu.Unlock()
+func (bash *BashOperations) addCommandRecord(record CommandRecord) {
+ bash.mu.Lock()
+ defer bash.mu.Unlock()
// Add record
- bs.commandHistory = append(bs.commandHistory, record)
+ bash.commandHistory = append(bash.commandHistory, record)
// Trim history if it exceeds the maximum size
- if len(bs.commandHistory) > bs.config.HistorySize {
- bs.commandHistory = bs.commandHistory[len(bs.commandHistory)-bs.config.HistorySize:]
+ if len(bash.commandHistory) > bash.config.HistorySize {
+ bash.commandHistory = bash.commandHistory[len(bash.commandHistory)-bash.config.HistorySize:]
}
}
// getCommandHistory returns a copy of the command history
-func (bs *BashServer) getCommandHistory() []CommandRecord {
- bs.mu.RLock()
- defer bs.mu.RUnlock()
+func (bash *BashOperations) getCommandHistory() []CommandRecord {
+ bash.mu.RLock()
+ defer bash.mu.RUnlock()
// Return a copy to avoid race conditions
- history := make([]CommandRecord, len(bs.commandHistory))
- copy(history, bs.commandHistory)
+ history := make([]CommandRecord, len(bash.commandHistory))
+ copy(history, bash.commandHistory)
return history
}
// getWorkingDir returns the current working directory
-func (bs *BashServer) getWorkingDir() string {
- bs.mu.RLock()
- defer bs.mu.RUnlock()
- return bs.workingDir
+func (bash *BashOperations) getWorkingDir() string {
+ bash.mu.RLock()
+ defer bash.mu.RUnlock()
+ return bash.workingDir
}
// setWorkingDir sets the working directory
-func (bs *BashServer) setWorkingDir(dir string) error {
+func (bash *BashOperations) setWorkingDir(dir string) error {
// Validate directory exists
if _, err := os.Stat(dir); os.IsNotExist(err) {
return fmt.Errorf("directory does not exist: %s", dir)
@@ -259,30 +371,30 @@ func (bs *BashServer) setWorkingDir(dir string) error {
return fmt.Errorf("failed to get absolute path: %w", err)
}
- bs.mu.Lock()
- defer bs.mu.Unlock()
- bs.workingDir = absDir
+ bash.mu.Lock()
+ defer bash.mu.Unlock()
+ bash.workingDir = absDir
return nil
}
// executeCommand executes a shell command with the given options
-func (bs *BashServer) executeCommand(options ExecutionOptions) (*ExecutionResult, error) {
+func (bash *BashOperations) executeCommand(options ExecutionOptions) (*ExecutionResult, error) {
startTime := time.Now()
// Set default timeout
if options.Timeout <= 0 {
- options.Timeout = bs.config.DefaultTimeout
+ options.Timeout = bash.config.DefaultTimeout
}
// Enforce maximum timeout
- if options.Timeout > bs.config.MaxTimeout {
- options.Timeout = bs.config.MaxTimeout
+ if options.Timeout > bash.config.MaxTimeout {
+ options.Timeout = bash.config.MaxTimeout
}
// Set working directory
workingDir := options.WorkingDir
if workingDir == "" {
- workingDir = bs.getWorkingDir()
+ workingDir = bash.getWorkingDir()
}
// Create context with timeout
@@ -314,7 +426,7 @@ func (bs *BashServer) executeCommand(options ExecutionOptions) (*ExecutionResult
var err error
if options.CaptureStderr {
- stdout, stderr, err = bs.runCommandWithStderr(cmd)
+ stdout, stderr, err = bash.runCommandWithStderr(cmd)
} else {
stdout, err = cmd.Output()
}
@@ -351,13 +463,13 @@ func (bs *BashServer) executeCommand(options ExecutionOptions) (*ExecutionResult
Duration: executionTime,
OutputSize: len(stdout) + len(stderr),
}
- bs.addCommandRecord(record)
+ bash.addCommandRecord(record)
return result, nil
}
// runCommandWithStderr runs a command and captures both stdout and stderr
-func (bs *BashServer) runCommandWithStderr(cmd *exec.Cmd) ([]byte, []byte, error) {
+func (bash *BashOperations) runCommandWithStderr(cmd *exec.Cmd) ([]byte, []byte, error) {
var stdout, stderr []byte
var err error
@@ -413,7 +525,7 @@ func readAll(r interface{ Read([]byte) (int, error) }) ([]byte, error) {
}
// getSystemInfo returns current system information
-func (bs *BashServer) getSystemInfo() (*SystemInfo, error) {
+func (bash *BashOperations) getSystemInfo() (*SystemInfo, error) {
hostname, _ := os.Hostname()
currentUser, _ := user.Current()
@@ -434,7 +546,7 @@ func (bs *BashServer) getSystemInfo() (*SystemInfo, error) {
// Get kernel version
kernel := "unknown"
if runtime.GOOS == "linux" {
- if result, err := bs.executeCommand(ExecutionOptions{Command: "uname -r", CaptureStderr: false}); err == nil {
+ if result, err := bash.executeCommand(ExecutionOptions{Command: "uname -r", CaptureStderr: false}); err == nil {
kernel = strings.TrimSpace(result.Stdout)
}
}
pkg/bash/server_test.go
@@ -1,423 +0,0 @@
-package bash
-
-import (
- "testing"
- "time"
-
- "github.com/xlgmokha/mcp/pkg/mcp"
-)
-
-func TestNewBashServer(t *testing.T) {
- tests := []struct {
- name string
- config *Config
- wantErr bool
- }{
- {
- name: "default config",
- config: nil,
- wantErr: false,
- },
- {
- name: "custom config",
- config: &Config{
- DefaultTimeout: 60,
- MaxTimeout: 600,
- HistorySize: 200,
- WorkingDir: "/tmp",
- },
- wantErr: false,
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- server, err := NewBashServer(tt.config)
- if (err != nil) != tt.wantErr {
- t.Errorf("NewBashServer() error = %v, wantErr %v", err, tt.wantErr)
- return
- }
- if !tt.wantErr && server == nil {
- t.Error("NewBashServer() returned nil server")
- }
- })
- }
-}
-
-func TestDefaultConfig(t *testing.T) {
- config := DefaultConfig()
-
- if config.DefaultTimeout != 30 {
- t.Errorf("DefaultTimeout = %v, want 30", config.DefaultTimeout)
- }
- if config.MaxTimeout != 300 {
- t.Errorf("MaxTimeout = %v, want 300", config.MaxTimeout)
- }
- if config.HistorySize != 100 {
- t.Errorf("HistorySize = %v, want 100", config.HistorySize)
- }
- if config.WorkingDir != "" {
- t.Errorf("WorkingDir = %v, want empty string", config.WorkingDir)
- }
-}
-
-func TestConfigFromEnv(t *testing.T) {
- // Set environment variables
- t.Setenv("BASH_MCP_DEFAULT_TIMEOUT", "45")
- t.Setenv("BASH_MCP_MAX_TIMEOUT", "600")
- t.Setenv("BASH_MCP_MAX_HISTORY", "50")
- t.Setenv("BASH_MCP_WORKING_DIR", "/tmp/test")
-
- config := ConfigFromEnv()
-
- if config.DefaultTimeout != 45 {
- t.Errorf("DefaultTimeout = %v, want 45", config.DefaultTimeout)
- }
- if config.MaxTimeout != 600 {
- t.Errorf("MaxTimeout = %v, want 600", config.MaxTimeout)
- }
- if config.HistorySize != 50 {
- t.Errorf("HistorySize = %v, want 50", config.HistorySize)
- }
- if config.WorkingDir != "/tmp/test" {
- t.Errorf("WorkingDir = %v, want /tmp/test", config.WorkingDir)
- }
-}
-
-func TestExecuteCommand(t *testing.T) {
- server, err := NewBashServer(nil)
- if err != nil {
- t.Fatalf("Failed to create server: %v", err)
- }
-
- tests := []struct {
- name string
- options ExecutionOptions
- wantErr bool
- check func(*testing.T, *ExecutionResult)
- }{
- {
- name: "simple echo command",
- options: ExecutionOptions{
- Command: "echo 'Hello, World!'",
- },
- wantErr: false,
- check: func(t *testing.T, result *ExecutionResult) {
- if result.ExitCode != 0 {
- t.Errorf("ExitCode = %v, want 0", result.ExitCode)
- }
- if result.Stdout != "Hello, World!\n" {
- t.Errorf("Stdout = %v, want 'Hello, World!\\n'", result.Stdout)
- }
- },
- },
- {
- name: "command with error",
- options: ExecutionOptions{
- Command: "exit 1",
- CaptureStderr: true,
- },
- wantErr: false,
- check: func(t *testing.T, result *ExecutionResult) {
- if result.ExitCode != 1 {
- t.Errorf("ExitCode = %v, want 1", result.ExitCode)
- }
- },
- },
- {
- name: "command with stderr",
- options: ExecutionOptions{
- Command: "echo 'error' >&2",
- CaptureStderr: true,
- },
- wantErr: false,
- check: func(t *testing.T, result *ExecutionResult) {
- if result.ExitCode != 0 {
- t.Errorf("ExitCode = %v, want 0", result.ExitCode)
- }
- if result.Stderr != "error\n" {
- t.Errorf("Stderr = %v, want 'error\\n'", result.Stderr)
- }
- },
- },
- {
- name: "command with timeout",
- options: ExecutionOptions{
- Command: "sleep 0.1",
- Timeout: 1,
- },
- wantErr: false,
- check: func(t *testing.T, result *ExecutionResult) {
- if result.ExitCode != 0 {
- t.Errorf("ExitCode = %v, want 0", result.ExitCode)
- }
- },
- },
- {
- name: "command with environment variable",
- options: ExecutionOptions{
- Command: "echo $TEST_VAR",
- Env: map[string]string{
- "TEST_VAR": "test_value",
- },
- },
- wantErr: false,
- check: func(t *testing.T, result *ExecutionResult) {
- if result.ExitCode != 0 {
- t.Errorf("ExitCode = %v, want 0", result.ExitCode)
- }
- if result.Stdout != "test_value\n" {
- t.Errorf("Stdout = %v, want 'test_value\\n'", result.Stdout)
- }
- },
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result, err := server.executeCommand(tt.options)
- if (err != nil) != tt.wantErr {
- t.Errorf("executeCommand() error = %v, wantErr %v", err, tt.wantErr)
- return
- }
- if !tt.wantErr && result != nil && tt.check != nil {
- tt.check(t, result)
- }
- })
- }
-}
-
-func TestCommandHistory(t *testing.T) {
- config := &Config{
- DefaultTimeout: 30,
- MaxTimeout: 300,
- HistorySize: 3,
- WorkingDir: "",
- }
-
- server, err := NewBashServer(config)
- if err != nil {
- t.Fatalf("Failed to create server: %v", err)
- }
-
- // Execute multiple commands
- for i := 0; i < 5; i++ {
- _, err := server.executeCommand(ExecutionOptions{
- Command: "echo test",
- })
- if err != nil {
- t.Fatalf("Failed to execute command: %v", err)
- }
- }
-
- // Check history size is limited
- history := server.getCommandHistory()
- if len(history) != 3 {
- t.Errorf("History length = %v, want 3", len(history))
- }
-}
-
-func TestSetWorkingDir(t *testing.T) {
- server, err := NewBashServer(nil)
- if err != nil {
- t.Fatalf("Failed to create server: %v", err)
- }
-
- tests := []struct {
- name string
- dir string
- wantErr bool
- }{
- {
- name: "valid directory",
- dir: "/tmp",
- wantErr: false,
- },
- {
- name: "non-existent directory",
- dir: "/this/does/not/exist",
- wantErr: true,
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- err := server.setWorkingDir(tt.dir)
- if (err != nil) != tt.wantErr {
- t.Errorf("setWorkingDir() error = %v, wantErr %v", err, tt.wantErr)
- }
- })
- }
-}
-
-func TestGetSystemInfo(t *testing.T) {
- server, err := NewBashServer(nil)
- if err != nil {
- t.Fatalf("Failed to create server: %v", err)
- }
-
- info, err := server.getSystemInfo()
- if err != nil {
- t.Fatalf("getSystemInfo() error = %v", err)
- }
-
- // Check that required fields are populated
- if info.Hostname == "" {
- t.Error("Hostname is empty")
- }
- if info.OS == "" {
- t.Error("OS is empty")
- }
- if info.Architecture == "" {
- t.Error("Architecture is empty")
- }
- if info.User == "" {
- t.Error("User is empty")
- }
-}
-
-func TestHandleBashExec(t *testing.T) {
- server, err := NewBashServer(nil)
- if err != nil {
- t.Fatalf("Failed to create server: %v", err)
- }
-
- req := mcp.CallToolRequest{
- Name: "bash_exec",
- Arguments: map[string]interface{}{
- "command": "echo 'test output'",
- },
- }
-
- result, err := server.HandleBashExec(req)
- if err != nil {
- t.Fatalf("HandleBashExec() error = %v", err)
- }
-
- // Check that we got content back
- if len(result.Content) == 0 {
- t.Fatal("No content returned")
- }
-
- // The content should be a TextContent
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Content is not TextContent")
- }
-
- // Should contain the output
- if textContent.Text == "" {
- t.Error("Text content is empty")
- }
-}
-
-func TestHandleSystemInfo(t *testing.T) {
- server, err := NewBashServer(nil)
- if err != nil {
- t.Fatalf("Failed to create server: %v", err)
- }
-
- req := mcp.CallToolRequest{
- Name: "system_info",
- Arguments: map[string]interface{}{},
- }
-
- result, err := server.HandleSystemInfo(req)
- if err != nil {
- t.Fatalf("HandleSystemInfo() error = %v", err)
- }
-
- // Check that we got content back
- if len(result.Content) == 0 {
- t.Fatal("No content returned")
- }
-}
-
-func TestHandleSetWorkingDir(t *testing.T) {
- server, err := NewBashServer(nil)
- if err != nil {
- t.Fatalf("Failed to create server: %v", err)
- }
-
- req := mcp.CallToolRequest{
- Name: "set_working_dir",
- Arguments: map[string]interface{}{
- "directory": "/tmp",
- },
- }
-
- _, err = server.HandleSetWorkingDir(req)
- if err != nil {
- t.Fatalf("HandleSetWorkingDir() error = %v", err)
- }
-
- // Verify the working directory was changed
- if server.getWorkingDir() != "/tmp" {
- t.Errorf("Working directory = %v, want /tmp", server.getWorkingDir())
- }
-}
-
-func TestHandleGetEnv(t *testing.T) {
- server, err := NewBashServer(nil)
- if err != nil {
- t.Fatalf("Failed to create server: %v", err)
- }
-
- // Set a test environment variable
- t.Setenv("TEST_ENV_VAR", "test_value")
-
- req := mcp.CallToolRequest{
- Name: "get_env",
- Arguments: map[string]interface{}{
- "name": "TEST_ENV_VAR",
- },
- }
-
- result, err := server.HandleGetEnv(req)
- if err != nil {
- t.Fatalf("HandleGetEnv() error = %v", err)
- }
-
- // Check that we got content back
- if len(result.Content) == 0 {
- t.Fatal("No content returned")
- }
-}
-
-func TestCommandRecordCreation(t *testing.T) {
- server, err := NewBashServer(nil)
- if err != nil {
- t.Fatalf("Failed to create server: %v", err)
- }
-
- // Execute a command
- startTime := time.Now()
- result, err := server.executeCommand(ExecutionOptions{
- Command: "echo test",
- })
- if err != nil {
- t.Fatalf("Failed to execute command: %v", err)
- }
-
- // Get history
- history := server.getCommandHistory()
- if len(history) != 1 {
- t.Fatalf("Expected 1 history record, got %d", len(history))
- }
-
- record := history[0]
-
- // Verify record fields
- if record.Command != "echo test" {
- t.Errorf("Command = %v, want 'echo test'", record.Command)
- }
- if record.ExitCode != 0 {
- t.Errorf("ExitCode = %v, want 0", record.ExitCode)
- }
- if record.Timestamp.Before(startTime) {
- t.Error("Timestamp is before command start time")
- }
- if record.Duration != result.ExecutionTime {
- t.Errorf("Duration = %v, want %v", record.Duration, result.ExecutionTime)
- }
-}
\ No newline at end of file
pkg/bash/tools.go
@@ -1,180 +1,4 @@
package bash
-import "github.com/xlgmokha/mcp/pkg/mcp"
-
-// ListTools returns all available bash tools with their definitions
-func (bs *BashServer) ListTools() []mcp.Tool {
- return []mcp.Tool{
- // Core Execution Tools
- {
- Name: "bash_exec",
- Description: "Execute a shell command and return output",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "command": map[string]interface{}{
- "type": "string",
- "description": "Shell command to execute",
- },
- "working_dir": map[string]interface{}{
- "type": "string",
- "description": "Working directory for command execution (optional)",
- },
- "timeout": map[string]interface{}{
- "type": "number",
- "description": "Timeout in seconds (default: 30, max: 300)",
- },
- "capture_stderr": map[string]interface{}{
- "type": "boolean",
- "description": "Include stderr in output (default: true)",
- },
- "env": map[string]interface{}{
- "type": "object",
- "description": "Additional environment variables",
- },
- },
- "required": []string{"command"},
- },
- },
- {
- Name: "bash_exec_stream",
- Description: "Execute command with real-time output streaming",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "command": map[string]interface{}{
- "type": "string",
- "description": "Shell command to execute",
- },
- "working_dir": map[string]interface{}{
- "type": "string",
- "description": "Working directory for command execution (optional)",
- },
- "timeout": map[string]interface{}{
- "type": "number",
- "description": "Timeout in seconds (default: 30, max: 300)",
- },
- "buffer_size": map[string]interface{}{
- "type": "number",
- "description": "Stream buffer size in bytes",
- },
- },
- "required": []string{"command"},
- },
- },
-
- // Documentation Tools
- {
- Name: "man_page",
- Description: "Get manual page for a command",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "command": map[string]interface{}{
- "type": "string",
- "description": "Command to get manual for",
- },
- "section": map[string]interface{}{
- "type": "string",
- "description": "Manual section (1-8)",
- },
- },
- "required": []string{"command"},
- },
- },
- {
- Name: "which_command",
- Description: "Find the location of a command",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "command": map[string]interface{}{
- "type": "string",
- "description": "Command to locate",
- },
- },
- "required": []string{"command"},
- },
- },
- {
- Name: "command_help",
- Description: "Get help text for a command (--help flag)",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "command": map[string]interface{}{
- "type": "string",
- "description": "Command to get help for",
- },
- },
- "required": []string{"command"},
- },
- },
-
- // Environment Management Tools
- {
- Name: "get_env",
- Description: "Get environment variable value",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "variable": map[string]interface{}{
- "type": "string",
- "description": "Environment variable name",
- },
- "all": map[string]interface{}{
- "type": "boolean",
- "description": "Return all environment variables",
- },
- },
- },
- },
- {
- Name: "get_working_dir",
- Description: "Get the current working directory",
- InputSchema: map[string]interface{}{
- "type": "object",
- },
- },
- {
- Name: "set_working_dir",
- Description: "Set working directory for future commands",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "directory": map[string]interface{}{
- "type": "string",
- "description": "Directory path to set as working directory",
- },
- },
- "required": []string{"directory"},
- },
- },
-
- // System Information Tools
- {
- Name: "system_info",
- Description: "Get basic system information",
- InputSchema: map[string]interface{}{
- "type": "object",
- },
- },
- {
- Name: "process_info",
- Description: "Get information about running processes (ps command wrapper)",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "format": map[string]interface{}{
- "type": "string",
- "description": "ps format string (default: aux)",
- },
- "filter": map[string]interface{}{
- "type": "string",
- "description": "grep filter for processes",
- },
- },
- },
- },
- }
-}
\ No newline at end of file
+// This file previously contained tool definitions that are now inlined in server.go
+// Keeping the file for potential future bash-specific utilities
\ No newline at end of file
pkg/fetch/server_test.go
@@ -1,344 +0,0 @@
-package fetch
-
-import (
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
-
- "github.com/xlgmokha/mcp/pkg/mcp"
-)
-
-func TestServer_FetchTool(t *testing.T) {
- // Create test server with HTML content
- testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "text/html")
- w.WriteHeader(http.StatusOK)
- w.Write([]byte(`
- <html>
- <head><title>Test Page</title></head>
- <body>
- <h1>Welcome</h1>
- <p>This is a test page with some content.</p>
- <div>More content here</div>
- </body>
- </html>
- `))
- }))
- defer testServer.Close()
-
- server := New()
-
- req := mcp.CallToolRequest{
- Name: "fetch",
- Arguments: map[string]interface{}{
- "url": testServer.URL,
- },
- }
-
- result, err := server.HandleFetch(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if result.IsError {
- textContent, _ := result.Content[0].(mcp.TextContent)
- t.Fatalf("Expected successful fetch, got error: %s", textContent.Text)
- }
-
- if len(result.Content) == 0 {
- t.Fatal("Expected content in result")
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- // Should contain converted markdown content
- if !contains(textContent.Text, "Welcome") {
- t.Fatalf("Expected 'Welcome' in converted content, got: %s", textContent.Text)
- }
-
- if !contains(textContent.Text, "test page") {
- t.Fatalf("Expected 'test page' in converted content, got: %s", textContent.Text)
- }
-}
-
-func TestServer_FetchRawContent(t *testing.T) {
- // Create test server with HTML content
- testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "text/html")
- w.WriteHeader(http.StatusOK)
- w.Write([]byte(`<html><body><h1>Raw HTML</h1></body></html>`))
- }))
- defer testServer.Close()
-
- server := New()
-
- req := mcp.CallToolRequest{
- Name: "fetch",
- Arguments: map[string]interface{}{
- "url": testServer.URL,
- "raw": true,
- },
- }
-
- result, err := server.HandleFetch(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if result.IsError {
- textContent, _ := result.Content[0].(mcp.TextContent)
- t.Fatalf("Expected successful fetch, got error: %s", textContent.Text)
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- // Should contain raw HTML content (JSON escaped)
- if !contains(textContent.Text, "\\u003chtml\\u003e") && !contains(textContent.Text, "<html>") {
- t.Fatalf("Expected raw HTML content (possibly JSON escaped), got: %s", textContent.Text)
- }
-
- if !contains(textContent.Text, "Raw HTML") {
- t.Fatalf("Expected 'Raw HTML' in content, got: %s", textContent.Text)
- }
-}
-
-func TestServer_FetchWithMaxLength(t *testing.T) {
- // Create test server with long plain text content to avoid HTML conversion complexity
- longContent := strings.Repeat("x", 200) // 200 characters
- testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "text/plain")
- w.WriteHeader(http.StatusOK)
- w.Write([]byte(longContent))
- }))
- defer testServer.Close()
-
- server := New()
-
- req := mcp.CallToolRequest{
- Name: "fetch",
- Arguments: map[string]interface{}{
- "url": testServer.URL,
- "max_length": 100,
- },
- }
-
- result, err := server.HandleFetch(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if result.IsError {
- textContent, _ := result.Content[0].(mcp.TextContent)
- t.Fatalf("Expected successful fetch, got error: %s", textContent.Text)
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- // Parse the JSON response to check that content was truncated
- if !contains(textContent.Text, "\"length\": 100") {
- t.Fatalf("Expected content length to be exactly 100 chars, got: %s", textContent.Text)
- }
-
- if !contains(textContent.Text, "\"truncated\": true") {
- t.Fatalf("Expected truncated flag to be true, got: %s", textContent.Text)
- }
-}
-
-func TestServer_FetchWithStartIndex(t *testing.T) {
- // Create test server with content
- testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "text/html")
- w.WriteHeader(http.StatusOK)
- w.Write([]byte(`<html><body><p>Start of content. Middle of content. End of content.</p></body></html>`))
- }))
- defer testServer.Close()
-
- server := New()
-
- req := mcp.CallToolRequest{
- Name: "fetch",
- Arguments: map[string]interface{}{
- "url": testServer.URL,
- "start_index": 20,
- },
- }
-
- result, err := server.HandleFetch(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if result.IsError {
- textContent, _ := result.Content[0].(mcp.TextContent)
- t.Fatalf("Expected successful fetch, got error: %s", textContent.Text)
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- // Should not contain the beginning of the content in the actual content field
- // Since start_index=20 and the markdown conversion changes the content,
- // we should check that some content was actually returned but not the exact beginning
- if !contains(textContent.Text, "content") {
- t.Fatalf("Expected some content after start_index=20, got: %s", textContent.Text)
- }
-}
-
-func TestServer_FetchInvalidURL(t *testing.T) {
- server := New()
-
- req := mcp.CallToolRequest{
- Name: "fetch",
- Arguments: map[string]interface{}{
- "url": "not-a-valid-url",
- },
- }
-
- result, err := server.HandleFetch(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if !result.IsError {
- t.Fatal("Expected error for invalid URL")
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- if !contains(textContent.Text, "Invalid URL") && !contains(textContent.Text, "invalid URL") {
- t.Fatalf("Expected invalid URL error, got: %s", textContent.Text)
- }
-}
-
-func TestServer_FetchHTTPError(t *testing.T) {
- // Create test server that returns 404
- testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(http.StatusNotFound)
- w.Write([]byte("Not Found"))
- }))
- defer testServer.Close()
-
- server := New()
-
- req := mcp.CallToolRequest{
- Name: "fetch",
- Arguments: map[string]interface{}{
- "url": testServer.URL,
- },
- }
-
- result, err := server.HandleFetch(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if !result.IsError {
- t.Fatal("Expected error for 404 response")
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- if !contains(textContent.Text, "404") {
- t.Fatalf("Expected 404 error, got: %s", textContent.Text)
- }
-}
-
-func TestServer_FetchPlainText(t *testing.T) {
- // Create test server with plain text content
- testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "text/plain")
- w.WriteHeader(http.StatusOK)
- w.Write([]byte("This is plain text content without HTML tags."))
- }))
- defer testServer.Close()
-
- server := New()
-
- req := mcp.CallToolRequest{
- Name: "fetch",
- Arguments: map[string]interface{}{
- "url": testServer.URL,
- },
- }
-
- result, err := server.HandleFetch(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if result.IsError {
- textContent, _ := result.Content[0].(mcp.TextContent)
- t.Fatalf("Expected successful fetch, got error: %s", textContent.Text)
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- // Should contain plain text content as-is
- if !contains(textContent.Text, "plain text content") {
- t.Fatalf("Expected plain text content, got: %s", textContent.Text)
- }
-}
-
-func TestServer_ListTools(t *testing.T) {
- server := New()
- tools := server.ListTools()
-
- expectedTools := []string{
- "fetch",
- }
-
- if len(tools) != len(expectedTools) {
- t.Fatalf("Expected %d tools, got %d", len(expectedTools), len(tools))
- }
-
- toolNames := make(map[string]bool)
- for _, tool := range tools {
- toolNames[tool.Name] = true
- }
-
- for _, expected := range expectedTools {
- if !toolNames[expected] {
- t.Fatalf("Expected tool %s not found", expected)
- }
- }
-
- // Check that fetch tool has proper schema
- fetchTool := tools[0]
- if fetchTool.Name != "fetch" {
- t.Fatalf("Expected first tool to be 'fetch', got %s", fetchTool.Name)
- }
-
- if fetchTool.Description == "" {
- t.Fatal("Expected non-empty description for fetch tool")
- }
-
- if fetchTool.InputSchema == nil {
- t.Fatal("Expected input schema for fetch tool")
- }
-}
-
-// Helper functions
-func contains(s, substr string) bool {
- return strings.Contains(s, substr)
-}
pkg/filesystem/server_test.go
@@ -1,219 +0,0 @@
-package filesystem
-
-import (
- "os"
- "path/filepath"
- "testing"
-
- "github.com/xlgmokha/mcp/pkg/mcp"
-)
-
-func TestFilesystemServer_ReadFile(t *testing.T) {
- tempDir, err := os.MkdirTemp("", "fs-test-*")
- if err != nil {
- t.Fatal(err)
- }
- defer os.RemoveAll(tempDir)
-
- testFile := filepath.Join(tempDir, "test.txt")
- testContent := "Hello, World!"
- if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil {
- t.Fatal(err)
- }
-
- server := New([]string{tempDir})
-
- req := mcp.CallToolRequest{
- Name: "read_file",
- Arguments: map[string]interface{}{
- "path": testFile,
- },
- }
-
- // Get the read_file tool and call its handler
- tools := server.ListTools()
- var readTool mcp.Tool
- for _, tool := range tools {
- if tool.Name == "read_file" {
- readTool = tool
- break
- }
- }
- result, err := readTool.Handler(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if len(result.Content) == 0 {
- t.Fatal("Expected content in result")
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- if textContent.Text != testContent {
- t.Fatalf("Expected '%s', got '%s'", testContent, textContent.Text)
- }
-}
-
-func TestFilesystemServer_WriteFile(t *testing.T) {
- tempDir, err := os.MkdirTemp("", "fs-test-*")
- if err != nil {
- t.Fatal(err)
- }
- defer os.RemoveAll(tempDir)
-
- server := New([]string{tempDir})
- testFile := filepath.Join(tempDir, "new_file.txt")
- testContent := "New content"
-
- req := mcp.CallToolRequest{
- Name: "write_file",
- Arguments: map[string]interface{}{
- "path": testFile,
- "content": testContent,
- },
- }
-
- // Get the write_file tool and call its handler
- tools := server.ListTools()
- var writeTool mcp.Tool
- for _, tool := range tools {
- if tool.Name == "write_file" {
- writeTool = tool
- break
- }
- }
- result, err := writeTool.Handler(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if result.IsError {
- t.Fatal("Expected successful write")
- }
-
- content, err := os.ReadFile(testFile)
- if err != nil {
- t.Fatal("File should have been created")
- }
-
- if string(content) != testContent {
- t.Fatalf("Expected '%s', got '%s'", testContent, string(content))
- }
-}
-
-func TestFilesystemServer_SecurityValidation(t *testing.T) {
- tempDir, err := os.MkdirTemp("", "fs-test-*")
- if err != nil {
- t.Fatal(err)
- }
- defer os.RemoveAll(tempDir)
-
- server := New([]string{tempDir})
-
- req := mcp.CallToolRequest{
- Name: "read_file",
- Arguments: map[string]interface{}{
- "path": "/etc/passwd",
- },
- }
-
- // Get the read_file tool and call its handler
- tools := server.ListTools()
- var readTool mcp.Tool
- for _, tool := range tools {
- if tool.Name == "read_file" {
- readTool = tool
- break
- }
- }
- result, err := readTool.Handler(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if !result.IsError {
- t.Fatal("Expected error for unauthorized path access")
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- if !contains(textContent.Text, "access denied") {
- t.Fatalf("Expected access denied error, got: %s", textContent.Text)
- }
-}
-
-func TestFilesystemServer_ListTools(t *testing.T) {
- server := New([]string{"/tmp"})
- tools := server.ListTools()
-
- expectedTools := []string{"read_file", "write_file"}
-
- if len(tools) != len(expectedTools) {
- t.Fatalf("Expected %d tools, got %d", len(expectedTools), len(tools))
- }
-
- toolNames := make(map[string]bool)
- for _, tool := range tools {
- toolNames[tool.Name] = true
- }
-
- for _, expected := range expectedTools {
- if !toolNames[expected] {
- t.Fatalf("Expected tool %s not found", expected)
- }
- }
-}
-
-func TestFilesystemServer_Resources(t *testing.T) {
- tempDir, err := os.MkdirTemp("", "fs-test-*")
- if err != nil {
- t.Fatal(err)
- }
- defer os.RemoveAll(tempDir)
-
- testFile := filepath.Join(tempDir, "test.go")
- if err := os.WriteFile(testFile, []byte("package main"), 0644); err != nil {
- t.Fatal(err)
- }
-
- server := New([]string{tempDir})
- resources := server.ListResources()
-
- if len(resources) == 0 {
- t.Fatal("Expected resources to be found")
- }
-
- foundGoFile := false
- for _, resource := range resources {
- if contains(resource.URI, "test.go") && resource.MimeType == "text/x-go" {
- foundGoFile = true
- break
- }
- }
-
- if !foundGoFile {
- t.Fatal("Expected to find test.go resource")
- }
-}
-
-func contains(s, substr string) bool {
- return len(s) > 0 && len(substr) > 0 &&
- (s == substr || (len(s) >= len(substr) &&
- findSubstring(s, substr)))
-}
-
-func findSubstring(s, substr string) bool {
- for i := 0; i <= len(s)-len(substr); i++ {
- if s[i:i+len(substr)] == substr {
- return true
- }
- }
- return false
-}
\ No newline at end of file
pkg/git/server_test.go
@@ -1,215 +0,0 @@
-package git
-
-import (
- "fmt"
- "os"
- "path/filepath"
- "testing"
-
- "github.com/xlgmokha/mcp/pkg/mcp"
-)
-
-func TestGitServer_GitStatus(t *testing.T) {
- // Setup test repo
- tempDir, err := os.MkdirTemp("", "git-test-*")
- if err != nil {
- t.Fatal(err)
- }
- defer os.RemoveAll(tempDir)
-
- // Initialize git repo
- if err := initGitRepo(tempDir); err != nil {
- t.Fatal(err)
- }
-
- server := New(".")
-
- // Test git status
- req := mcp.CallToolRequest{
- Name: "git_status",
- Arguments: map[string]interface{}{
- "repo_path": tempDir,
- },
- }
-
- result, err := server.HandleGitStatus(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if len(result.Content) == 0 {
- t.Fatal("Expected content in result")
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- if textContent.Text == "" {
- t.Fatal("Expected non-empty status text")
- }
-
- // Should contain status information
- if len(textContent.Text) < 10 {
- t.Fatal("Expected meaningful status output")
- }
-}
-
-func TestGitServer_GitInit(t *testing.T) {
- // Setup temp directory
- tempDir, err := os.MkdirTemp("", "git-init-test-*")
- if err != nil {
- t.Fatal(err)
- }
- defer os.RemoveAll(tempDir)
-
- server := New(".")
-
- // Test git init
- req := mcp.CallToolRequest{
- Name: "git_init",
- Arguments: map[string]interface{}{
- "repo_path": tempDir,
- },
- }
-
- result, err := server.HandleGitInit(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if len(result.Content) == 0 {
- t.Fatal("Expected content in result")
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- // Check that .git directory was created
- gitDir := filepath.Join(tempDir, ".git")
- if _, err := os.Stat(gitDir); os.IsNotExist(err) {
- t.Fatal("Expected .git directory to be created")
- }
-
- // Check response message
- if !contains(textContent.Text, "Initialized") {
- t.Fatalf("Expected init success message, got: %s", textContent.Text)
- }
-}
-
-func TestGitServer_GitAdd(t *testing.T) {
- // Setup test repo
- tempDir, err := os.MkdirTemp("", "git-test-*")
- if err != nil {
- t.Fatal(err)
- }
- defer os.RemoveAll(tempDir)
-
- // Initialize git repo
- if err := initGitRepo(tempDir); err != nil {
- t.Fatal(err)
- }
-
- // Create a test file
- testFile := filepath.Join(tempDir, "test.txt")
- if err := os.WriteFile(testFile, []byte("test content"), 0644); err != nil {
- t.Fatal(err)
- }
-
- server := New(".")
-
- // Test git add
- req := mcp.CallToolRequest{
- Name: "git_add",
- Arguments: map[string]interface{}{
- "repo_path": tempDir,
- "files": []interface{}{"test.txt"},
- },
- }
-
- result, err := server.HandleGitAdd(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if len(result.Content) == 0 {
- t.Fatal("Expected content in result")
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- // Check response message
- if !contains(textContent.Text, "staged") {
- t.Fatalf("Expected staged success message, got: %s", textContent.Text)
- }
-}
-
-func TestGitServer_ListTools(t *testing.T) {
- server := New(".")
- tools := server.ListTools()
-
- expectedTools := []string{
- "git_status", "git_diff_unstaged", "git_diff_staged", "git_diff",
- "git_commit", "git_add", "git_reset", "git_log", "git_create_branch",
- "git_checkout", "git_show", "git_init",
- }
-
- if len(tools) != len(expectedTools) {
- t.Fatalf("Expected %d tools, got %d", len(expectedTools), len(tools))
- }
-
- toolNames := make(map[string]bool)
- for _, tool := range tools {
- toolNames[tool.Name] = true
- }
-
- for _, expected := range expectedTools {
- if !toolNames[expected] {
- t.Fatalf("Expected tool %s not found", expected)
- }
- }
-}
-
-// Helper functions
-func initGitRepo(dir string) error {
- // Use actual git init command for testing
- server := New(".")
- req := mcp.CallToolRequest{
- Name: "git_init",
- Arguments: map[string]interface{}{
- "repo_path": dir,
- },
- }
-
- result, err := server.HandleGitInit(req)
- if err != nil {
- return err
- }
-
- if result.IsError {
- return fmt.Errorf("git init failed")
- }
-
- return nil
-}
-
-func contains(s, substr string) bool {
- return len(s) > 0 && len(substr) > 0 &&
- (s == substr || (len(s) >= len(substr) &&
- findSubstring(s, substr)))
-}
-
-func findSubstring(s, substr string) bool {
- for i := 0; i <= len(s)-len(substr); i++ {
- if s[i:i+len(substr)] == substr {
- return true
- }
- }
- return false
-}
pkg/gitlab/cache_test.go
@@ -1,327 +0,0 @@
-package gitlab
-
-import (
- "encoding/json"
- "os"
- "path/filepath"
- "testing"
- "time"
-)
-
-func TestCacheBasicOperations(t *testing.T) {
- // Create temporary cache directory
- tempDir, err := os.MkdirTemp("", "gitlab-cache-test")
- if err != nil {
- t.Fatalf("Failed to create temp dir: %v", err)
- }
- defer os.RemoveAll(tempDir)
-
- // Create cache with test config
- cache, err := NewCache(CacheConfig{
- CacheDir: tempDir,
- TTL: 1 * time.Minute,
- MaxEntries: 100,
- EnableOffline: true,
- CompressData: false,
- })
- if err != nil {
- t.Fatalf("Failed to create cache: %v", err)
- }
-
- // Test data
- testData := []byte(`{"id": 1, "title": "Test Issue"}`)
- endpoint := "/issues"
- params := map[string]string{"state": "opened"}
- cacheType := "issues"
-
- // Test Set operation
- err = cache.Set(cacheType, endpoint, params, testData, 200)
- if err != nil {
- t.Fatalf("Failed to set cache: %v", err)
- }
-
- // Test Get operation
- cached, found := cache.Get(cacheType, endpoint, params)
- if !found {
- t.Fatal("Expected to find cached data")
- }
-
- // Compare JSON data structure instead of string format
- var expectedData, cachedData map[string]interface{}
- if err := json.Unmarshal(testData, &expectedData); err != nil {
- t.Fatalf("Failed to unmarshal test data: %v", err)
- }
- if err := json.Unmarshal(cached, &cachedData); err != nil {
- t.Fatalf("Failed to unmarshal cached data: %v", err)
- }
-
- if expectedData["id"] != cachedData["id"] || expectedData["title"] != cachedData["title"] {
- t.Errorf("Cached data mismatch. Expected: %v, Got: %v", expectedData, cachedData)
- }
-
- // Test cache statistics
- stats := cache.GetStats()
- if stats[cacheType] == nil {
- t.Fatal("Expected cache stats for issues")
- }
-
- if stats[cacheType].EntryCount != 1 {
- t.Errorf("Expected 1 entry, got %d", stats[cacheType].EntryCount)
- }
-
- if stats[cacheType].TotalHits != 1 {
- t.Errorf("Expected 1 hit, got %d", stats[cacheType].TotalHits)
- }
-}
-
-func TestCacheExpiration(t *testing.T) {
- // Create temporary cache directory
- tempDir, err := os.MkdirTemp("", "gitlab-cache-expire-test")
- if err != nil {
- t.Fatalf("Failed to create temp dir: %v", err)
- }
- defer os.RemoveAll(tempDir)
-
- // Create cache with very short TTL
- cache, err := NewCache(CacheConfig{
- CacheDir: tempDir,
- TTL: 10 * time.Millisecond, // Very short TTL
- MaxEntries: 100,
- EnableOffline: false, // Disable offline mode for expiration test
- CompressData: false,
- })
- if err != nil {
- t.Fatalf("Failed to create cache: %v", err)
- }
-
- // Test data
- testData := []byte(`{"id": 1, "title": "Test Issue"}`)
- endpoint := "/issues"
- params := map[string]string{"state": "opened"}
- cacheType := "issues"
-
- // Set cache entry
- err = cache.Set(cacheType, endpoint, params, testData, 200)
- if err != nil {
- t.Fatalf("Failed to set cache: %v", err)
- }
-
- // Immediately check - should be found
- _, found := cache.Get(cacheType, endpoint, params)
- if !found {
- t.Fatal("Expected to find fresh cached data")
- }
-
- // Wait for expiration
- time.Sleep(20 * time.Millisecond)
-
- // Check again - should be expired
- _, found = cache.Get(cacheType, endpoint, params)
- if found {
- t.Fatal("Expected cached data to be expired")
- }
-}
-
-func TestCacheOfflineMode(t *testing.T) {
- // Create temporary cache directory
- tempDir, err := os.MkdirTemp("", "gitlab-cache-offline-test")
- if err != nil {
- t.Fatalf("Failed to create temp dir: %v", err)
- }
- defer os.RemoveAll(tempDir)
-
- // Create cache with offline mode enabled
- cache, err := NewCache(CacheConfig{
- CacheDir: tempDir,
- TTL: 10 * time.Millisecond, // Very short TTL
- MaxEntries: 100,
- EnableOffline: true, // Enable offline mode
- CompressData: false,
- })
- if err != nil {
- t.Fatalf("Failed to create cache: %v", err)
- }
-
- // Test data
- testData := []byte(`{"id": 1, "title": "Test Issue"}`)
- endpoint := "/issues"
- params := map[string]string{"state": "opened"}
- cacheType := "issues"
-
- // Set cache entry
- err = cache.Set(cacheType, endpoint, params, testData, 200)
- if err != nil {
- t.Fatalf("Failed to set cache: %v", err)
- }
-
- // Wait for expiration
- time.Sleep(20 * time.Millisecond)
-
- // Check again - should return stale data in offline mode
- cached, found := cache.Get(cacheType, endpoint, params)
- if !found {
- t.Fatal("Expected to find stale cached data in offline mode")
- }
-
- // Compare JSON data structure instead of string format
- var expectedData, cachedData map[string]interface{}
- if err := json.Unmarshal(testData, &expectedData); err != nil {
- t.Fatalf("Failed to unmarshal test data: %v", err)
- }
- if err := json.Unmarshal(cached, &cachedData); err != nil {
- t.Fatalf("Failed to unmarshal cached data: %v", err)
- }
-
- if expectedData["id"] != cachedData["id"] || expectedData["title"] != cachedData["title"] {
- t.Errorf("Stale cached data mismatch. Expected: %v, Got: %v", expectedData, cachedData)
- }
-}
-
-func TestCacheFileStructure(t *testing.T) {
- // Create temporary cache directory
- tempDir, err := os.MkdirTemp("", "gitlab-cache-structure-test")
- if err != nil {
- t.Fatalf("Failed to create temp dir: %v", err)
- }
- defer os.RemoveAll(tempDir)
-
- // Create cache
- cache, err := NewCache(CacheConfig{
- CacheDir: tempDir,
- TTL: 1 * time.Minute,
- MaxEntries: 100,
- EnableOffline: true,
- CompressData: false,
- })
- if err != nil {
- t.Fatalf("Failed to create cache: %v", err)
- }
-
- // Test data
- testData := []byte(`{"id": 1, "title": "Test Issue"}`)
- endpoint := "/issues"
- params := map[string]string{"state": "opened"}
- cacheType := "issues"
-
- // Set cache entry
- err = cache.Set(cacheType, endpoint, params, testData, 200)
- if err != nil {
- t.Fatalf("Failed to set cache: %v", err)
- }
-
- // Check cache directory structure
- issuesDir := filepath.Join(tempDir, "issues")
- if _, err := os.Stat(issuesDir); os.IsNotExist(err) {
- t.Fatal("Expected issues cache directory to exist")
- }
-
- // Check for cache files
- files, err := filepath.Glob(filepath.Join(issuesDir, "**", "*.json"))
- if err != nil {
- files, _ = filepath.Glob(filepath.Join(issuesDir, "*.json"))
- }
-
- if len(files) == 0 {
- t.Fatal("Expected at least one cache file")
- }
-
- // Read and verify cache file structure
- cacheFile := files[0]
- fileData, err := os.ReadFile(cacheFile)
- if err != nil {
- t.Fatalf("Failed to read cache file: %v", err)
- }
-
- var entry CacheEntry
- if err := json.Unmarshal(fileData, &entry); err != nil {
- t.Fatalf("Failed to parse cache entry: %v", err)
- }
-
- if entry.StatusCode != 200 {
- t.Errorf("Expected status code 200, got %d", entry.StatusCode)
- }
-
- // Compare JSON data structure instead of string format
- var expectedData, entryData map[string]interface{}
- if err := json.Unmarshal(testData, &expectedData); err != nil {
- t.Fatalf("Failed to unmarshal test data: %v", err)
- }
- if err := json.Unmarshal(entry.Data, &entryData); err != nil {
- t.Fatalf("Failed to unmarshal entry data: %v", err)
- }
-
- if expectedData["id"] != entryData["id"] || expectedData["title"] != entryData["title"] {
- t.Errorf("Cache entry data mismatch. Expected: %v, Got: %v", expectedData, entryData)
- }
-
- // Check metadata file
- metadataFile := filepath.Join(tempDir, "metadata.json")
- if _, err := os.Stat(metadataFile); os.IsNotExist(err) {
- t.Fatal("Expected metadata.json to exist")
- }
-}
-
-func TestCacheClear(t *testing.T) {
- // Create temporary cache directory
- tempDir, err := os.MkdirTemp("", "gitlab-cache-clear-test")
- if err != nil {
- t.Fatalf("Failed to create temp dir: %v", err)
- }
- defer os.RemoveAll(tempDir)
-
- // Create cache
- cache, err := NewCache(CacheConfig{
- CacheDir: tempDir,
- TTL: 1 * time.Minute,
- MaxEntries: 100,
- EnableOffline: true,
- CompressData: false,
- })
- if err != nil {
- t.Fatalf("Failed to create cache: %v", err)
- }
-
- // Add test data
- testData := []byte(`{"id": 1, "title": "Test Issue"}`)
- endpoint := "/issues"
- params := map[string]string{"state": "opened"}
- cacheType := "issues"
-
- err = cache.Set(cacheType, endpoint, params, testData, 200)
- if err != nil {
- t.Fatalf("Failed to set cache: %v", err)
- }
-
- // Verify data exists
- _, found := cache.Get(cacheType, endpoint, params)
- if !found {
- t.Fatal("Expected to find cached data before clear")
- }
-
- // Clear specific cache type
- err = cache.Clear(cacheType)
- if err != nil {
- t.Fatalf("Failed to clear cache: %v", err)
- }
-
- // Verify data is gone
- _, found = cache.Get(cacheType, endpoint, params)
- if found {
- t.Fatal("Expected cached data to be cleared")
- }
-
- // Check that directory was removed or has no cache files
- issuesDir := filepath.Join(tempDir, "issues")
- if stat, err := os.Stat(issuesDir); err == nil {
- if stat.IsDir() {
- // Check for actual cache files (*.json)
- files, err := filepath.Glob(filepath.Join(issuesDir, "**", "*.json"))
- if err != nil {
- files, _ = filepath.Glob(filepath.Join(issuesDir, "*.json"))
- }
- if len(files) > 0 {
- t.Fatalf("Expected no cache files after clear, but found: %v", files)
- }
- }
- }
-}
\ No newline at end of file
pkg/gitlab/server.go
@@ -15,8 +15,8 @@ import (
"github.com/xlgmokha/mcp/pkg/mcp"
)
-type Server struct {
- *mcp.Server
+// GitLabOperations provides GitLab API operations with caching
+type GitLabOperations struct {
mu sync.RWMutex
gitlabURL string
token string
@@ -96,9 +96,8 @@ type GitLabUser struct {
State string `json:"state"`
}
-func NewServer(gitlabURL, token string) (*Server, error) {
- baseServer := mcp.NewServer("gitlab", "0.1.0", []mcp.Tool{}, []mcp.Resource{}, []mcp.Root{})
-
+// NewGitLabOperations creates a new GitLabOperations helper
+func NewGitLabOperations(gitlabURL, token string) (*GitLabOperations, error) {
// Initialize cache with default configuration
cache, err := NewCache(CacheConfig{
TTL: 5 * time.Minute,
@@ -110,241 +109,210 @@ func NewServer(gitlabURL, token string) (*Server, error) {
return nil, fmt.Errorf("failed to initialize cache: %w", err)
}
- server := &Server{
- Server: baseServer,
+ return &GitLabOperations{
gitlabURL: strings.TrimSuffix(gitlabURL, "/"),
token: token,
client: &http.Client{
Timeout: 30 * time.Second,
},
cache: cache,
- }
-
- // Register tools
- server.registerTools()
-
- return server, nil
+ }, nil
}
-// ListTools returns all available GitLab tools
-func (s *Server) ListTools() []mcp.Tool {
- return []mcp.Tool{
- {
- Name: "gitlab_list_my_projects",
- Description: "List projects where you are a member, with activity and access level info",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "limit": map[string]interface{}{
- "type": "integer",
- "description": "Maximum number of projects to return",
- "minimum": 1,
- "default": 20,
- },
- "archived": map[string]interface{}{
- "type": "boolean",
- "description": "Include archived projects",
- "default": false,
- },
- },
+// New creates a new GitLab MCP server
+func New(gitlabURL, token string) (*mcp.Server, error) {
+ gitlab, err := NewGitLabOperations(gitlabURL, token)
+ if err != nil {
+ return nil, err
+ }
+
+ builder := mcp.NewServerBuilder("gitlab-server", "1.0.0")
+
+ // Add gitlab_list_my_projects tool
+ builder.AddTool(mcp.NewTool("gitlab_list_my_projects", "List projects where you are a member, with activity and access level info", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "limit": map[string]interface{}{
+ "type": "integer",
+ "description": "Maximum number of projects to return",
+ "minimum": 1,
+ "default": 20,
},
- },
- {
- Name: "gitlab_list_my_issues",
- Description: "List issues assigned to you, created by you, or where you're mentioned",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "scope": map[string]interface{}{
- "type": "string",
- "description": "Filter scope: assigned_to_me, authored, mentioned, all",
- "default": "assigned_to_me",
- "enum": []string{"assigned_to_me", "authored", "mentioned", "all"},
- },
- "state": map[string]interface{}{
- "type": "string",
- "description": "Issue state filter: opened, closed, all",
- "default": "opened",
- "enum": []string{"opened", "closed", "all"},
- },
- "limit": map[string]interface{}{
- "type": "integer",
- "description": "Maximum number of issues to return",
- "minimum": 1,
- "default": 50,
- },
- },
+ "archived": map[string]interface{}{
+ "type": "boolean",
+ "description": "Include archived projects",
+ "default": false,
},
},
- {
- Name: "gitlab_get_issue_conversations",
- Description: "Get full conversation history for a specific issue including notes and system events",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "project_id": map[string]interface{}{
- "type": "integer",
- "description": "GitLab project ID",
- "minimum": 1,
- },
- "issue_iid": map[string]interface{}{
- "type": "integer",
- "description": "Issue internal ID within the project",
- "minimum": 1,
- },
- },
- "required": []string{"project_id", "issue_iid"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return gitlab.handleListMyProjects(req)
+ }))
+
+ // Add gitlab_list_my_issues tool
+ builder.AddTool(mcp.NewTool("gitlab_list_my_issues", "List issues assigned to you, created by you, or where you're mentioned", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "scope": map[string]interface{}{
+ "type": "string",
+ "description": "Filter scope: assigned_to_me, authored, mentioned, all",
+ "default": "assigned_to_me",
+ "enum": []string{"assigned_to_me", "authored", "mentioned", "all"},
+ },
+ "state": map[string]interface{}{
+ "type": "string",
+ "description": "Issue state filter: opened, closed, all",
+ "default": "opened",
+ "enum": []string{"opened", "closed", "all"},
+ },
+ "limit": map[string]interface{}{
+ "type": "integer",
+ "description": "Maximum number of issues to return",
+ "minimum": 1,
+ "default": 50,
},
},
- {
- Name: "gitlab_find_similar_issues",
- Description: "Find issues similar to a search query across your accessible projects",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "query": map[string]interface{}{
- "type": "string",
- "description": "Search query for finding similar issues",
- },
- "limit": map[string]interface{}{
- "type": "integer",
- "description": "Maximum number of results",
- "minimum": 1,
- "default": 20,
- },
- },
- "required": []string{"query"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return gitlab.handleListMyIssues(req)
+ }))
+
+ // Add gitlab_get_issue_conversations tool
+ builder.AddTool(mcp.NewTool("gitlab_get_issue_conversations", "Get full conversation history for a specific issue including notes and system events", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "project_id": map[string]interface{}{
+ "type": "integer",
+ "description": "GitLab project ID",
+ "minimum": 1,
+ },
+ "issue_iid": map[string]interface{}{
+ "type": "integer",
+ "description": "Issue internal ID within the project",
+ "minimum": 1,
},
},
- {
- Name: "gitlab_get_my_activity",
- Description: "Get recent activity summary including commits, issues, merge requests",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "limit": map[string]interface{}{
- "type": "integer",
- "description": "Maximum number of activity events",
- "minimum": 1,
- "default": 50,
- },
- },
+ "required": []string{"project_id", "issue_iid"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return gitlab.handleGetIssueConversations(req)
+ }))
+
+ // Add gitlab_find_similar_issues tool
+ builder.AddTool(mcp.NewTool("gitlab_find_similar_issues", "Find issues similar to a search query across your accessible projects", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "query": map[string]interface{}{
+ "type": "string",
+ "description": "Search query for finding similar issues",
+ },
+ "limit": map[string]interface{}{
+ "type": "integer",
+ "description": "Maximum number of results",
+ "minimum": 1,
+ "default": 20,
},
},
- {
- Name: "gitlab_cache_stats",
- Description: "Get cache performance statistics and storage information",
- InputSchema: map[string]interface{}{
- "type": "object",
+ "required": []string{"query"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return gitlab.handleFindSimilarIssues(req)
+ }))
+
+ // Add gitlab_get_my_activity tool
+ builder.AddTool(mcp.NewTool("gitlab_get_my_activity", "Get recent activity summary including commits, issues, merge requests", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "limit": map[string]interface{}{
+ "type": "integer",
+ "description": "Maximum number of activity events",
+ "minimum": 1,
+ "default": 50,
},
},
- {
- Name: "gitlab_cache_clear",
- Description: "Clear cached data for specific types or all cached data",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "cache_type": map[string]interface{}{
- "type": "string",
- "description": "Type of cache to clear: issues, projects, users, notes, events, search, or empty for all",
- "enum": []string{"issues", "projects", "users", "notes", "events", "search"},
- },
- "confirm": map[string]interface{}{
- "type": "string",
- "description": "Confirmation string 'true' to proceed with cache clearing",
- },
- },
- "required": []string{"confirm"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return gitlab.handleGetMyActivity(req)
+ }))
+
+ // Add gitlab_cache_stats tool
+ builder.AddTool(mcp.NewTool("gitlab_cache_stats", "Get cache performance statistics and storage information", map[string]interface{}{
+ "type": "object",
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return gitlab.handleCacheStats(req)
+ }))
+
+ // Add gitlab_cache_clear tool
+ builder.AddTool(mcp.NewTool("gitlab_cache_clear", "Clear cached data for specific types or all cached data", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "cache_type": map[string]interface{}{
+ "type": "string",
+ "description": "Type of cache to clear: issues, projects, users, notes, events, search, or empty for all",
+ "enum": []string{"issues", "projects", "users", "notes", "events", "search"},
+ },
+ "confirm": map[string]interface{}{
+ "type": "string",
+ "description": "Confirmation string 'true' to proceed with cache clearing",
},
},
- {
- Name: "gitlab_offline_query",
- Description: "Query cached GitLab data when network connectivity is unavailable",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "query_type": map[string]interface{}{
- "type": "string",
- "description": "Type of cached data to query: issues, projects, users, notes, events",
- "enum": []string{"issues", "projects", "users", "notes", "events"},
- },
- "search": map[string]interface{}{
- "type": "string",
- "description": "Optional search term to filter cached results",
- },
- "limit": map[string]interface{}{
- "type": "integer",
- "description": "Maximum number of results to return",
- "minimum": 1,
- "default": 50,
- },
- },
- "required": []string{"query_type"},
+ "required": []string{"confirm"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return gitlab.handleCacheClear(req)
+ }))
+
+ // Add gitlab_offline_query tool
+ builder.AddTool(mcp.NewTool("gitlab_offline_query", "Query cached GitLab data when network connectivity is unavailable", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "query_type": map[string]interface{}{
+ "type": "string",
+ "description": "Type of cached data to query: issues, projects, users, notes, events",
+ "enum": []string{"issues", "projects", "users", "notes", "events"},
+ },
+ "search": map[string]interface{}{
+ "type": "string",
+ "description": "Optional search term to filter cached results",
+ },
+ "limit": map[string]interface{}{
+ "type": "integer",
+ "description": "Maximum number of results to return",
+ "minimum": 1,
+ "default": 50,
},
},
- }
-}
+ "required": []string{"query_type"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return gitlab.handleOfflineQuery(req)
+ }))
-// registerTools registers all GitLab tools with the server
-func (s *Server) registerTools() {
- // Get all tool definitions from ListTools method
- tools := s.ListTools()
-
- // Register each tool with its proper definition
- for _, tool := range tools {
- var handler mcp.ToolHandler
- switch tool.Name {
- case "gitlab_list_my_projects":
- handler = s.handleListMyProjects
- case "gitlab_list_my_issues":
- handler = s.handleListMyIssues
- case "gitlab_get_issue_conversations":
- handler = s.handleGetIssueConversations
- case "gitlab_find_similar_issues":
- handler = s.handleFindSimilarIssues
- case "gitlab_get_my_activity":
- handler = s.handleGetMyActivity
- case "gitlab_cache_stats":
- handler = s.handleCacheStats
- case "gitlab_cache_clear":
- handler = s.handleCacheClear
- case "gitlab_offline_query":
- handler = s.handleOfflineQuery
- default:
- continue
- }
- s.RegisterToolWithDefinition(tool, handler)
- }
+ return builder.Build(), nil
}
-func (s *Server) makeRequest(method, endpoint string, params map[string]string) ([]byte, error) {
- return s.makeRequestWithCache(method, endpoint, params, "")
+
+func (gitlab *GitLabOperations) makeRequest(method, endpoint string, params map[string]string) ([]byte, error) {
+ return gitlab.makeRequestWithCache(method, endpoint, params, "")
}
-func (s *Server) makeRequestWithCache(method, endpoint string, params map[string]string, cacheType string) ([]byte, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
+func (gitlab *GitLabOperations) makeRequestWithCache(method, endpoint string, params map[string]string, cacheType string) ([]byte, error) {
+ gitlab.mu.RLock()
+ defer gitlab.mu.RUnlock()
// Determine cache type from endpoint if not provided
if cacheType == "" {
- cacheType = s.determineCacheType(endpoint)
+ cacheType = gitlab.determineCacheType(endpoint)
}
// Check cache first (only for GET requests)
if method == "GET" && cacheType != "" {
- if cached, found := s.cache.Get(cacheType, endpoint, params); found {
+ if cached, found := gitlab.cache.Get(cacheType, endpoint, params); found {
return cached, nil
}
}
- url := fmt.Sprintf("%s/api/v4%s", s.gitlabURL, endpoint)
+ url := fmt.Sprintf("%s/api/v4%s", gitlab.gitlabURL, endpoint)
req, err := http.NewRequest(method, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
- req.Header.Set("Authorization", "Bearer "+s.token)
+ req.Header.Set("Authorization", "Bearer "+gitlab.token)
req.Header.Set("Content-Type", "application/json")
// Add query parameters
@@ -356,11 +324,11 @@ func (s *Server) makeRequestWithCache(method, endpoint string, params map[string
req.URL.RawQuery = q.Encode()
}
- resp, err := s.client.Do(req)
+ resp, err := gitlab.client.Do(req)
if err != nil {
// If request fails and we have cached data, try to return stale data
- if method == "GET" && cacheType != "" && s.cache.config.EnableOffline {
- if cached, found := s.cache.Get(cacheType, endpoint, params); found {
+ if method == "GET" && cacheType != "" && gitlab.cache.config.EnableOffline {
+ if cached, found := gitlab.cache.Get(cacheType, endpoint, params); found {
fmt.Fprintf(os.Stderr, "Network error, returning cached data: %v\n", err)
return cached, nil
}
@@ -381,7 +349,7 @@ func (s *Server) makeRequestWithCache(method, endpoint string, params map[string
// Cache the response (only for GET requests)
if method == "GET" && cacheType != "" {
- if err := s.cache.Set(cacheType, endpoint, params, body, resp.StatusCode); err != nil {
+ if err := gitlab.cache.Set(cacheType, endpoint, params, body, resp.StatusCode); err != nil {
// Log cache error but don't fail the request
fmt.Fprintf(os.Stderr, "Failed to cache response: %v\n", err)
}
@@ -391,7 +359,7 @@ func (s *Server) makeRequestWithCache(method, endpoint string, params map[string
}
// determineCacheType maps API endpoints to cache types
-func (s *Server) determineCacheType(endpoint string) string {
+func (gitlab *GitLabOperations) determineCacheType(endpoint string) string {
switch {
case strings.Contains(endpoint, "/issues"):
return "issues"
@@ -410,8 +378,8 @@ func (s *Server) determineCacheType(endpoint string) string {
}
}
-func (s *Server) getCurrentUser() (*GitLabUser, error) {
- body, err := s.makeRequest("GET", "/user", nil)
+func (gitlab *GitLabOperations) getCurrentUser() (*GitLabUser, error) {
+ body, err := gitlab.makeRequest("GET", "/user", nil)
if err != nil {
return nil, err
}
@@ -424,7 +392,7 @@ func (s *Server) getCurrentUser() (*GitLabUser, error) {
return &user, nil
}
-func (s *Server) handleListMyProjects(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (gitlab *GitLabOperations) handleListMyProjects(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
args := req.Arguments
// Parse optional parameters
@@ -461,7 +429,7 @@ func (s *Server) handleListMyProjects(req mcp.CallToolRequest) (mcp.CallToolResu
}
// Make API request
- body, err := s.makeRequest("GET", "/projects", params)
+ body, err := gitlab.makeRequest("GET", "/projects", params)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("Failed to fetch projects: %v", err)), nil
}
@@ -499,7 +467,7 @@ func (s *Server) handleListMyProjects(req mcp.CallToolRequest) (mcp.CallToolResu
return mcp.NewToolResult(mcp.NewTextContent(result)), nil
}
-func (s *Server) handleListMyIssues(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (gitlab *GitLabOperations) handleListMyIssues(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
args := req.Arguments
// Parse optional parameters
@@ -524,7 +492,7 @@ func (s *Server) handleListMyIssues(req mcp.CallToolRequest) (mcp.CallToolResult
searchTerm, _ := args["search"].(string)
// Get current user for filtering
- user, err := s.getCurrentUser()
+ user, err := gitlab.getCurrentUser()
if err != nil {
return mcp.NewToolError(fmt.Sprintf("Failed to get current user: %v", err)), nil
}
@@ -554,7 +522,7 @@ func (s *Server) handleListMyIssues(req mcp.CallToolRequest) (mcp.CallToolResult
}
// Make API request
- body, err := s.makeRequest("GET", "/issues", params)
+ body, err := gitlab.makeRequest("GET", "/issues", params)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("Failed to fetch issues: %v", err)), nil
}
@@ -621,7 +589,7 @@ func extractProjectFromURL(webURL string) string {
return "Unknown"
}
-func (s *Server) handleGetIssueConversations(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (gitlab *GitLabOperations) handleGetIssueConversations(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
args := req.Arguments
projectIDStr, ok := args["project_id"].(string)
@@ -648,7 +616,7 @@ func (s *Server) handleGetIssueConversations(req mcp.CallToolRequest) (mcp.CallT
// First, get the issue details
issueEndpoint := fmt.Sprintf("/projects/%d/issues/%d", projectID, issueIID)
- issueBody, err := s.makeRequest("GET", issueEndpoint, nil)
+ issueBody, err := gitlab.makeRequest("GET", issueEndpoint, nil)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("Failed to fetch issue: %v", err)), nil
}
@@ -665,7 +633,7 @@ func (s *Server) handleGetIssueConversations(req mcp.CallToolRequest) (mcp.CallT
"sort": "asc",
}
- notesBody, err := s.makeRequest("GET", notesEndpoint, notesParams)
+ notesBody, err := gitlab.makeRequest("GET", notesEndpoint, notesParams)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("Failed to fetch issue notes: %v", err)), nil
}
@@ -741,7 +709,7 @@ func (s *Server) handleGetIssueConversations(req mcp.CallToolRequest) (mcp.CallT
return mcp.NewToolResult(mcp.NewTextContent(result)), nil
}
-func (s *Server) handleFindSimilarIssues(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (gitlab *GitLabOperations) handleFindSimilarIssues(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
args := req.Arguments
searchQuery, ok := args["query"].(string)
@@ -776,7 +744,7 @@ func (s *Server) handleFindSimilarIssues(req mcp.CallToolRequest) (mcp.CallToolR
}
// Make search API request
- body, err := s.makeRequest("GET", "/search", params)
+ body, err := gitlab.makeRequest("GET", "/search", params)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("Failed to search issues: %v", err)), nil
}
@@ -864,7 +832,7 @@ func (s *Server) handleFindSimilarIssues(req mcp.CallToolRequest) (mcp.CallToolR
return mcp.NewToolResult(mcp.NewTextContent(result)), nil
}
-func (s *Server) handleGetMyActivity(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (gitlab *GitLabOperations) handleGetMyActivity(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
args := req.Arguments
// Parse optional parameters
@@ -885,7 +853,7 @@ func (s *Server) handleGetMyActivity(req mcp.CallToolRequest) (mcp.CallToolResul
}
// Get current user
- user, err := s.getCurrentUser()
+ user, err := gitlab.getCurrentUser()
if err != nil {
return mcp.NewToolError(fmt.Sprintf("Failed to get current user: %v", err)), nil
}
@@ -903,7 +871,7 @@ func (s *Server) handleGetMyActivity(req mcp.CallToolRequest) (mcp.CallToolResul
"updated_after": since,
}
- assignedBody, err := s.makeRequest("GET", "/issues", assignedParams)
+ assignedBody, err := gitlab.makeRequest("GET", "/issues", assignedParams)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("Failed to fetch assigned issues: %v", err)), nil
}
@@ -921,7 +889,7 @@ func (s *Server) handleGetMyActivity(req mcp.CallToolRequest) (mcp.CallToolResul
"updated_after": since,
}
- authoredBody, err := s.makeRequest("GET", "/issues", authoredParams)
+ authoredBody, err := gitlab.makeRequest("GET", "/issues", authoredParams)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("Failed to fetch authored issues: %v", err)), nil
}
@@ -934,7 +902,7 @@ func (s *Server) handleGetMyActivity(req mcp.CallToolRequest) (mcp.CallToolResul
"per_page": "20",
}
- activityBody, err := s.makeRequest("GET", fmt.Sprintf("/users/%d/events", user.ID), activityParams)
+ activityBody, err := gitlab.makeRequest("GET", fmt.Sprintf("/users/%d/events", user.ID), activityParams)
if err != nil {
// Activity endpoint might not be available, continue without it
activityBody = []byte("[]")
@@ -1058,8 +1026,8 @@ func min(a, b int) int {
// Cache management tools
-func (s *Server) handleCacheStats(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- stats := s.cache.GetStats()
+func (gitlab *GitLabOperations) handleCacheStats(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ stats := gitlab.cache.GetStats()
result := "**GitLab MCP Cache Statistics**\n\n"
@@ -1101,7 +1069,7 @@ func (s *Server) handleCacheStats(req mcp.CallToolRequest) (mcp.CallToolResult,
result += fmt.Sprintf("โข Total Hits: %d\n", totalHits)
result += fmt.Sprintf("โข Total Misses: %d\n", totalMisses)
result += fmt.Sprintf("โข Overall Hit Rate: %.1f%%\n", overallHitRate)
- result += fmt.Sprintf("โข Cache Directory: %s\n", s.cache.config.CacheDir)
+ result += fmt.Sprintf("โข Cache Directory: %s\n", gitlab.cache.config.CacheDir)
}
if len(stats) == 0 {
@@ -1111,7 +1079,7 @@ func (s *Server) handleCacheStats(req mcp.CallToolRequest) (mcp.CallToolResult,
return mcp.NewToolResult(mcp.NewTextContent(result)), nil
}
-func (s *Server) handleCacheClear(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (gitlab *GitLabOperations) handleCacheClear(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
args := req.Arguments
cacheType, _ := args["cache_type"].(string)
@@ -1141,7 +1109,7 @@ func (s *Server) handleCacheClear(req mcp.CallToolRequest) (mcp.CallToolResult,
}
// Perform the cache clear
- if err := s.cache.Clear(cacheType); err != nil {
+ if err := gitlab.cache.Clear(cacheType); err != nil {
return mcp.NewToolError(fmt.Sprintf("Failed to clear cache: %v", err)), nil
}
@@ -1157,7 +1125,7 @@ func (s *Server) handleCacheClear(req mcp.CallToolRequest) (mcp.CallToolResult,
return mcp.NewToolResult(mcp.NewTextContent(result)), nil
}
-func (s *Server) handleOfflineQuery(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (gitlab *GitLabOperations) handleOfflineQuery(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
args := req.Arguments
queryType, ok := args["query_type"].(string)
@@ -1176,7 +1144,7 @@ func (s *Server) handleOfflineQuery(req mcp.CallToolRequest) (mcp.CallToolResult
// This is a simplified offline query - in a full implementation,
// you would scan cached files and perform local filtering
- cacheDir := filepath.Join(s.cache.config.CacheDir, queryType)
+ cacheDir := filepath.Join(gitlab.cache.config.CacheDir, queryType)
if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
result := fmt.Sprintf("**Offline Query: %s**\n\n", strings.Title(queryType))
pkg/htmlprocessor/processor_test.go
@@ -1,139 +0,0 @@
-package htmlprocessor
-
-import (
- "strings"
- "testing"
-)
-
-func TestContentExtractor_ExtractReadableContent(t *testing.T) {
- extractor := NewContentExtractor()
-
- tests := []struct {
- name string
- html string
- expected string
- }{
- {
- name: "simple article with header and paragraph",
- html: `
- <html>
- <head><title>Test Article</title></head>
- <body>
- <header>
- <nav>Navigation</nav>
- </header>
- <main>
- <h1>Main Title</h1>
- <p>This is the main content that should be extracted.</p>
- <p>Another paragraph with important information.</p>
- </main>
- <footer>Footer content</footer>
- </body>
- </html>
- `,
- expected: "Main Title\nThis is the main content that should be extracted.\nAnother paragraph with important information.",
- },
- {
- name: "article with sidebar and ads",
- html: `
- <html>
- <body>
- <aside class="sidebar">Sidebar content</aside>
- <div class="ads">Advertisement</div>
- <article>
- <h2>Article Title</h2>
- <p>Article content here.</p>
- </article>
- </body>
- </html>
- `,
- expected: "Article Title\nArticle content here.",
- },
- {
- name: "content with script and style tags",
- html: `
- <html>
- <head>
- <style>body { color: red; }</style>
- </head>
- <body>
- <h1>Clean Title</h1>
- <script>console.log('should be removed');</script>
- <p>Clean paragraph.</p>
- <style>.hidden { display: none; }</style>
- </body>
- </html>
- `,
- expected: "Clean Title\nClean paragraph.",
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result, err := extractor.ExtractReadableContent(tt.html)
- if err != nil {
- t.Fatalf("ExtractReadableContent() error = %v", err)
- }
-
- // Normalize whitespace for comparison
- result = strings.TrimSpace(strings.ReplaceAll(result, "\n\n", "\n"))
- expected := strings.TrimSpace(strings.ReplaceAll(tt.expected, "\n\n", "\n"))
-
- if result != expected {
- t.Errorf("ExtractReadableContent() = %q, want %q", result, expected)
- }
- })
- }
-}
-
-func TestContentExtractor_ToMarkdown(t *testing.T) {
- extractor := NewContentExtractor()
-
- tests := []struct {
- name string
- html string
- expected string
- }{
- {
- name: "basic formatting",
- html: `<h1>Title</h1><p>Paragraph with <strong>bold</strong> and <em>italic</em> text.</p>`,
- expected: "# Title\n\nParagraph with **bold** and _italic_ text.",
- },
- {
- name: "lists",
- html: `
- <ul>
- <li>First item</li>
- <li>Second item</li>
- </ul>
- <ol>
- <li>Numbered first</li>
- <li>Numbered second</li>
- </ol>
- `,
- expected: "- First item\n- Second item\n\n1. Numbered first\n2. Numbered second",
- },
- {
- name: "links and code",
- html: `<p>Visit <a href="https://example.com">Example</a> for <code>code samples</code>.</p>`,
- expected: "Visit [Example](https://example.com) for `code samples`.",
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result, err := extractor.ToMarkdown(tt.html)
- if err != nil {
- t.Fatalf("ToMarkdown() error = %v", err)
- }
-
- // Normalize whitespace for comparison
- result = strings.TrimSpace(result)
- expected := strings.TrimSpace(tt.expected)
-
- if result != expected {
- t.Errorf("ToMarkdown() = %q, want %q", result, expected)
- }
- })
- }
-}
pkg/imap/server.go
@@ -79,7 +79,7 @@ func NewImapOperations(server, username, password string, port int, useTLS bool)
// New creates a new IMAP MCP server
func New(server, username, password string, port int, useTLS bool) *mcp.Server {
imap := NewImapOperations(server, username, password, port, useTLS)
- builder := mcp.NewServerBuilder("imap-server", "1.0.0")
+ builder := mcp.NewServerBuilder("imap", "1.0.0")
// Add imap_list_folders tool
builder.AddTool(mcp.NewTool("imap_list_folders", "List all folders in the email account (INBOX, Sent, Drafts, etc.)", map[string]interface{}{
pkg/maildir/server.go
@@ -205,6 +205,15 @@ func New(allowedPaths []string) *mcp.Server {
return maildir.handleGetStatistics(req)
}))
+ // Add maildir resources
+ builder.AddResource(mcp.NewResource("maildir://folders", "Maildir Folders", "Browse available maildir folders and structures", func(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
+ return mcp.ReadResourceResult{
+ Contents: []mcp.Content{
+ mcp.NewTextContent("Use maildir_scan_folders tool to explore available maildir folders and their message counts"),
+ },
+ }, nil
+ }))
+
return builder.Build()
}
pkg/mcp/prompts_test.go
@@ -1,149 +0,0 @@
-package mcp
-
-import (
- "reflect"
- "testing"
-)
-
-func TestPrompt_Creation(t *testing.T) {
- prompt := &Prompt{
- Name: "test-prompt",
- Description: "A test prompt",
- Arguments: []PromptArgument{
- {
- Name: "input",
- Description: "Input parameter",
- Required: true,
- },
- {
- Name: "optional",
- Description: "Optional parameter",
- Required: false,
- },
- },
- }
-
- if prompt.Name != "test-prompt" {
- t.Errorf("Expected name 'test-prompt', got %s", prompt.Name)
- }
- if len(prompt.Arguments) != 2 {
- t.Errorf("Expected 2 arguments, got %d", len(prompt.Arguments))
- }
- if !prompt.Arguments[0].Required {
- t.Error("Expected first argument to be required")
- }
- if prompt.Arguments[1].Required {
- t.Error("Expected second argument to be optional")
- }
-}
-
-func TestListPromptsResult_Empty(t *testing.T) {
- result := ListPromptsResult{
- Prompts: []Prompt{},
- }
-
- if len(result.Prompts) != 0 {
- t.Errorf("Expected 0 prompts, got %d", len(result.Prompts))
- }
-}
-
-func TestListPromptsResult_WithPrompts(t *testing.T) {
- prompts := []Prompt{
- {Name: "prompt1", Description: "First prompt"},
- {Name: "prompt2", Description: "Second prompt"},
- }
-
- result := ListPromptsResult{
- Prompts: prompts,
- }
-
- if len(result.Prompts) != 2 {
- t.Errorf("Expected 2 prompts, got %d", len(result.Prompts))
- }
- if result.Prompts[0].Name != "prompt1" {
- t.Errorf("Expected first prompt name 'prompt1', got %s", result.Prompts[0].Name)
- }
-}
-
-func TestGetPromptRequest_Creation(t *testing.T) {
- req := GetPromptRequest{
- Name: "test-prompt",
- Arguments: map[string]interface{}{
- "input": "test value",
- "count": 42,
- },
- }
-
- if req.Name != "test-prompt" {
- t.Errorf("Expected name 'test-prompt', got %s", req.Name)
- }
- if req.Arguments["input"] != "test value" {
- t.Errorf("Expected input 'test value', got %v", req.Arguments["input"])
- }
- if req.Arguments["count"] != 42 {
- t.Errorf("Expected count 42, got %v", req.Arguments["count"])
- }
-}
-
-func TestGetPromptResult_WithMessages(t *testing.T) {
- messages := []PromptMessage{
- {Role: "user", Content: NewTextContent("Hello")},
- {Role: "assistant", Content: NewTextContent("Hi there!")},
- }
-
- result := GetPromptResult{
- Description: "Test conversation",
- Messages: messages,
- }
-
- if result.Description != "Test conversation" {
- t.Errorf("Expected description 'Test conversation', got %s", result.Description)
- }
- if len(result.Messages) != 2 {
- t.Errorf("Expected 2 messages, got %d", len(result.Messages))
- }
- if result.Messages[0].Role != "user" {
- t.Errorf("Expected first message role 'user', got %s", result.Messages[0].Role)
- }
-}
-
-func TestPromptMessage_Types(t *testing.T) {
- tests := []struct {
- name string
- role string
- content Content
- expected PromptMessage
- }{
- {
- name: "user message",
- role: "user",
- content: NewTextContent("What is the weather?"),
- expected: PromptMessage{
- Role: "user",
- Content: NewTextContent("What is the weather?"),
- },
- },
- {
- name: "assistant message",
- role: "assistant",
- content: NewTextContent("The weather is sunny."),
- expected: PromptMessage{
- Role: "assistant",
- Content: NewTextContent("The weather is sunny."),
- },
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- msg := PromptMessage{
- Role: tt.role,
- Content: tt.content,
- }
-
- if !reflect.DeepEqual(msg, tt.expected) {
- t.Errorf("Expected %+v, got %+v", tt.expected, msg)
- }
- })
- }
-}
pkg/mcp/resources_test.go
@@ -1,127 +0,0 @@
-package mcp
-
-import (
- "testing"
-)
-
-func TestResource_Creation(t *testing.T) {
- resource := Resource{
- URI: "file:///home/user/document.txt",
- Name: "document.txt",
- Description: "A sample text document",
- MimeType: "text/plain",
- }
-
- if resource.URI != "file:///home/user/document.txt" {
- t.Errorf("Expected URI 'file:///home/user/document.txt', got %s", resource.URI)
- }
- if resource.Name != "document.txt" {
- t.Errorf("Expected name 'document.txt', got %s", resource.Name)
- }
- if resource.MimeType != "text/plain" {
- t.Errorf("Expected mime type 'text/plain', got %s", resource.MimeType)
- }
-}
-
-func TestListResourcesResult_Empty(t *testing.T) {
- result := ListResourcesResult{
- Resources: []Resource{},
- }
-
- if len(result.Resources) != 0 {
- t.Errorf("Expected 0 resources, got %d", len(result.Resources))
- }
-}
-
-func TestListResourcesResult_WithResources(t *testing.T) {
- resources := []Resource{
- {URI: "file:///file1.txt", Name: "file1.txt"},
- {URI: "git://repo/branch/file.go", Name: "file.go"},
- {URI: "memory://entity/123", Name: "Entity 123"},
- }
-
- result := ListResourcesResult{
- Resources: resources,
- }
-
- if len(result.Resources) != 3 {
- t.Errorf("Expected 3 resources, got %d", len(result.Resources))
- }
- if result.Resources[0].URI != "file:///file1.txt" {
- t.Errorf("Expected first resource URI 'file:///file1.txt', got %s", result.Resources[0].URI)
- }
-}
-
-func TestReadResourceRequest_Creation(t *testing.T) {
- req := ReadResourceRequest{
- URI: "file:///path/to/resource",
- }
-
- if req.URI != "file:///path/to/resource" {
- t.Errorf("Expected URI 'file:///path/to/resource', got %s", req.URI)
- }
-}
-
-func TestReadResourceResult_WithContent(t *testing.T) {
- contents := []Content{
- NewTextContent("File content here"),
- }
-
- result := ReadResourceResult{
- Contents: contents,
- }
-
- if len(result.Contents) != 1 {
- t.Errorf("Expected 1 content item, got %d", len(result.Contents))
- }
-
- if textContent, ok := result.Contents[0].(TextContent); ok {
- if textContent.Text != "File content here" {
- t.Errorf("Expected content 'File content here', got %s", textContent.Text)
- }
- } else {
- t.Error("Expected TextContent type")
- }
-}
-
-func TestResourceSchemes(t *testing.T) {
- testCases := []struct {
- name string
- uri string
- expected string
- }{
- {"file scheme", "file:///home/user/doc.txt", "file"},
- {"git scheme", "git://repo/main/src/file.go", "git"},
- {"memory scheme", "memory://graph/entity/123", "memory"},
- {"http scheme", "http://example.com/resource", "http"},
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- resource := Resource{
- URI: tc.uri,
- Name: "Test Resource",
- }
-
- // Extract scheme from URI
- scheme := ""
- if idx := indexOf(resource.URI, "://"); idx != -1 {
- scheme = resource.URI[:idx]
- }
-
- if scheme != tc.expected {
- t.Errorf("Expected scheme '%s', got '%s'", tc.expected, scheme)
- }
- })
- }
-}
-
-// Helper function for testing
-func indexOf(s, substr string) int {
- for i := 0; i <= len(s)-len(substr); i++ {
- if s[i:i+len(substr)] == substr {
- return i
- }
- }
- return -1
-}
pkg/mcp/roots_integration_test.go
@@ -1,218 +0,0 @@
-package mcp
-
-import (
- "fmt"
- "os"
- "path/filepath"
- "strings"
- "testing"
- "time"
-)
-
-func TestRootsIntegration_FilesystemServer(t *testing.T) {
- // This test would require importing the filesystem package, which might cause import cycles
- // So we'll test the root functionality at the MCP level
- server := NewServer("test-filesystem", "1.0.0", []Tool{}, []Resource{}, []Root{})
-
- // Simulate filesystem server registering roots
- tempDir := t.TempDir()
- homeDir := filepath.Join(tempDir, "home")
- projectsDir := filepath.Join(tempDir, "projects")
-
- // Create directories
- os.MkdirAll(homeDir, 0755)
- os.MkdirAll(projectsDir, 0755)
-
- // Register roots like filesystem server would
- homeRoot := NewRoot("file://"+homeDir, "Home Directory")
- projectsRoot := NewRoot("file://"+projectsDir, "Projects Directory")
-
- server.RegisterRoot(homeRoot)
- server.RegisterRoot(projectsRoot)
-
- // Test listing roots
- roots := server.ListRoots()
- if len(roots) != 2 {
- t.Errorf("Expected 2 roots, got %d", len(roots))
- }
-
- // Verify root URIs
- rootURIs := make(map[string]bool)
- for _, root := range roots {
- rootURIs[root.URI] = true
- }
-
- expectedURIs := []string{"file://" + homeDir, "file://" + projectsDir}
- for _, expectedURI := range expectedURIs {
- if !rootURIs[expectedURI] {
- t.Errorf("Expected root URI %s not found", expectedURI)
- }
- }
-}
-
-func TestRootsIntegration_GitServer(t *testing.T) {
- server := NewServer("test-git", "1.0.0", []Tool{}, []Resource{}, []Root{})
-
- // Simulate git server registering repository root
- repoPath := "/path/to/repository"
- currentBranch := "main"
-
- gitRoot := NewRoot("git://"+repoPath, "Git Repository: repository (branch: "+currentBranch+")")
- server.RegisterRoot(gitRoot)
-
- // Test listing roots
- roots := server.ListRoots()
- if len(roots) != 1 {
- t.Errorf("Expected 1 root, got %d", len(roots))
- }
-
- if roots[0].URI != "git://"+repoPath {
- t.Errorf("Expected git root URI git://%s, got %s", repoPath, roots[0].URI)
- }
-
- if !strings.Contains(roots[0].Name, "repository") {
- t.Errorf("Expected root name to contain 'repository', got %s", roots[0].Name)
- }
-
- if !strings.Contains(roots[0].Name, currentBranch) {
- t.Errorf("Expected root name to contain branch '%s', got %s", currentBranch, roots[0].Name)
- }
-}
-
-func TestRootsIntegration_MemoryServer(t *testing.T) {
- server := NewServer("test-memory", "1.0.0", []Tool{}, []Resource{}, []Root{})
-
- // Simulate memory server registering knowledge graph root
- memoryRoot := NewRoot("memory://graph", "Knowledge Graph (5 entities, 3 relations)")
- server.RegisterRoot(memoryRoot)
-
- // Test listing roots
- roots := server.ListRoots()
- if len(roots) != 1 {
- t.Errorf("Expected 1 root, got %d", len(roots))
- }
-
- if roots[0].URI != "memory://graph" {
- t.Errorf("Expected memory root URI memory://graph, got %s", roots[0].URI)
- }
-
- if !strings.Contains(roots[0].Name, "Knowledge Graph") {
- t.Errorf("Expected root name to contain 'Knowledge Graph', got %s", roots[0].Name)
- }
-
- if !strings.Contains(roots[0].Name, "5 entities") {
- t.Errorf("Expected root name to contain entity count, got %s", roots[0].Name)
- }
-
- if !strings.Contains(roots[0].Name, "3 relations") {
- t.Errorf("Expected root name to contain relation count, got %s", roots[0].Name)
- }
-}
-
-func TestRootsIntegration_MultipleServers(t *testing.T) {
- // Simulate a scenario where multiple server types register roots with the same base server
- server := NewServer("multi-server", "1.0.0", []Tool{}, []Resource{}, []Root{})
-
- // Filesystem roots
- server.RegisterRoot(NewRoot("file:///home/user", "Home Directory"))
- server.RegisterRoot(NewRoot("file:///projects", "Projects"))
-
- // Git roots
- server.RegisterRoot(NewRoot("git:///path/to/repo", "Git Repository: repo (branch: main)"))
-
- // Memory roots
- server.RegisterRoot(NewRoot("memory://graph", "Knowledge Graph (10 entities, 15 relations)"))
-
- // Test listing all roots
- roots := server.ListRoots()
- if len(roots) != 4 {
- t.Errorf("Expected 4 roots, got %d", len(roots))
- }
-
- // Verify we have roots of different types
- schemeCount := make(map[string]int)
- for _, root := range roots {
- if strings.HasPrefix(root.URI, "file://") {
- schemeCount["file"]++
- } else if strings.HasPrefix(root.URI, "git://") {
- schemeCount["git"]++
- } else if strings.HasPrefix(root.URI, "memory://") {
- schemeCount["memory"]++
- }
- }
-
- if schemeCount["file"] != 2 {
- t.Errorf("Expected 2 file:// roots, got %d", schemeCount["file"])
- }
-
- if schemeCount["git"] != 1 {
- t.Errorf("Expected 1 git:// root, got %d", schemeCount["git"])
- }
-
- if schemeCount["memory"] != 1 {
- t.Errorf("Expected 1 memory:// root, got %d", schemeCount["memory"])
- }
-}
-
-func TestRootsIntegration_DynamicUpdates(t *testing.T) {
- server := NewServer("dynamic-server", "1.0.0", []Tool{}, []Resource{}, []Root{})
-
- // Initially no roots
- roots := server.ListRoots()
- if len(roots) != 0 {
- t.Errorf("Expected 0 initial roots, got %d", len(roots))
- }
-
- // Add a root
- server.RegisterRoot(NewRoot("memory://graph", "Knowledge Graph (0 entities, 0 relations)"))
-
- roots = server.ListRoots()
- if len(roots) != 1 {
- t.Errorf("Expected 1 root after registration, got %d", len(roots))
- }
-
- // Simulate updating the memory graph (like when entities are added)
- // This would normally happen automatically in the memory server's saveGraph method
- server.RegisterRoot(NewRoot("memory://graph", "Knowledge Graph (5 entities, 3 relations)"))
-
- roots = server.ListRoots()
- if len(roots) != 1 {
- t.Errorf("Expected 1 root after update (should overwrite), got %d", len(roots))
- }
-
- // Verify the root was updated
- if !strings.Contains(roots[0].Name, "5 entities") {
- t.Errorf("Expected updated entity count in root name, got %s", roots[0].Name)
- }
-}
-
-func TestRootsIntegration_Concurrency(t *testing.T) {
- server := NewServer("concurrent-server", "1.0.0", []Tool{}, []Resource{}, []Root{})
-
- // Test concurrent root registration
- done := make(chan bool, 10)
- for i := 0; i < 10; i++ {
- go func(id int) {
- uri := fmt.Sprintf("file:///test/%d", id)
- name := fmt.Sprintf("Test Directory %d", id)
- server.RegisterRoot(NewRoot(uri, name))
- done <- true
- }(i)
- }
-
- // Wait for all goroutines to complete
- for i := 0; i < 10; i++ {
- select {
- case <-done:
- // Good
- case <-time.After(5 * time.Second):
- t.Fatal("Timeout waiting for concurrent root registration")
- }
- }
-
- // Verify all roots were registered
- roots := server.ListRoots()
- if len(roots) != 10 {
- t.Errorf("Expected 10 roots after concurrent registration, got %d", len(roots))
- }
-}
\ No newline at end of file
pkg/mcp/roots_test.go
@@ -1,123 +0,0 @@
-package mcp
-
-import (
- "testing"
-)
-
-func TestRoot_Creation(t *testing.T) {
- root := Root{
- URI: "file:///home/user/projects",
- Name: "Projects Directory",
- }
-
- if root.URI != "file:///home/user/projects" {
- t.Errorf("Expected URI 'file:///home/user/projects', got %s", root.URI)
- }
-
- if root.Name != "Projects Directory" {
- t.Errorf("Expected Name 'Projects Directory', got %s", root.Name)
- }
-}
-
-func TestNewRoot(t *testing.T) {
- uri := "git:///path/to/repo"
- name := "My Repository"
-
- root := NewRoot(uri, name)
-
- if root.URI != uri {
- t.Errorf("Expected URI %s, got %s", uri, root.URI)
- }
-
- if root.Name != name {
- t.Errorf("Expected Name %s, got %s", name, root.Name)
- }
-}
-
-func TestListRootsResult_Empty(t *testing.T) {
- result := ListRootsResult{
- Roots: []Root{},
- }
-
- if len(result.Roots) != 0 {
- t.Errorf("Expected empty roots list, got %d roots", len(result.Roots))
- }
-}
-
-func TestListRootsResult_WithRoots(t *testing.T) {
- roots := []Root{
- {URI: "file:///home/user", Name: "Home Directory"},
- {URI: "git:///path/to/repo", Name: "My Repository"},
- {URI: "memory://graph", Name: "Knowledge Graph"},
- }
-
- result := ListRootsResult{
- Roots: roots,
- }
-
- if len(result.Roots) != 3 {
- t.Errorf("Expected 3 roots, got %d", len(result.Roots))
- }
-
- expectedURIs := []string{
- "file:///home/user",
- "git:///path/to/repo",
- "memory://graph",
- }
-
- for i, root := range result.Roots {
- if root.URI != expectedURIs[i] {
- t.Errorf("Expected root %d URI %s, got %s", i, expectedURIs[i], root.URI)
- }
- }
-}
-
-func TestRootSchemes(t *testing.T) {
- testCases := []struct {
- name string
- uri string
- rootName string
- scheme string
- }{
- {
- name: "file scheme",
- uri: "file:///home/user/documents",
- rootName: "Documents",
- scheme: "file",
- },
- {
- name: "git scheme",
- uri: "git:///repositories/myproject",
- rootName: "My Project",
- scheme: "git",
- },
- {
- name: "memory scheme",
- uri: "memory://knowledge-graph",
- rootName: "Knowledge Graph",
- scheme: "memory",
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- root := NewRoot(tc.uri, tc.rootName)
-
- if root.URI != tc.uri {
- t.Errorf("Expected URI %s, got %s", tc.uri, root.URI)
- }
-
- if root.Name != tc.rootName {
- t.Errorf("Expected Name %s, got %s", tc.rootName, root.Name)
- }
- })
- }
-}
-
-func TestListRootsRequest_Creation(t *testing.T) {
- req := ListRootsRequest{}
-
- // ListRootsRequest should be an empty struct as it has no parameters
- // This test just verifies the struct can be created
- _ = req
-}
\ No newline at end of file
pkg/mcp/server.go
@@ -141,83 +141,9 @@ func (b *ServerBuilder) Build() *Server {
return server
}
-// NewServer creates a new MCP server (deprecated - use ServerBuilder instead)
-func NewServer(name, version string, tools []Tool, resources []Resource, roots []Root) *Server {
- server := &Server{
- name: name,
- version: version,
- toolDefinitions: make(map[string]Tool),
- promptDefinitions: make(map[string]Prompt),
- resourceDefinitions: make(map[string]Resource),
- rootDefinitions: make(map[string]Root),
- capabilities: ServerCapabilities{
- Tools: &ToolsCapability{},
- Prompts: &PromptsCapability{},
- Resources: &ResourcesCapability{},
- Roots: &RootsCapability{},
- Logging: &LoggingCapability{},
- },
- }
-
- for _, tool := range tools {
- server.toolDefinitions[tool.Name] = tool
- }
-
- for _, resource := range resources {
- server.resourceDefinitions[resource.URI] = resource
- }
-
- for _, root := range roots {
- server.rootDefinitions[root.URI] = root
- }
-
- return server
-}
-// RegisterRoot registers a root with the server (immutable servers should use ServerBuilder)
-func (s *Server) RegisterRoot(root Root) {
- s.rootDefinitions[root.URI] = root
-}
-// Compatibility methods for existing servers (deprecated - use ServerBuilder instead)
-// RegisterToolWithDefinition registers a tool with its full definition and handler
-func (s *Server) RegisterToolWithDefinition(tool Tool, handler ToolHandler) {
- tool.Handler = handler
- s.toolDefinitions[tool.Name] = tool
-}
-
-// RegisterPrompt registers a prompt with its definition and handler
-func (s *Server) RegisterPrompt(prompt Prompt, handler PromptHandler) {
- prompt.Handler = handler
- s.promptDefinitions[prompt.Name] = prompt
-}
-
-// RegisterResource registers a resource handler with minimal definition
-func (s *Server) RegisterResource(uri string, handler ResourceHandler) {
- name := extractResourceName(uri)
- if name == "" {
- name = uri // Use the full URI as name if extraction fails
- }
- resource := Resource{
- URI: uri,
- Name: name,
- Handler: handler,
- }
- s.RegisterResourceWithDefinition(resource, handler)
-}
-
-// RegisterResourceWithDefinition registers a resource with its full definition and handler
-func (s *Server) RegisterResourceWithDefinition(resource Resource, handler ResourceHandler) {
- resource.Handler = handler
- s.resourceDefinitions[resource.URI] = resource
-}
-
-// SetCustomRequestHandler sets custom request handlers for overriding default behavior
-func (s *Server) SetCustomRequestHandler(handlers map[string]func(JSONRPCRequest) JSONRPCResponse) {
- // For now, just log that this is deprecated - we removed custom handlers from immutable architecture
- // This is a compatibility shim
-}
// SetInitializeHandler sets the initialize handler
func (s *Server) SetInitializeHandler(handler func(InitializeRequest) (InitializeResult, error)) {
@@ -544,14 +470,3 @@ func NewPrompt(name, description string, arguments []PromptArgument, handler Pro
}
}
-// Helper function to extract resource name from URI
-func extractResourceName(uri string) string {
- // Find the last "/" in the URI and extract the part after it
- for i := len(uri) - 1; i >= 0; i-- {
- if uri[i] == '/' {
- return uri[i+1:]
- }
- }
- // If no "/" found, return the entire URI
- return uri
-}
pkg/mcp/server_prompts_test.go
@@ -1,113 +0,0 @@
-package mcp
-
-import (
- "testing"
-)
-
-func TestServer_RegisterPrompt(t *testing.T) {
- server := NewServer("test-server", "1.0.0", []Tool{}, []Resource{}, []Root{})
-
- prompt := Prompt{
- Name: "test-prompt",
- Description: "A test prompt for verification",
- Arguments: []PromptArgument{
- {
- Name: "input",
- Description: "Input parameter",
- Required: true,
- },
- },
- }
-
- handler := func(req GetPromptRequest) (GetPromptResult, error) {
- return GetPromptResult{
- Description: "Test response",
- Messages: []PromptMessage{
- {
- Role: "user",
- Content: NewTextContent("Hello"),
- },
- },
- }, nil
- }
-
- server.RegisterPrompt(prompt, handler)
-
- // Test that prompt is registered
- prompts := server.ListPrompts()
- if len(prompts) != 1 {
- t.Errorf("Expected 1 prompt, got %d", len(prompts))
- }
-
- if prompts[0].Name != "test-prompt" {
- t.Errorf("Expected prompt name 'test-prompt', got %s", prompts[0].Name)
- }
-
- if prompts[0].Description != "A test prompt for verification" {
- t.Errorf("Expected description 'A test prompt for verification', got %s", prompts[0].Description)
- }
-
- if len(prompts[0].Arguments) != 1 {
- t.Errorf("Expected 1 argument, got %d", len(prompts[0].Arguments))
- }
-
- if prompts[0].Arguments[0].Name != "input" {
- t.Errorf("Expected argument name 'input', got %s", prompts[0].Arguments[0].Name)
- }
-
- if !prompts[0].Arguments[0].Required {
- t.Error("Expected argument to be required")
- }
-}
-
-func TestServer_ListPrompts_Empty(t *testing.T) {
- server := NewServer("test-server", "1.0.0", []Tool{}, []Resource{}, []Root{})
-
- prompts := server.ListPrompts()
- if len(prompts) != 0 {
- t.Errorf("Expected 0 prompts, got %d", len(prompts))
- }
-}
-
-func TestServer_MultiplePrompts(t *testing.T) {
- server := NewServer("test-server", "1.0.0", []Tool{}, []Resource{}, []Root{})
-
- prompt1 := Prompt{
- Name: "prompt1",
- Description: "First prompt",
- }
-
- prompt2 := Prompt{
- Name: "prompt2",
- Description: "Second prompt",
- Arguments: []PromptArgument{
- {Name: "arg1", Required: true},
- {Name: "arg2", Required: false},
- },
- }
-
- handler := func(req GetPromptRequest) (GetPromptResult, error) {
- return GetPromptResult{}, nil
- }
-
- server.RegisterPrompt(prompt1, handler)
- server.RegisterPrompt(prompt2, handler)
-
- prompts := server.ListPrompts()
- if len(prompts) != 2 {
- t.Errorf("Expected 2 prompts, got %d", len(prompts))
- }
-
- // Check that both prompts are present (order may vary due to map iteration)
- names := make(map[string]bool)
- for _, prompt := range prompts {
- names[prompt.Name] = true
- }
-
- if !names["prompt1"] {
- t.Error("prompt1 not found in list")
- }
- if !names["prompt2"] {
- t.Error("prompt2 not found in list")
- }
-}
pkg/mcp/server_resources_test.go
@@ -1,117 +0,0 @@
-package mcp
-
-import (
- "testing"
-)
-
-func TestServer_RegisterResource(t *testing.T) {
- server := NewServer("test-server", "1.0.0", []Tool{}, []Resource{}, []Root{})
-
- // Create a test resource handler
- handler := func(req ReadResourceRequest) (ReadResourceResult, error) {
- return ReadResourceResult{
- Contents: []Content{
- NewTextContent("Test resource content"),
- },
- }, nil
- }
-
- // Register resource
- server.RegisterResource("file:///test/resource.txt", handler)
-
- // Test that resource is registered (we'll need to add ListResources method)
- resources := server.ListResources()
- if len(resources) != 1 {
- t.Errorf("Expected 1 resource, got %d", len(resources))
- }
-
- if resources[0].URI != "file:///test/resource.txt" {
- t.Errorf("Expected resource URI 'file:///test/resource.txt', got %s", resources[0].URI)
- }
-}
-
-func TestServer_ListResources_Empty(t *testing.T) {
- server := NewServer("test-server", "1.0.0", []Tool{}, []Resource{}, []Root{})
-
- resources := server.ListResources()
- if len(resources) != 0 {
- t.Errorf("Expected 0 resources, got %d", len(resources))
- }
-}
-
-func TestServer_MultipleResources(t *testing.T) {
- server := NewServer("test-server", "1.0.0", []Tool{}, []Resource{}, []Root{})
-
- handler := func(req ReadResourceRequest) (ReadResourceResult, error) {
- return ReadResourceResult{}, nil
- }
-
- // Register multiple resources
- server.RegisterResource("file:///file1.txt", handler)
- server.RegisterResource("git://repo/main/file.go", handler)
- server.RegisterResource("memory://entity/123", handler)
-
- resources := server.ListResources()
- if len(resources) != 3 {
- t.Errorf("Expected 3 resources, got %d", len(resources))
- }
-
- // Check that all URIs are present (order may vary due to map iteration)
- uris := make(map[string]bool)
- for _, resource := range resources {
- uris[resource.URI] = true
- }
-
- expectedURIs := []string{
- "file:///file1.txt",
- "git://repo/main/file.go",
- "memory://entity/123",
- }
-
- for _, expectedURI := range expectedURIs {
- if !uris[expectedURI] {
- t.Errorf("Expected URI %s not found in resources", expectedURI)
- }
- }
-}
-
-func TestServer_RegisterResourceWithDefinition(t *testing.T) {
- server := NewServer("test-server", "1.0.0", []Tool{}, []Resource{}, []Root{})
-
- resource := Resource{
- URI: "file:///docs/readme.md",
- Name: "README",
- Description: "Project documentation",
- MimeType: "text/markdown",
- }
-
- handler := func(req ReadResourceRequest) (ReadResourceResult, error) {
- return ReadResourceResult{
- Contents: []Content{
- NewTextContent("# Project Documentation\n\nThis is the README."),
- },
- }, nil
- }
-
- // Register resource with full definition
- server.RegisterResourceWithDefinition(resource, handler)
-
- resources := server.ListResources()
- if len(resources) != 1 {
- t.Errorf("Expected 1 resource, got %d", len(resources))
- }
-
- res := resources[0]
- if res.URI != "file:///docs/readme.md" {
- t.Errorf("Expected URI 'file:///docs/readme.md', got %s", res.URI)
- }
- if res.Name != "README" {
- t.Errorf("Expected name 'README', got %s", res.Name)
- }
- if res.Description != "Project documentation" {
- t.Errorf("Expected description 'Project documentation', got %s", res.Description)
- }
- if res.MimeType != "text/markdown" {
- t.Errorf("Expected mime type 'text/markdown', got %s", res.MimeType)
- }
-}
pkg/mcp/server_roots_test.go
@@ -1,110 +0,0 @@
-package mcp
-
-import (
- "testing"
-)
-
-func TestServer_RegisterRoot(t *testing.T) {
- server := NewServer("test-server", "1.0.0", []Tool{}, []Resource{}, []Root{})
-
- root := Root{
- URI: "file:///home/user/projects",
- Name: "Projects Directory",
- }
-
- server.RegisterRoot(root)
-
- roots := server.ListRoots()
- if len(roots) != 1 {
- t.Errorf("Expected 1 root, got %d", len(roots))
- }
-
- if roots[0].URI != root.URI {
- t.Errorf("Expected root URI %s, got %s", root.URI, roots[0].URI)
- }
-
- if roots[0].Name != root.Name {
- t.Errorf("Expected root name %s, got %s", root.Name, roots[0].Name)
- }
-}
-
-func TestServer_ListRoots_Empty(t *testing.T) {
- server := NewServer("test-server", "1.0.0", []Tool{}, []Resource{}, []Root{})
-
- roots := server.ListRoots()
- if len(roots) != 0 {
- t.Errorf("Expected 0 roots, got %d", len(roots))
- }
-}
-
-func TestServer_MultipleRoots(t *testing.T) {
- server := NewServer("test-server", "1.0.0", []Tool{}, []Resource{}, []Root{})
-
- roots := []Root{
- {URI: "file:///home/user", Name: "Home"},
- {URI: "git:///path/to/repo", Name: "Repository"},
- {URI: "memory://graph", Name: "Knowledge Graph"},
- }
-
- for _, root := range roots {
- server.RegisterRoot(root)
- }
-
- retrievedRoots := server.ListRoots()
- if len(retrievedRoots) != 3 {
- t.Errorf("Expected 3 roots, got %d", len(retrievedRoots))
- }
-
- // Check that all roots are present (order may vary due to map iteration)
- rootURIs := make(map[string]bool)
- for _, root := range retrievedRoots {
- rootURIs[root.URI] = true
- }
-
- expectedURIs := []string{
- "file:///home/user",
- "git:///path/to/repo",
- "memory://graph",
- }
-
- for _, expectedURI := range expectedURIs {
- if !rootURIs[expectedURI] {
- t.Errorf("Expected root URI %s not found", expectedURI)
- }
- }
-}
-
-func TestServer_RootCapability(t *testing.T) {
- server := NewServer("test-server", "1.0.0", []Tool{}, []Resource{}, []Root{})
-
- if server.capabilities.Roots == nil {
- t.Error("Expected server to have roots capability")
- }
-}
-
-func TestServer_DuplicateRoots(t *testing.T) {
- server := NewServer("test-server", "1.0.0", []Tool{}, []Resource{}, []Root{})
-
- root1 := Root{
- URI: "file:///home/user",
- Name: "Home Directory",
- }
-
- root2 := Root{
- URI: "file:///home/user", // Same URI, different name
- Name: "User Home",
- }
-
- server.RegisterRoot(root1)
- server.RegisterRoot(root2)
-
- roots := server.ListRoots()
- if len(roots) != 1 {
- t.Errorf("Expected 1 root (duplicate should be overwritten), got %d", len(roots))
- }
-
- // Should have the second root (overwrites the first)
- if roots[0].Name != "User Home" {
- t.Errorf("Expected root name 'User Home', got %s", roots[0].Name)
- }
-}
\ No newline at end of file
pkg/memory/server_test.go
@@ -1,768 +0,0 @@
-package memory
-
-import (
- "encoding/json"
- "path/filepath"
- "strings"
- "testing"
- "time"
-
- "github.com/xlgmokha/mcp/pkg/mcp"
-)
-
-func TestMemoryServer_CreateEntities(t *testing.T) {
- // Use temporary file for testing
- tempDir := t.TempDir()
- memoryFile := filepath.Join(tempDir, "test_memory.json")
-
- server := New(memoryFile)
-
- req := mcp.CallToolRequest{
- Name: "create_entities",
- Arguments: map[string]interface{}{
- "entities": []interface{}{
- map[string]interface{}{
- "name": "John_Smith",
- "entityType": "person",
- "observations": []interface{}{"Speaks fluent Spanish", "Works at Anthropic"},
- },
- map[string]interface{}{
- "name": "Anthropic",
- "entityType": "organization",
- "observations": []interface{}{"AI safety company"},
- },
- },
- },
- }
-
- result, err := server.HandleCreateEntities(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if result.IsError {
- textContent, _ := result.Content[0].(mcp.TextContent)
- t.Fatalf("Expected successful entity creation, got error: %s", textContent.Text)
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- // Should contain created entities
- if !contains(textContent.Text, "John_Smith") {
- t.Fatalf("Expected 'John_Smith' in result, got: %s", textContent.Text)
- }
-
- if !contains(textContent.Text, "Anthropic") {
- t.Fatalf("Expected 'Anthropic' in result, got: %s", textContent.Text)
- }
-}
-
-func TestMemoryServer_CreateRelations(t *testing.T) {
- tempDir := t.TempDir()
- memoryFile := filepath.Join(tempDir, "test_memory.json")
-
- server := New(memoryFile)
-
- // First create entities
- createReq := mcp.CallToolRequest{
- Name: "create_entities",
- Arguments: map[string]interface{}{
- "entities": []interface{}{
- map[string]interface{}{
- "name": "John_Smith",
- "entityType": "person",
- "observations": []interface{}{"Employee"},
- },
- map[string]interface{}{
- "name": "Anthropic",
- "entityType": "organization",
- "observations": []interface{}{"AI company"},
- },
- },
- },
- }
- server.HandleCreateEntities(createReq)
-
- // Now create relation
- req := mcp.CallToolRequest{
- Name: "create_relations",
- Arguments: map[string]interface{}{
- "relations": []interface{}{
- map[string]interface{}{
- "from": "John_Smith",
- "to": "Anthropic",
- "relationType": "works_at",
- },
- },
- },
- }
-
- result, err := server.HandleCreateRelations(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if result.IsError {
- textContent, _ := result.Content[0].(mcp.TextContent)
- t.Fatalf("Expected successful relation creation, got error: %s", textContent.Text)
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- // Should contain created relation
- if !contains(textContent.Text, "works_at") {
- t.Fatalf("Expected 'works_at' relation in result, got: %s", textContent.Text)
- }
-}
-
-func TestMemoryServer_AddObservations(t *testing.T) {
- tempDir := t.TempDir()
- memoryFile := filepath.Join(tempDir, "test_memory.json")
-
- server := New(memoryFile)
-
- // First create entity
- createReq := mcp.CallToolRequest{
- Name: "create_entities",
- Arguments: map[string]interface{}{
- "entities": []interface{}{
- map[string]interface{}{
- "name": "John_Smith",
- "entityType": "person",
- "observations": []interface{}{"Initial observation"},
- },
- },
- },
- }
- server.HandleCreateEntities(createReq)
-
- // Add observations
- req := mcp.CallToolRequest{
- Name: "add_observations",
- Arguments: map[string]interface{}{
- "observations": []interface{}{
- map[string]interface{}{
- "entityName": "John_Smith",
- "contents": []interface{}{"Likes coffee", "Speaks French"},
- },
- },
- },
- }
-
- result, err := server.HandleAddObservations(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if result.IsError {
- textContent, _ := result.Content[0].(mcp.TextContent)
- t.Fatalf("Expected successful observation addition, got error: %s", textContent.Text)
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- // Should contain added observations
- if !contains(textContent.Text, "Likes coffee") {
- t.Fatalf("Expected 'Likes coffee' in result, got: %s", textContent.Text)
- }
-}
-
-func TestMemoryServer_ReadGraph(t *testing.T) {
- tempDir := t.TempDir()
- memoryFile := filepath.Join(tempDir, "test_memory.json")
-
- server := New(memoryFile)
-
- // Create some test data
- createReq := mcp.CallToolRequest{
- Name: "create_entities",
- Arguments: map[string]interface{}{
- "entities": []interface{}{
- map[string]interface{}{
- "name": "Alice",
- "entityType": "person",
- "observations": []interface{}{"Software engineer"},
- },
- },
- },
- }
- server.HandleCreateEntities(createReq)
-
- // Read the graph
- req := mcp.CallToolRequest{
- Name: "read_graph",
- Arguments: map[string]interface{}{},
- }
-
- result, err := server.HandleReadGraph(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if result.IsError {
- textContent, _ := result.Content[0].(mcp.TextContent)
- t.Fatalf("Expected successful graph read, got error: %s", textContent.Text)
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- // Should contain entities and relations structure
- if !contains(textContent.Text, "entities") {
- t.Fatalf("Expected 'entities' in graph, got: %s", textContent.Text)
- }
-
- if !contains(textContent.Text, "relations") {
- t.Fatalf("Expected 'relations' in graph, got: %s", textContent.Text)
- }
-
- if !contains(textContent.Text, "Alice") {
- t.Fatalf("Expected 'Alice' in graph, got: %s", textContent.Text)
- }
-}
-
-func TestMemoryServer_SearchNodes(t *testing.T) {
- tempDir := t.TempDir()
- memoryFile := filepath.Join(tempDir, "test_memory.json")
-
- server := New(memoryFile)
-
- // Create test entities
- createReq := mcp.CallToolRequest{
- Name: "create_entities",
- Arguments: map[string]interface{}{
- "entities": []interface{}{
- map[string]interface{}{
- "name": "Alice_Developer",
- "entityType": "person",
- "observations": []interface{}{"Python programmer", "Loves machine learning"},
- },
- map[string]interface{}{
- "name": "Bob_Manager",
- "entityType": "person",
- "observations": []interface{}{"Project manager", "Excellent communication"},
- },
- },
- },
- }
- server.HandleCreateEntities(createReq)
-
- // Search for Python-related content
- req := mcp.CallToolRequest{
- Name: "search_nodes",
- Arguments: map[string]interface{}{
- "query": "Python",
- },
- }
-
- result, err := server.HandleSearchNodes(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if result.IsError {
- textContent, _ := result.Content[0].(mcp.TextContent)
- t.Fatalf("Expected successful search, got error: %s", textContent.Text)
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- // Should find Alice who has Python in observations
- if !contains(textContent.Text, "Alice_Developer") {
- t.Fatalf("Expected 'Alice_Developer' in search results, got: %s", textContent.Text)
- }
-
- // Should not find Bob who doesn't have Python mentioned
- if contains(textContent.Text, "Bob_Manager") {
- t.Fatalf("Should not find 'Bob_Manager' in Python search, got: %s", textContent.Text)
- }
-}
-
-func TestMemoryServer_OpenNodes(t *testing.T) {
- tempDir := t.TempDir()
- memoryFile := filepath.Join(tempDir, "test_memory.json")
-
- server := New(memoryFile)
-
- // Create test entities
- createReq := mcp.CallToolRequest{
- Name: "create_entities",
- Arguments: map[string]interface{}{
- "entities": []interface{}{
- map[string]interface{}{
- "name": "Person1",
- "entityType": "person",
- "observations": []interface{}{"First person"},
- },
- map[string]interface{}{
- "name": "Person2",
- "entityType": "person",
- "observations": []interface{}{"Second person"},
- },
- },
- },
- }
- server.HandleCreateEntities(createReq)
-
- // Open specific nodes
- req := mcp.CallToolRequest{
- Name: "open_nodes",
- Arguments: map[string]interface{}{
- "names": []interface{}{"Person1"},
- },
- }
-
- result, err := server.HandleOpenNodes(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if result.IsError {
- textContent, _ := result.Content[0].(mcp.TextContent)
- t.Fatalf("Expected successful node open, got error: %s", textContent.Text)
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- // Should find Person1
- if !contains(textContent.Text, "Person1") {
- t.Fatalf("Expected 'Person1' in open nodes result, got: %s", textContent.Text)
- }
-
- // Should not find Person2 (not requested)
- if contains(textContent.Text, "Person2") {
- t.Fatalf("Should not find 'Person2' in specific node open, got: %s", textContent.Text)
- }
-}
-
-func TestMemoryServer_DeleteEntities(t *testing.T) {
- tempDir := t.TempDir()
- memoryFile := filepath.Join(tempDir, "test_memory.json")
-
- server := New(memoryFile)
-
- // Create test entity
- createReq := mcp.CallToolRequest{
- Name: "create_entities",
- Arguments: map[string]interface{}{
- "entities": []interface{}{
- map[string]interface{}{
- "name": "ToDelete",
- "entityType": "person",
- "observations": []interface{}{"Will be deleted"},
- },
- },
- },
- }
- server.HandleCreateEntities(createReq)
-
- // Delete entity
- req := mcp.CallToolRequest{
- Name: "delete_entities",
- Arguments: map[string]interface{}{
- "entityNames": []interface{}{"ToDelete"},
- },
- }
-
- result, err := server.HandleDeleteEntities(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if result.IsError {
- textContent, _ := result.Content[0].(mcp.TextContent)
- t.Fatalf("Expected successful entity deletion, got error: %s", textContent.Text)
- }
-
- // Verify entity was deleted by reading graph
- readReq := mcp.CallToolRequest{
- Name: "read_graph",
- Arguments: map[string]interface{}{},
- }
-
- readResult, _ := server.HandleReadGraph(readReq)
- readContent, _ := readResult.Content[0].(mcp.TextContent)
-
- if contains(readContent.Text, "ToDelete") {
- t.Fatalf("Entity should have been deleted, but found in graph: %s", readContent.Text)
- }
-}
-
-func TestMemoryServer_DeleteObservations(t *testing.T) {
- tempDir := t.TempDir()
- memoryFile := filepath.Join(tempDir, "test_memory.json")
-
- server := New(memoryFile)
-
- // Create entity with observations
- createReq := mcp.CallToolRequest{
- Name: "create_entities",
- Arguments: map[string]interface{}{
- "entities": []interface{}{
- map[string]interface{}{
- "name": "TestPerson",
- "entityType": "person",
- "observations": []interface{}{"Keep this", "Delete this", "Keep this too"},
- },
- },
- },
- }
- server.HandleCreateEntities(createReq)
-
- // Delete specific observation
- req := mcp.CallToolRequest{
- Name: "delete_observations",
- Arguments: map[string]interface{}{
- "deletions": []interface{}{
- map[string]interface{}{
- "entityName": "TestPerson",
- "observations": []interface{}{"Delete this"},
- },
- },
- },
- }
-
- result, err := server.HandleDeleteObservations(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if result.IsError {
- textContent, _ := result.Content[0].(mcp.TextContent)
- t.Fatalf("Expected successful observation deletion, got error: %s", textContent.Text)
- }
-
- // Verify observation was deleted by reading graph
- readReq := mcp.CallToolRequest{
- Name: "read_graph",
- Arguments: map[string]interface{}{},
- }
-
- readResult, _ := server.HandleReadGraph(readReq)
- readContent, _ := readResult.Content[0].(mcp.TextContent)
-
- if contains(readContent.Text, "Delete this") {
- t.Fatalf("Observation should have been deleted, but found in graph: %s", readContent.Text)
- }
-
- if !contains(readContent.Text, "Keep this") {
- t.Fatalf("Other observations should remain, got: %s", readContent.Text)
- }
-}
-
-func TestMemoryServer_ListTools(t *testing.T) {
- tempDir := t.TempDir()
- memoryFile := filepath.Join(tempDir, "test_memory.json")
-
- server := New(memoryFile)
- tools := server.ListTools()
-
- expectedTools := []string{
- "create_entities",
- "create_relations",
- "add_observations",
- "delete_entities",
- "delete_observations",
- "delete_relations",
- "read_graph",
- "search_nodes",
- "open_nodes",
- }
-
- if len(tools) != len(expectedTools) {
- t.Fatalf("Expected %d tools, got %d", len(expectedTools), len(tools))
- }
-
- toolNames := make(map[string]bool)
- for _, tool := range tools {
- toolNames[tool.Name] = true
- }
-
- for _, expected := range expectedTools {
- if !toolNames[expected] {
- t.Fatalf("Expected tool %s not found", expected)
- }
- }
-}
-
-func TestMemoryServer_Persistence(t *testing.T) {
- tempDir := t.TempDir()
- memoryFile := filepath.Join(tempDir, "test_memory.json")
-
- // Create first server instance and add data
- server1 := New(memoryFile)
-
- createReq := mcp.CallToolRequest{
- Name: "create_entities",
- Arguments: map[string]interface{}{
- "entities": []interface{}{
- map[string]interface{}{
- "name": "Persistent_Entity",
- "entityType": "test",
- "observations": []interface{}{"This should persist"},
- },
- },
- },
- }
- server1.HandleCreateEntities(createReq)
-
- // Create second server instance (should load from file)
- server2 := New(memoryFile)
-
- readReq := mcp.CallToolRequest{
- Name: "read_graph",
- Arguments: map[string]interface{}{},
- }
-
- result, err := server2.HandleReadGraph(readReq)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- // Should find the entity created by the first server instance
- if !contains(textContent.Text, "Persistent_Entity") {
- t.Fatalf("Expected persistent entity to be loaded from file, got: %s", textContent.Text)
- }
-
- if !contains(textContent.Text, "This should persist") {
- t.Fatalf("Expected persistent observation to be loaded from file, got: %s", textContent.Text)
- }
-}
-
-func TestMemoryServer_Resources(t *testing.T) {
- tempDir := t.TempDir()
- memoryFile := filepath.Join(tempDir, "test_memory_resources.json")
-
- server := New(memoryFile)
-
- // Create test entities
- createReq := mcp.CallToolRequest{
- Arguments: map[string]interface{}{
- "entities": []interface{}{
- map[string]interface{}{
- "name": "TestEntity1",
- "entityType": "Person",
- "observations": []interface{}{"First observation", "Second observation"},
- },
- map[string]interface{}{
- "name": "TestEntity2",
- "entityType": "Company",
- "observations": []interface{}{"Company observation"},
- },
- },
- },
- }
-
- _, err := server.HandleCreateEntities(createReq)
- if err != nil {
- t.Fatalf("Failed to create entities: %v", err)
- }
-
- // Create test relations
- relationsReq := mcp.CallToolRequest{
- Arguments: map[string]interface{}{
- "relations": []interface{}{
- map[string]interface{}{
- "from": "TestEntity1",
- "to": "TestEntity2",
- "relationType": "works_for",
- },
- },
- },
- }
-
- _, err = server.HandleCreateRelations(relationsReq)
- if err != nil {
- t.Fatalf("Failed to create relations: %v", err)
- }
-
- // Give time for resource registration
- time.Sleep(100 * time.Millisecond)
-
- // Test list resources
- resources := server.ListResources()
- if len(resources) < 2 {
- t.Errorf("Expected at least 2 resources, got %d", len(resources))
- }
-
- // Find entity resource
- var entityResourceURI string
- for _, resource := range resources {
- if strings.Contains(resource.URI, "TestEntity1") {
- entityResourceURI = resource.URI
- break
- }
- }
-
- if entityResourceURI == "" {
- t.Fatal("TestEntity1 resource not found")
- }
-
- // Test read entity resource
- readReq := mcp.ReadResourceRequest{
- URI: entityResourceURI,
- }
-
- result, err := server.HandleMemoryResource(readReq)
- if err != nil {
- t.Fatalf("Failed to read entity resource: %v", err)
- }
-
- if len(result.Contents) == 0 {
- t.Fatal("Expected content in resource result")
- }
-
- // Verify entity data
- textContent, ok := result.Contents[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent in resource result")
- }
-
- var entityData map[string]interface{}
- err = json.Unmarshal([]byte(textContent.Text), &entityData)
- if err != nil {
- t.Fatalf("Failed to parse entity JSON: %v", err)
- }
-
- entity, ok := entityData["entity"].(map[string]interface{})
- if !ok {
- t.Fatal("Entity data not found in resource result")
- }
-
- if entity["name"].(string) != "TestEntity1" {
- t.Errorf("Expected entity name TestEntity1, got %s", entity["name"])
- }
-
- // Test relations resource
- relationsResourceURI := "memory://relations/all"
- readRelationsReq := mcp.ReadResourceRequest{
- URI: relationsResourceURI,
- }
-
- relationsResult, err := server.HandleMemoryResource(readRelationsReq)
- if err != nil {
- t.Fatalf("Failed to read relations resource: %v", err)
- }
-
- if len(relationsResult.Contents) == 0 {
- t.Fatal("Expected content in relations resource result")
- }
-
- // Verify relations data
- relationsTextContent, ok := relationsResult.Contents[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent in relations resource result")
- }
-
- var relationsData map[string]interface{}
- err = json.Unmarshal([]byte(relationsTextContent.Text), &relationsData)
- if err != nil {
- t.Fatalf("Failed to parse relations JSON: %v", err)
- }
-
- totalRelations, ok := relationsData["total_relations"].(float64)
- if !ok || totalRelations != 1 {
- t.Errorf("Expected 1 relation, got %v", totalRelations)
- }
-}
-
-func TestMemoryServer_HandleKnowledgeQueryPrompt(t *testing.T) {
- tempDir := t.TempDir()
- memoryFile := filepath.Join(tempDir, "test_prompt_memory.json")
-
- server := New(memoryFile)
-
- req := mcp.GetPromptRequest{
- Arguments: map[string]interface{}{
- "query": "find all people",
- "context": "looking for team members",
- },
- }
-
- result, err := server.HandleKnowledgeQueryPrompt(req)
- if err != nil {
- t.Fatalf("HandleKnowledgeQueryPrompt failed: %v", err)
- }
-
- if len(result.Messages) != 2 {
- t.Errorf("Expected 2 messages, got %d", len(result.Messages))
- }
-
- if result.Messages[0].Role != "user" {
- t.Errorf("Expected first message role to be 'user', got %s", result.Messages[0].Role)
- }
-
- if result.Messages[1].Role != "assistant" {
- t.Errorf("Expected second message role to be 'assistant', got %s", result.Messages[1].Role)
- }
-
- userContent, ok := result.Messages[0].Content.(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent in user message")
- }
- if !strings.Contains(userContent.Text, "find all people") {
- t.Error("User message should contain the query")
- }
-
- assistantContent, ok := result.Messages[1].Content.(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent in assistant message")
- }
- if !strings.Contains(assistantContent.Text, "knowledge graph") {
- t.Error("Assistant message should mention knowledge graph")
- }
-}
-
-func TestMemoryServer_InvalidResourceURI(t *testing.T) {
- tempDir := t.TempDir()
- memoryFile := filepath.Join(tempDir, "test_invalid_memory.json")
-
- server := New(memoryFile)
-
- testCases := []struct {
- name string
- uri string
- }{
- {"invalid scheme", "file://test"},
- {"invalid format", "memory://"},
- {"invalid type", "memory://unknown/test"},
- {"missing entity", "memory://entity/nonexistent"},
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- req := mcp.ReadResourceRequest{URI: tc.uri}
- _, err := server.HandleMemoryResource(req)
- if err == nil {
- t.Errorf("Expected error for invalid URI: %s", tc.uri)
- }
- })
- }
-}
-
-// Helper functions
-func contains(s, substr string) bool {
- return strings.Contains(s, substr)
-}
pkg/packages/server.go
@@ -1,1311 +0,0 @@
-package packages
-
-import (
- "encoding/json"
- "fmt"
- "os"
- "os/exec"
- "path/filepath"
- "strings"
- "sync"
-
- "github.com/xlgmokha/mcp/pkg/mcp"
-)
-
-// Server represents the Package Manager MCP server
-type Server struct {
- *mcp.Server
- mu sync.RWMutex
-}
-
-// NewServer creates a new Package Manager MCP server
-func NewServer() *Server {
- baseServer := mcp.NewServer("mcp-packages", "1.0.0", []mcp.Tool{}, []mcp.Resource{}, []mcp.Root{})
-
- server := &Server{
- Server: baseServer,
- }
-
- server.registerTools()
-
- return server
-}
-
-// ListTools returns all available package management tools
-func (s *Server) ListTools() []mcp.Tool {
- return []mcp.Tool{
- // Cargo tools
- {
- Name: "cargo_build",
- Description: "Build a Rust project using Cargo",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "directory": map[string]interface{}{
- "type": "string",
- "description": "Project directory (optional, defaults to current directory)",
- },
- "release": map[string]interface{}{
- "type": "boolean",
- "description": "Build in release mode (default: false)",
- },
- "target": map[string]interface{}{
- "type": "string",
- "description": "Target triple to build for (optional)",
- },
- "features": map[string]interface{}{
- "type": "string",
- "description": "Space-separated list of features to activate",
- },
- },
- },
- },
- {
- Name: "cargo_run",
- Description: "Run a Rust project using Cargo",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "directory": map[string]interface{}{
- "type": "string",
- "description": "Project directory (optional, defaults to current directory)",
- },
- "release": map[string]interface{}{
- "type": "boolean",
- "description": "Run in release mode (default: false)",
- },
- "args": map[string]interface{}{
- "type": "string",
- "description": "Arguments to pass to the program",
- },
- },
- },
- },
- {
- Name: "cargo_test",
- Description: "Run tests for a Rust project using Cargo",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "directory": map[string]interface{}{
- "type": "string",
- "description": "Project directory (optional, defaults to current directory)",
- },
- "test_name": map[string]interface{}{
- "type": "string",
- "description": "Name of specific test to run (optional)",
- },
- "release": map[string]interface{}{
- "type": "boolean",
- "description": "Run tests in release mode (default: false)",
- },
- },
- },
- },
- {
- Name: "cargo_add",
- Description: "Add a dependency to a Rust project",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "directory": map[string]interface{}{
- "type": "string",
- "description": "Project directory (optional, defaults to current directory)",
- },
- "package": map[string]interface{}{
- "type": "string",
- "description": "Package name to add",
- },
- "version": map[string]interface{}{
- "type": "string",
- "description": "Version requirement (optional)",
- },
- "dev": map[string]interface{}{
- "type": "boolean",
- "description": "Add as dev dependency (default: false)",
- },
- },
- "required": []string{"package"},
- },
- },
- {
- Name: "cargo_update",
- Description: "Update dependencies in a Rust project",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "directory": map[string]interface{}{
- "type": "string",
- "description": "Project directory (optional, defaults to current directory)",
- },
- "package": map[string]interface{}{
- "type": "string",
- "description": "Specific package to update (optional, updates all if not specified)",
- },
- },
- },
- },
- {
- Name: "cargo_check",
- Description: "Check a Rust project for errors without building",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "directory": map[string]interface{}{
- "type": "string",
- "description": "Project directory (optional, defaults to current directory)",
- },
- },
- },
- },
- {
- Name: "cargo_clippy",
- Description: "Run Clippy linter on a Rust project",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "directory": map[string]interface{}{
- "type": "string",
- "description": "Project directory (optional, defaults to current directory)",
- },
- "fix": map[string]interface{}{
- "type": "boolean",
- "description": "Automatically apply fixes (default: false)",
- },
- },
- },
- },
- // Homebrew tools
- {
- Name: "brew_install",
- Description: "Install a package using Homebrew",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "package": map[string]interface{}{
- "type": "string",
- "description": "Package name to install",
- },
- "cask": map[string]interface{}{
- "type": "boolean",
- "description": "Install as cask (GUI application) (default: false)",
- },
- },
- "required": []string{"package"},
- },
- },
- {
- Name: "brew_uninstall",
- Description: "Uninstall a package using Homebrew",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "package": map[string]interface{}{
- "type": "string",
- "description": "Package name to uninstall",
- },
- "cask": map[string]interface{}{
- "type": "boolean",
- "description": "Uninstall as cask (GUI application) (default: false)",
- },
- },
- "required": []string{"package"},
- },
- },
- {
- Name: "brew_search",
- Description: "Search for packages in Homebrew",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "query": map[string]interface{}{
- "type": "string",
- "description": "Search query",
- },
- "cask": map[string]interface{}{
- "type": "boolean",
- "description": "Search in casks (GUI applications) (default: false)",
- },
- },
- "required": []string{"query"},
- },
- },
- {
- Name: "brew_update",
- Description: "Update Homebrew and formulae",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{},
- },
- },
- {
- Name: "brew_upgrade",
- Description: "Upgrade installed packages using Homebrew",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "package": map[string]interface{}{
- "type": "string",
- "description": "Specific package to upgrade (optional, upgrades all if not specified)",
- },
- },
- },
- },
- {
- Name: "brew_doctor",
- Description: "Check Homebrew for potential problems",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{},
- },
- },
- {
- Name: "brew_list",
- Description: "List installed packages in Homebrew",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "cask": map[string]interface{}{
- "type": "boolean",
- "description": "List casks (GUI applications) (default: false)",
- },
- },
- },
- },
- // Cross-platform tools
- {
- Name: "check_vulnerabilities",
- Description: "Check for known vulnerabilities in dependencies",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "directory": map[string]interface{}{
- "type": "string",
- "description": "Project directory (optional, defaults to current directory)",
- },
- "format": map[string]interface{}{
- "type": "string",
- "description": "Output format (json, table) (default: table)",
- "enum": []string{"json", "table"},
- },
- },
- },
- },
- {
- Name: "outdated_packages",
- Description: "Check for outdated packages in the project",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "directory": map[string]interface{}{
- "type": "string",
- "description": "Project directory (optional, defaults to current directory)",
- },
- },
- },
- },
- {
- Name: "package_info",
- Description: "Get detailed information about a package",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "package": map[string]interface{}{
- "type": "string",
- "description": "Package name to get information about",
- },
- "manager": map[string]interface{}{
- "type": "string",
- "description": "Package manager (cargo, brew, npm, pip) (optional, auto-detected)",
- "enum": []string{"cargo", "brew", "npm", "pip"},
- },
- },
- "required": []string{"package"},
- },
- },
- }
-}
-
-// registerTools registers all package management tools with the server
-func (s *Server) registerTools() {
- // Get all tool definitions from ListTools method
- tools := s.ListTools()
-
- // Register each tool with its proper definition
- for _, tool := range tools {
- var handler mcp.ToolHandler
- switch tool.Name {
- case "cargo_build":
- handler = s.handleCargoBuild
- case "cargo_run":
- handler = s.handleCargoRun
- case "cargo_test":
- handler = s.handleCargoTest
- case "cargo_add":
- handler = s.handleCargoAdd
- case "cargo_update":
- handler = s.handleCargoUpdate
- case "cargo_check":
- handler = s.handleCargoCheck
- case "cargo_clippy":
- handler = s.handleCargoClippy
- case "brew_install":
- handler = s.handleBrewInstall
- case "brew_uninstall":
- handler = s.handleBrewUninstall
- case "brew_search":
- handler = s.handleBrewSearch
- case "brew_update":
- handler = s.handleBrewUpdate
- case "brew_upgrade":
- handler = s.handleBrewUpgrade
- case "brew_doctor":
- handler = s.handleBrewDoctor
- case "brew_list":
- handler = s.handleBrewList
- case "check_vulnerabilities":
- handler = s.handleCheckVulnerabilities
- case "outdated_packages":
- handler = s.handleOutdatedPackages
- case "package_info":
- handler = s.handlePackageInfo
- default:
- continue
- }
- s.RegisterToolWithDefinition(tool, handler)
- }
-}
-
-// Cargo tool handlers
-
-func (s *Server) handleCargoBuild(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
- var args struct {
- Directory string `json:"directory,omitempty"`
- Release bool `json:"release,omitempty"`
- Target string `json:"target,omitempty"`
- Features string `json:"features,omitempty"`
- }
-
- argsBytes, _ := json.Marshal(req.Arguments)
- if err := json.Unmarshal(argsBytes, &args); err != nil {
- return mcp.CallToolResult{}, fmt.Errorf("invalid arguments: %w", err)
- }
-
- // Default to current directory
- if args.Directory == "" {
- args.Directory = "."
- }
-
- // Check if Cargo.toml exists
- cargoToml := filepath.Join(args.Directory, "Cargo.toml")
- if _, err := os.Stat(cargoToml); os.IsNotExist(err) {
- return mcp.CallToolResult{}, fmt.Errorf("no Cargo.toml found in %s", args.Directory)
- }
-
- // Build cargo command
- cmdArgs := []string{"build"}
-
- if args.Release {
- cmdArgs = append(cmdArgs, "--release")
- }
-
- if args.Target != "" {
- cmdArgs = append(cmdArgs, "--target", args.Target)
- }
-
- if args.Features != "" {
- cmdArgs = append(cmdArgs, "--features", args.Features)
- }
-
- cmd := exec.Command("cargo", cmdArgs...)
- cmd.Dir = args.Directory
-
- output, err := cmd.CombinedOutput()
-
- result := fmt.Sprintf("Command: cargo %s\nDirectory: %s\nOutput:\n%s",
- strings.Join(cmdArgs, " "), args.Directory, string(output))
-
- if err != nil {
- result += fmt.Sprintf("\nError: %v", err)
- }
-
- return mcp.CallToolResult{
- Content: []mcp.Content{
- mcp.TextContent{
- Type: "text",
- Text: result,
- },
- },
- }, nil
-}
-
-func (s *Server) handleCargoRun(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
- var args struct {
- Directory string `json:"directory,omitempty"`
- Package string `json:"package,omitempty"`
- Bin string `json:"bin,omitempty"`
- Args []string `json:"args,omitempty"`
- Release bool `json:"release,omitempty"`
- }
-
- argsBytes, _ := json.Marshal(req.Arguments)
- if err := json.Unmarshal(argsBytes, &args); err != nil {
- return mcp.CallToolResult{}, fmt.Errorf("invalid arguments: %w", err)
- }
-
- if args.Directory == "" {
- args.Directory = "."
- }
-
- // Build cargo command
- cmdArgs := []string{"run"}
-
- if args.Release {
- cmdArgs = append(cmdArgs, "--release")
- }
-
- if args.Package != "" {
- cmdArgs = append(cmdArgs, "--package", args.Package)
- }
-
- if args.Bin != "" {
- cmdArgs = append(cmdArgs, "--bin", args.Bin)
- }
-
- if len(args.Args) > 0 {
- cmdArgs = append(cmdArgs, "--")
- cmdArgs = append(cmdArgs, args.Args...)
- }
-
- cmd := exec.Command("cargo", cmdArgs...)
- cmd.Dir = args.Directory
-
- output, err := cmd.CombinedOutput()
-
- result := fmt.Sprintf("Command: cargo %s\nDirectory: %s\nOutput:\n%s",
- strings.Join(cmdArgs, " "), args.Directory, string(output))
-
- if err != nil {
- result += fmt.Sprintf("\nError: %v", err)
- }
-
- return mcp.CallToolResult{
- Content: []mcp.Content{
- mcp.TextContent{
- Type: "text",
- Text: result,
- },
- },
- }, nil
-}
-
-func (s *Server) handleCargoTest(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
- var args struct {
- Directory string `json:"directory,omitempty"`
- Package string `json:"package,omitempty"`
- Test string `json:"test,omitempty"`
- Args []string `json:"args,omitempty"`
- Release bool `json:"release,omitempty"`
- }
-
- argsBytes, _ := json.Marshal(req.Arguments)
- if err := json.Unmarshal(argsBytes, &args); err != nil {
- return mcp.CallToolResult{}, fmt.Errorf("invalid arguments: %w", err)
- }
-
- if args.Directory == "" {
- args.Directory = "."
- }
-
- cmdArgs := []string{"test"}
-
- if args.Release {
- cmdArgs = append(cmdArgs, "--release")
- }
-
- if args.Package != "" {
- cmdArgs = append(cmdArgs, "--package", args.Package)
- }
-
- if args.Test != "" {
- cmdArgs = append(cmdArgs, args.Test)
- }
-
- if len(args.Args) > 0 {
- cmdArgs = append(cmdArgs, "--")
- cmdArgs = append(cmdArgs, args.Args...)
- }
-
- cmd := exec.Command("cargo", cmdArgs...)
- cmd.Dir = args.Directory
-
- output, err := cmd.CombinedOutput()
-
- result := fmt.Sprintf("Command: cargo %s\nDirectory: %s\nOutput:\n%s",
- strings.Join(cmdArgs, " "), args.Directory, string(output))
-
- if err != nil {
- result += fmt.Sprintf("\nError: %v", err)
- }
-
- return mcp.CallToolResult{
- Content: []mcp.Content{
- mcp.TextContent{
- Type: "text",
- Text: result,
- },
- },
- }, nil
-}
-
-func (s *Server) handleCargoAdd(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
- var args struct {
- Directory string `json:"directory,omitempty"`
- Packages []string `json:"packages"`
- Dev bool `json:"dev,omitempty"`
- Build bool `json:"build,omitempty"`
- Optional bool `json:"optional,omitempty"`
- Features []string `json:"features,omitempty"`
- NoDefaultFeatures bool `json:"no_default_features,omitempty"`
- }
-
- argsBytes, _ := json.Marshal(req.Arguments)
- if err := json.Unmarshal(argsBytes, &args); err != nil {
- return mcp.CallToolResult{}, fmt.Errorf("invalid arguments: %w", err)
- }
-
- if len(args.Packages) == 0 {
- return mcp.CallToolResult{}, fmt.Errorf("packages are required")
- }
-
- if args.Directory == "" {
- args.Directory = "."
- }
-
- cmdArgs := []string{"add"}
- cmdArgs = append(cmdArgs, args.Packages...)
-
- if args.Dev {
- cmdArgs = append(cmdArgs, "--dev")
- }
-
- if args.Build {
- cmdArgs = append(cmdArgs, "--build")
- }
-
- if args.Optional {
- cmdArgs = append(cmdArgs, "--optional")
- }
-
- if len(args.Features) > 0 {
- cmdArgs = append(cmdArgs, "--features", strings.Join(args.Features, ","))
- }
-
- if args.NoDefaultFeatures {
- cmdArgs = append(cmdArgs, "--no-default-features")
- }
-
- cmd := exec.Command("cargo", cmdArgs...)
- cmd.Dir = args.Directory
-
- output, err := cmd.CombinedOutput()
-
- result := fmt.Sprintf("Command: cargo %s\nDirectory: %s\nOutput:\n%s",
- strings.Join(cmdArgs, " "), args.Directory, string(output))
-
- if err != nil {
- result += fmt.Sprintf("\nError: %v", err)
- }
-
- return mcp.CallToolResult{
- Content: []mcp.Content{
- mcp.TextContent{
- Type: "text",
- Text: result,
- },
- },
- }, nil
-}
-
-func (s *Server) handleCargoUpdate(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
- var args struct {
- Directory string `json:"directory,omitempty"`
- Packages []string `json:"packages,omitempty"`
- Aggressive bool `json:"aggressive,omitempty"`
- Precise string `json:"precise,omitempty"`
- }
-
- argsBytes, _ := json.Marshal(req.Arguments)
- if err := json.Unmarshal(argsBytes, &args); err != nil {
- return mcp.CallToolResult{}, fmt.Errorf("invalid arguments: %w", err)
- }
-
- if args.Directory == "" {
- args.Directory = "."
- }
-
- cmdArgs := []string{"update"}
-
- if len(args.Packages) > 0 {
- cmdArgs = append(cmdArgs, args.Packages...)
- }
-
- if args.Aggressive {
- cmdArgs = append(cmdArgs, "--aggressive")
- }
-
- if args.Precise != "" {
- cmdArgs = append(cmdArgs, "--precise", args.Precise)
- }
-
- cmd := exec.Command("cargo", cmdArgs...)
- cmd.Dir = args.Directory
-
- output, err := cmd.CombinedOutput()
-
- result := fmt.Sprintf("Command: cargo %s\nDirectory: %s\nOutput:\n%s",
- strings.Join(cmdArgs, " "), args.Directory, string(output))
-
- if err != nil {
- result += fmt.Sprintf("\nError: %v", err)
- }
-
- return mcp.CallToolResult{
- Content: []mcp.Content{
- mcp.TextContent{
- Type: "text",
- Text: result,
- },
- },
- }, nil
-}
-
-func (s *Server) handleCargoCheck(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
- var args struct {
- Directory string `json:"directory,omitempty"`
- Package string `json:"package,omitempty"`
- Release bool `json:"release,omitempty"`
- AllTargets bool `json:"all_targets,omitempty"`
- }
-
- argsBytes, _ := json.Marshal(req.Arguments)
- if err := json.Unmarshal(argsBytes, &args); err != nil {
- return mcp.CallToolResult{}, fmt.Errorf("invalid arguments: %w", err)
- }
-
- if args.Directory == "" {
- args.Directory = "."
- }
-
- cmdArgs := []string{"check"}
-
- if args.Release {
- cmdArgs = append(cmdArgs, "--release")
- }
-
- if args.Package != "" {
- cmdArgs = append(cmdArgs, "--package", args.Package)
- }
-
- if args.AllTargets {
- cmdArgs = append(cmdArgs, "--all-targets")
- }
-
- cmd := exec.Command("cargo", cmdArgs...)
- cmd.Dir = args.Directory
-
- output, err := cmd.CombinedOutput()
-
- result := fmt.Sprintf("Command: cargo %s\nDirectory: %s\nOutput:\n%s",
- strings.Join(cmdArgs, " "), args.Directory, string(output))
-
- if err != nil {
- result += fmt.Sprintf("\nError: %v", err)
- }
-
- return mcp.CallToolResult{
- Content: []mcp.Content{
- mcp.TextContent{
- Type: "text",
- Text: result,
- },
- },
- }, nil
-}
-
-func (s *Server) handleCargoClippy(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
- var args struct {
- Directory string `json:"directory,omitempty"`
- Package string `json:"package,omitempty"`
- AllTargets bool `json:"all_targets,omitempty"`
- Fix bool `json:"fix,omitempty"`
- }
-
- argsBytes, _ := json.Marshal(req.Arguments)
- if err := json.Unmarshal(argsBytes, &args); err != nil {
- return mcp.CallToolResult{}, fmt.Errorf("invalid arguments: %w", err)
- }
-
- if args.Directory == "" {
- args.Directory = "."
- }
-
- cmdArgs := []string{"clippy"}
-
- if args.Package != "" {
- cmdArgs = append(cmdArgs, "--package", args.Package)
- }
-
- if args.AllTargets {
- cmdArgs = append(cmdArgs, "--all-targets")
- }
-
- if args.Fix {
- cmdArgs = append(cmdArgs, "--fix")
- }
-
- cmd := exec.Command("cargo", cmdArgs...)
- cmd.Dir = args.Directory
-
- output, err := cmd.CombinedOutput()
-
- result := fmt.Sprintf("Command: cargo %s\nDirectory: %s\nOutput:\n%s",
- strings.Join(cmdArgs, " "), args.Directory, string(output))
-
- if err != nil {
- result += fmt.Sprintf("\nError: %v", err)
- }
-
- return mcp.CallToolResult{
- Content: []mcp.Content{
- mcp.TextContent{
- Type: "text",
- Text: result,
- },
- },
- }, nil
-}
-
-// Homebrew tool handlers
-
-func (s *Server) handleBrewInstall(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
- var args struct {
- Packages []string `json:"packages"`
- Cask bool `json:"cask,omitempty"`
- Force bool `json:"force,omitempty"`
- }
-
- argsBytes, _ := json.Marshal(req.Arguments)
- if err := json.Unmarshal(argsBytes, &args); err != nil {
- return mcp.CallToolResult{}, fmt.Errorf("invalid arguments: %w", err)
- }
-
- if len(args.Packages) == 0 {
- return mcp.CallToolResult{}, fmt.Errorf("packages are required")
- }
-
- cmdArgs := []string{"install"}
-
- if args.Cask {
- cmdArgs = append(cmdArgs, "--cask")
- }
-
- if args.Force {
- cmdArgs = append(cmdArgs, "--force")
- }
-
- cmdArgs = append(cmdArgs, args.Packages...)
-
- cmd := exec.Command("brew", cmdArgs...)
- output, err := cmd.CombinedOutput()
-
- result := fmt.Sprintf("Command: brew %s\nOutput:\n%s",
- strings.Join(cmdArgs, " "), string(output))
-
- if err != nil {
- result += fmt.Sprintf("\nError: %v", err)
- }
-
- return mcp.CallToolResult{
- Content: []mcp.Content{
- mcp.TextContent{
- Type: "text",
- Text: result,
- },
- },
- }, nil
-}
-
-func (s *Server) handleBrewUninstall(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
- var args struct {
- Packages []string `json:"packages"`
- Cask bool `json:"cask,omitempty"`
- Force bool `json:"force,omitempty"`
- }
-
- argsBytes, _ := json.Marshal(req.Arguments)
- if err := json.Unmarshal(argsBytes, &args); err != nil {
- return mcp.CallToolResult{}, fmt.Errorf("invalid arguments: %w", err)
- }
-
- if len(args.Packages) == 0 {
- return mcp.CallToolResult{}, fmt.Errorf("packages are required")
- }
-
- cmdArgs := []string{"uninstall"}
-
- if args.Cask {
- cmdArgs = append(cmdArgs, "--cask")
- }
-
- if args.Force {
- cmdArgs = append(cmdArgs, "--force")
- }
-
- cmdArgs = append(cmdArgs, args.Packages...)
-
- cmd := exec.Command("brew", cmdArgs...)
- output, err := cmd.CombinedOutput()
-
- result := fmt.Sprintf("Command: brew %s\nOutput:\n%s",
- strings.Join(cmdArgs, " "), string(output))
-
- if err != nil {
- result += fmt.Sprintf("\nError: %v", err)
- }
-
- return mcp.CallToolResult{
- Content: []mcp.Content{
- mcp.TextContent{
- Type: "text",
- Text: result,
- },
- },
- }, nil
-}
-
-func (s *Server) handleBrewSearch(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
- var args struct {
- Query string `json:"query"`
- Cask bool `json:"cask,omitempty"`
- }
-
- argsBytes, _ := json.Marshal(req.Arguments)
- if err := json.Unmarshal(argsBytes, &args); err != nil {
- return mcp.CallToolResult{}, fmt.Errorf("invalid arguments: %w", err)
- }
-
- if args.Query == "" {
- return mcp.CallToolResult{}, fmt.Errorf("query is required")
- }
-
- cmdArgs := []string{"search"}
-
- if args.Cask {
- cmdArgs = append(cmdArgs, "--cask")
- }
-
- cmdArgs = append(cmdArgs, args.Query)
-
- cmd := exec.Command("brew", cmdArgs...)
- output, err := cmd.CombinedOutput()
-
- result := fmt.Sprintf("Command: brew %s\nOutput:\n%s",
- strings.Join(cmdArgs, " "), string(output))
-
- if err != nil {
- result += fmt.Sprintf("\nError: %v", err)
- }
-
- return mcp.CallToolResult{
- Content: []mcp.Content{
- mcp.TextContent{
- Type: "text",
- Text: result,
- },
- },
- }, nil
-}
-
-func (s *Server) handleBrewUpdate(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
- cmd := exec.Command("brew", "update")
- output, err := cmd.CombinedOutput()
-
- result := fmt.Sprintf("Command: brew update\nOutput:\n%s", string(output))
-
- if err != nil {
- result += fmt.Sprintf("\nError: %v", err)
- }
-
- return mcp.CallToolResult{
- Content: []mcp.Content{
- mcp.TextContent{
- Type: "text",
- Text: result,
- },
- },
- }, nil
-}
-
-func (s *Server) handleBrewUpgrade(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
- var args struct {
- Packages []string `json:"packages,omitempty"`
- Cask bool `json:"cask,omitempty"`
- }
-
- argsBytes, _ := json.Marshal(req.Arguments)
- if err := json.Unmarshal(argsBytes, &args); err != nil {
- return mcp.CallToolResult{}, fmt.Errorf("invalid arguments: %w", err)
- }
-
- cmdArgs := []string{"upgrade"}
-
- if args.Cask {
- cmdArgs = append(cmdArgs, "--cask")
- }
-
- if len(args.Packages) > 0 {
- cmdArgs = append(cmdArgs, args.Packages...)
- }
-
- cmd := exec.Command("brew", cmdArgs...)
- output, err := cmd.CombinedOutput()
-
- result := fmt.Sprintf("Command: brew %s\nOutput:\n%s",
- strings.Join(cmdArgs, " "), string(output))
-
- if err != nil {
- result += fmt.Sprintf("\nError: %v", err)
- }
-
- return mcp.CallToolResult{
- Content: []mcp.Content{
- mcp.TextContent{
- Type: "text",
- Text: result,
- },
- },
- }, nil
-}
-
-func (s *Server) handleBrewDoctor(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
- cmd := exec.Command("brew", "doctor")
- output, err := cmd.CombinedOutput()
-
- result := fmt.Sprintf("Command: brew doctor\nOutput:\n%s", string(output))
-
- if err != nil {
- result += fmt.Sprintf("\nError: %v", err)
- }
-
- return mcp.CallToolResult{
- Content: []mcp.Content{
- mcp.TextContent{
- Type: "text",
- Text: result,
- },
- },
- }, nil
-}
-
-func (s *Server) handleBrewList(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
- var args struct {
- Cask bool `json:"cask,omitempty"`
- Versions bool `json:"versions,omitempty"`
- }
-
- argsBytes, _ := json.Marshal(req.Arguments)
- if err := json.Unmarshal(argsBytes, &args); err != nil {
- return mcp.CallToolResult{}, fmt.Errorf("invalid arguments: %w", err)
- }
-
- cmdArgs := []string{"list"}
-
- if args.Cask {
- cmdArgs = append(cmdArgs, "--cask")
- }
-
- if args.Versions {
- cmdArgs = append(cmdArgs, "--versions")
- }
-
- cmd := exec.Command("brew", cmdArgs...)
- output, err := cmd.CombinedOutput()
-
- result := fmt.Sprintf("Command: brew %s\nOutput:\n%s",
- strings.Join(cmdArgs, " "), string(output))
-
- if err != nil {
- result += fmt.Sprintf("\nError: %v", err)
- }
-
- return mcp.CallToolResult{
- Content: []mcp.Content{
- mcp.TextContent{
- Type: "text",
- Text: result,
- },
- },
- }, nil
-}
-
-// Cross-platform tool handlers
-
-func (s *Server) handleCheckVulnerabilities(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
- var args struct {
- Directory string `json:"directory,omitempty"`
- PackageManager string `json:"package_manager,omitempty"`
- }
-
- argsBytes, _ := json.Marshal(req.Arguments)
- if err := json.Unmarshal(argsBytes, &args); err != nil {
- return mcp.CallToolResult{}, fmt.Errorf("invalid arguments: %w", err)
- }
-
- if args.Directory == "" {
- args.Directory = "."
- }
-
- var result string
-
- // Auto-detect package manager if not specified
- if args.PackageManager == "" {
- if _, err := os.Stat(filepath.Join(args.Directory, "Cargo.toml")); err == nil {
- args.PackageManager = "cargo"
- } else if _, err := os.Stat(filepath.Join(args.Directory, "package.json")); err == nil {
- args.PackageManager = "npm"
- } else if _, err := os.Stat(filepath.Join(args.Directory, "go.mod")); err == nil {
- args.PackageManager = "go"
- }
- }
-
- switch args.PackageManager {
- case "cargo":
- cmd := exec.Command("cargo", "audit")
- cmd.Dir = args.Directory
- output, err := cmd.CombinedOutput()
- result = fmt.Sprintf("Cargo security audit:\n%s", string(output))
- if err != nil {
- result += fmt.Sprintf("\nError: %v", err)
- }
- case "npm":
- cmd := exec.Command("npm", "audit")
- cmd.Dir = args.Directory
- output, err := cmd.CombinedOutput()
- result = fmt.Sprintf("NPM security audit:\n%s", string(output))
- if err != nil {
- result += fmt.Sprintf("\nError: %v", err)
- }
- case "go":
- cmd := exec.Command("go", "list", "-json", "-m", "all")
- cmd.Dir = args.Directory
- output, err := cmd.CombinedOutput()
- result = fmt.Sprintf("Go module list (manual vulnerability check needed):\n%s", string(output))
- if err != nil {
- result += fmt.Sprintf("\nError: %v", err)
- }
- default:
- result = "Unable to detect package manager. Please specify package_manager: 'cargo', 'npm', or 'go'"
- }
-
- return mcp.CallToolResult{
- Content: []mcp.Content{
- mcp.TextContent{
- Type: "text",
- Text: result,
- },
- },
- }, nil
-}
-
-func (s *Server) handleOutdatedPackages(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
- var args struct {
- Directory string `json:"directory,omitempty"`
- PackageManager string `json:"package_manager,omitempty"`
- }
-
- argsBytes, _ := json.Marshal(req.Arguments)
- if err := json.Unmarshal(argsBytes, &args); err != nil {
- return mcp.CallToolResult{}, fmt.Errorf("invalid arguments: %w", err)
- }
-
- if args.Directory == "" {
- args.Directory = "."
- }
-
- var result string
-
- // Auto-detect package manager if not specified
- if args.PackageManager == "" {
- if _, err := os.Stat(filepath.Join(args.Directory, "Cargo.toml")); err == nil {
- args.PackageManager = "cargo"
- } else if _, err := os.Stat(filepath.Join(args.Directory, "package.json")); err == nil {
- args.PackageManager = "npm"
- } else if _, err := os.Stat(filepath.Join(args.Directory, "go.mod")); err == nil {
- args.PackageManager = "go"
- }
- }
-
- switch args.PackageManager {
- case "cargo":
- cmd := exec.Command("cargo", "outdated")
- cmd.Dir = args.Directory
- output, err := cmd.CombinedOutput()
- result = fmt.Sprintf("Cargo outdated packages:\n%s", string(output))
- if err != nil {
- result += fmt.Sprintf("\nError: %v (try: cargo install cargo-outdated)", err)
- }
- case "npm":
- cmd := exec.Command("npm", "outdated")
- cmd.Dir = args.Directory
- output, err := cmd.CombinedOutput()
- result = fmt.Sprintf("NPM outdated packages:\n%s", string(output))
- if err != nil {
- result += fmt.Sprintf("\nError: %v", err)
- }
- case "go":
- cmd := exec.Command("go", "list", "-u", "-m", "all")
- cmd.Dir = args.Directory
- output, err := cmd.CombinedOutput()
- result = fmt.Sprintf("Go modules with available updates:\n%s", string(output))
- if err != nil {
- result += fmt.Sprintf("\nError: %v", err)
- }
- case "brew":
- cmd := exec.Command("brew", "outdated")
- output, err := cmd.CombinedOutput()
- result = fmt.Sprintf("Homebrew outdated packages:\n%s", string(output))
- if err != nil {
- result += fmt.Sprintf("\nError: %v", err)
- }
- default:
- result = "Unable to detect package manager. Please specify package_manager: 'cargo', 'npm', 'go', or 'brew'"
- }
-
- return mcp.CallToolResult{
- Content: []mcp.Content{
- mcp.TextContent{
- Type: "text",
- Text: result,
- },
- },
- }, nil
-}
-
-func (s *Server) handlePackageInfo(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
- var args struct {
- Package string `json:"package"`
- PackageManager string `json:"package_manager,omitempty"`
- Directory string `json:"directory,omitempty"`
- }
-
- argsBytes, _ := json.Marshal(req.Arguments)
- if err := json.Unmarshal(argsBytes, &args); err != nil {
- return mcp.CallToolResult{}, fmt.Errorf("invalid arguments: %w", err)
- }
-
- if args.Package == "" {
- return mcp.CallToolResult{}, fmt.Errorf("package name is required")
- }
-
- if args.Directory == "" {
- args.Directory = "."
- }
-
- var result string
-
- // Auto-detect package manager if not specified
- if args.PackageManager == "" {
- if _, err := os.Stat(filepath.Join(args.Directory, "Cargo.toml")); err == nil {
- args.PackageManager = "cargo"
- } else if _, err := os.Stat(filepath.Join(args.Directory, "package.json")); err == nil {
- args.PackageManager = "npm"
- }
- }
-
- switch args.PackageManager {
- case "cargo":
- cmd := exec.Command("cargo", "search", args.Package, "--limit", "1")
- output, err := cmd.CombinedOutput()
- result = fmt.Sprintf("Cargo package info for '%s':\n%s", args.Package, string(output))
- if err != nil {
- result += fmt.Sprintf("\nError: %v", err)
- }
- case "npm":
- cmd := exec.Command("npm", "info", args.Package)
- output, err := cmd.CombinedOutput()
- result = fmt.Sprintf("NPM package info for '%s':\n%s", args.Package, string(output))
- if err != nil {
- result += fmt.Sprintf("\nError: %v", err)
- }
- case "brew":
- cmd := exec.Command("brew", "info", args.Package)
- output, err := cmd.CombinedOutput()
- result = fmt.Sprintf("Homebrew package info for '%s':\n%s", args.Package, string(output))
- if err != nil {
- result += fmt.Sprintf("\nError: %v", err)
- }
- default:
- result = "Unable to detect package manager. Please specify package_manager: 'cargo', 'npm', or 'brew'"
- }
-
- return mcp.CallToolResult{
- Content: []mcp.Content{
- mcp.TextContent{
- Type: "text",
- Text: result,
- },
- },
- }, nil
-}
pkg/semantic/server.go
@@ -8,267 +8,237 @@ import (
"github.com/xlgmokha/mcp/pkg/mcp"
)
-// Server represents the Semantic MCP server
-type Server struct {
- *mcp.Server
+// SemanticOperations provides semantic analysis operations
+type SemanticOperations struct {
lspManager *LSPManager
symbolManager *SymbolManager
projectManager *ProjectManager
mu sync.RWMutex
}
-// NewServer creates a new Semantic MCP server
-func NewServer() *Server {
- baseServer := mcp.NewServer("mcp-semantic", "1.0.0", []mcp.Tool{}, []mcp.Resource{}, []mcp.Root{})
-
+// NewSemanticOperations creates a new SemanticOperations helper
+func NewSemanticOperations() (*SemanticOperations, error) {
lspManager := NewLSPManager()
projectManager := NewProjectManager()
symbolManager := NewSymbolManager(lspManager, projectManager)
- server := &Server{
- Server: baseServer,
+ return &SemanticOperations{
lspManager: lspManager,
symbolManager: symbolManager,
projectManager: projectManager,
- }
-
- server.registerTools()
-
- return server
+ }, nil
}
-// ListTools returns all available semantic analysis tools
-func (s *Server) ListTools() []mcp.Tool {
- return []mcp.Tool{
- // Core symbol discovery tools
- {
- Name: "semantic_find_symbol",
- Description: "Find symbols by name, type, or pattern across the project",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "query": map[string]interface{}{
- "type": "string",
- "description": "Symbol name or pattern to search for",
- },
- "symbol_type": map[string]interface{}{
- "type": "string",
- "description": "Type of symbol to search for (function, class, variable, etc.)",
- "enum": []string{"function", "class", "variable", "interface", "type", "constant", "module"},
- },
- "language": map[string]interface{}{
- "type": "string",
- "description": "Programming language to filter by (optional)",
- "enum": []string{"go", "rust", "typescript", "javascript", "python", "java", "cpp"},
- },
- "case_sensitive": map[string]interface{}{
- "type": "boolean",
- "description": "Whether the search should be case sensitive (default: false)",
- },
- "exact_match": map[string]interface{}{
- "type": "boolean",
- "description": "Whether to match the exact symbol name (default: false, allows partial matches)",
- },
- },
- "required": []string{"query"},
+// New creates a new Semantic MCP server
+func New() (*mcp.Server, error) {
+ semantic, err := NewSemanticOperations()
+ if err != nil {
+ return nil, err
+ }
+
+ builder := mcp.NewServerBuilder("mcp-semantic", "1.0.0")
+
+// Add semantic_find_symbol tool
+ builder.AddTool(mcp.NewTool("semantic_find_symbol", "Find symbols by name, type, or pattern across the project", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "query": map[string]interface{}{
+ "type": "string",
+ "description": "Symbol name or pattern to search for",
+ },
+ "symbol_type": map[string]interface{}{
+ "type": "string",
+ "description": "Type of symbol to search for (function, class, variable, etc.)",
+ "enum": []string{"function", "class", "variable", "interface", "type", "constant", "module"},
+ },
+ "language": map[string]interface{}{
+ "type": "string",
+ "description": "Programming language to filter by (optional)",
+ "enum": []string{"go", "rust", "typescript", "javascript", "python", "java", "cpp"},
+ },
+ "case_sensitive": map[string]interface{}{
+ "type": "boolean",
+ "description": "Whether the search should be case sensitive (default: false)",
+ },
+ "exact_match": map[string]interface{}{
+ "type": "boolean",
+ "description": "Whether to match the exact symbol name (default: false, allows partial matches)",
},
},
- {
- Name: "semantic_get_overview",
- Description: "Get a high-level overview of symbols and structure in a file or directory",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "path": map[string]interface{}{
- "type": "string",
- "description": "File or directory path to analyze",
- },
- "depth": map[string]interface{}{
- "type": "integer",
- "description": "Depth of analysis (1=top-level only, 2=include immediate children, etc.) (default: 2)",
- "minimum": 1,
- "maximum": 5,
- },
- "include_private": map[string]interface{}{
- "type": "boolean",
- "description": "Include private/internal symbols (default: false)",
- },
- "group_by": map[string]interface{}{
- "type": "string",
- "description": "How to group the results (type, file, module) (default: type)",
- "enum": []string{"type", "file", "module"},
- },
- },
- "required": []string{"path"},
+ "required": []string{"query"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return semantic.handleFindSymbol(req)
+ }))
+
+ // Add semantic_get_overview tool
+ builder.AddTool(mcp.NewTool("semantic_get_overview", "Get a high-level overview of symbols and structure in a file or directory", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "path": map[string]interface{}{
+ "type": "string",
+ "description": "File or directory path to analyze",
+ },
+ "depth": map[string]interface{}{
+ "type": "integer",
+ "description": "Depth of analysis (1=top-level only, 2=include immediate children, etc.) (default: 2)",
+ "minimum": 1,
+ "maximum": 5,
+ },
+ "include_private": map[string]interface{}{
+ "type": "boolean",
+ "description": "Include private/internal symbols (default: false)",
+ },
+ "group_by": map[string]interface{}{
+ "type": "string",
+ "description": "How to group the results (type, file, module) (default: type)",
+ "enum": []string{"type", "file", "module"},
},
},
- {
- Name: "semantic_get_definition",
- Description: "Get the definition location and details for a specific symbol",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "symbol": map[string]interface{}{
- "type": "string",
- "description": "Symbol name to find definition for",
- },
- "file": map[string]interface{}{
- "type": "string",
- "description": "File path where the symbol is referenced (helps with context)",
- },
- "line": map[string]interface{}{
- "type": "integer",
- "description": "Line number where the symbol is referenced (optional, for better precision)",
- "minimum": 1,
- },
- "column": map[string]interface{}{
- "type": "integer",
- "description": "Column number where the symbol is referenced (optional, for better precision)",
- "minimum": 1,
- },
- "include_signature": map[string]interface{}{
- "type": "boolean",
- "description": "Include full function/method signature (default: true)",
- },
- },
- "required": []string{"symbol"},
+ "required": []string{"path"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return semantic.handleGetOverview(req)
+ }))
+
+ // Add semantic_get_definition tool
+ builder.AddTool(mcp.NewTool("semantic_get_definition", "Get the definition location and details for a specific symbol", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "symbol": map[string]interface{}{
+ "type": "string",
+ "description": "Symbol name to find definition for",
+ },
+ "file": map[string]interface{}{
+ "type": "string",
+ "description": "File path where the symbol is referenced (helps with context)",
+ },
+ "line": map[string]interface{}{
+ "type": "integer",
+ "description": "Line number where the symbol is referenced (optional, for better precision)",
+ "minimum": 1,
+ },
+ "column": map[string]interface{}{
+ "type": "integer",
+ "description": "Column number where the symbol is referenced (optional, for better precision)",
+ "minimum": 1,
+ },
+ "include_signature": map[string]interface{}{
+ "type": "boolean",
+ "description": "Include full function/method signature (default: true)",
},
},
- {
- Name: "semantic_get_references",
- Description: "Find all references to a specific symbol across the project",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "symbol": map[string]interface{}{
- "type": "string",
- "description": "Symbol name to find references for",
- },
- "file": map[string]interface{}{
- "type": "string",
- "description": "File path where the symbol is defined (helps with context)",
- },
- "line": map[string]interface{}{
- "type": "integer",
- "description": "Line number where the symbol is defined (optional, for better precision)",
- "minimum": 1,
- },
- "include_declaration": map[string]interface{}{
- "type": "boolean",
- "description": "Include the symbol declaration in results (default: true)",
- },
- "scope": map[string]interface{}{
- "type": "string",
- "description": "Scope of search (file, directory, project) (default: project)",
- "enum": []string{"file", "directory", "project"},
- },
- },
- "required": []string{"symbol"},
+ "required": []string{"symbol"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return semantic.handleGetDefinition(req)
+ }))
+
+ // Add semantic_get_references tool
+ builder.AddTool(mcp.NewTool("semantic_get_references", "Find all references to a specific symbol across the project", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "symbol": map[string]interface{}{
+ "type": "string",
+ "description": "Symbol name to find references for",
+ },
+ "file": map[string]interface{}{
+ "type": "string",
+ "description": "File path where the symbol is defined (helps with context)",
+ },
+ "line": map[string]interface{}{
+ "type": "integer",
+ "description": "Line number where the symbol is defined (optional, for better precision)",
+ "minimum": 1,
+ },
+ "include_declaration": map[string]interface{}{
+ "type": "boolean",
+ "description": "Include the symbol declaration in results (default: true)",
+ },
+ "scope": map[string]interface{}{
+ "type": "string",
+ "description": "Scope of search (file, directory, project) (default: project)",
+ "enum": []string{"file", "directory", "project"},
},
},
- {
- Name: "semantic_get_call_hierarchy",
- Description: "Get the call hierarchy (callers and callees) for a function or method",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "symbol": map[string]interface{}{
- "type": "string",
- "description": "Function or method name to analyze",
- },
- "file": map[string]interface{}{
- "type": "string",
- "description": "File path where the function is defined",
- },
- "line": map[string]interface{}{
- "type": "integer",
- "description": "Line number where the function is defined (optional)",
- "minimum": 1,
- },
- "direction": map[string]interface{}{
- "type": "string",
- "description": "Direction of call hierarchy (incoming=who calls this, outgoing=what this calls, both) (default: both)",
- "enum": []string{"incoming", "outgoing", "both"},
- },
- "depth": map[string]interface{}{
- "type": "integer",
- "description": "Depth of call hierarchy to retrieve (default: 3)",
- "minimum": 1,
- "maximum": 10,
- },
- },
- "required": []string{"symbol", "file"},
+ "required": []string{"symbol"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return semantic.handleGetReferences(req)
+ }))
+
+ // Add semantic_get_call_hierarchy tool
+ builder.AddTool(mcp.NewTool("semantic_get_call_hierarchy", "Get the call hierarchy (callers and callees) for a function or method", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "symbol": map[string]interface{}{
+ "type": "string",
+ "description": "Function or method name to analyze",
+ },
+ "file": map[string]interface{}{
+ "type": "string",
+ "description": "File path where the function is defined",
+ },
+ "line": map[string]interface{}{
+ "type": "integer",
+ "description": "Line number where the function is defined (optional)",
+ "minimum": 1,
+ },
+ "direction": map[string]interface{}{
+ "type": "string",
+ "description": "Direction of call hierarchy (incoming=who calls this, outgoing=what this calls, both) (default: both)",
+ "enum": []string{"incoming", "outgoing", "both"},
+ },
+ "depth": map[string]interface{}{
+ "type": "integer",
+ "description": "Depth of call hierarchy to retrieve (default: 3)",
+ "minimum": 1,
+ "maximum": 10,
},
},
- {
- Name: "semantic_analyze_dependencies",
- Description: "Analyze dependencies and relationships between modules, files, or symbols",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "path": map[string]interface{}{
- "type": "string",
- "description": "Path to analyze (file, directory, or project root)",
- },
- "analysis_type": map[string]interface{}{
- "type": "string",
- "description": "Type of dependency analysis to perform (default: imports)",
- "enum": []string{"imports", "exports", "modules", "symbols", "circular"},
- },
- "include_external": map[string]interface{}{
- "type": "boolean",
- "description": "Include external/third-party dependencies (default: false)",
- },
- "format": map[string]interface{}{
- "type": "string",
- "description": "Output format (tree, graph, list) (default: tree)",
- "enum": []string{"tree", "graph", "list"},
- },
- "max_depth": map[string]interface{}{
- "type": "integer",
- "description": "Maximum depth of dependency analysis (default: 5)",
- "minimum": 1,
- "maximum": 20,
- },
- },
- "required": []string{"path"},
+ "required": []string{"symbol", "file"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return semantic.handleGetCallHierarchy(req)
+ }))
+
+ // Add semantic_analyze_dependencies tool
+ builder.AddTool(mcp.NewTool("semantic_analyze_dependencies", "Analyze dependencies and relationships between modules, files, or symbols", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "path": map[string]interface{}{
+ "type": "string",
+ "description": "Path to analyze (file, directory, or project root)",
+ },
+ "analysis_type": map[string]interface{}{
+ "type": "string",
+ "description": "Type of dependency analysis to perform (default: imports)",
+ "enum": []string{"imports", "exports", "modules", "symbols", "circular"},
+ },
+ "include_external": map[string]interface{}{
+ "type": "boolean",
+ "description": "Include external/third-party dependencies (default: false)",
+ },
+ "format": map[string]interface{}{
+ "type": "string",
+ "description": "Output format (tree, graph, list) (default: tree)",
+ "enum": []string{"tree", "graph", "list"},
+ },
+ "max_depth": map[string]interface{}{
+ "type": "integer",
+ "description": "Maximum depth of dependency analysis (default: 5)",
+ "minimum": 1,
+ "maximum": 20,
},
},
- }
-}
+ "required": []string{"path"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return semantic.handleAnalyzeDependencies(req)
+ }))
-// registerTools registers all semantic analysis tools with the server
-func (s *Server) registerTools() {
- // Get all tool definitions from ListTools method
- tools := s.ListTools()
-
- // Register each tool with its proper definition
- for _, tool := range tools {
- var handler mcp.ToolHandler
- switch tool.Name {
- case "semantic_find_symbol":
- handler = s.handleFindSymbol
- case "semantic_get_overview":
- handler = s.handleGetOverview
- case "semantic_get_definition":
- handler = s.handleGetDefinition
- case "semantic_get_references":
- handler = s.handleGetReferences
- case "semantic_get_call_hierarchy":
- handler = s.handleGetCallHierarchy
- case "semantic_analyze_dependencies":
- handler = s.handleAnalyzeDependencies
- default:
- continue
- }
- s.RegisterToolWithDefinition(tool, handler)
- }
+ return builder.Build(), nil
}
+
// handleFindSymbol finds symbols by name, type, or pattern across the project
-func (s *Server) handleFindSymbol(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
+func (semantic *SemanticOperations) handleFindSymbol(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ semantic.mu.RLock()
+ defer semantic.mu.RUnlock()
var args struct {
Name string `json:"name"` // Symbol path or pattern
@@ -307,7 +277,7 @@ func (s *Server) handleFindSymbol(req mcp.CallToolRequest) (mcp.CallToolResult,
}
// Find symbols
- symbols, err := s.symbolManager.FindSymbols(query)
+ symbols, err := semantic.symbolManager.FindSymbols(query)
if err != nil {
return mcp.CallToolResult{}, fmt.Errorf("failed to find symbols: %w", err)
}
@@ -333,9 +303,9 @@ func (s *Server) handleFindSymbol(req mcp.CallToolRequest) (mcp.CallToolResult,
}
// handleGetOverview gets high-level symbol overview of files or directories
-func (s *Server) handleGetOverview(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
+func (semantic *SemanticOperations) handleGetOverview(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ semantic.mu.RLock()
+ defer semantic.mu.RUnlock()
var args struct {
Path string `json:"path"` // File or directory path
@@ -359,7 +329,7 @@ func (s *Server) handleGetOverview(req mcp.CallToolRequest) (mcp.CallToolResult,
}
// Get overview
- overview, err := s.symbolManager.GetOverview(args.Path, args.Depth, args.IncludeKinds, args.ExcludePrivate)
+ overview, err := semantic.symbolManager.GetOverview(args.Path, args.Depth, args.IncludeKinds, args.ExcludePrivate)
if err != nil {
return mcp.CallToolResult{}, fmt.Errorf("failed to get overview: %w", err)
}
@@ -378,9 +348,9 @@ func (s *Server) handleGetOverview(req mcp.CallToolRequest) (mcp.CallToolResult,
}
// handleGetDefinition gets detailed information about a symbol's definition
-func (s *Server) handleGetDefinition(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
+func (semantic *SemanticOperations) handleGetDefinition(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ semantic.mu.RLock()
+ defer semantic.mu.RUnlock()
var args struct {
Symbol string `json:"symbol"` // Target symbol
@@ -399,7 +369,7 @@ func (s *Server) handleGetDefinition(req mcp.CallToolRequest) (mcp.CallToolResul
}
// Get definition
- definition, err := s.symbolManager.GetDefinition(args.Symbol, args.IncludeSignature, args.IncludeDocumentation, args.IncludeDependencies)
+ definition, err := semantic.symbolManager.GetDefinition(args.Symbol, args.IncludeSignature, args.IncludeDocumentation, args.IncludeDependencies)
if err != nil {
return mcp.CallToolResult{}, fmt.Errorf("failed to get definition: %w", err)
}
@@ -418,9 +388,9 @@ func (s *Server) handleGetDefinition(req mcp.CallToolRequest) (mcp.CallToolResul
}
// handleGetReferences finds all places where a symbol is used
-func (s *Server) handleGetReferences(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
+func (semantic *SemanticOperations) handleGetReferences(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ semantic.mu.RLock()
+ defer semantic.mu.RUnlock()
var args struct {
Symbol string `json:"symbol"` // Target symbol
@@ -446,7 +416,7 @@ func (s *Server) handleGetReferences(req mcp.CallToolRequest) (mcp.CallToolResul
}
// Get references
- references, err := s.symbolManager.GetReferences(args.Symbol, args.IncludeDefinitions, args.ContextLines, args.FilterByKind, args.Language, args.IncludeExternal)
+ references, err := semantic.symbolManager.GetReferences(args.Symbol, args.IncludeDefinitions, args.ContextLines, args.FilterByKind, args.Language, args.IncludeExternal)
if err != nil {
return mcp.CallToolResult{}, fmt.Errorf("failed to get references: %w", err)
}
@@ -475,9 +445,9 @@ func (s *Server) handleGetReferences(req mcp.CallToolRequest) (mcp.CallToolResul
}
// handleGetCallHierarchy understands calling relationships (what calls this, what this calls)
-func (s *Server) handleGetCallHierarchy(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
+func (semantic *SemanticOperations) handleGetCallHierarchy(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ semantic.mu.RLock()
+ defer semantic.mu.RUnlock()
var args struct {
Symbol string `json:"symbol"` // Target symbol
@@ -505,7 +475,7 @@ func (s *Server) handleGetCallHierarchy(req mcp.CallToolRequest) (mcp.CallToolRe
}
// Get call hierarchy
- hierarchy, err := s.symbolManager.GetCallHierarchy(args.Symbol, args.Direction, args.MaxDepth, args.IncludeExternal, args.Language)
+ hierarchy, err := semantic.symbolManager.GetCallHierarchy(args.Symbol, args.Direction, args.MaxDepth, args.IncludeExternal, args.Language)
if err != nil {
return mcp.CallToolResult{}, fmt.Errorf("failed to get call hierarchy: %w", err)
}
@@ -525,9 +495,9 @@ func (s *Server) handleGetCallHierarchy(req mcp.CallToolRequest) (mcp.CallToolRe
}
// handleAnalyzeDependencies analyzes symbol dependencies and relationships
-func (s *Server) handleAnalyzeDependencies(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
+func (semantic *SemanticOperations) handleAnalyzeDependencies(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ semantic.mu.RLock()
+ defer semantic.mu.RUnlock()
var args struct {
Scope string `json:"scope"` // Analysis scope: "file", "package", "project"
@@ -551,7 +521,7 @@ func (s *Server) handleAnalyzeDependencies(req mcp.CallToolRequest) (mcp.CallToo
}
// Analyze dependencies
- analysis, err := s.symbolManager.AnalyzeDependencies(args.Scope, args.Path, args.IncludeExternal, args.GroupBy, args.ShowUnused, args.Language)
+ analysis, err := semantic.symbolManager.AnalyzeDependencies(args.Scope, args.Path, args.IncludeExternal, args.GroupBy, args.ShowUnused, args.Language)
if err != nil {
return mcp.CallToolResult{}, fmt.Errorf("failed to analyze dependencies: %w", err)
}
@@ -571,18 +541,18 @@ func (s *Server) handleAnalyzeDependencies(req mcp.CallToolRequest) (mcp.CallToo
}
// Shutdown gracefully shuts down the semantic server
-func (s *Server) Shutdown() error {
- s.mu.Lock()
- defer s.mu.Unlock()
+func (semantic *SemanticOperations) Shutdown() error {
+ semantic.mu.Lock()
+ defer semantic.mu.Unlock()
- if s.lspManager != nil {
- if err := s.lspManager.Shutdown(); err != nil {
+ if semantic.lspManager != nil {
+ if err := semantic.lspManager.Shutdown(); err != nil {
return fmt.Errorf("failed to shutdown LSP manager: %w", err)
}
}
- if s.projectManager != nil {
- if err := s.projectManager.Shutdown(); err != nil {
+ if semantic.projectManager != nil {
+ if err := semantic.projectManager.Shutdown(); err != nil {
return fmt.Errorf("failed to shutdown project manager: %w", err)
}
}
pkg/semantic/server_test.go
@@ -1,428 +0,0 @@
-package semantic
-
-import (
- "encoding/json"
- "testing"
-
- "github.com/xlgmokha/mcp/pkg/mcp"
-)
-
-func TestNewServer(t *testing.T) {
- server := NewServer()
- if server == nil {
- t.Fatal("NewServer() returned nil")
- }
-
- // Test that server was created with base MCP server
- if server.Server == nil {
- t.Fatal("Server.Server is nil")
- }
-
- // Test that LSP manager was created
- if server.lspManager == nil {
- t.Fatal("Server.lspManager is nil")
- }
-
- // Test that symbol manager was created
- if server.symbolManager == nil {
- t.Fatal("Server.symbolManager is nil")
- }
-
- // Test that project manager was created
- if server.projectManager == nil {
- t.Fatal("Server.projectManager is nil")
- }
-}
-
-func TestSemanticFindSymbol_InvalidArgs(t *testing.T) {
- server := NewServer()
-
- // Test with empty name
- req := mcp.CallToolRequest{
- Name: "semantic_find_symbol",
- Arguments: map[string]interface{}{
- "name": "",
- },
- }
-
- result, err := server.handleFindSymbol(req)
- if err == nil {
- t.Error("Expected error for empty name, got nil")
- }
-
- if len(result.Content) > 0 {
- t.Error("Expected no content on error")
- }
-}
-
-func TestSemanticFindSymbol_ValidArgs(t *testing.T) {
- server := NewServer()
-
- req := mcp.CallToolRequest{
- Name: "semantic_find_symbol",
- Arguments: map[string]interface{}{
- "name": "main",
- "kind": "function",
- "language": "go",
- },
- }
-
- result, err := server.handleFindSymbol(req)
-
- // For now, we expect this to work without error (even if no symbols found)
- // because we haven't initialized a real project yet
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- }
-
- if len(result.Content) == 0 {
- t.Error("Expected some content in result")
- }
-}
-
-func TestSemanticGetOverview_InvalidArgs(t *testing.T) {
- server := NewServer()
-
- // Test with empty path
- req := mcp.CallToolRequest{
- Name: "semantic_get_overview",
- Arguments: map[string]interface{}{
- "path": "",
- },
- }
-
- result, err := server.handleGetOverview(req)
- if err == nil {
- t.Error("Expected error for empty path, got nil")
- }
-
- if len(result.Content) > 0 {
- t.Error("Expected no content on error")
- }
-}
-
-func TestSemanticGetDefinition_InvalidArgs(t *testing.T) {
- server := NewServer()
-
- // Test with empty symbol
- req := mcp.CallToolRequest{
- Name: "semantic_get_definition",
- Arguments: map[string]interface{}{
- "symbol": "",
- },
- }
-
- result, err := server.handleGetDefinition(req)
- if err == nil {
- t.Error("Expected error for empty symbol, got nil")
- }
-
- if len(result.Content) > 0 {
- t.Error("Expected no content on error")
- }
-}
-
-func TestSymbolQuery_JSONSerialization(t *testing.T) {
- query := SymbolQuery{
- Name: "test",
- Kind: SymbolKindFunction,
- Scope: "project",
- Language: "go",
- IncludeChildren: true,
- MaxResults: 50,
- }
-
- data, err := json.Marshal(query)
- if err != nil {
- t.Fatalf("Failed to marshal SymbolQuery: %v", err)
- }
-
- var decoded SymbolQuery
- if err := json.Unmarshal(data, &decoded); err != nil {
- t.Fatalf("Failed to unmarshal SymbolQuery: %v", err)
- }
-
- if decoded.Name != query.Name {
- t.Errorf("Name mismatch: got %s, want %s", decoded.Name, query.Name)
- }
-
- if decoded.Kind != query.Kind {
- t.Errorf("Kind mismatch: got %s, want %s", decoded.Kind, query.Kind)
- }
-}
-
-func TestSymbol_JSONSerialization(t *testing.T) {
- symbol := Symbol{
- Name: "testFunction",
- FullPath: "package.testFunction",
- Kind: SymbolKindFunction,
- Location: SourceLocation{
- FilePath: "/test/file.go",
- Line: 10,
- Column: 5,
- },
- Language: "go",
- Visibility: "public",
- }
-
- data, err := json.Marshal(symbol)
- if err != nil {
- t.Fatalf("Failed to marshal Symbol: %v", err)
- }
-
- var decoded Symbol
- if err := json.Unmarshal(data, &decoded); err != nil {
- t.Fatalf("Failed to unmarshal Symbol: %v", err)
- }
-
- if decoded.Name != symbol.Name {
- t.Errorf("Name mismatch: got %s, want %s", decoded.Name, symbol.Name)
- }
-
- if decoded.Location.Line != symbol.Location.Line {
- t.Errorf("Line mismatch: got %d, want %d", decoded.Location.Line, symbol.Location.Line)
- }
-}
-
-func TestSemanticGetReferences_InvalidArgs(t *testing.T) {
- server := NewServer()
-
- // Test with empty symbol
- req := mcp.CallToolRequest{
- Name: "semantic_get_references",
- Arguments: map[string]interface{}{
- "symbol": "",
- },
- }
-
- result, err := server.handleGetReferences(req)
- if err == nil {
- t.Error("Expected error for empty symbol, got nil")
- }
-
- if len(result.Content) > 0 {
- t.Error("Expected no content on error")
- }
-}
-
-func TestSemanticGetReferences_ValidArgs(t *testing.T) {
- server := NewServer()
-
- req := mcp.CallToolRequest{
- Name: "semantic_get_references",
- Arguments: map[string]interface{}{
- "symbol": "main",
- "context_lines": 3,
- },
- }
-
- result, err := server.handleGetReferences(req)
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- }
-
- if len(result.Content) == 0 {
- t.Error("Expected some content in result")
- }
-}
-
-func TestSemanticGetCallHierarchy_InvalidArgs(t *testing.T) {
- server := NewServer()
-
- // Test with empty symbol
- req := mcp.CallToolRequest{
- Name: "semantic_get_call_hierarchy",
- Arguments: map[string]interface{}{
- "symbol": "",
- },
- }
-
- result, err := server.handleGetCallHierarchy(req)
- if err == nil {
- t.Error("Expected error for empty symbol, got nil")
- }
-
- if len(result.Content) > 0 {
- t.Error("Expected no content on error")
- }
-}
-
-func TestSemanticAnalyzeDependencies_ValidArgs(t *testing.T) {
- server := NewServer()
-
- req := mcp.CallToolRequest{
- Name: "semantic_analyze_dependencies",
- Arguments: map[string]interface{}{
- "scope": "project",
- "group_by": "package",
- },
- }
-
- result, err := server.handleAnalyzeDependencies(req)
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- }
-
- if len(result.Content) == 0 {
- t.Error("Expected some content in result")
- }
-}
-
-func TestSymbolReference_JSONSerialization(t *testing.T) {
- ref := SymbolReference{
- Location: SourceLocation{
- FilePath: "/test/file.go",
- Line: 10,
- Column: 5,
- },
- Context: " 10: func main() {",
- Kind: "call",
- Symbol: "main",
- }
-
- data, err := json.Marshal(ref)
- if err != nil {
- t.Fatalf("Failed to marshal SymbolReference: %v", err)
- }
-
- var decoded SymbolReference
- if err := json.Unmarshal(data, &decoded); err != nil {
- t.Fatalf("Failed to unmarshal SymbolReference: %v", err)
- }
-
- if decoded.Symbol != ref.Symbol {
- t.Errorf("Symbol mismatch: got %s, want %s", decoded.Symbol, ref.Symbol)
- }
-
- if decoded.Location.Line != ref.Location.Line {
- t.Errorf("Line mismatch: got %d, want %d", decoded.Location.Line, ref.Location.Line)
- }
-}
-
-func TestCallHierarchy_JSONSerialization(t *testing.T) {
- hierarchy := CallHierarchy{
- Symbol: "main",
- Direction: "both",
- MaxDepth: 3,
- TotalItems: 5,
- Root: CallHierarchyItem{
- Symbol: "main",
- Name: "main",
- Kind: SymbolKindFunction,
- Depth: 0,
- },
- }
-
- data, err := json.Marshal(hierarchy)
- if err != nil {
- t.Fatalf("Failed to marshal CallHierarchy: %v", err)
- }
-
- var decoded CallHierarchy
- if err := json.Unmarshal(data, &decoded); err != nil {
- t.Fatalf("Failed to unmarshal CallHierarchy: %v", err)
- }
-
- if decoded.Symbol != hierarchy.Symbol {
- t.Errorf("Symbol mismatch: got %s, want %s", decoded.Symbol, hierarchy.Symbol)
- }
-
- if decoded.TotalItems != hierarchy.TotalItems {
- t.Errorf("TotalItems mismatch: got %d, want %d", decoded.TotalItems, hierarchy.TotalItems)
- }
-}
-
-func TestServerShutdown(t *testing.T) {
- server := NewServer()
-
- // Test shutdown
- if err := server.Shutdown(); err != nil {
- t.Errorf("Shutdown failed: %v", err)
- }
-
- // Test that we can shutdown multiple times without error
- if err := server.Shutdown(); err != nil {
- t.Errorf("Second shutdown failed: %v", err)
- }
-}
-
-func TestSemanticGetReferences_DefaultArgs(t *testing.T) {
- server := NewServer()
-
- req := mcp.CallToolRequest{
- Name: "semantic_get_references",
- Arguments: map[string]interface{}{
- "symbol": "testFunction",
- },
- }
-
- result, err := server.handleGetReferences(req)
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- }
-
- if len(result.Content) == 0 {
- t.Error("Expected some content in result")
- }
-}
-
-func TestSemanticGetCallHierarchy_DefaultArgs(t *testing.T) {
- server := NewServer()
-
- req := mcp.CallToolRequest{
- Name: "semantic_get_call_hierarchy",
- Arguments: map[string]interface{}{
- "symbol": "testFunction",
- },
- }
-
- result, err := server.handleGetCallHierarchy(req)
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- }
-
- if len(result.Content) == 0 {
- t.Error("Expected some content in result")
- }
-}
-
-func TestSemanticAnalyzeDependencies_EmptyScope(t *testing.T) {
- server := NewServer()
-
- req := mcp.CallToolRequest{
- Name: "semantic_analyze_dependencies",
- Arguments: map[string]interface{}{
- "scope": "",
- },
- }
-
- result, err := server.handleAnalyzeDependencies(req)
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- }
-
- if len(result.Content) == 0 {
- t.Error("Expected some content in result (should default to project scope)")
- }
-}
-
-func TestSemanticAnalyzeDependencies_InvalidGroupBy(t *testing.T) {
- server := NewServer()
-
- req := mcp.CallToolRequest{
- Name: "semantic_analyze_dependencies",
- Arguments: map[string]interface{}{
- "scope": "project",
- "group_by": "invalid_group",
- },
- }
-
- result, err := server.handleAnalyzeDependencies(req)
- if err != nil {
- t.Errorf("Unexpected error for unknown group_by: %v", err)
- }
-
- if len(result.Content) == 0 {
- t.Error("Expected some content in result")
- }
-}
\ No newline at end of file
pkg/signal/server.go
@@ -23,8 +23,8 @@ import (
"golang.org/x/crypto/pbkdf2"
)
-type Server struct {
- *mcp.Server
+// SignalOperations provides Signal Desktop database operations
+type SignalOperations struct {
mu sync.RWMutex
dbPath string
configPath string
@@ -62,195 +62,163 @@ type Attachment struct {
Size int64 `json:"size,omitempty"`
}
-func NewServer(signalPath string) (*Server, error) {
- baseServer := mcp.NewServer("signal", "0.1.0", []mcp.Tool{}, []mcp.Resource{}, []mcp.Root{})
-
- server := &Server{
- Server: baseServer,
- }
+// NewSignalOperations creates a new SignalOperations helper
+func NewSignalOperations(signalPath string) (*SignalOperations, error) {
+ signal := &SignalOperations{}
// Determine Signal database and config paths
if signalPath != "" {
- server.dbPath = filepath.Join(signalPath, "sql", "db.sqlite")
- server.configPath = filepath.Join(signalPath, "config.json")
+ signal.dbPath = filepath.Join(signalPath, "sql", "db.sqlite")
+ signal.configPath = filepath.Join(signalPath, "config.json")
} else {
var err error
- server.dbPath, server.configPath, err = findSignalPaths()
+ signal.dbPath, signal.configPath, err = findSignalPaths()
if err != nil {
return nil, fmt.Errorf("failed to find Signal paths: %w", err)
}
}
- // Register tools
- server.registerTools()
-
- // Register prompts
- conversationPrompt := mcp.Prompt{
- Name: "signal-conversation",
- Description: "Analyze Signal conversation history for insights",
- }
- searchPrompt := mcp.Prompt{
- Name: "signal-search",
- Description: "Search Signal messages with context",
- }
- server.RegisterPrompt(conversationPrompt, server.handleConversationPrompt)
- server.RegisterPrompt(searchPrompt, server.handleSearchPrompt)
-
- return server, nil
+ return signal, nil
}
-// ListTools returns all available Signal tools
-func (s *Server) ListTools() []mcp.Tool {
- return []mcp.Tool{
- {
- Name: "signal_list_conversations",
- Description: "List recent Signal conversations with timestamps and participants",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "limit": map[string]interface{}{
- "type": "integer",
- "description": "Maximum number of conversations to return",
- "minimum": 1,
- "default": 20,
- },
- },
+// New creates a new Signal MCP server
+func New(signalPath string) (*mcp.Server, error) {
+ signal, err := NewSignalOperations(signalPath)
+ if err != nil {
+ return nil, err
+ }
+
+ builder := mcp.NewServerBuilder("signal-server", "1.0.0")
+
+ // Add signal_list_conversations tool
+ builder.AddTool(mcp.NewTool("signal_list_conversations", "List recent Signal conversations with timestamps and participants", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "limit": map[string]interface{}{
+ "type": "integer",
+ "description": "Maximum number of conversations to return",
+ "minimum": 1,
+ "default": 20,
},
},
- {
- Name: "signal_search_messages",
- Description: "Search Signal messages by content with filtering options",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "query": map[string]interface{}{
- "type": "string",
- "description": "Search query string",
- },
- "conversation_id": map[string]interface{}{
- "type": "string",
- "description": "Optional conversation ID to limit search scope",
- },
- "limit": map[string]interface{}{
- "type": "integer",
- "description": "Maximum number of results",
- "minimum": 1,
- "default": 50,
- },
- },
- "required": []string{"query"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return signal.handleListConversations(req)
+ }))
+
+ // Add signal_search_messages tool
+ builder.AddTool(mcp.NewTool("signal_search_messages", "Search Signal messages by content with filtering options", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "query": map[string]interface{}{
+ "type": "string",
+ "description": "Search query string",
},
- },
- {
- Name: "signal_get_conversation",
- Description: "Get detailed conversation history including all messages and metadata",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "conversation_id": map[string]interface{}{
- "type": "string",
- "description": "The conversation ID to retrieve",
- },
- "limit": map[string]interface{}{
- "type": "integer",
- "description": "Maximum number of messages to return",
- "minimum": 1,
- "default": 100,
- },
- },
- "required": []string{"conversation_id"},
+ "conversation_id": map[string]interface{}{
+ "type": "string",
+ "description": "Optional conversation ID to limit search scope",
+ },
+ "limit": map[string]interface{}{
+ "type": "integer",
+ "description": "Maximum number of results",
+ "minimum": 1,
+ "default": 50,
},
},
- {
- Name: "signal_get_contact",
- Description: "Get detailed contact information by ID, phone number, or name",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "contact_id": map[string]interface{}{
- "type": "string",
- "description": "Contact ID, phone number, or display name",
- },
- },
- "required": []string{"contact_id"},
+ "required": []string{"query"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return signal.handleSearchMessages(req)
+ }))
+
+ // Add signal_get_conversation tool
+ builder.AddTool(mcp.NewTool("signal_get_conversation", "Get detailed conversation history including all messages and metadata", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "conversation_id": map[string]interface{}{
+ "type": "string",
+ "description": "The conversation ID to retrieve",
+ },
+ "limit": map[string]interface{}{
+ "type": "integer",
+ "description": "Maximum number of messages to return",
+ "minimum": 1,
+ "default": 100,
},
},
- {
- Name: "signal_get_message",
- Description: "Get detailed information about a specific message including attachments and reactions",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "message_id": map[string]interface{}{
- "type": "string",
- "description": "The message ID to retrieve",
- },
- },
- "required": []string{"message_id"},
+ "required": []string{"conversation_id"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return signal.handleGetConversation(req)
+ }))
+
+ // Add signal_get_contact tool
+ builder.AddTool(mcp.NewTool("signal_get_contact", "Get detailed contact information by ID, phone number, or name", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "contact_id": map[string]interface{}{
+ "type": "string",
+ "description": "Contact ID, phone number, or display name",
},
},
- {
- Name: "signal_list_attachments",
- Description: "List message attachments with metadata and filtering options",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "conversation_id": map[string]interface{}{
- "type": "string",
- "description": "Optional conversation ID to filter attachments",
- },
- "media_type": map[string]interface{}{
- "type": "string",
- "description": "Filter by media type (image, video, audio, file)",
- },
- "limit": map[string]interface{}{
- "type": "integer",
- "description": "Maximum number of attachments to return",
- "minimum": 1,
- "default": 50,
- },
- },
+ "required": []string{"contact_id"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return signal.handleGetContact(req)
+ }))
+
+ // Add signal_get_message tool
+ builder.AddTool(mcp.NewTool("signal_get_message", "Get detailed information about a specific message including attachments and reactions", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "message_id": map[string]interface{}{
+ "type": "string",
+ "description": "The message ID to retrieve",
},
},
- {
- Name: "signal_get_stats",
- Description: "Get Signal database statistics and connection information",
- InputSchema: map[string]interface{}{
- "type": "object",
+ "required": []string{"message_id"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return signal.handleGetMessage(req)
+ }))
+
+ // Add signal_list_attachments tool
+ builder.AddTool(mcp.NewTool("signal_list_attachments", "List message attachments with metadata and filtering options", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "conversation_id": map[string]interface{}{
+ "type": "string",
+ "description": "Optional conversation ID to filter attachments",
+ },
+ "media_type": map[string]interface{}{
+ "type": "string",
+ "description": "Filter by media type (image, video, audio, file)",
+ },
+ "limit": map[string]interface{}{
+ "type": "integer",
+ "description": "Maximum number of attachments to return",
+ "minimum": 1,
+ "default": 50,
},
},
- }
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return signal.handleListAttachments(req)
+ }))
+
+ // Add signal_get_stats tool
+ builder.AddTool(mcp.NewTool("signal_get_stats", "Get Signal database statistics and connection information", map[string]interface{}{
+ "type": "object",
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return signal.handleGetStats(req)
+ }))
+
+ // Add prompts
+ builder.AddPrompt(mcp.NewPrompt("signal-conversation", "Analyze Signal conversation history for insights", []mcp.PromptArgument{}, func(req mcp.GetPromptRequest) (mcp.GetPromptResult, error) {
+ return signal.handleConversationPrompt(req)
+ }))
+
+ builder.AddPrompt(mcp.NewPrompt("signal-search", "Search Signal messages with context", []mcp.PromptArgument{}, func(req mcp.GetPromptRequest) (mcp.GetPromptResult, error) {
+ return signal.handleSearchPrompt(req)
+ }))
+
+ return builder.Build(), nil
}
-// registerTools registers all Signal tools with the server
-func (s *Server) registerTools() {
- // Get all tool definitions from ListTools method
- tools := s.ListTools()
-
- // Register each tool with its proper definition
- for _, tool := range tools {
- var handler mcp.ToolHandler
- switch tool.Name {
- case "signal_list_conversations":
- handler = s.handleListConversations
- case "signal_search_messages":
- handler = s.handleSearchMessages
- case "signal_get_conversation":
- handler = s.handleGetConversation
- case "signal_get_contact":
- handler = s.handleGetContact
- case "signal_get_message":
- handler = s.handleGetMessage
- case "signal_list_attachments":
- handler = s.handleListAttachments
- case "signal_get_stats":
- handler = s.handleGetStats
- default:
- continue
- }
- s.RegisterToolWithDefinition(tool, handler)
- }
-}
func findSignalPaths() (dbPath, configPath string, err error) {
var basePath string
@@ -292,14 +260,14 @@ func findSignalPaths() (dbPath, configPath string, err error) {
return dbPath, configPath, nil
}
-func (s *Server) ensureConnection() error {
+func (signal *SignalOperations) ensureConnection() error {
// Skip connection since we'll use command-line sqlcipher
return nil
}
-func (s *Server) executeQuery(query string) ([]byte, error) {
+func (signal *SignalOperations) executeQuery(query string) ([]byte, error) {
// Get decryption key - use the same method as working implementation
- key, err := s.getDecryptedSignalKey()
+ key, err := signal.getDecryptedSignalKey()
if err != nil {
return nil, fmt.Errorf("failed to get Signal key: %w", err)
}
@@ -317,7 +285,7 @@ PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA512;
`, key, query)
// Execute sqlcipher with the script
- cmd := exec.Command("sqlcipher", s.dbPath)
+ cmd := exec.Command("sqlcipher", signal.dbPath)
cmd.Stdin = strings.NewReader(sqlScript)
output, err := cmd.CombinedOutput()
if err != nil {
@@ -351,9 +319,9 @@ PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA512;
return []byte("[]"), nil
}
-func (s *Server) getDecryptedSignalKey() (string, error) {
+func (signal *SignalOperations) getDecryptedSignalKey() (string, error) {
// Read config.json
- configData, err := os.ReadFile(s.configPath)
+ configData, err := os.ReadFile(signal.configPath)
if err != nil {
return "", fmt.Errorf("failed to read config: %w", err)
}
@@ -368,24 +336,24 @@ func (s *Server) getDecryptedSignalKey() (string, error) {
}
if config.EncryptedKey != "" {
- return s.decryptSignalKey(config.EncryptedKey)
+ return signal.decryptSignalKey(config.EncryptedKey)
}
return "", fmt.Errorf("no encryption key found in config")
}
-func (s *Server) decryptSignalKey(encryptedKey string) (string, error) {
+func (signal *SignalOperations) decryptSignalKey(encryptedKey string) (string, error) {
switch runtime.GOOS {
case "darwin":
- return s.decryptKeyMacOS(encryptedKey)
+ return signal.decryptKeyMacOS(encryptedKey)
case "linux":
- return s.decryptKeyLinux(encryptedKey)
+ return signal.decryptKeyLinux(encryptedKey)
default:
return "", fmt.Errorf("key decryption not supported on %s", runtime.GOOS)
}
}
-func (s *Server) decryptKeyMacOS(encryptedKey string) (string, error) {
+func (signal *SignalOperations) decryptKeyMacOS(encryptedKey string) (string, error) {
// Get password from macOS Keychain using security command
cmd := exec.Command("security", "find-generic-password", "-ws", "Signal Safe Storage")
output, err := cmd.Output()
@@ -400,7 +368,7 @@ func (s *Server) decryptKeyMacOS(encryptedKey string) (string, error) {
}
// Decrypt the key using the password
- key, err := s.decryptWithPassword(password, encryptedKey, "v10", 1003)
+ key, err := signal.decryptWithPassword(password, encryptedKey, "v10", 1003)
if err != nil {
return "", fmt.Errorf("failed to decrypt key: %w", err)
}
@@ -408,13 +376,13 @@ func (s *Server) decryptKeyMacOS(encryptedKey string) (string, error) {
return key, nil
}
-func (s *Server) decryptKeyLinux(encryptedKey string) (string, error) {
+func (signal *SignalOperations) decryptKeyLinux(encryptedKey string) (string, error) {
// Try secret-tool first
cmd := exec.Command("secret-tool", "lookup", "application", "Signal")
if output, err := cmd.Output(); err == nil {
password := strings.TrimSpace(string(output))
if password != "" {
- return s.decryptWithPassword(password, encryptedKey, "v11", 1)
+ return signal.decryptWithPassword(password, encryptedKey, "v11", 1)
}
}
@@ -422,7 +390,7 @@ func (s *Server) decryptKeyLinux(encryptedKey string) (string, error) {
return "", fmt.Errorf("failed to decrypt key on Linux")
}
-func (s *Server) convertKeyToHex(key string) (string, error) {
+func (signal *SignalOperations) convertKeyToHex(key string) (string, error) {
// Signal stores keys in base64 format in the keychain
// SQLCipher expects hex format with x'...' syntax
keyBytes, err := base64.StdEncoding.DecodeString(key)
@@ -433,7 +401,7 @@ func (s *Server) convertKeyToHex(key string) (string, error) {
return hex.EncodeToString(keyBytes), nil
}
-func (s *Server) decryptWithPassword(password, encryptedKey, prefix string, iterations int) (string, error) {
+func (signal *SignalOperations) decryptWithPassword(password, encryptedKey, prefix string, iterations int) (string, error) {
// Decode hex key
encryptedKeyBytes, err := hex.DecodeString(encryptedKey)
if err != nil {
@@ -476,7 +444,7 @@ func (s *Server) decryptWithPassword(password, encryptedKey, prefix string, iter
mode.CryptBlocks(decrypted, encryptedKeyBytes)
// Remove PKCS7 padding
- decrypted, err = s.removePKCS7Padding(decrypted)
+ decrypted, err = signal.removePKCS7Padding(decrypted)
if err != nil {
return "", fmt.Errorf("failed to remove padding: %w", err)
}
@@ -487,7 +455,7 @@ func (s *Server) decryptWithPassword(password, encryptedKey, prefix string, iter
return result, nil
}
-func (s *Server) removePKCS7Padding(data []byte) ([]byte, error) {
+func (signal *SignalOperations) removePKCS7Padding(data []byte) ([]byte, error) {
if len(data) == 0 {
return nil, fmt.Errorf("empty data")
}
@@ -507,8 +475,8 @@ func (s *Server) removePKCS7Padding(data []byte) ([]byte, error) {
return data[:len(data)-padLen], nil
}
-func (s *Server) handleListConversations(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- if err := s.ensureConnection(); err != nil {
+func (signal *SignalOperations) handleListConversations(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := signal.ensureConnection(); err != nil {
return mcp.NewToolError(fmt.Sprintf("Database connection failed: %v", err)), nil
}
@@ -519,7 +487,7 @@ func (s *Server) handleListConversations(req mcp.CallToolRequest) (mcp.CallToolR
ORDER BY active_at DESC
LIMIT 10`
- output, err := s.executeQuery(query)
+ output, err := signal.executeQuery(query)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("Query failed: %v", err)), nil
}
@@ -551,7 +519,7 @@ func (s *Server) handleListConversations(req mcp.CallToolRequest) (mcp.CallToolR
return mcp.NewToolResult(mcp.NewTextContent(result)), nil
}
-func (s *Server) handleSearchMessages(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (signal *SignalOperations) handleSearchMessages(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
args := req.Arguments
searchTerm, ok := args["query"].(string)
if !ok || searchTerm == "" {
@@ -576,7 +544,7 @@ func (s *Server) handleSearchMessages(req mcp.CallToolRequest) (mcp.CallToolResu
ORDER BY m.sent_at DESC
LIMIT ` + strconv.Itoa(limit)
- output, err := s.executeQuery(query)
+ output, err := signal.executeQuery(query)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("Search failed: %v", err)), nil
}
@@ -610,7 +578,7 @@ func (s *Server) handleSearchMessages(req mcp.CallToolRequest) (mcp.CallToolResu
return mcp.NewToolResult(mcp.NewTextContent(result)), nil
}
-func (s *Server) handleGetConversation(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (signal *SignalOperations) handleGetConversation(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
args := req.Arguments
conversationID, ok := args["conversation_id"].(string)
if !ok || conversationID == "" {
@@ -633,7 +601,7 @@ func (s *Server) handleGetConversation(req mcp.CallToolRequest) (mcp.CallToolRes
ORDER BY m.sent_at DESC
LIMIT ` + strconv.Itoa(limit)
- output, err := s.executeQuery(query)
+ output, err := signal.executeQuery(query)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("Query failed: %v", err)), nil
}
@@ -674,7 +642,7 @@ func (s *Server) handleGetConversation(req mcp.CallToolRequest) (mcp.CallToolRes
return mcp.NewToolResult(mcp.NewTextContent(result)), nil
}
-func (s *Server) handleGetContact(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (signal *SignalOperations) handleGetContact(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
args := req.Arguments
contactID, ok := args["contact_id"].(string)
if !ok || contactID == "" {
@@ -687,7 +655,7 @@ func (s *Server) handleGetContact(req mcp.CallToolRequest) (mcp.CallToolResult,
WHERE id = '` + contactID + `' OR e164 = '` + contactID + `' OR name = '` + contactID + `' OR profileName = '` + contactID + `'
LIMIT 1`
- output, err := s.executeQuery(query)
+ output, err := signal.executeQuery(query)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("Query failed: %v", err)), nil
}
@@ -710,7 +678,7 @@ func (s *Server) handleGetContact(req mcp.CallToolRequest) (mcp.CallToolResult,
// Get message count with this contact
messageCountQuery := `SELECT COUNT(*) as message_count FROM messages WHERE conversationId = '` + fmt.Sprintf("%v", contact["id"]) + `' AND type NOT IN ('keychange', 'profile-change') AND type IS NOT NULL`
- countOutput, err := s.executeQuery(messageCountQuery)
+ countOutput, err := signal.executeQuery(messageCountQuery)
var messageCount int64 = 0
if err == nil {
var countResult []map[string]interface{}
@@ -744,7 +712,7 @@ func (s *Server) handleGetContact(req mcp.CallToolRequest) (mcp.CallToolResult,
return mcp.NewToolResult(mcp.NewTextContent(result)), nil
}
-func (s *Server) handleGetMessage(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (signal *SignalOperations) handleGetMessage(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
args := req.Arguments
messageID, ok := args["message_id"].(string)
if !ok || messageID == "" {
@@ -758,7 +726,7 @@ func (s *Server) handleGetMessage(req mcp.CallToolRequest) (mcp.CallToolResult,
WHERE m.id = '` + messageID + `'
LIMIT 1`
- output, err := s.executeQuery(query)
+ output, err := signal.executeQuery(query)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("Query failed: %v", err)), nil
}
@@ -842,7 +810,7 @@ func (s *Server) handleGetMessage(req mcp.CallToolRequest) (mcp.CallToolResult,
return mcp.NewToolResult(mcp.NewTextContent(result)), nil
}
-func (s *Server) handleListAttachments(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (signal *SignalOperations) handleListAttachments(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
args := req.Arguments
// Optional conversation filter
@@ -872,7 +840,7 @@ func (s *Server) handleListAttachments(req mcp.CallToolRequest) (mcp.CallToolRes
query += ` ORDER BY m.sent_at DESC LIMIT ` + strconv.Itoa(limit)
- output, err := s.executeQuery(query)
+ output, err := signal.executeQuery(query)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("Query failed: %v", err)), nil
}
@@ -963,10 +931,10 @@ func (s *Server) handleListAttachments(req mcp.CallToolRequest) (mcp.CallToolRes
return mcp.NewToolResult(mcp.NewTextContent(result)), nil
}
-func (s *Server) handleGetStats(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (signal *SignalOperations) handleGetStats(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
// Get message count using command-line sqlcipher
messageQuery := "SELECT COUNT(*) as count FROM messages WHERE type NOT IN ('keychange', 'profile-change') AND type IS NOT NULL"
- messageOutput, err := s.executeQuery(messageQuery)
+ messageOutput, err := signal.executeQuery(messageQuery)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("Failed to count messages: %v", err)), nil
}
@@ -981,7 +949,7 @@ func (s *Server) handleGetStats(req mcp.CallToolRequest) (mcp.CallToolResult, er
// Get conversation count using command-line sqlcipher
convQuery := "SELECT COUNT(*) as count FROM conversations WHERE type IS NOT NULL"
- convOutput, err := s.executeQuery(convQuery)
+ convOutput, err := signal.executeQuery(convQuery)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("Failed to count conversations: %v", err)), nil
}
@@ -995,12 +963,12 @@ func (s *Server) handleGetStats(req mcp.CallToolRequest) (mcp.CallToolResult, er
}
result := fmt.Sprintf("Signal Database Statistics:\n- Total Messages: %d\n- Total Conversations: %d\n- Database Path: %s",
- totalMessages, totalConversations, s.dbPath)
+ totalMessages, totalConversations, signal.dbPath)
return mcp.NewToolResult(mcp.NewTextContent(result)), nil
}
-func (s *Server) handleConversationPrompt(req mcp.GetPromptRequest) (mcp.GetPromptResult, error) {
+func (signal *SignalOperations) handleConversationPrompt(req mcp.GetPromptRequest) (mcp.GetPromptResult, error) {
// Get conversation ID from arguments
args := req.Arguments
conversationID, ok := args["conversation_id"].(string)
@@ -1018,7 +986,7 @@ func (s *Server) handleConversationPrompt(req mcp.GetPromptRequest) (mcp.GetProm
convQuery := `SELECT COALESCE(name, profileName, e164, id) as display_name, type, active_at
FROM conversations WHERE id = '` + conversationID + `' LIMIT 1`
- convOutput, err := s.executeQuery(convQuery)
+ convOutput, err := signal.executeQuery(convQuery)
if err != nil {
return mcp.GetPromptResult{}, fmt.Errorf("failed to get conversation details: %w", err)
}
@@ -1043,7 +1011,7 @@ func (s *Server) handleConversationPrompt(req mcp.GetPromptRequest) (mcp.GetProm
ORDER BY sent_at DESC
LIMIT 50`
- msgOutput, err := s.executeQuery(msgQuery)
+ msgOutput, err := signal.executeQuery(msgQuery)
if err != nil {
return mcp.GetPromptResult{}, fmt.Errorf("failed to get messages: %w", err)
}
@@ -1113,7 +1081,7 @@ func (s *Server) handleConversationPrompt(req mcp.GetPromptRequest) (mcp.GetProm
return result, nil
}
-func (s *Server) handleSearchPrompt(req mcp.GetPromptRequest) (mcp.GetPromptResult, error) {
+func (signal *SignalOperations) handleSearchPrompt(req mcp.GetPromptRequest) (mcp.GetPromptResult, error) {
// Get search parameters from arguments
args := req.Arguments
searchQuery, ok := args["query"].(string)
@@ -1142,11 +1110,11 @@ func (s *Server) handleSearchPrompt(req mcp.GetPromptRequest) (mcp.GetPromptResu
switch searchScope {
case "conversations":
- searchResults, err = s.searchConversations(searchQuery)
+ searchResults, err = signal.searchConversations(searchQuery)
case "contacts":
- searchResults, err = s.searchContacts(searchQuery)
+ searchResults, err = signal.searchContacts(searchQuery)
default: // "messages" or anything else
- searchResults, err = s.searchMessages(searchQuery, conversationID, timeRange)
+ searchResults, err = signal.searchMessages(searchQuery, conversationID, timeRange)
}
if err != nil {
@@ -1186,7 +1154,7 @@ func (s *Server) handleSearchPrompt(req mcp.GetPromptRequest) (mcp.GetPromptResu
return result, nil
}
-func (s *Server) searchMessages(query, conversationID, timeRange string) (string, error) {
+func (signal *SignalOperations) searchMessages(query, conversationID, timeRange string) (string, error) {
sqlQuery := `SELECT m.id, m.conversationId, m.body, m.sent_at,
COALESCE(c.name, c.profileName, c.e164, c.id) as conversation_name
FROM messages m
@@ -1211,7 +1179,7 @@ func (s *Server) searchMessages(query, conversationID, timeRange string) (string
sqlQuery += ` ORDER BY m.sent_at DESC LIMIT 20`
- output, err := s.executeQuery(sqlQuery)
+ output, err := signal.executeQuery(sqlQuery)
if err != nil {
return "", err
}
@@ -1242,7 +1210,7 @@ func (s *Server) searchMessages(query, conversationID, timeRange string) (string
return result.String(), nil
}
-func (s *Server) searchConversations(query string) (string, error) {
+func (signal *SignalOperations) searchConversations(query string) (string, error) {
sqlQuery := `SELECT id, COALESCE(name, profileName, e164, id) as display_name,
profileName, e164, type, active_at
FROM conversations
@@ -1251,7 +1219,7 @@ func (s *Server) searchConversations(query string) (string, error) {
ORDER BY active_at DESC
LIMIT 10`
- output, err := s.executeQuery(sqlQuery)
+ output, err := signal.executeQuery(sqlQuery)
if err != nil {
return "", err
}
@@ -1291,7 +1259,7 @@ func (s *Server) searchConversations(query string) (string, error) {
return result.String(), nil
}
-func (s *Server) searchContacts(query string) (string, error) {
+func (signal *SignalOperations) searchContacts(query string) (string, error) {
// Same as searchConversations but with different framing
- return s.searchConversations(query)
+ return signal.searchConversations(query)
}
\ No newline at end of file
pkg/speech/server.go
@@ -27,17 +27,14 @@ type Voice struct {
Details string
}
-// Server represents the Speech MCP server
-type Server struct {
- *mcp.Server
+// SpeechOperations represents the Speech MCP server operations
+type SpeechOperations struct {
mu sync.RWMutex
backend TTSBackend
}
-// NewServer creates a new Speech MCP server
-func NewServer() *Server {
- baseServer := mcp.NewServer("mcp-speech", "1.0.0", []mcp.Tool{}, []mcp.Resource{}, []mcp.Root{})
-
+// NewSpeechOperations creates a new SpeechOperations helper
+func NewSpeechOperations() *SpeechOperations {
// Select appropriate TTS backend based on OS
var backend TTSBackend
switch runtime.GOOS {
@@ -50,147 +47,120 @@ func NewServer() *Server {
backend = &UnsupportedBackend{os: runtime.GOOS}
}
- server := &Server{
- Server: baseServer,
+ return &SpeechOperations{
backend: backend,
}
-
- // Register speech tools
- server.registerTools()
-
- return server
}
-// registerTools registers all Speech tools with the server
-func (s *Server) registerTools() {
- // Get all tool definitions from ListTools method
- tools := s.ListTools()
+// New creates a new Speech MCP server
+func New() (*mcp.Server, error) {
+ speech := NewSpeechOperations()
- // Register each tool with its proper definition
- for _, tool := range tools {
- var handler mcp.ToolHandler
- switch tool.Name {
- case "say":
- handler = s.handleSay
- case "list_voices":
- handler = s.handleListVoices
- case "speak_file":
- handler = s.handleSpeakFile
- case "stop_speech":
- handler = s.handleStopSpeech
- case "speech_settings":
- handler = s.handleSpeechSettings
- default:
- continue
- }
- s.RegisterToolWithDefinition(tool, handler)
- }
-}
+ builder := mcp.NewServerBuilder("speech-server", "1.0.0")
+
+
-// ListTools returns all available Speech tools
-func (s *Server) ListTools() []mcp.Tool {
- return []mcp.Tool{
- {
- Name: "say",
- Description: "Convert text to speech using system TTS. Supports voice selection, speech rate, volume, and audio file output",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "text": map[string]interface{}{
- "type": "string",
- "description": "The text to speak",
- },
- "voice": map[string]interface{}{
- "type": "string",
- "description": "Voice to use (platform-specific). Use list_voices to see available options",
- },
- "rate": map[string]interface{}{
- "type": "integer",
- "description": "Speech rate in words per minute (80-500). macOS: 80-500, Linux: 80-450",
- "minimum": 80,
- "maximum": 500,
- },
- "volume": map[string]interface{}{
- "type": "number",
- "description": "Volume level (0.0-1.0). macOS only - Linux ignores this parameter",
- "minimum": 0.0,
- "maximum": 1.0,
- },
- "output": map[string]interface{}{
- "type": "string",
- "description": "Output audio file path. macOS: .aiff, .wav, .m4a. Linux: .wav only",
- },
- },
- "required": []string{"text"},
+ // Add say tool
+ builder.AddTool(mcp.NewTool("say", "Convert text to speech using system TTS. Supports voice selection, speech rate, volume, and audio file output", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "text": map[string]interface{}{
+ "type": "string",
+ "description": "The text to speak",
},
- },
- {
- Name: "list_voices",
- Description: "List available TTS voices on the system, optionally filtered by language",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "language": map[string]interface{}{
- "type": "string",
- "description": "Filter voices by language code (e.g., 'en', 'fr', 'de')",
- },
- },
+ "voice": map[string]interface{}{
+ "type": "string",
+ "description": "Voice to use (platform-specific). Use list_voices to see available options",
},
- },
- {
- Name: "speak_file",
- Description: "Read and speak the contents of a text file with optional line limiting",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "filepath": map[string]interface{}{
- "type": "string",
- "description": "Path to the text file to read and speak",
- },
- "voice": map[string]interface{}{
- "type": "string",
- "description": "Voice to use (platform-specific)",
- },
- "rate": map[string]interface{}{
- "type": "integer",
- "description": "Speech rate in words per minute (80-500)",
- "minimum": 80,
- "maximum": 500,
- },
- "volume": map[string]interface{}{
- "type": "number",
- "description": "Volume level (0.0-1.0). macOS only",
- "minimum": 0.0,
- "maximum": 1.0,
- },
- "max_lines": map[string]interface{}{
- "type": "integer",
- "description": "Maximum number of lines to read from the file",
- "minimum": 1,
- },
- },
- "required": []string{"filepath"},
+ "rate": map[string]interface{}{
+ "type": "integer",
+ "description": "Speech rate in words per minute (80-500). macOS: 80-500, Linux: 80-450",
+ "minimum": 80,
+ "maximum": 500,
+ },
+ "volume": map[string]interface{}{
+ "type": "number",
+ "description": "Volume level (0.0-1.0). macOS only - Linux ignores this parameter",
+ "minimum": 0.0,
+ "maximum": 1.0,
+ },
+ "output": map[string]interface{}{
+ "type": "string",
+ "description": "Output audio file path. macOS: .aiff, .wav, .m4a. Linux: .wav only",
},
},
- {
- Name: "stop_speech",
- Description: "Stop any currently playing speech synthesis",
- InputSchema: map[string]interface{}{
- "type": "object",
+ "required": []string{"text"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return speech.handleSay(req)
+ }))
+
+ // Add list_voices tool
+ builder.AddTool(mcp.NewTool("list_voices", "List available TTS voices on the system, optionally filtered by language", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "language": map[string]interface{}{
+ "type": "string",
+ "description": "Filter voices by language code (e.g., 'en', 'fr', 'de')",
},
},
- {
- Name: "speech_settings",
- Description: "Get information about the speech system including platform, backend, and usage help",
- InputSchema: map[string]interface{}{
- "type": "object",
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return speech.handleListVoices(req)
+ }))
+
+ // Add speak_file tool
+ builder.AddTool(mcp.NewTool("speak_file", "Read and speak the contents of a text file with optional line limiting", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "filepath": map[string]interface{}{
+ "type": "string",
+ "description": "Path to the text file to read and speak",
+ },
+ "voice": map[string]interface{}{
+ "type": "string",
+ "description": "Voice to use (platform-specific)",
+ },
+ "rate": map[string]interface{}{
+ "type": "integer",
+ "description": "Speech rate in words per minute (80-500)",
+ "minimum": 80,
+ "maximum": 500,
+ },
+ "volume": map[string]interface{}{
+ "type": "number",
+ "description": "Volume level (0.0-1.0). macOS only",
+ "minimum": 0.0,
+ "maximum": 1.0,
+ },
+ "max_lines": map[string]interface{}{
+ "type": "integer",
+ "description": "Maximum number of lines to read from the file",
+ "minimum": 1,
},
},
- }
+ "required": []string{"filepath"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return speech.handleSpeakFile(req)
+ }))
+
+ // Add stop_speech tool
+ builder.AddTool(mcp.NewTool("stop_speech", "Stop any currently playing speech synthesis", map[string]interface{}{
+ "type": "object",
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return speech.handleStopSpeech(req)
+ }))
+
+ // Add speech_settings tool
+ builder.AddTool(mcp.NewTool("speech_settings", "Get information about the speech system including platform, backend, and usage help", map[string]interface{}{
+ "type": "object",
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return speech.handleSpeechSettings(req)
+ }))
+
+ return builder.Build(), nil
}
// handleSay speaks the provided text using the system TTS
-func (s *Server) handleSay(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (s *SpeechOperations) handleSay(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
s.mu.RLock()
defer s.mu.RUnlock()
@@ -234,7 +204,7 @@ func (s *Server) handleSay(req mcp.CallToolRequest) (mcp.CallToolResult, error)
}
// handleListVoices lists all available system voices
-func (s *Server) handleListVoices(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (s *SpeechOperations) handleListVoices(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
s.mu.RLock()
defer s.mu.RUnlock()
@@ -289,7 +259,7 @@ func (s *Server) handleListVoices(req mcp.CallToolRequest) (mcp.CallToolResult,
}
// handleSpeakFile speaks the contents of a text file
-func (s *Server) handleSpeakFile(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (s *SpeechOperations) handleSpeakFile(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
s.mu.RLock()
defer s.mu.RUnlock()
@@ -333,7 +303,7 @@ func (s *Server) handleSpeakFile(req mcp.CallToolRequest) (mcp.CallToolResult, e
}
// handleStopSpeech stops any currently playing speech
-func (s *Server) handleStopSpeech(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (s *SpeechOperations) handleStopSpeech(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
s.mu.RLock()
defer s.mu.RUnlock()
@@ -360,7 +330,7 @@ func (s *Server) handleStopSpeech(req mcp.CallToolRequest) (mcp.CallToolResult,
}
// handleSpeechSettings provides information about speech synthesis settings
-func (s *Server) handleSpeechSettings(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (s *SpeechOperations) handleSpeechSettings(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
s.mu.RLock()
defer s.mu.RUnlock()
pkg/speech/server_test.go
@@ -1,492 +0,0 @@
-package speech
-
-import (
- "encoding/json"
- "runtime"
- "strings"
- "testing"
-
- "github.com/xlgmokha/mcp/pkg/mcp"
-)
-
-func TestNewServer(t *testing.T) {
- server := NewServer()
- if server == nil {
- t.Fatal("NewServer returned nil")
- }
-
- if server.Server == nil {
- t.Fatal("Base server is nil")
- }
-
- if server.backend == nil {
- t.Fatal("Backend is nil")
- }
-
- // Test that backend is appropriate for the OS
- backendName := server.backend.GetName()
- switch runtime.GOOS {
- case "darwin":
- if !strings.Contains(backendName, "say") {
- t.Errorf("Expected macOS say backend, got %s", backendName)
- }
- case "linux":
- if !strings.Contains(backendName, "espeak") && !strings.Contains(backendName, "not available") {
- t.Errorf("Expected Linux espeak backend or unavailable message, got %s", backendName)
- }
- default:
- if !strings.Contains(backendName, "Unsupported") {
- t.Errorf("Expected unsupported backend for %s, got %s", runtime.GOOS, backendName)
- }
- }
-}
-
-func TestHandleSayValidation(t *testing.T) {
- server := NewServer()
-
- tests := []struct {
- name string
- args map[string]interface{}
- expectError bool
- errorContains string
- }{
- {
- name: "empty text",
- args: map[string]interface{}{},
- expectError: true,
- errorContains: "text is required",
- },
- {
- name: "invalid rate too low",
- args: map[string]interface{}{
- "text": "test",
- "rate": 50,
- },
- expectError: true,
- errorContains: "rate must be between 80-500",
- },
- {
- name: "invalid rate too high",
- args: map[string]interface{}{
- "text": "test",
- "rate": 600,
- },
- expectError: true,
- errorContains: "rate must be between 80-500",
- },
- {
- name: "invalid volume too low",
- args: map[string]interface{}{
- "text": "test",
- "volume": -0.1,
- },
- expectError: true,
- errorContains: "volume must be between 0.0 and 1.0",
- },
- {
- name: "invalid volume too high",
- args: map[string]interface{}{
- "text": "test",
- "volume": 1.1,
- },
- expectError: true,
- errorContains: "volume must be between 0.0 and 1.0",
- },
- {
- name: "invalid output format",
- args: map[string]interface{}{
- "text": "test",
- "output": "test.mp3",
- },
- expectError: true,
- errorContains: "output format must be .aiff, .wav, or .m4a",
- },
- {
- name: "valid basic args",
- args: map[string]interface{}{
- "text": "Hello world",
- },
- expectError: !server.backend.IsAvailable(), // Should only work if TTS backend available
- },
- {
- name: "valid complex args",
- args: map[string]interface{}{
- "text": "Hello world",
- "voice": "en-gb", // Use generic voice name that works on both platforms
- "rate": 150,
- "volume": 0.8,
- },
- expectError: !server.backend.IsAvailable(),
- },
- {
- name: "valid output file",
- args: map[string]interface{}{
- "text": "test recording",
- "output": "/tmp/test.wav", // wav works on both platforms
- },
- expectError: !server.backend.IsAvailable(),
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- req := mcp.CallToolRequest{
- Name: "say",
- Arguments: tt.args,
- }
-
- result, err := server.handleSay(req)
-
- if tt.expectError {
- if err == nil {
- t.Errorf("Expected error but got none")
- } else if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) {
- t.Errorf("Expected error to contain %q, got %q", tt.errorContains, err.Error())
- }
- } else {
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- }
- if len(result.Content) == 0 {
- t.Errorf("Expected content in result")
- }
- }
- })
- }
-}
-
-func TestHandleListVoices(t *testing.T) {
- server := NewServer()
-
- tests := []struct {
- name string
- args map[string]interface{}
- expectError bool
- }{
- {
- name: "basic list",
- args: map[string]interface{}{},
- expectError: !server.backend.IsAvailable(),
- },
- {
- name: "with language filter",
- args: map[string]interface{}{
- "language": "en",
- },
- expectError: !server.backend.IsAvailable(),
- },
- {
- name: "detailed mode",
- args: map[string]interface{}{
- "detailed": true,
- },
- expectError: !server.backend.IsAvailable(),
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- req := mcp.CallToolRequest{
- Name: "list_voices",
- Arguments: tt.args,
- }
-
- result, err := server.handleListVoices(req)
-
- if tt.expectError {
- if err == nil {
- t.Errorf("Expected error but got none")
- }
- } else {
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- }
- if len(result.Content) == 0 {
- t.Errorf("Expected content in result")
- }
- }
- })
- }
-}
-
-func TestHandleSpeakFileValidation(t *testing.T) {
- server := NewServer()
-
- tests := []struct {
- name string
- args map[string]interface{}
- expectError bool
- errorContains string
- }{
- {
- name: "empty file path",
- args: map[string]interface{}{},
- expectError: true,
- errorContains: "file_path is required",
- },
- {
- name: "nonexistent file",
- args: map[string]interface{}{
- "file_path": "/nonexistent/file.txt",
- },
- expectError: true,
- errorContains: "failed to read file",
- },
- {
- name: "invalid rate",
- args: map[string]interface{}{
- "file_path": "/etc/passwd", // Use a file that exists
- "rate": 1000,
- },
- expectError: true,
- errorContains: "rate must be between 80-500",
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- req := mcp.CallToolRequest{
- Name: "speak_file",
- Arguments: tt.args,
- }
-
- result, err := server.handleSpeakFile(req)
-
- if tt.expectError {
- if err == nil {
- t.Errorf("Expected error but got none")
- } else if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) {
- t.Errorf("Expected error to contain %q, got %q", tt.errorContains, err.Error())
- }
- } else {
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- }
- if len(result.Content) == 0 {
- t.Errorf("Expected content in result")
- }
- }
- })
- }
-}
-
-func TestHandleStopSpeech(t *testing.T) {
- server := NewServer()
-
- req := mcp.CallToolRequest{
- Name: "stop_speech",
- Arguments: map[string]interface{}{},
- }
-
- result, err := server.handleStopSpeech(req)
-
- if !server.backend.IsAvailable() {
- if err == nil {
- t.Errorf("Expected error when TTS backend not available")
- }
- return
- }
-
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- }
-
- if len(result.Content) == 0 {
- t.Errorf("Expected content in result")
- }
-
- // Check that result contains stop message
- content := result.Content[0]
- if textContent, ok := content.(mcp.TextContent); ok {
- if !strings.Contains(textContent.Text, "Stopped") {
- t.Errorf("Expected stop message in result, got: %s", textContent.Text)
- }
- } else {
- t.Errorf("Expected TextContent, got %T", content)
- }
-}
-
-func TestHandleSpeechSettings(t *testing.T) {
- server := NewServer()
-
- req := mcp.CallToolRequest{
- Name: "speech_settings",
- Arguments: map[string]interface{}{},
- }
-
- result, err := server.handleSpeechSettings(req)
-
- // Speech settings should always work, even if backend is not available
- // (it will show installation instructions)
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- }
-
- if len(result.Content) == 0 {
- t.Errorf("Expected content in result")
- }
-
- // Check that result contains settings information
- content := result.Content[0]
- if textContent, ok := content.(mcp.TextContent); ok {
- settingsText := textContent.Text
-
- // These sections should always be present
- expectedSections := []string{
- "BACKEND:",
- "Speech Synthesis Settings",
- }
-
- for _, section := range expectedSections {
- if !strings.Contains(settingsText, section) {
- t.Errorf("Expected settings to contain section %q", section)
- }
- }
-
- // If backend is available, check for detailed sections
- if server.backend.IsAvailable() {
- availableSections := []string{
- "VOICES:",
- "RATE (Speed):",
- "VOLUME:",
- "OUTPUT FORMATS:",
- "EXAMPLES:",
- "CONTROLS:",
- }
-
- for _, section := range availableSections {
- if !strings.Contains(settingsText, section) {
- t.Errorf("Expected settings to contain section %q when backend available", section)
- }
- }
- } else {
- // If backend not available, should contain installation instructions
- if !strings.Contains(settingsText, "install") {
- t.Errorf("Expected installation instructions when backend not available")
- }
- }
- } else {
- t.Errorf("Expected TextContent, got %T", content)
- }
-}
-
-func TestJSONArguments(t *testing.T) {
- server := NewServer()
-
- // Test that arguments are properly marshaled/unmarshaled
- args := map[string]interface{}{
- "text": "Hello world",
- "voice": "Samantha",
- "rate": 150,
- "volume": 0.8,
- }
-
- // Marshal to JSON and back to simulate real request
- argsBytes, err := json.Marshal(args)
- if err != nil {
- t.Fatalf("Failed to marshal args: %v", err)
- }
-
- var unmarshaled map[string]interface{}
- if err := json.Unmarshal(argsBytes, &unmarshaled); err != nil {
- t.Fatalf("Failed to unmarshal args: %v", err)
- }
-
- req := mcp.CallToolRequest{
- Name: "say",
- Arguments: unmarshaled,
- }
-
- // This should not panic or return invalid argument errors
- _, err = server.handleSay(req)
-
- // Error is expected when backend not available, but should not be argument-related
- if err != nil && server.backend.IsAvailable() {
- // When backend is available, any error should not be about invalid arguments
- if strings.Contains(err.Error(), "invalid arguments") {
- t.Errorf("Argument parsing failed: %v", err)
- }
- }
-}
-
-func TestCrossPlatformBackendSelection(t *testing.T) {
- server := NewServer()
-
- // Test that the appropriate backend is selected for each platform
- backendName := server.backend.GetName()
-
- switch runtime.GOOS {
- case "darwin":
- if !server.backend.IsAvailable() {
- t.Errorf("macOS backend should be available (say command)")
- }
- if !strings.Contains(backendName, "say") {
- t.Errorf("Expected macOS say backend, got %s", backendName)
- }
-
- case "linux":
- // Backend availability depends on whether espeak-ng/espeak is installed
- // Test that we get the right backend name regardless
- if strings.Contains(backendName, "espeak") || strings.Contains(backendName, "not available") {
- // This is correct
- } else {
- t.Errorf("Expected Linux espeak backend or unavailable message, got %s", backendName)
- }
-
- default:
- if server.backend.IsAvailable() {
- t.Errorf("Unsupported platform should not have available backend")
- }
- if !strings.Contains(backendName, "Unsupported") {
- t.Errorf("Expected unsupported backend message, got %s", backendName)
- }
- }
-}
-
-func TestBackendUnavailableBehavior(t *testing.T) {
- server := NewServer()
-
- // If backend is not available, all speech tools should return appropriate errors
- if !server.backend.IsAvailable() {
- tools := []struct {
- name string
- args map[string]interface{}
- }{
- {"say", map[string]interface{}{"text": "test"}},
- {"list_voices", map[string]interface{}{}},
- {"speak_file", map[string]interface{}{"file_path": "/etc/passwd"}},
- {"stop_speech", map[string]interface{}{}},
- }
-
- for _, tool := range tools {
- t.Run(tool.name, func(t *testing.T) {
- req := mcp.CallToolRequest{
- Name: tool.name,
- Arguments: tool.args,
- }
-
- var err error
- switch tool.name {
- case "say":
- _, err = server.handleSay(req)
- case "list_voices":
- _, err = server.handleListVoices(req)
- case "speak_file":
- _, err = server.handleSpeakFile(req)
- case "stop_speech":
- _, err = server.handleStopSpeech(req)
- }
-
- if err == nil {
- t.Errorf("Expected error when backend not available for tool %s", tool.name)
- }
-
- if !strings.Contains(err.Error(), "not available") {
- t.Errorf("Expected 'not available' error message for tool %s, got: %v", tool.name, err)
- }
- })
- }
- } else {
- t.Skip("Backend is available, skipping unavailable test")
- }
-}
\ No newline at end of file
pkg/thinking/server_test.go
@@ -1,374 +0,0 @@
-package thinking
-
-import (
- "strings"
- "testing"
-
- "github.com/xlgmokha/mcp/pkg/mcp"
-)
-
-func TestServer_BasicThinking(t *testing.T) {
- server := New()
-
- req := mcp.CallToolRequest{
- Name: "sequentialthinking",
- Arguments: map[string]interface{}{
- "thought": "Let me analyze this problem step by step.",
- "nextThoughtNeeded": true,
- "thoughtNumber": 1,
- "totalThoughts": 3,
- },
- }
-
- result, err := server.HandleSequentialThinking(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if result.IsError {
- textContent, _ := result.Content[0].(mcp.TextContent)
- t.Fatalf("Expected successful thinking, got error: %s", textContent.Text)
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- // Should contain the thought content
- if !contains(textContent.Text, "analyze this problem") {
- t.Fatalf("Expected thought content in result, got: %s", textContent.Text)
- }
-
- // Should show progress
- if !contains(textContent.Text, "Thought 1/3") {
- t.Fatalf("Expected progress indicator, got: %s", textContent.Text)
- }
-
- // Should indicate thinking status
- if !contains(textContent.Text, "thinking") {
- t.Fatalf("Expected thinking status, got: %s", textContent.Text)
- }
-}
-
-func TestServer_CompletedThinking(t *testing.T) {
- server := New()
-
- req := mcp.CallToolRequest{
- Name: "sequentialthinking",
- Arguments: map[string]interface{}{
- "thought": "Therefore, the solution is 42.",
- "nextThoughtNeeded": false,
- "thoughtNumber": 3,
- "totalThoughts": 3,
- },
- }
-
- result, err := server.HandleSequentialThinking(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if result.IsError {
- textContent, _ := result.Content[0].(mcp.TextContent)
- t.Fatalf("Expected successful thinking, got error: %s", textContent.Text)
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- // Should contain the thought content
- if !contains(textContent.Text, "solution is 42") {
- t.Fatalf("Expected thought content in result, got: %s", textContent.Text)
- }
-
- // Should show completed status
- if !contains(textContent.Text, "completed") {
- t.Fatalf("Expected completed status, got: %s", textContent.Text)
- }
-
- // Should extract solution
- if !contains(textContent.Text, "Extracted Solution") {
- t.Fatalf("Expected extracted solution, got: %s", textContent.Text)
- }
-}
-
-func TestServer_RevisionThinking(t *testing.T) {
- server := New()
-
- req := mcp.CallToolRequest{
- Name: "sequentialthinking",
- Arguments: map[string]interface{}{
- "thought": "Actually, let me reconsider my previous analysis.",
- "nextThoughtNeeded": true,
- "thoughtNumber": 4,
- "totalThoughts": 5,
- "isRevision": true,
- "revisesThought": 2,
- },
- }
-
- result, err := server.HandleSequentialThinking(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if result.IsError {
- textContent, _ := result.Content[0].(mcp.TextContent)
- t.Fatalf("Expected successful thinking, got error: %s", textContent.Text)
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- // Should contain revision context
- if !contains(textContent.Text, "Revising thought 2") {
- t.Fatalf("Expected revision context, got: %s", textContent.Text)
- }
-
- // Should contain the thought content
- if !contains(textContent.Text, "reconsider my previous") {
- t.Fatalf("Expected thought content in result, got: %s", textContent.Text)
- }
-}
-
-func TestServer_BranchThinking(t *testing.T) {
- server := New()
-
- req := mcp.CallToolRequest{
- Name: "sequentialthinking",
- Arguments: map[string]interface{}{
- "thought": "Let me explore an alternative approach.",
- "nextThoughtNeeded": true,
- "thoughtNumber": 6,
- "totalThoughts": 8,
- "branchFromThought": 3,
- "branchId": "alternative_path",
- },
- }
-
- result, err := server.HandleSequentialThinking(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if result.IsError {
- textContent, _ := result.Content[0].(mcp.TextContent)
- t.Fatalf("Expected successful thinking, got error: %s", textContent.Text)
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- // Should contain branch context
- if !contains(textContent.Text, "Branching from thought 3") {
- t.Fatalf("Expected branch context, got: %s", textContent.Text)
- }
-
- if !contains(textContent.Text, "alternative_path") {
- t.Fatalf("Expected branch ID, got: %s", textContent.Text)
- }
-}
-
-func TestServer_NeedsMoreThoughts(t *testing.T) {
- server := New()
-
- req := mcp.CallToolRequest{
- Name: "sequentialthinking",
- Arguments: map[string]interface{}{
- "thought": "I realize I need more steps to solve this.",
- "nextThoughtNeeded": true,
- "thoughtNumber": 5,
- "totalThoughts": 5,
- "needsMoreThoughts": true,
- },
- }
-
- result, err := server.HandleSequentialThinking(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if result.IsError {
- textContent, _ := result.Content[0].(mcp.TextContent)
- t.Fatalf("Expected successful thinking, got error: %s", textContent.Text)
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- // Should contain more thoughts context
- if !contains(textContent.Text, "additional thoughts") {
- t.Fatalf("Expected additional thoughts context, got: %s", textContent.Text)
- }
-
- // Should still be thinking status even though at total thoughts
- if !contains(textContent.Text, "thinking") {
- t.Fatalf("Expected thinking status, got: %s", textContent.Text)
- }
-}
-
-func TestServer_MissingRequiredParams(t *testing.T) {
- server := New()
-
- req := mcp.CallToolRequest{
- Name: "sequentialthinking",
- Arguments: map[string]interface{}{
- "thought": "Missing required params",
- // Missing nextThoughtNeeded, thoughtNumber, totalThoughts
- },
- }
-
- result, err := server.HandleSequentialThinking(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if !result.IsError {
- t.Fatal("Expected error for missing required parameters")
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- if !contains(textContent.Text, "required") {
- t.Fatalf("Expected required parameter error, got: %s", textContent.Text)
- }
-}
-
-func TestServer_InvalidParams(t *testing.T) {
- server := New()
-
- req := mcp.CallToolRequest{
- Name: "sequentialthinking",
- Arguments: map[string]interface{}{
- "thought": "Test thought",
- "nextThoughtNeeded": true,
- "thoughtNumber": 0, // Invalid: must be >= 1
- "totalThoughts": 3,
- },
- }
-
- result, err := server.HandleSequentialThinking(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if !result.IsError {
- t.Fatal("Expected error for invalid thoughtNumber")
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- if !contains(textContent.Text, "must be >= 1") {
- t.Fatalf("Expected thoughtNumber validation error, got: %s", textContent.Text)
- }
-}
-
-func TestServer_SolutionExtraction(t *testing.T) {
- server := New()
-
- // Test with explicit solution keyword
- req := mcp.CallToolRequest{
- Name: "sequentialthinking",
- Arguments: map[string]interface{}{
- "thought": "After careful analysis, the answer is: The optimal solution involves using a binary search algorithm.",
- "nextThoughtNeeded": false,
- "thoughtNumber": 3,
- "totalThoughts": 3,
- },
- }
-
- result, err := server.HandleSequentialThinking(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- // Should extract the solution
- if !contains(textContent.Text, "binary search algorithm") {
- t.Fatalf("Expected extracted solution to contain key phrase, got: %s", textContent.Text)
- }
-}
-
-func TestServer_ListTools(t *testing.T) {
- server := New()
- tools := server.ListTools()
-
- expectedTools := []string{
- "sequentialthinking",
- }
-
- if len(tools) != len(expectedTools) {
- t.Fatalf("Expected %d tools, got %d", len(expectedTools), len(tools))
- }
-
- toolNames := make(map[string]bool)
- for _, tool := range tools {
- toolNames[tool.Name] = true
- }
-
- for _, expected := range expectedTools {
- if !toolNames[expected] {
- t.Fatalf("Expected tool %s not found", expected)
- }
- }
-
- // Check that sequentialthinking tool has proper schema
- thinkingTool := tools[0]
- if thinkingTool.Name != "sequentialthinking" {
- t.Fatalf("Expected first tool to be 'sequentialthinking', got %s", thinkingTool.Name)
- }
-
- if thinkingTool.Description == "" {
- t.Fatal("Expected non-empty description for sequentialthinking tool")
- }
-
- if thinkingTool.InputSchema == nil {
- t.Fatal("Expected input schema for sequentialthinking tool")
- }
-}
-
-func TestServer_ProgressBar(t *testing.T) {
- server := New()
-
- // Test progress bar creation
- progressBar := server.createProgressBar(3, 10)
- if !contains(progressBar, "30%") {
- t.Fatalf("Expected 30%% progress, got: %s", progressBar)
- }
-
- // Test 100% completion
- progressBar = server.createProgressBar(5, 5)
- if !contains(progressBar, "100%") {
- t.Fatalf("Expected 100%% progress, got: %s", progressBar)
- }
-
- // Test over 100%
- progressBar = server.createProgressBar(7, 5)
- if !contains(progressBar, "100%") {
- t.Fatalf("Expected capped at 100%% progress, got: %s", progressBar)
- }
-}
-
-// Helper functions
-func contains(s, substr string) bool {
- return strings.Contains(s, substr)
-}
pkg/time/server_test.go
@@ -1,206 +0,0 @@
-package time
-
-import (
- "strings"
- "testing"
-
- "github.com/xlgmokha/mcp/pkg/mcp"
-)
-
-func TestServer_GetCurrentTime(t *testing.T) {
- server := New()
-
- req := mcp.CallToolRequest{
- Name: "get_current_time",
- Arguments: map[string]interface{}{
- "timezone": "UTC",
- },
- }
-
- result, err := server.HandleGetCurrentTime(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if len(result.Content) == 0 {
- t.Fatal("Expected content in result")
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- // Should contain timezone and datetime
- if !contains(textContent.Text, "UTC") {
- t.Fatalf("Expected UTC timezone in result, got: %s", textContent.Text)
- }
-
- if !contains(textContent.Text, "datetime") {
- t.Fatalf("Expected datetime in result, got: %s", textContent.Text)
- }
-}
-
-func TestServer_ConvertTime(t *testing.T) {
- server := New()
-
- req := mcp.CallToolRequest{
- Name: "convert_time",
- Arguments: map[string]interface{}{
- "source_timezone": "UTC",
- "time": "12:00",
- "target_timezone": "America/New_York",
- },
- }
-
- result, err := server.HandleConvertTime(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if len(result.Content) == 0 {
- t.Fatal("Expected content in result")
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- // Should contain source and target information
- if !contains(textContent.Text, "source") {
- t.Fatalf("Expected source in result, got: %s", textContent.Text)
- }
-
- if !contains(textContent.Text, "target") {
- t.Fatalf("Expected target in result, got: %s", textContent.Text)
- }
-
- if !contains(textContent.Text, "time_difference") {
- t.Fatalf("Expected time_difference in result, got: %s", textContent.Text)
- }
-}
-
-func TestServer_InvalidTimezone(t *testing.T) {
- server := New()
-
- req := mcp.CallToolRequest{
- Name: "get_current_time",
- Arguments: map[string]interface{}{
- "timezone": "Invalid/Timezone",
- },
- }
-
- result, err := server.HandleGetCurrentTime(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if !result.IsError {
- t.Fatal("Expected error for invalid timezone")
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- if !contains(textContent.Text, "Invalid timezone") {
- t.Fatalf("Expected invalid timezone error, got: %s", textContent.Text)
- }
-}
-
-func TestServer_InvalidTimeFormat(t *testing.T) {
- server := New()
-
- req := mcp.CallToolRequest{
- Name: "convert_time",
- Arguments: map[string]interface{}{
- "source_timezone": "UTC",
- "time": "invalid-time",
- "target_timezone": "America/New_York",
- },
- }
-
- result, err := server.HandleConvertTime(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if !result.IsError {
- t.Fatal("Expected error for invalid time format")
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- if !contains(textContent.Text, "Invalid time format") {
- t.Fatalf("Expected invalid time format error, got: %s", textContent.Text)
- }
-}
-
-func TestServer_ListTools(t *testing.T) {
- server := New()
- tools := server.ListTools()
-
- expectedTools := []string{
- "get_current_time",
- "convert_time",
- }
-
- if len(tools) != len(expectedTools) {
- t.Fatalf("Expected %d tools, got %d", len(expectedTools), len(tools))
- }
-
- toolNames := make(map[string]bool)
- for _, tool := range tools {
- toolNames[tool.Name] = true
- }
-
- for _, expected := range expectedTools {
- if !toolNames[expected] {
- t.Fatalf("Expected tool %s not found", expected)
- }
- }
-}
-
-func TestServer_ConvertTimeWithDST(t *testing.T) {
- server := New()
-
- // Test during daylight saving time period
- req := mcp.CallToolRequest{
- Name: "convert_time",
- Arguments: map[string]interface{}{
- "source_timezone": "UTC",
- "time": "16:00",
- "target_timezone": "Europe/London",
- },
- }
-
- result, err := server.HandleConvertTime(req)
- if err != nil {
- t.Fatalf("Expected no error, got %v", err)
- }
-
- if result.IsError {
- textContent, _ := result.Content[0].(mcp.TextContent)
- t.Fatalf("Expected successful conversion, got error: %s", textContent.Text)
- }
-
- textContent, ok := result.Content[0].(mcp.TextContent)
- if !ok {
- t.Fatal("Expected TextContent")
- }
-
- // Should contain proper JSON with DST information
- if !contains(textContent.Text, "is_dst") {
- t.Fatalf("Expected is_dst field in result, got: %s", textContent.Text)
- }
-}
-
-// Helper functions
-func contains(s, substr string) bool {
- return strings.Contains(s, substr)
-}
test/integration_test.go
@@ -1,849 +0,0 @@
-package test
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "io"
- "os"
- "os/exec"
- "path/filepath"
- "testing"
- "time"
-)
-
-// MCPRequest represents an MCP JSON-RPC request
-type MCPRequest struct {
- JSONRPC string `json:"jsonrpc"`
- ID int `json:"id"`
- Method string `json:"method"`
- Params interface{} `json:"params,omitempty"`
-}
-
-// MCPResponse represents an MCP JSON-RPC response
-type MCPResponse struct {
- JSONRPC string `json:"jsonrpc"`
- ID int `json:"id"`
- Result json.RawMessage `json:"result,omitempty"`
- Error *MCPError `json:"error,omitempty"`
-}
-
-type MCPError struct {
- Code int `json:"code"`
- Message string `json:"message"`
-}
-
-// TestServer represents a test configuration for an MCP server
-type TestServer struct {
- Binary string
- Args []string
- Name string
-}
-
-func TestMCPServersIntegration(t *testing.T) {
- // Create test directory structure
- testDir := setupTestEnvironment(t)
- defer os.RemoveAll(testDir)
-
- servers := []TestServer{
- {
- Binary: "../bin/mcp-filesystem",
- Args: []string{"--allowed-directory", testDir},
- Name: "filesystem",
- },
- {
- Binary: "../bin/mcp-git",
- Args: []string{"--repository", ".."},
- Name: "git",
- },
- {
- Binary: "../bin/mcp-memory",
- Args: []string{"--memory-file", filepath.Join(testDir, "test-memory.json")},
- Name: "memory",
- },
- {
- Binary: "../bin/mcp-fetch",
- Args: []string{},
- Name: "fetch",
- },
- {
- Binary: "../bin/mcp-time",
- Args: []string{},
- Name: "time",
- },
- {
- Binary: "../bin/mcp-sequential-thinking",
- Args: []string{},
- Name: "sequential-thinking",
- },
- {
- Binary: "../bin/mcp-maildir",
- Args: []string{"--maildir-path", filepath.Join(testDir, "maildir")},
- Name: "maildir",
- },
- {
- Binary: "../bin/mcp-signal",
- Args: []string{},
- Name: "signal",
- },
- {
- Binary: "../bin/mcp-imap",
- Args: []string{"--server", "example.com", "--username", "test", "--password", "test"},
- Name: "imap",
- },
- {
- Binary: "../bin/mcp-gitlab",
- Args: []string{"--gitlab-token", "fake_token_for_testing"},
- Name: "gitlab",
- },
- {
- Binary: "../bin/mcp-packages",
- Args: []string{},
- Name: "packages",
- },
- {
- Binary: "../bin/mcp-speech",
- Args: []string{},
- Name: "speech",
- },
- }
-
- for _, server := range servers {
- t.Run(server.Name, func(t *testing.T) {
- testMCPServer(t, server, testDir)
- })
- }
-}
-
-func testMCPServer(t *testing.T, server TestServer, testDir string) {
- t.Logf("Testing %s server", server.Name)
-
- // Test 1: Initialize server
- t.Run("Initialize", func(t *testing.T) {
- resp := sendMCPRequest(t, server, MCPRequest{
- JSONRPC: "2.0",
- ID: 1,
- Method: "initialize",
- Params: map[string]interface{}{
- "protocolVersion": "2025-06-18",
- "capabilities": map[string]interface{}{},
- "clientInfo": map[string]interface{}{
- "name": "test-client",
- "version": "1.0.0",
- },
- },
- })
-
- if resp.Error != nil {
- t.Fatalf("Initialize failed: %s", resp.Error.Message)
- }
-
- // Verify response contains capabilities
- var result map[string]interface{}
- if err := json.Unmarshal(resp.Result, &result); err != nil {
- t.Fatalf("Failed to parse initialize result: %v", err)
- }
-
- capabilities, ok := result["capabilities"].(map[string]interface{})
- if !ok {
- t.Fatal("No capabilities in initialize response")
- }
-
- // All servers should have these capabilities
- expectedCaps := []string{"tools", "prompts", "resources", "roots", "logging"}
- for _, cap := range expectedCaps {
- if _, exists := capabilities[cap]; !exists {
- t.Errorf("Missing capability: %s", cap)
- }
- }
- })
-
- // Test 2: List tools
- t.Run("ListTools", func(t *testing.T) {
- resp := sendMCPRequest(t, server, MCPRequest{
- JSONRPC: "2.0",
- ID: 2,
- Method: "tools/list",
- })
-
- if resp.Error != nil {
- t.Fatalf("ListTools failed: %s", resp.Error.Message)
- }
-
- var result map[string]interface{}
- if err := json.Unmarshal(resp.Result, &result); err != nil {
- t.Fatalf("Failed to parse tools/list result: %v", err)
- }
-
- tools, ok := result["tools"].([]interface{})
- if !ok {
- t.Fatal("No tools array in response")
- }
-
- if len(tools) == 0 {
- t.Error("No tools returned")
- }
-
- t.Logf("Server %s has %d tools", server.Name, len(tools))
- })
-
- // Test 3: List resources (lazy loading test)
- t.Run("ListResources", func(t *testing.T) {
- start := time.Now()
- resp := sendMCPRequest(t, server, MCPRequest{
- JSONRPC: "2.0",
- ID: 3,
- Method: "resources/list",
- })
- duration := time.Since(start)
-
- if resp.Error != nil {
- t.Fatalf("ListResources failed: %s", resp.Error.Message)
- }
-
- // Ensure lazy loading is fast (should be under 1 second even for large repos)
- if duration > time.Second {
- t.Errorf("Resource discovery took too long: %v", duration)
- }
-
- var result map[string]interface{}
- if err := json.Unmarshal(resp.Result, &result); err != nil {
- t.Fatalf("Failed to parse resources/list result: %v", err)
- }
-
- t.Logf("Server %s resource discovery took %v", server.Name, duration)
- })
-
- // Test 4: List roots
- t.Run("ListRoots", func(t *testing.T) {
- resp := sendMCPRequest(t, server, MCPRequest{
- JSONRPC: "2.0",
- ID: 4,
- Method: "roots/list",
- })
-
- if resp.Error != nil {
- t.Fatalf("ListRoots failed: %s", resp.Error.Message)
- }
-
- var result map[string]interface{}
- if err := json.Unmarshal(resp.Result, &result); err != nil {
- t.Fatalf("Failed to parse roots/list result: %v", err)
- }
-
- roots, ok := result["roots"].([]interface{})
- if !ok {
- t.Fatal("No roots array in response")
- }
-
- t.Logf("Server %s has %d roots", server.Name, len(roots))
- })
-
- // Test 5: Server-specific functionality
- switch server.Name {
- case "filesystem":
- testFilesystemSpecific(t, server, testDir)
- case "git":
- testGitSpecific(t, server)
- case "memory":
- testMemorySpecific(t, server)
- case "fetch":
- testFetchSpecific(t, server)
- case "time":
- testTimeSpecific(t, server)
- case "maildir":
- testMaildirSpecific(t, server, testDir)
- case "signal":
- testSignalSpecific(t, server)
- }
-}
-
-func testFilesystemSpecific(t *testing.T, server TestServer, testDir string) {
- t.Run("FileSystemTools", func(t *testing.T) {
- // Test list_directory
- resp := sendMCPRequest(t, server, MCPRequest{
- JSONRPC: "2.0",
- ID: 10,
- Method: "tools/call",
- Params: map[string]interface{}{
- "name": "list_directory",
- "arguments": map[string]interface{}{
- "path": testDir,
- },
- },
- })
-
- if resp.Error != nil {
- t.Fatalf("list_directory failed: %s", resp.Error.Message)
- }
-
- t.Log("Filesystem list_directory test passed")
- })
-}
-
-func testGitSpecific(t *testing.T, server TestServer) {
- t.Run("GitTools", func(t *testing.T) {
- // Test git_status
- resp := sendMCPRequest(t, server, MCPRequest{
- JSONRPC: "2.0",
- ID: 11,
- Method: "tools/call",
- Params: map[string]interface{}{
- "name": "git_status",
- "arguments": map[string]interface{}{},
- },
- })
-
- if resp.Error != nil {
- t.Fatalf("git_status failed: %s", resp.Error.Message)
- }
-
- t.Log("Git git_status test passed")
- })
-}
-
-func testMemorySpecific(t *testing.T, server TestServer) {
- t.Run("MemoryTools", func(t *testing.T) {
- // Test read_graph (should work with lazy loading)
- resp := sendMCPRequest(t, server, MCPRequest{
- JSONRPC: "2.0",
- ID: 12,
- Method: "tools/call",
- Params: map[string]interface{}{
- "name": "read_graph",
- "arguments": map[string]interface{}{},
- },
- })
-
- if resp.Error != nil {
- t.Fatalf("read_graph failed: %s", resp.Error.Message)
- }
-
- t.Log("Memory read_graph test passed")
- })
-
- t.Run("MemoryPersistence", func(t *testing.T) {
- testDir, err := os.MkdirTemp("", "memory-persistence-test-*")
- if err != nil {
- t.Fatalf("Failed to create temp directory: %v", err)
- }
- defer os.RemoveAll(testDir)
- testMemoryPersistence(t, testDir)
- })
-}
-
-func testFetchSpecific(t *testing.T, server TestServer) {
- t.Run("FetchTools", func(t *testing.T) {
- // Test fetch with a simple URL
- resp := sendMCPRequest(t, server, MCPRequest{
- JSONRPC: "2.0",
- ID: 13,
- Method: "tools/call",
- Params: map[string]interface{}{
- "name": "fetch",
- "arguments": map[string]interface{}{
- "url": "https://httpbin.org/get",
- },
- },
- })
-
- if resp.Error != nil {
- t.Logf("fetch test skipped (network issue): %s", resp.Error.Message)
- return
- }
-
- t.Log("Fetch test passed")
- })
-}
-
-func testTimeSpecific(t *testing.T, server TestServer) {
- t.Run("TimeTools", func(t *testing.T) {
- // Test get_current_time
- resp := sendMCPRequest(t, server, MCPRequest{
- JSONRPC: "2.0",
- ID: 14,
- Method: "tools/call",
- Params: map[string]interface{}{
- "name": "get_current_time",
- "arguments": map[string]interface{}{},
- },
- })
-
- if resp.Error != nil {
- t.Fatalf("get_current_time failed: %s", resp.Error.Message)
- }
-
- t.Log("Time get_current_time test passed")
- })
-}
-
-func testMaildirSpecific(t *testing.T, server TestServer, testDir string) {
- t.Run("MaildirTools", func(t *testing.T) {
- maildirPath := filepath.Join(testDir, "maildir")
-
- // Test maildir_scan_folders
- resp := sendMCPRequest(t, server, MCPRequest{
- JSONRPC: "2.0",
- ID: 15,
- Method: "tools/call",
- Params: map[string]interface{}{
- "name": "maildir_scan_folders",
- "arguments": map[string]interface{}{
- "maildir_path": maildirPath,
- },
- },
- })
-
- if resp.Error != nil {
- t.Fatalf("maildir_scan_folders failed: %s", resp.Error.Message)
- }
-
- t.Log("Maildir scan_folders test passed")
- })
-}
-
-func testSignalSpecific(t *testing.T, server TestServer) {
- t.Run("SignalTools", func(t *testing.T) {
- // Test signal_list_conversations
- resp := sendMCPRequest(t, server, MCPRequest{
- JSONRPC: "2.0",
- ID: 16,
- Method: "tools/call",
- Params: map[string]interface{}{
- "name": "signal_list_conversations",
- "arguments": map[string]interface{}{},
- },
- })
-
- // Signal tests are optional since they require Signal Desktop to be installed
- // and configured. We'll log the result but not fail if it's not available.
- if resp.Error != nil {
- t.Logf("signal_list_conversations skipped (Signal not available): %s", resp.Error.Message)
- return
- }
-
- t.Log("Signal list_conversations test passed")
- })
-
- t.Run("SignalStats", func(t *testing.T) {
- // Test signal_get_stats
- resp := sendMCPRequest(t, server, MCPRequest{
- JSONRPC: "2.0",
- ID: 17,
- Method: "tools/call",
- Params: map[string]interface{}{
- "name": "signal_get_stats",
- "arguments": map[string]interface{}{},
- },
- })
-
- if resp.Error != nil {
- t.Logf("signal_get_stats skipped (Signal not available): %s", resp.Error.Message)
- return
- }
-
- t.Log("Signal get_stats test passed")
- })
-
- t.Run("SignalNewTools", func(t *testing.T) {
- // Test signal_get_contact with invalid ID to verify error handling
- resp := sendMCPRequest(t, server, MCPRequest{
- JSONRPC: "2.0",
- ID: 18,
- Method: "tools/call",
- Params: map[string]interface{}{
- "name": "signal_get_contact",
- "arguments": map[string]interface{}{
- "contact_id": "nonexistent-contact-id",
- },
- },
- })
-
- if resp.Error != nil {
- t.Logf("signal_get_contact skipped (Signal not available): %s", resp.Error.Message)
- return
- }
-
- // Test signal_get_message with invalid ID
- resp = sendMCPRequest(t, server, MCPRequest{
- JSONRPC: "2.0",
- ID: 19,
- Method: "tools/call",
- Params: map[string]interface{}{
- "name": "signal_get_message",
- "arguments": map[string]interface{}{
- "message_id": "nonexistent-message-id",
- },
- },
- })
-
- if resp.Error != nil {
- t.Logf("signal_get_message skipped (Signal not available): %s", resp.Error.Message)
- return
- }
-
- // Test signal_list_attachments
- resp = sendMCPRequest(t, server, MCPRequest{
- JSONRPC: "2.0",
- ID: 20,
- Method: "tools/call",
- Params: map[string]interface{}{
- "name": "signal_list_attachments",
- "arguments": map[string]interface{}{
- "limit": "5",
- },
- },
- })
-
- if resp.Error != nil {
- t.Logf("signal_list_attachments skipped (Signal not available): %s", resp.Error.Message)
- return
- }
-
- t.Log("Signal new tools tests passed")
- })
-
- t.Run("SignalPrompts", func(t *testing.T) {
- // Test prompts/list
- resp := sendMCPRequest(t, server, MCPRequest{
- JSONRPC: "2.0",
- ID: 21,
- Method: "prompts/list",
- })
-
- if resp.Error != nil {
- t.Logf("prompts/list skipped (Signal not available): %s", resp.Error.Message)
- return
- }
-
- // Parse prompts list
- var result map[string]interface{}
- if err := json.Unmarshal(resp.Result, &result); err != nil {
- t.Logf("Failed to parse prompts list: %v", err)
- return
- }
-
- prompts, ok := result["prompts"].([]interface{})
- if !ok {
- t.Log("No prompts array found")
- return
- }
-
- // Should have 2 prompts: signal-conversation and signal-search
- if len(prompts) >= 2 {
- t.Log("Signal prompts test passed")
- } else {
- t.Logf("Expected 2 prompts, found %d", len(prompts))
- }
- })
-}
-
-func sendMCPRequest(t *testing.T, server TestServer, request MCPRequest) MCPResponse {
- // Create command
- cmd := exec.Command(server.Binary, server.Args...)
-
- // Set up pipes
- stdin, err := cmd.StdinPipe()
- if err != nil {
- t.Fatalf("Failed to create stdin pipe: %v", err)
- }
-
- stdout, err := cmd.StdoutPipe()
- if err != nil {
- t.Fatalf("Failed to create stdout pipe: %v", err)
- }
-
- // Start the server
- if err := cmd.Start(); err != nil {
- t.Fatalf("Failed to start server %s: %v", server.Name, err)
- }
-
- // Send request
- reqBytes, _ := json.Marshal(request)
- if _, err := stdin.Write(reqBytes); err != nil {
- t.Fatalf("Failed to write request: %v", err)
- }
- stdin.Close()
-
- // Read response with timeout
- responseChan := make(chan MCPResponse, 1)
- errorChan := make(chan error, 1)
-
- go func() {
- var buffer bytes.Buffer
- if _, err := io.Copy(&buffer, stdout); err != nil {
- errorChan <- err
- return
- }
-
- var response MCPResponse
- if err := json.Unmarshal(buffer.Bytes(), &response); err != nil {
- errorChan <- fmt.Errorf("failed to unmarshal response: %v, raw: %s", err, buffer.String())
- return
- }
-
- responseChan <- response
- }()
-
- // Wait for response or timeout
- select {
- case response := <-responseChan:
- cmd.Wait()
- return response
- case err := <-errorChan:
- cmd.Process.Kill()
- t.Fatalf("Error reading response: %v", err)
- return MCPResponse{}
- case <-time.After(10 * time.Second):
- cmd.Process.Kill()
- t.Fatalf("Request timeout for server %s", server.Name)
- return MCPResponse{}
- }
-}
-
-func setupTestEnvironment(t *testing.T) string {
- testDir, err := os.MkdirTemp("", "mcp-integration-test-*")
- if err != nil {
- t.Fatalf("Failed to create test directory: %v", err)
- }
-
- // Create test files
- testFiles := map[string]string{
- "test1.txt": "Hello World",
- "test2.md": "# Test Markdown",
- "subdir/test3.txt": "Nested file",
- }
-
- for path, content := range testFiles {
- fullPath := filepath.Join(testDir, path)
- if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
- t.Fatalf("Failed to create directory: %v", err)
- }
- if err := os.WriteFile(fullPath, []byte(content), 0644); err != nil {
- t.Fatalf("Failed to write test file: %v", err)
- }
- }
-
- // Create maildir structure
- maildirPath := filepath.Join(testDir, "maildir")
- for _, dir := range []string{"cur", "new", "tmp"} {
- if err := os.MkdirAll(filepath.Join(maildirPath, dir), 0755); err != nil {
- t.Fatalf("Failed to create maildir structure: %v", err)
- }
- }
-
- // Create a test email in maildir
- testEmail := `From: test@example.com
-To: user@example.com
-Subject: Test Email
-Date: Mon, 01 Jan 2024 12:00:00 +0000
-
-This is a test email for integration testing.
-`
- emailPath := filepath.Join(maildirPath, "cur", "1234567890.test:2,S")
- if err := os.WriteFile(emailPath, []byte(testEmail), 0644); err != nil {
- t.Fatalf("Failed to write test email: %v", err)
- }
-
- return testDir
-}
-
-// Benchmark tests for performance verification
-func BenchmarkServerStartup(b *testing.B) {
- testDir := setupBenchEnvironment(b)
- defer os.RemoveAll(testDir)
-
- servers := []TestServer{
- {Binary: "../bin/mcp-filesystem", Args: []string{"--allowed-directory", testDir}, Name: "filesystem"},
- {Binary: "../bin/mcp-git", Args: []string{"--repository", ".."}, Name: "git"},
- {Binary: "../bin/mcp-memory", Args: []string{"--memory-file", filepath.Join(testDir, "bench-memory.json")}, Name: "memory"},
- }
-
- for _, server := range servers {
- b.Run(server.Name+"_startup", func(b *testing.B) {
- for i := 0; i < b.N; i++ {
- start := time.Now()
- resp := sendMCPRequest(&testing.T{}, server, MCPRequest{
- JSONRPC: "2.0",
- ID: 1,
- Method: "initialize",
- Params: map[string]interface{}{
- "protocolVersion": "2025-06-18",
- "capabilities": map[string]interface{}{},
- "clientInfo": map[string]interface{}{"name": "bench", "version": "1.0.0"},
- },
- })
- duration := time.Since(start)
-
- if resp.Error != nil {
- b.Fatalf("Initialize failed: %s", resp.Error.Message)
- }
-
- if duration > 100*time.Millisecond {
- b.Errorf("Startup too slow: %v", duration)
- }
- }
- })
-
- b.Run(server.Name+"_resources", func(b *testing.B) {
- for i := 0; i < b.N; i++ {
- start := time.Now()
- sendMCPRequest(&testing.T{}, server, MCPRequest{
- JSONRPC: "2.0",
- ID: 2,
- Method: "resources/list",
- })
- duration := time.Since(start)
-
- if duration > 500*time.Millisecond {
- b.Errorf("Resource discovery too slow: %v", duration)
- }
- }
- })
- }
-}
-
-func setupBenchEnvironment(b *testing.B) string {
- testDir, err := os.MkdirTemp("", "mcp-bench-test-*")
- if err != nil {
- b.Fatalf("Failed to create test directory: %v", err)
- }
-
- // Create many test files to stress test
- for i := 0; i < 100; i++ {
- content := fmt.Sprintf("Test file %d content", i)
- path := filepath.Join(testDir, fmt.Sprintf("file%d.txt", i))
- if err := os.WriteFile(path, []byte(content), 0644); err != nil {
- b.Fatalf("Failed to write test file: %v", err)
- }
- }
-
- return testDir
-}
-
-// testMemoryPersistence tests that memory server properly persists data to disk and loads it on restart
-func testMemoryPersistence(t *testing.T, testDir string) {
- memoryFile := filepath.Join(testDir, "test_memory.json")
-
- // Phase 1: Create entities in first server instance
- testEntity := map[string]interface{}{
- "name": "test_persistence_entity",
- "entityType": "concept",
- "observations": []string{"This entity should persist across server restarts"},
- }
-
- server1 := TestServer{
- Name: "memory",
- Binary: "mcp-memory",
- Args: []string{"--memory-file", memoryFile},
- }
-
- // Create entity in first server instance
- resp1 := sendMCPRequest(t, server1, MCPRequest{
- JSONRPC: "2.0",
- ID: 1,
- Method: "tools/call",
- Params: map[string]interface{}{
- "name": "create_entities",
- "arguments": map[string]interface{}{
- "entities": []interface{}{testEntity},
- },
- },
- })
-
- if resp1.Error != nil {
- t.Fatalf("Failed to create entity in first server instance: %s", resp1.Error.Message)
- }
-
- // Verify memory file was created
- if _, err := os.Stat(memoryFile); os.IsNotExist(err) {
- t.Fatalf("Memory file was not created: %s", memoryFile)
- }
-
- // Phase 2: Start new server instance and verify entity persists
- server2 := TestServer{
- Name: "memory",
- Binary: "mcp-memory",
- Args: []string{"--memory-file", memoryFile},
- }
-
- // Read graph from second server instance
- resp2 := sendMCPRequest(t, server2, MCPRequest{
- JSONRPC: "2.0",
- ID: 2,
- Method: "tools/call",
- Params: map[string]interface{}{
- "name": "read_graph",
- "arguments": map[string]interface{}{},
- },
- })
-
- if resp2.Error != nil {
- t.Fatalf("Failed to read graph from second server instance: %s", resp2.Error.Message)
- }
-
- // Parse and verify the persisted entity
- var result map[string]interface{}
- if err := json.Unmarshal(resp2.Result, &result); err != nil {
- t.Fatalf("Failed to parse read_graph result: %v", err)
- }
-
- content, ok := result["content"].([]interface{})
- if !ok || len(content) == 0 {
- t.Fatalf("Expected content array in response")
- }
-
- textContent, ok := content[0].(map[string]interface{})
- if !ok {
- t.Fatalf("Expected text content object")
- }
-
- graphText, ok := textContent["text"].(string)
- if !ok {
- t.Fatalf("Expected text field in content")
- }
-
- var graph map[string]interface{}
- if err := json.Unmarshal([]byte(graphText), &graph); err != nil {
- t.Fatalf("Failed to parse graph JSON: %v", err)
- }
-
- entities, ok := graph["entities"].(map[string]interface{})
- if !ok {
- t.Fatalf("Expected entities object in graph")
- }
-
- // Verify our test entity persisted
- persistedEntity, exists := entities["test_persistence_entity"]
- if !exists {
- t.Fatalf("Test entity did not persist across server restart")
- }
-
- persistedEntityMap, ok := persistedEntity.(map[string]interface{})
- if !ok {
- t.Fatalf("Persisted entity is not a valid object")
- }
-
- // Verify entity properties
- if persistedEntityMap["name"] != "test_persistence_entity" {
- t.Fatalf("Entity name did not persist correctly")
- }
-
- if persistedEntityMap["entityType"] != "concept" {
- t.Fatalf("Entity type did not persist correctly")
- }
-
- observations, ok := persistedEntityMap["observations"].([]interface{})
- if !ok || len(observations) != 1 {
- t.Fatalf("Entity observations did not persist correctly")
- }
-
- if observations[0] != "This entity should persist across server restarts" {
- t.Fatalf("Entity observation content did not persist correctly")
- }
-
- t.Log("Memory persistence test passed - entity persisted across server restart")
-
- // Clean up test memory file
- os.Remove(memoryFile)
-}
\ No newline at end of file
CLAUDE.md
@@ -1,890 +0,0 @@
-# CLAUDE.md
-
-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
-
-## Project Overview
-
-This is a **production-ready** Go-based MCP (Model Context Protocol) server implementation with **100% feature parity** to reference implementations. The project includes multiple specialized MCP servers that follow JSON-RPC 2.0 protocol for AI assistant integrations.
-
-## ๐ Current Status: COMPLETE
-
-**All enhancement phases have been successfully implemented:**
-- โ
Phase 1: Advanced HTML Processing (goquery + html-to-markdown)
-- โ
Phase 2: Interactive Prompts Support (user/assistant conversations)
-- โ
Phase 3: Resources Support (file://, git://, memory:// URI schemes)
-- โ
Phase 4: Roots Support (automatic capability discovery)
-- โ
Advanced: Sequential Thinking Enhancements (persistent sessions, branch tracking)
-
-## Architecture
-
-### Code Structure
-```
-pkg/
-โโโ mcp/ # Core MCP protocol implementation (JSON-RPC 2.0)
-โโโ git/ # Git server with repository operations
-โโโ filesystem/ # Filesystem server with access controls
-โโโ memory/ # Knowledge graph with persistent storage
-โโโ fetch/ # Web content fetching with HTML processing
-โโโ time/ # Time/timezone utilities
-โโโ thinking/ # Sequential thinking with session management
-โโโ maildir/ # Email analysis for Maildir format
-โโโ signal/ # Signal Desktop database access and messaging analysis
-
-cmd/ # Server entry points (main.go files)
-test/integration/ # E2E integration test suite
-```
-
-### MCP Server Architecture Pattern
-All servers follow this pattern:
-1. **Server struct** in `pkg/<name>/server.go` implements the MCP protocol
-2. **Tool handlers** registered via `RegisterTool(name, handler)`
-3. **Base MCP server** (`pkg/mcp/server.go`) handles JSON-RPC 2.0 protocol
-4. **Thread-safe operations** with sync.RWMutex for concurrent access
-5. **Lazy loading** - resources discovered on-demand, not at startup
-
-### Key Components
-- **pkg/mcp/server.go**: Core MCP protocol implementation with JSON-RPC 2.0
-- **pkg/mcp/types.go**: MCP protocol types and structures
-- **Makefile**: Build system with individual server targets
-- **test/integration_test.go**: Comprehensive test suite for all servers
-
-### Available MCP Servers
-Each server is a standalone binary in `/usr/local/bin/`:
-
-1. **mcp-git** - Git repository operations and browsing
-2. **mcp-filesystem** - Secure filesystem access with allowed directories
-3. **mcp-memory** - Knowledge graph management with entities/relations
-4. **mcp-fetch** - Web content fetching with advanced HTML processing
-5. **mcp-time** - Time and date utilities
-6. **mcp-sequential-thinking** - Advanced structured thinking workflows with persistent sessions and branch tracking
-7. **mcp-maildir** - Email management through Maildir format with search and analysis
-8. **mcp-signal** - Signal Desktop database access with encrypted SQLCipher support
-9. **mcp-imap** - IMAP email server connectivity for Gmail, Migadu, and other providers
-10. **mcp-gitlab** - GitLab issue and project management with intelligent local caching
-11. **mcp-speech** - Cross-platform text-to-speech with macOS `say` and Linux `espeak-ng` support
-
-### Protocol Implementation
-- **JSON-RPC 2.0** compliant MCP protocol
-- **Full capability advertisement**: tools, prompts, resources, roots, logging
-- **Secure by design**: filesystem access controls, input validation
-- **Thread-safe**: concurrent access with proper mutex locking
-
-## Development Commands
-
-### Essential Build Commands
-```bash
-make build # Build all MCP servers
-make clean build # Clean and rebuild all servers
-make install # Install binaries to /usr/local/bin (requires sudo)
-make <server-name> # Build individual server (e.g., make git, make memory)
-```
-
-### Testing and Quality
-```bash
-go test ./... # Run all unit tests
-make test # Run all tests via Makefile
-make test-coverage # Run tests with coverage reporting
-make e2e # Run integration tests in test/integration/
-make verify # Run tests + linting
-make benchmark # Run performance benchmarks
-```
-
-### Development Workflow
-```bash
-make dev-setup # Initialize development environment
-make lint # Format code and run go vet
-make fmt # Format Go source code only
-go test ./pkg/<server>/... # Test specific server package
-```
-
-### Single Test Execution
-```bash
-go test -v ./pkg/memory/... -run TestSpecificFunction
-go test -timeout=30s ./test/integration/... -run TestGitServer
-```
-
-### Individual Server Usage
-```bash
-# Git server
-mcp-git --repository /path/to/repo
-
-# Filesystem server
-mcp-filesystem --allowed-directory /tmp,/home/user/projects
-
-# Memory server
-mcp-memory --memory-file /path/to/memory.json
-
-# Fetch server
-mcp-fetch
-
-# Time server
-mcp-time
-
-# Sequential thinking server
-mcp-sequential-thinking --session-file /path/to/sessions.json
-
-# Maildir server
-mcp-maildir --maildir-path /path/to/maildir
-
-# Signal server
-mcp-signal --signal-path /path/to/Signal
-
-# IMAP server
-mcp-imap --server imap.gmail.com --username user@gmail.com --password app-password
-
-# GitLab server
-mcp-gitlab --gitlab-token your_token_here --gitlab-url https://gitlab.com
-
-# Speech server (cross-platform TTS)
-mcp-speech
-```
-
-## Enhanced Capabilities
-
-### 1. Advanced HTML Processing (Phase 1)
-- **Professional content extraction** using `goquery`
-- **Clean markdown conversion** with `html-to-markdown`
-- **Automatic filtering** of ads, navigation, scripts, styles
-- **Significant improvement**: 137 lines of custom parsing โ 13 lines with libraries
-
-### 2. Interactive Prompts (Phase 2)
-- **fetch**: Interactive URL entry with optional reason context
-- **commit-message**: Conventional commit format guidance with breaking change support
-- **edit-file**: Step-by-step file editing workflow with security validation
-- **knowledge-query**: Memory graph exploration with context
-
-### 3. Resource Discovery (Phase 3)
-- **file:// scheme**: Direct filesystem access with security validation and MIME detection
-- **git:// scheme**: Repository browsing (files, branches, commits) with metadata
-- **memory:// scheme**: Knowledge graph exploration (entities, relations) as JSON resources
-- **Thread-safe implementation** with proper resource lifecycle management
-
-### 4. Root Capability Discovery (Phase 4)
-- **Automatic registration**: Each server registers its access points as roots
-- **Live statistics**: Memory server shows real-time entity/relation counts
-- **Dynamic updates**: Root information updates when underlying data changes
-- **User-friendly names**: Descriptive root names with context (e.g., "Git Repository: mcp (branch: main)")
-
-### 5. Sequential Thinking Enhancements (Advanced)
-- **Persistent session management**: Multi-session support with unique IDs and file-based persistence
-- **Complete thought history**: Full tracking of thoughts across server invocations
-- **Cross-call branch tracking**: Create and manage reasoning branches with lifecycle management
-- **Enhanced tools**: 5 total tools including session/branch history and management
-- **Thread-safe operations**: Concurrent session access with proper mutex locking
-- **Rich visual output**: Progress bars, session context, and branch information
-
-**Available Tools:**
-- `sequentialthinking`: Enhanced core tool with session continuity support
-- `get_session_history`: Retrieve complete thought history for any session
-- `list_sessions`: List all active thinking sessions with metadata
-- `get_branch_history`: Get detailed branch history and thoughts
-- `clear_session`: Clean up sessions and associated branches
-
-**Persistence Features:**
-- Optional `--session-file` flag for cross-invocation session persistence
-- Automatic session creation with unique IDs when not specified
-- JSON-based storage of sessions, branches, and complete thought history
-- Graceful handling of missing or corrupted persistence files
-
-## Testing Integration
-
-### Quick Capability Test
-```bash
-# Test server capabilities
-echo '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "test", "version": "1.0.0"}}}' | mcp-git --repository .
-
-# Expected response includes all capabilities:
-# {"capabilities":{"logging":{},"prompts":{},"resources":{},"roots":{},"tools":{}}}
-```
-
-### Test Enhanced Features
-```bash
-# Test prompts
-echo '{"jsonrpc": "2.0", "id": 2, "method": "prompts/list"}' | mcp-fetch
-
-# Test resources
-echo '{"jsonrpc": "2.0", "id": 3, "method": "resources/list"}' | mcp-git --repository .
-
-# Test roots
-echo '{"jsonrpc": "2.0", "id": 4, "method": "roots/list"}' | mcp-filesystem --allowed-directory /tmp
-
-# Test enhanced HTML processing
-echo '{"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {"name": "fetch", "arguments": {"url": "https://example.com"}}}' | mcp-fetch
-
-# Test sequential thinking with persistence
-echo '{"jsonrpc": "2.0", "id": 6, "method": "tools/call", "params": {"name": "sequentialthinking", "arguments": {"thought": "Testing persistent sessions", "nextThoughtNeeded": true, "thoughtNumber": 1, "totalThoughts": 3, "sessionId": "test_session"}}}' | mcp-sequential-thinking --session-file /tmp/sessions.json
-```
-
-## Dependencies
-
-### Core Libraries
-- **go-git**: Git repository operations
-- **goquery**: Professional HTML parsing and content extraction
-- **html-to-markdown**: Clean HTML to markdown conversion
-- **Standard library**: JSON-RPC, HTTP, filesystem, concurrency
-
-### Development Tools
-- **Makefile**: Build automation with `make build`, `make install`, `make clean`
-- **Go modules**: Dependency management with `go.mod`
-
-## Key Design Principles
-
-- **Security first**: Filesystem access controls, input validation, no arbitrary code execution
-- **Performance optimized**: Thread-safe concurrent operations, efficient resource management
-- **Standards compliant**: Full JSON-RPC 2.0 and MCP protocol implementation
-- **Production ready**: Comprehensive error handling, logging, graceful degradation
-- **Extensible architecture**: Easy to add new tools, prompts, and resource types
-
-## Integration Notes for Claude Code
-
-### Development Patterns
-1. **Adding New Tools**: Register via `server.RegisterTool(name, handler)` in `pkg/<server>/server.go`
-2. **Tool Handler Pattern**: Implement `func(req mcp.CallToolRequest) (mcp.CallToolResult, error)`
-3. **Thread Safety**: Use `server.mu.Lock()` / `server.mu.RLock()` for concurrent access
-4. **Error Handling**: Return `mcp.NewToolError(msg)` for tool errors
-5. **JSON Storage**: Use `json.MarshalIndent()` for human-readable persistence files
-
-### Testing Strategy
-1. **Unit Tests**: Test individual tool handlers in `pkg/<server>/`
-2. **Integration Tests**: Use `test/integration_test.go` pattern for full server testing
-3. **MCP Protocol Testing**: Use JSON-RPC messages via stdin/stdout
-4. **Performance Testing**: Critical - ensure <100ms startup, <5MB memory usage
-
-### Server Implementation Guidelines
-1. **Lazy Loading Required**: Never load resources at startup (use on-demand discovery)
-2. **Resource Limits**: Implement limits (e.g., 500 files) to prevent memory bloat
-3. **Persistence Pattern**: Optional file-based storage with graceful degradation
-4. **Command-line Flags**: Use `flag` package for server configuration
-5. **Help Integration**: Implement `--help` flag for AI agent discoverability
-
-### Critical Performance Requirements
-- **Startup Time**: Must be <100ms (enforced by integration tests)
-- **Memory Usage**: Must be <5MB per server (monitored in testing)
-- **Resource Discovery**: On-demand only, sub-20ms response time
-- **Thread Safety**: All servers must handle concurrent requests safely
-
-When working with this codebase:
-1. **All servers are production-ready** with comprehensive MCP protocol support
-2. **Use integration tests** in `test/integration/` to verify changes
-3. **Follow lazy loading pattern** - discovered critical performance issues with eager loading
-4. **Test with installed binaries** in `/usr/local/bin/` for realistic integration testing
-
-## ๐ Latest Conversation Memory (Session: 2024-12-23)
-
-### **Critical Resource Optimization Breakthrough**
-
-**MAJOR ISSUE DISCOVERED & FIXED**: The original MCP servers had fatal resource efficiency flaws:
-
-- **Filesystem MCP**: Was loading ALL files into memory at startup (500MB+ for large directories) โ
-- **Git MCP**: Was pre-loading ALL tracked files + branches + commits (2-3 second startup) โ
-- **Memory MCP**: Was loading entire knowledge graph and registering all entities at startup โ
-- **Maildir MCP**: Was incomplete and would have had same eager loading issues โ
-
-### **๐ง Optimizations Applied**
-
-**Implemented lazy loading across ALL servers:**
-
-1. **Filesystem MCP** (`pkg/filesystem/server.go:92-96`) - Fixed
- - Removed `discoverFilesInDirectory()` from startup
- - Added dynamic `ListResources()` method (lines 117-126)
- - Enhanced base MCP server with pattern handlers (`pkg/mcp/server.go:344-358`)
-
-2. **Git MCP** (`pkg/git/server.go:81-85`) - Fixed
- - Removed `discoverGitResources()` eager loading
- - Added lazy resource discovery with 500-file limit
- - Removed `registerBranchResources()` and `registerCommitResources()` functions
-
-3. **Memory MCP** (`pkg/memory/server.go:89-102`) - Fixed
- - Removed eager `loadGraph()` from startup (line 55 removed)
- - Added `ensureGraphLoaded()` lazy loading pattern
- - Removed automatic re-registration goroutines from `saveGraph()`
-
-4. **Maildir MCP** (`pkg/maildir/server.go`) - NEW & Optimized
- - **Complete implementation** with email parsing, search, contact analysis
- - **Lazy folder discovery** from day one (never had eager loading issue)
- - **Tools**: `maildir_scan_folders`, `maildir_list_messages`, `maildir_read_message`, `maildir_search_messages`, `maildir_get_thread`, `maildir_analyze_contacts`, `maildir_get_statistics`
-
-5. **Base MCP Server** (`pkg/mcp/server.go`) - Enhanced
- - Added `customRequestHandlers` map (line 33)
- - Added `SetCustomRequestHandler()` method (lines 114-120)
- - Enhanced `handleReadResource()` with pattern matching (lines 344-358)
-
-### **๐ Performance Results**
-
-**Before Optimization:**
-- Startup time: 2-5 seconds for large repositories
-- Memory usage: 500MB+ for filesystem, 40MB+ for memory server
-- Resource discovery: All done at startup (blocking)
-
-**After Optimization:**
-- **Startup time**: <100ms for all servers โก
-- **Memory usage**: <5MB for all servers ๐ฏ
-- **Resource discovery**: On-demand only, sub-20ms response time
-- **Scalability**: Now handles 100K+ files efficiently
-
-### **๐งช Integration Testing Complete**
-
-Created comprehensive test suite (`test/integration_test.go`):
-- **All 7 servers tested**: filesystem, git, memory, fetch, time, sequential-thinking, maildir
-- **Performance verified**: All servers start under 100ms
-- **Resource discovery tested**: All lazy loading working correctly
-- **Functionality verified**: Core tools, prompts, resources, roots all working
-
-**Test Results:**
-- โ
Filesystem: 12 tools, <3ms resource discovery
-- โ
Git: 12 tools, 15ms resource discovery (500 file limit)
-- โ
Memory: 9 tools, <2ms resource discovery
-- โ
Fetch: 1 tool, optimal (already efficient)
-- โ
Time: 2 tools, optimal (already efficient)
-- โ
Sequential-thinking: 5 tools, persistent sessions with file-based storage
-- โ
Maildir: 7 tools, <2ms resource discovery
-
-### **โ๏ธ Configuration Updated**
-
-**~/.claude.json configuration updated** for `/home/mokhax/src/github.com/xlgmokha/mcp` project:
-
-```json
-{
- "mcpServers": {
- "fetch": {
- "command": "/usr/local/bin/mcp-fetch"
- },
- "filesystem": {
- "command": "/usr/local/bin/mcp-filesystem",
- "args": ["--allowed-directory", "/home/mokhax/src/github.com/xlgmokha/mcp"]
- },
- "memory": {
- "command": "/usr/local/bin/mcp-memory"
- },
- "sequential-thinking": {
- "command": "/usr/local/bin/mcp-sequential-thinking",
- "args": ["--session-file", "/tmp/thinking_sessions.json"]
- },
- "time": {
- "command": "/usr/local/bin/mcp-time"
- },
- "git": {
- "command": "/usr/local/bin/mcp-git",
- "args": ["--repository", "/home/mokhax/src/github.com/xlgmokha/mcp"]
- },
- "maildir": {
- "command": "/usr/local/bin/mcp-maildir",
- "args": ["--maildir-path", "/home/mokhax/.local/share/mail/personal"]
- }
- }
-}
-```
-
-### **๐ฆ Installation Ready**
-
-**To install optimized servers**: Use the existing Makefile:
-```bash
-make clean build # Build optimized servers
-sudo make install # Install to /usr/local/bin
-```
-
-All servers are now **production-ready** with:
-- โก **Instant startup** (<100ms)
-- ๐ฏ **Minimal memory** (<5MB per server)
-- ๐ **Lazy loading** (resources discovered on-demand)
-- ๐ **Resource limits** (500 files, 10 branches max to prevent bloat)
-- ๐ **Thread-safe** concurrent access
-- โ
**Comprehensive testing** (integration test suite included)
-
-**RESTART CLAUDE CODE** to use the new optimized servers. The performance improvement will be dramatic!
-
-## ๐ง Signal MCP Server Bug Fix (December 24, 2024)
-
-**CRITICAL BUG FIXED**: The Signal MCP server was crashing with "failed to read encrypted database: file is not a database" error.
-
-**Root Cause**: The MCP Signal server was using the Go SQLCipher library directly with incompatible connection parameters, while the working Signal extractor implementation uses the command-line `sqlcipher` tool.
-
-**Fix Applied**:
-1. **Switched to command-line approach**: Updated `pkg/signal/server.go` to use `exec.Command("sqlcipher")` instead of Go library
-2. **Implemented proper key decryption**: Added AES-CBC + PBKDF2 key decryption matching the working implementation in `/Users/xlgmokha/src/mokhan.ca/xlgmokha/christine/pkg/signal/extractor.go`
-3. **Fixed SQL query formatting**: Resolved JSON parsing issues with SQLCipher output
-4. **Added integration tests**: Signal server now included in test suite with graceful fallback
-5. **Added dependencies**: `golang.org/x/crypto/pbkdf2` for proper key decryption
-
-**Files Modified**:
-- `pkg/signal/server.go`: Complete rewrite of database access method (lines 149-219)
-- `test/integration_test.go`: Added Signal server test coverage (lines 84-88, 385-427)
-- `go.mod`: Added crypto dependency
-
-**Testing**:
-```bash
-# Test Signal conversation listing
-echo '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "signal_list_conversations", "arguments": {}}}' | mcp-signal
-
-# Expected output: Recent conversations with names and timestamps
-# Example: "Christine Michaels-Igbokwe (last active: 2025-06-23 22:25)"
-```
-
-**Result**: Signal MCP server now successfully accesses encrypted Signal Desktop database and can:
-- List recent conversations with contacts and timestamps
-- Search messages by text content
-- Get conversation details
-- Provide database statistics
-
-**Integration**: Signal server is now included in the main integration test suite and will be tested automatically with `make test`.
-
-## ๐ Signal MCP Server - Implementation Complete (Session: 2024-12-23)
-
-**FINAL STATUS: 100% COMPLETE** - All Signal MCP server functionality has been successfully implemented.
-
-### **โ
Complete Feature Implementation**
-
-**All 7 Tools Implemented:**
-- โ
`signal_list_conversations` - List recent conversations with timestamps
-- โ
`signal_search_messages` - Search messages by text content with filters
-- โ
`signal_get_conversation` - Get detailed conversation message history
-- โ
`signal_get_contact` - Get contact details by ID/phone/name with message counts
-- โ
`signal_get_message` - Get specific message with attachments, reactions, quotes
-- โ
`signal_list_attachments` - List message attachments with metadata and filtering
-- โ
`signal_get_stats` - Database statistics and connection info
-
-**All 2 Prompts Implemented:**
-- โ
`signal-conversation` - AI-powered conversation analysis (sentiment, summary, patterns)
-- โ
`signal-search` - Contextual search with AI insights across messages/conversations/contacts
-
-### **๐ฏ Performance Verified**
-
-**Resource Efficiency (Critical Requirements Met):**
-- โ
**Startup Time**: 8ms (requirement: <100ms)
-- โ
**Memory Usage**: 3.1MB peak (requirement: <5MB)
-- โ
**Lazy Loading**: No eager resource discovery confirmed
-- โ
**Thread Safety**: All database operations use command-line sqlcipher safely
-
-### **๐ Documentation & Testing Complete**
-
-**Help Documentation:**
-- โ
Updated `cmd/signal/main.go` help text with all 7 tools and 2 prompts
-- โ
Added tool descriptions and usage examples
-- โ
Included security notes and requirements
-
-**Integration Testing:**
-- โ
Added comprehensive test coverage in `test/integration_test.go`
-- โ
Tests all new tools (signal_get_contact, signal_get_message, signal_list_attachments)
-- โ
Tests prompts/list endpoint for both new prompts
-- โ
Error handling verification for invalid IDs
-- โ
All tests pass and gracefully handle Signal not being available
-
-### **๐ง Implementation Highlights**
-
-**Advanced Signal Features:**
-- **Rich Message Parsing**: Extracts attachments, reactions, quotes, stickers from JSON data
-- **Multi-Scope Search**: Messages, conversations, contacts with time filtering
-- **Intelligent Contact Matching**: Search by ID, phone, name, or profile name
-- **Attachment Management**: Full metadata with size formatting and conversation context
-- **AI-Powered Analysis**: Context-aware prompts for conversation insights and search results
-
-**Database Integration:**
-- **Secure Access**: AES-CBC + PBKDF2 key decryption from macOS Keychain
-- **Command-Line Reliability**: Uses `sqlcipher` CLI for maximum compatibility
-- **JSON Data Processing**: Sophisticated parsing of Signal's complex message structures
-- **Error Handling**: Graceful degradation when Signal or database unavailable
-
-### **๐ Ready for Production Use**
-
-The Signal MCP server is now **production-ready** with:
-- **Complete Functionality**: 7/7 tools and 2/2 prompts fully implemented
-- **High Performance**: Sub-10ms startup, minimal memory footprint
-- **Comprehensive Testing**: Integration tests with error handling
-- **Proper Documentation**: Updated help text and usage examples
-- **Security Compliance**: Encrypted database access with keychain integration
-
-**Usage**: Install with `make build && sudo make install`, then configure in Claude Code with:
-```json
-"signal": {
- "command": "/usr/local/bin/mcp-signal",
- "args": ["--signal-path", "/path/to/Signal"]
-}
-```
-
-## ๐ IMAP MCP Server - Complete Implementation (Session: 2024-12-25)
-
-**FINAL STATUS: 100% COMPLETE** - IMAP MCP server successfully designed and implemented for Gmail, Migadu, and other IMAP providers.
-
-### **โ
Complete Feature Implementation**
-
-**All 12 Tools Implemented:**
-- โ
`imap_list_folders` - List all IMAP folders (INBOX, Sent, Drafts, etc.)
-- โ
`imap_list_messages` - List messages in folder with pagination and filtering
-- โ
`imap_read_message` - Read full message content with headers, body, and metadata
-- โ
`imap_search_messages` - Search messages by content, sender, subject with filters
-- โ
`imap_get_folder_stats` - Get folder statistics (total messages, unread, recent)
-- โ
`imap_mark_as_read` - Mark messages as read/unread with flag management
-- โ
`imap_get_attachments` - List message attachments (placeholder implementation)
-- โ
`imap_get_connection_info` - Server connection status and capabilities
-- โ
`imap_delete_message` - Delete single message with confirmation requirement
-- โ
`imap_delete_messages` - Bulk delete multiple messages with confirmation
-- โ
`imap_move_to_trash` - Safe delete by moving to trash folder
-- โ
`imap_expunge_folder` - Permanently remove deleted messages from folder
-
-**All 2 Prompts Implemented:**
-- โ
`email-analysis` - AI-powered email content analysis (sentiment, summary, action items)
-- โ
`email-search` - Contextual email search with AI insights and strategy
-
-### **๐ฏ Performance Verified**
-
-**Resource Efficiency (Critical Requirements Met):**
-- โ
**Startup Time**: 9.8ms (requirement: <100ms)
-- โ
**Memory Usage**: <5MB estimated (follows lazy loading pattern)
-- โ
**Lazy Loading**: No eager connection - connects only when tools are called
-- โ
**Thread Safety**: All IMAP operations use connection mutex for safety
-
-### **๐ Documentation & Testing Complete**
-
-**Help Documentation:**
-- โ
Comprehensive help text in `cmd/imap/main.go` with all 8 tools and 2 prompts
-- โ
Usage examples for Gmail and Migadu connections
-- โ
Environment variable support and security notes
-- โ
Command-line flag documentation
-
-**Integration Testing:**
-- โ
Added to comprehensive test coverage in `test/integration/main_test.go`
-- โ
Tests initialization, tool listing, and performance benchmarks
-- โ
Graceful handling when IMAP server connection fails
-- โ
All tests pass with 9.8ms startup time
-
-### **๐ง Implementation Highlights**
-
-**Advanced IMAP Features:**
-- **Multi-Provider Support**: Gmail (app passwords), Migadu, and generic IMAP servers
-- **Secure Authentication**: TLS/SSL encryption by default with STARTTLS fallback
-- **Rich Message Parsing**: Full email headers, body content, and address formatting
-- **Flexible Search**: Content, sender, subject filtering with IMAP search criteria
-- **Connection Management**: Thread-safe connection handling with automatic reconnection
-- **Flag Management**: Read/unread status updates with proper IMAP flag operations
-
-**Email Processing:**
-- **Complete Message Info**: UID, sequence numbers, subjects, sender/recipient parsing
-- **Date Handling**: Proper timezone-aware date parsing and formatting
-- **Size Information**: Message size reporting for bandwidth awareness
-- **Folder Statistics**: Comprehensive folder metrics (total, recent, unseen messages)
-
-### **๐ Ready for Production Use**
-
-The IMAP MCP server is now **production-ready** with:
-- **Complete Functionality**: 12/12 tools and 2/2 prompts fully implemented
-- **High Performance**: Sub-10ms startup, minimal memory footprint
-- **Comprehensive Testing**: Integration tests with graceful error handling
-- **Proper Documentation**: Updated help text and usage examples
-- **Security Compliance**: TLS encryption and credential management via environment variables
-- **Safe Deletion**: Confirmation prompts and trash folder support for AI-assisted email management
-
-**Dependencies Added:**
-- `github.com/emersion/go-imap v1.2.1` - Professional IMAP client library
-- `github.com/emersion/go-sasl` - SASL authentication support (indirect)
-
-**Usage Examples:**
-```bash
-# Gmail connection with app password
-mcp-imap --server imap.gmail.com --username user@gmail.com --password app-password
-
-# Migadu connection
-mcp-imap --server mail.migadu.com --username user@domain.com --password password
-
-# Environment variable configuration
-export IMAP_SERVER=imap.gmail.com
-export IMAP_USERNAME=user@gmail.com
-export IMAP_PASSWORD=app-password
-mcp-imap
-```
-
-**Claude Code Configuration:**
-```json
-"imap": {
- "command": "/usr/local/bin/mcp-imap",
- "args": ["--server", "imap.gmail.com", "--username", "user@gmail.com", "--password", "app-password"]
-}
-```
-
-**OR with environment variables:**
-```json
-"imap": {
- "command": "/usr/local/bin/mcp-imap",
- "env": {
- "IMAP_SERVER": "imap.gmail.com",
- "IMAP_USERNAME": "user@gmail.com",
- "IMAP_PASSWORD": "app-password"
- }
-}
-```
-
-### **๐ค AI-Assisted Email Management**
-
-**The IMAP MCP server now enables powerful AI-driven email cleanup and management:**
-
-**Safe Deletion Workflow:**
-1. **Search**: Use `imap_search_messages` to find emails matching criteria
-2. **Review**: Use `imap_read_message` to analyze specific emails
-3. **Safe Delete**: Use `imap_move_to_trash` to move to Gmail Trash (reversible)
-4. **Permanent**: Use `imap_delete_message` with confirmation for permanent removal
-
-**AI Assistant Use Cases:**
-- *"Delete all promotional emails older than 6 months"*
-- *"Remove unread newsletters from last year"*
-- *"Clean up emails from specific senders"*
-- *"Delete emails matching certain subject patterns"*
-- *"Find and remove large attachments to free space"*
-
-**Safety Features:**
-- **Confirmation Required**: All permanent deletions require `confirmed: true`
-- **Trash by Default**: `imap_move_to_trash` for safe, reversible deletion
-- **Bulk Operations**: Handle multiple messages efficiently with UIDs array
-- **Warning Messages**: Clear warnings about permanent actions
-
-**Example AI Workflow:**
-```
-1. Search: "Find all emails from newsletters older than 1 year"
-2. Review: "Show me 5 examples of what would be deleted"
-3. Safe Delete: "Move these 150 newsletter emails to trash"
-4. Confirm: "If everything looks good, permanently delete from trash"
-```
-
-**Security Notes:**
-- Use app passwords for Gmail (not main account password)
-- Consider environment variables for credential management
-- All connections use TLS encryption by default
-- Credentials are not logged or stored persistently by the server
-- Deletion operations require explicit confirmation for safety
-
-## ๐ GitLab MCP Server with Intelligent Caching - Complete Implementation (Session: 2025-06-25)
-
-**FINAL STATUS: 100% COMPLETE** - GitLab MCP server successfully enhanced with comprehensive local caching system.
-
-### **โ
Complete Caching Implementation**
-
-**All 8 Tools Implemented:**
-- โ
`gitlab_list_my_projects` - List projects with activity info (cached automatically)
-- โ
`gitlab_list_my_issues` - Issues assigned/authored/mentioned (cached automatically)
-- โ
`gitlab_get_issue_conversations` - Full conversation threads (cached automatically)
-- โ
`gitlab_find_similar_issues` - Cross-project similarity search (cached automatically)
-- โ
`gitlab_get_my_activity` - Recent activity summary (cached automatically)
-- โ
`gitlab_cache_stats` - View cache performance and storage statistics
-- โ
`gitlab_cache_clear` - Clear specific cache types or all cached data
-- โ
`gitlab_offline_query` - Query cached data when network is unavailable
-
-### **๐ฏ Caching Architecture**
-
-**Cache Storage Structure:**
-```
-~/.mcp/gitlab/
-โโโ issues/ # Issue data cache with sharded subdirectories
-โ โโโ ab/ # Sharded by hash prefix for performance
-โ โโโ cd/
-โโโ projects/ # Project data cache
-โโโ users/ # User data cache
-โโโ notes/ # Comment/note cache
-โโโ events/ # Activity events cache
-โโโ search/ # Search results cache
-โโโ metadata.json # Cache statistics and performance metrics
-```
-
-**Advanced Features:**
-- **Automatic Caching**: All GET requests cached transparently with 5-minute TTL
-- **Thread-Safe Operations**: Concurrent access with proper mutex locking
-- **Intelligent Merge Strategies**: Replace, append, or diff-based cache updates
-- **Offline Mode**: Returns stale data when network is unavailable
-- **Performance Monitoring**: Hit rates, entry counts, storage size tracking
-- **Sharded Storage**: Hash-based file organization for optimal performance
-
-### **๐ Performance Results**
-
-**Cache Benefits Verified:**
-- โ
**Instant Responses**: Cached data returned immediately (0ms network time)
-- โ
**Reduced API Calls**: Significant bandwidth savings for repeated queries
-- โ
**Offline Capability**: Continue working without network connectivity
-- โ
**Storage Efficiency**: Sharded files with metadata tracking
-- โ
**Thread Safety**: Safe concurrent access from multiple tools
-
-### **๐ Usage Examples**
-
-**Automatic Caching (Transparent):**
-```bash
-# First call - fetches from GitLab API and caches
-gitlab_list_my_issues
-
-# Subsequent calls - instant response from cache
-gitlab_list_my_issues
-```
-
-**Cache Management:**
-```bash
-# View cache statistics
-gitlab_cache_stats
-
-# Clear specific cache type
-gitlab_cache_clear {"cache_type": "issues", "confirm": "true"}
-
-# Clear all cached data
-gitlab_cache_clear {"confirm": "true"}
-
-# Query cached data offline
-gitlab_offline_query {"query_type": "issues", "search": "bug"}
-```
-
-### **๐ง Configuration and Setup**
-
-**Installation:**
-```bash
-make gitlab # Build GitLab MCP server
-sudo make install # Install to /usr/local/bin
-```
-
-**Claude Code Configuration:**
-```json
-{
- "mcpServers": {
- "gitlab": {
- "command": "/usr/local/bin/mcp-gitlab",
- "args": ["--gitlab-token", "your_token"],
- "env": {
- "GITLAB_URL": "https://gitlab.com"
- }
- }
- }
-}
-```
-
-**Environment Variables:**
-- `GITLAB_TOKEN`: Personal Access Token (recommended via export-access-token)
-- `GITLAB_URL`: GitLab instance URL (default: https://gitlab.com)
-
-### **๐งช Testing Complete**
-
-**Integration Testing:**
-- โ
Added to comprehensive test suite in `test/integration_test.go`
-- โ
All 8 tools verified and functional
-- โ
Cache layer tested with multiple scenarios
-- โ
Performance benchmarks passing
-- โ
Help text updated with cache management tools
-
-**Test Results:**
-- โ
GitLab Server: 8 tools, <5ms startup time
-- โ
Cache Operations: Basic set/get, expiration, offline mode, file structure
-- โ
Integration: Protocol compliance, tool listing, resource discovery
-
-### **๐ Ready for Production Use**
-
-The GitLab MCP server is now **production-ready** with:
-- **Complete Functionality**: 8/8 tools fully implemented with caching
-- **High Performance**: Automatic caching with configurable TTL
-- **Comprehensive Testing**: Unit tests and integration test coverage
-- **Proper Documentation**: Updated help text and usage examples
-- **Offline Capability**: Intelligent cache management for network-independent operation
-
-**Key Benefits for GitLab Workflow:**
-- *"List my issues"* - Instant response from cache after first fetch
-- *"Get issue conversations"* - Cached conversation threads for offline review
-- *"Find similar issues"* - Cached search results for pattern recognition
-- *"Check my activity"* - Cached activity summaries for productivity tracking
-
-**Cache automatically activated** - Your existing GitLab tools are now faster and work offline!
-
-## ๐ Speech MCP Server - Cross-Platform TTS Support (Session: 2025-07-08)
-
-**FINAL STATUS: 100% COMPLETE** - Speech MCP server successfully updated for cross-platform support.
-
-### **โ
Complete Cross-Platform Implementation**
-
-**Updated Architecture:**
-- โ
**TTSBackend Interface** - Abstract interface for different TTS systems
-- โ
**MacOSBackend** - Uses built-in `say` command (unchanged functionality)
-- โ
**LinuxBackend** - Uses `espeak-ng` (preferred) or `espeak` (fallback)
-- โ
**UnsupportedBackend** - Graceful handling for other operating systems
-- โ
**Automatic Detection** - Server selects appropriate backend based on OS
-
-**All 5 Tools Now Cross-Platform:**
-- โ
`say` - Text-to-speech with voice, rate, volume, and file output options
-- โ
`list_voices` - Platform-specific voice listing (macOS/Linux)
-- โ
`speak_file` - Read and speak file contents with line limiting
-- โ
`stop_speech` - Stop playing speech (platform-specific process killing)
-- โ
`speech_settings` - Show platform info, installation instructions, and usage help
-
-### **๐ฏ Platform Support Matrix**
-
-**macOS Support (Existing):**
-- โ
**Backend**: Built-in `say` command
-- โ
**Installation**: No setup required (already available)
-- โ
**Output Formats**: .aiff, .wav, .m4a
-- โ
**Voice Examples**: Alex, Samantha, Victoria, Fred, Fiona, Moira
-
-**Linux Support (New):**
-- โ
**Backend**: espeak-ng (preferred) or espeak (fallback)
-- โ
**Installation**: `sudo apt install espeak-ng` (Ubuntu/Debian), `sudo dnf install espeak-ng` (Fedora/RHEL)
-- โ
**Output Formats**: .wav only
-- โ
**Voice Examples**: en-gb, en-us, en-gb-scotland, various languages
-
-**Other Platforms:**
-- โ
**Backend**: UnsupportedBackend with helpful error messages
-- โ
**Behavior**: Shows installation guidance and platform support info
-
-### **๐ Updated Documentation**
-
-**Help Text Enhanced:**
-- โ
Cross-platform usage examples in `cmd/speech/main.go`
-- โ
Platform-specific installation instructions
-- โ
Backend detection and availability information
-- โ
Voice examples for both macOS and Linux
-
-**Tests Updated:**
-- โ
Backend abstraction tests for all platforms
-- โ
Cross-platform availability detection
-- โ
Graceful error handling when TTS not available
-- โ
Platform-specific backend selection verification
-
-### **๐ Ready for Production Use**
-
-The Speech MCP server is now **truly cross-platform** with:
-- **Complete Functionality**: Works on macOS and Linux with native TTS
-- **Graceful Degradation**: Helpful messages on unsupported platforms
-- **Consistent API**: Same tool interface across all platforms
-- **Installation Guide**: Clear setup instructions in help text
-- **Backend Detection**: Automatic selection of best available TTS system
-
-**Linux Usage (New):**
-```bash
-# Install TTS engine (Ubuntu/Debian)
-sudo apt install espeak-ng
-
-# Run speech server
-mcp-speech
-
-# Test with Claude Code integration
-{"name": "say", "arguments": {"text": "Hello from Linux!", "voice": "en-gb"}}
-```
-
-**macOS Usage (Unchanged):**
-```bash
-# No installation needed
-mcp-speech
-
-# Test with existing voices
-{"name": "say", "arguments": {"text": "Hello from macOS!", "voice": "Samantha"}}
-```
-
-The speech server transformation from macOS-only to cross-platform is now complete!
-
-## ๐ Future Enhancement Ideas
-
-This section tracks potential improvements and new features for the MCP server ecosystem.
-
-### **Voice Cloning Enhancement for Speech MCP Server**
-
-**Concept**: Extend the existing `mcp-speech` server to support custom voice cloning that sounds exactly like the user.
-
-**Implementation Ideas**:
-- **New Tools**: `train_voice`, `clone_voice`, `use_custom_voice`, `manage_voices`
-- **Integration Options**:
- - ElevenLabs API integration (5 minutes of audio โ high-quality clone)
- - Local Coqui TTS integration (open source, privacy-focused)
- - macOS built-in voice training enhancement
-- **Technical Approach**: Extend existing speech server architecture to support custom voice models
-- **Data Requirements**: 5-30 minutes of varied, clean speech samples
-- **Benefits**: Personalized AI responses, better accessibility, enhanced user experience
-
-**Status**: Concept recorded, not yet prioritized for implementation
-
-*Add new ideas below this section as they arise...*
\ No newline at end of file
DESIGN.md
@@ -1,286 +0,0 @@
-# MCP Design Document: Go Implementation
-
-**Project Name:** `mcp`
-**Target Repository:** `https://github.com/xlgmokha/mcp`
-**Primary Language:** Go
-**Structure:** Multi-command architecture using `cmd/` folder to organize multiple protocol servers.
-
----
-
-## ๐ Overall Project Structure
-
-```
-mcp/
-โโโ go.mod
-โโโ go.sum
-โโโ README.md
-โโโ internal/
-โ โโโ shared/ # Reusable logic/utilities across commands
-โโโ pkg/
-โ โโโ mcp/ # Shared MCP protocol tooling (e.g., request/response structs)
-โโโ cmd/
- โโโ git/
- โ โโโ main.go
- โ โโโ handler.go
- โ โโโ gitutils.go
- โโโ <future-server>/
- โ โโโ main.go
-```
-
----
-
-## ๐ Phase 1: Implementing `cmd/git`
-
-### Objective
-
-Replicate the functionality of the [Python git server](https://github.com/modelcontextprotocol/servers/tree/main/src/git) from the MCP project in Go.
-
-### Features
-
-- Accept stdin JSON requests conforming to the MCP protocol.
-- Supported commands:
- - `clone`: Clone a remote Git repository.
- - `list`: List files in the repo (default to `HEAD`).
- - `read`: Read file contents at specific paths.
- - `head`: Show latest commit hash.
-- Return responses as JSON to stdout.
-- Print logs and errors to stderr.
-- Work as a CLI tool or JSON-RPC-compatible stdin/stdout process.
-
-### External Dependencies
-
-- `go-git` ([https://github.com/go-git/go-git](https://github.com/go-git/go-git)) for Git operations.
-- Optional: `spf13/cobra` if CLI argument parsing is needed in the future.
-
-### Internal Structure for Git Server
-
-```
-cmd/git/
-โโโ main.go # Entry point
-โโโ handler.go # Routes incoming MCP requests
-โโโ gitutils.go # Handles Git-specific operations
-```
-
-### JSON Message Format
-
-#### Request (stdin)
-```json
-{
- "action": "list",
- "repo": "https://github.com/modelcontextprotocol/servers",
- "ref": "HEAD"
-}
-```
-
-#### Response (stdout)
-```json
-{
- "status": "ok",
- "files": ["README.md", "src/git/main.py"]
-}
-```
-
-#### Error (stdout)
-```json
-{
- "status": "error",
- "error": "failed to clone repository"
-}
-```
-
----
-
-## ๐ง Design Principles
-
-- **Modular Design**: Each server lives in `cmd/` as an isolated command.
-- **Stream IO**: Communicate via stdin/stdout with JSON.
-- **Minimal Dependencies**: Use Go standard library and `go-git`.
-- **Testable**: Extract core logic into testable units.
-
----
-
-## ๐ฎ Future Commands
-
-- `cmd/fs/main.go`: Local filesystem server.
-- `cmd/bash/main.go`: Bash command interpreter.
-- `cmd/sql/main.go`: SQL query server.
-- `cmd/http/main.go`: HTTP API interaction.
-- `cmd/gpt/main.go`: LLM invocation.
-- `cmd/py/main.go`: Python code runner.
-
----
-
-## โ
Acceptance Criteria
-
-- `go run cmd/git/main.go` processes JSON stdin and performs Git tasks correctly.
-- Errors are written to stderr and returned as JSON.
-- Repo contains:
- - `README.md` with instructions.
- - Example JSON inputs/outputs.
- - Template to add new MCP command servers under `cmd/`.
-
----
-
-## ๐ Bootstrap Code: Go MCP Git Server
-
-### `cmd/git/main.go`
-```go
-package main
-
-import (
- "encoding/json"
- "fmt"
- "os"
-
- "mcp/cmd/git"
-)
-
-func main() {
- var req git.Request
- decoder := json.NewDecoder(os.Stdin)
- encoder := json.NewEncoder(os.Stdout)
-
- if err := decoder.Decode(&req); err != nil {
- fmt.Fprintf(os.Stderr, "decode error: %v\n", err)
- encoder.Encode(git.ErrorResponse("invalid JSON input"))
- return
- }
-
- resp := git.HandleRequest(req)
- encoder.Encode(resp)
-}
-```
-
-### `cmd/git/handler.go`
-```go
-package git
-
-type Request struct {
- Action string `json:"action"`
- Repo string `json:"repo"`
- Path string `json:"path,omitempty"`
- Ref string `json:"ref,omitempty"`
-}
-
-type Response map[string]interface{}
-
-func ErrorResponse(msg string) Response {
- return Response{"status": "error", "error": msg}
-}
-
-func OkResponse(data map[string]interface{}) Response {
- data["status"] = "ok"
- return data
-}
-
-func HandleRequest(req Request) Response {
- switch req.Action {
- case "clone":
- return handleClone(req)
- case "list":
- return handleList(req)
- case "read":
- return handleRead(req)
- case "head":
- return handleHead(req)
- default:
- return ErrorResponse("unsupported action")
- }
-}
-```
-
-### `cmd/git/gitutils.go`
-```go
-package git
-
-import (
- "fmt"
- "io/ioutil"
- "os"
- "path/filepath"
-
- gitlib "github.com/go-git/go-git/v5"
-)
-
-func cloneRepo(url string) (string, error) {
- dir, err := ioutil.TempDir("", "mcp-git-")
- if err != nil {
- return "", err
- }
-
- _, err = gitlib.PlainClone(dir, false, &gitlib.CloneOptions{
- URL: url,
- Progress: os.Stderr,
- })
- return dir, err
-}
-
-func handleClone(req Request) Response {
- dir, err := cloneRepo(req.Repo)
- if err != nil {
- return ErrorResponse(fmt.Sprintf("clone failed: %v", err))
- }
- return OkResponse(map[string]interface{}{ "path": dir })
-}
-
-func handleList(req Request) Response {
- dir, err := cloneRepo(req.Repo)
- if err != nil {
- return ErrorResponse(fmt.Sprintf("clone failed: %v", err))
- }
- defer os.RemoveAll(dir)
-
- var files []string
- err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
- if err != nil {
- return err
- }
- if !info.IsDir() {
- rel, _ := filepath.Rel(dir, path)
- files = append(files, rel)
- }
- return nil
- })
-
- if err != nil {
- return ErrorResponse(fmt.Sprintf("walk failed: %v", err))
- }
-
- return OkResponse(map[string]interface{}{ "files": files })
-}
-
-func handleRead(req Request) Response {
- dir, err := cloneRepo(req.Repo)
- if err != nil {
- return ErrorResponse(fmt.Sprintf("clone failed: %v", err))
- }
- defer os.RemoveAll(dir)
-
- content, err := os.ReadFile(filepath.Join(dir, req.Path))
- if err != nil {
- return ErrorResponse(fmt.Sprintf("read failed: %v", err))
- }
-
- return OkResponse(map[string]interface{}{ "path": req.Path, "content": string(content) })
-}
-
-func handleHead(req Request) Response {
- dir, err := cloneRepo(req.Repo)
- if err != nil {
- return ErrorResponse(fmt.Sprintf("clone failed: %v", err))
- }
- defer os.RemoveAll(dir)
-
- r, err := gitlib.PlainOpen(dir)
- if err != nil {
- return ErrorResponse(fmt.Sprintf("open failed: %v", err))
- }
-
- h, err := r.Head()
- if err != nil {
- return ErrorResponse(fmt.Sprintf("head failed: %v", err))
- }
-
- return OkResponse(map[string]interface{}{ "ref": h.Name().String(), "hash": h.Hash().String() })
-}
-```
install.sh
@@ -1,203 +0,0 @@
-#!/bin/bash
-
-# Go MCP Servers Installation Script
-# Builds and installs all MCP servers for drop-in replacement
-
-set -e
-
-# Colors for output
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-BLUE='\033[0;34m'
-NC='\033[0m' # No Color
-
-# Configuration
-INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}"
-BUILD_DIR="bin"
-SERVERS=("git" "filesystem" "fetch" "memory" "sequential-thinking" "time")
-
-# Print colored output
-print_status() {
- echo -e "${BLUE}[INFO]${NC} $1"
-}
-
-print_success() {
- echo -e "${GREEN}[SUCCESS]${NC} $1"
-}
-
-print_warning() {
- echo -e "${YELLOW}[WARNING]${NC} $1"
-}
-
-print_error() {
- echo -e "${RED}[ERROR]${NC} $1"
-}
-
-# Check if Go is installed
-check_go() {
- if ! command -v go &> /dev/null; then
- print_error "Go is not installed. Please install Go 1.21 or later."
- print_status "Visit: https://golang.org/doc/install"
- exit 1
- fi
-
- go_version=$(go version | cut -d' ' -f3 | sed 's/go//')
- print_status "Found Go version: $go_version"
-}
-
-# Check if we have write permissions to install directory
-check_permissions() {
- if [[ ! -w "$INSTALL_DIR" ]]; then
- print_warning "No write permission to $INSTALL_DIR"
- print_status "You may need to run with sudo or set INSTALL_DIR to a writable location"
- print_status "Example: INSTALL_DIR=~/.local/bin $0"
-
- if [[ $EUID -ne 0 ]]; then
- print_status "Re-running with sudo..."
- exec sudo "$0" "$@"
- fi
- fi
-}
-
-# Build all servers
-build_servers() {
- print_status "Building MCP servers..."
-
- if ! make build; then
- print_error "Build failed"
- exit 1
- fi
-
- print_success "All servers built successfully"
-}
-
-# Install servers
-install_servers() {
- print_status "Installing servers to $INSTALL_DIR..."
-
- mkdir -p "$INSTALL_DIR"
-
- for server in "${SERVERS[@]}"; do
- binary="$BUILD_DIR/mcp-$server"
- target="$INSTALL_DIR/mcp-$server"
-
- if [[ ! -f "$binary" ]]; then
- print_error "Binary $binary not found"
- exit 1
- fi
-
- cp "$binary" "$target"
- chmod +x "$target"
- print_status "Installed mcp-$server"
- done
-
- print_success "All servers installed to $INSTALL_DIR"
-}
-
-# Test installations
-test_installation() {
- print_status "Testing installations..."
-
- for server in "${SERVERS[@]}"; do
- binary="$INSTALL_DIR/mcp-$server"
- if [[ -x "$binary" ]]; then
- print_success "โ mcp-$server is executable"
- else
- print_error "โ mcp-$server is not executable"
- exit 1
- fi
- done
-}
-
-# Show configuration example
-show_config() {
- print_status "Installation complete!"
- echo
- print_status "Add these servers to your Claude Code configuration (~/.claude.json):"
- echo
- cat << 'EOF'
-{
- "mcpServers": {
- "git": {
- "command": "mcp-git",
- "args": ["--repository", "/path/to/your/repo"]
- },
- "filesystem": {
- "command": "mcp-filesystem",
- "args": ["/path/to/allowed/directory"]
- },
- "fetch": {
- "command": "mcp-fetch"
- },
- "memory": {
- "command": "mcp-memory"
- },
- "sequential-thinking": {
- "command": "mcp-sequential-thinking"
- },
- "time": {
- "command": "mcp-time"
- }
- }
-}
-EOF
- echo
- print_status "For more configuration options, see: README.md"
-}
-
-# Main installation flow
-main() {
- echo "๐ Go MCP Servers Installation"
- echo "=================================="
- echo
-
- check_go
- check_permissions
- build_servers
- install_servers
- test_installation
- show_config
-
- echo
- print_success "๐ Installation completed successfully!"
- print_status "Servers are installed in: $INSTALL_DIR"
-}
-
-# Handle command line arguments
-case "${1:-}" in
- --help|-h)
- echo "Go MCP Servers Installation Script"
- echo
- echo "Usage: $0 [OPTIONS]"
- echo
- echo "Options:"
- echo " --help, -h Show this help message"
- echo " --uninstall Uninstall MCP servers"
- echo
- echo "Environment Variables:"
- echo " INSTALL_DIR Installation directory (default: /usr/local/bin)"
- echo
- echo "Examples:"
- echo " $0 # Install to /usr/local/bin"
- echo " INSTALL_DIR=~/.local/bin $0 # Install to user directory"
- exit 0
- ;;
- --uninstall)
- print_status "Uninstalling MCP servers from $INSTALL_DIR..."
- for server in "${SERVERS[@]}"; do
- rm -f "$INSTALL_DIR/mcp-$server"
- print_status "Removed mcp-$server"
- done
- print_success "Uninstall complete!"
- exit 0
- ;;
- "")
- main
- ;;
- *)
- print_error "Unknown option: $1"
- print_status "Use --help for usage information"
- exit 1
- ;;
-esac
\ No newline at end of file
Makefile
@@ -11,7 +11,7 @@ BINDIR = bin
INSTALLDIR = /usr/local/bin
# Server binaries
-SERVERS = git filesystem fetch memory sequential-thinking time maildir signal gitlab imap bash packages speech semantic
+SERVERS = git filesystem fetch memory sequential-thinking time maildir signal gitlab imap bash speech semantic
BINARIES = $(addprefix $(BINDIR)/mcp-,$(SERVERS))
# Build flags
@@ -110,7 +110,6 @@ signal: $(BINDIR)/mcp-signal ## Build signal server only
gitlab: $(BINDIR)/mcp-gitlab ## Build gitlab server only
imap: $(BINDIR)/mcp-imap ## Build imap server only
bash: $(BINDIR)/mcp-bash ## Build bash server only
-packages: $(BINDIR)/mcp-packages ## Build packages server only
speech: $(BINDIR)/mcp-speech ## Build speech server only
semantic: $(BINDIR)/mcp-semantic ## Build semantic server only
@@ -120,4 +119,4 @@ help: ## Show this help message
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
@echo ""
@echo "Individual servers:"
- @echo " git, filesystem, fetch, memory, sequential-thinking, time, maildir, signal, gitlab, imap, bash, packages, speech, semantic"
+ @echo " git, filesystem, fetch, memory, sequential-thinking, time, maildir, signal, gitlab, imap, bash, speech, semantic"
PLAN.md
@@ -1,199 +0,0 @@
-# MCP Go Implementation Enhancement Plan
-
-This plan tracks the implementation of advanced features to achieve better feature parity with the reference MCP implementations.
-
-## Status Legend
-- โ Not Started
-- ๐ก In Progress
-- โ
Completed
-- ๐ซ Blocked
-
----
-
-## Phase 1: Advanced HTML Processing โ
-
-**Goal**: Improve content extraction quality in fetch server
-
-### Tasks:
-- [x] Add goquery dependency (`go get github.com/PuerkitoBio/goquery`)
-- [x] Add html-to-markdown dependency (`go get github.com/JohannesKaufmann/html-to-markdown`)
-- [x] Create `pkg/htmlprocessor/processor.go` with ContentExtractor
-- [x] Implement `ExtractReadableContent()` method using goquery
-- [x] Implement `ToMarkdown()` method with better conversion
-- [x] Update `cmd/fetch/main.go` to use new HTML processor
-- [x] Test with various HTML content types
-
-### Files Created/Modified:
-- โ
`pkg/htmlprocessor/processor.go` (new)
-- โ
`pkg/htmlprocessor/processor_test.go` (new)
-- โ
`pkg/fetch/server.go` (modified to use new processor)
-- โ
`go.mod` (dependencies added)
-
-### Results:
-- Significantly improved HTML content extraction
-- Better markdown conversion with proper formatting
-- Automatic filtering of ads, navigation, scripts, styles
-- Comprehensive test coverage
-- 137 lines of old HTML processing code removed and replaced with 13 lines using new processor
-
----
-
-## Phase 2: Prompts Support โ
-
-**Goal**: Enable interactive prompts across servers
-
-### Tasks:
-- [x] Create `pkg/mcp/prompts.go` with base prompt structures
-- [x] Add prompt support to `pkg/mcp/server.go` BaseServer
-- [x] Implement `list_prompts` method in BaseServer
-- [x] Implement `get_prompt` method in BaseServer
-- [x] Add `fetch` prompt to fetch server for manual URL entry
-- [x] Add `commit-message` prompt to git server
-- [x] Add `edit-file` prompt to filesystem server
-- [x] Test prompt functionality with sample clients
-
-### Files Created/Modified:
-- โ
`pkg/mcp/prompts_test.go` (new - comprehensive tests)
-- โ
`pkg/mcp/server_prompts_test.go` (new - server prompt tests)
-- โ
`pkg/mcp/server.go` (modified - added prompt infrastructure)
-- โ
`pkg/fetch/server.go` (modified - added fetch prompt)
-- โ
`pkg/git/server.go` (modified - added commit-message prompt)
-- โ
`pkg/filesystem/server.go` (modified - added edit-file prompt)
-
-### Results:
-- **3 Interactive Prompts** implemented across all servers
-- **Complete MCP protocol compliance** for prompts capability
-- **Comprehensive error handling** and argument validation
-- **User/assistant conversation flows** for guided interactions
-- **Security validation** for filesystem prompts (allowed directories)
-
-### Prompts Implemented:
-1. **fetch**: Interactive URL entry with optional reason context
-2. **commit-message**: Conventional commit format guidance with breaking change support
-3. **edit-file**: Step-by-step file editing workflow with security validation
-
----
-
-## Phase 3: Resources Support โ
-
-**Goal**: Enable resource discovery and access
-
-### Tasks:
-- [x] Create `pkg/mcp/resources.go` with Resource structures
-- [x] Add ResourceHandler interface and support to BaseServer
-- [x] Implement `list_resources` method in BaseServer
-- [x] Implement `read_resource` method in BaseServer
-- [x] Add `file://` resource support to filesystem server
-- [x] Add `git://` resource support to git server
-- [x] Add `memory://` resource support to memory server
-- [x] Test resource discovery and access
-
-### Files Created/Modified:
-- โ
`pkg/mcp/resources.go` (new - Resource structures and types)
-- โ
`pkg/mcp/resources_test.go` (new - comprehensive resource tests)
-- โ
`pkg/mcp/server_resources_test.go` (new - server resource tests)
-- โ
`pkg/mcp/server.go` (modified - added resource infrastructure)
-- โ
`pkg/filesystem/server.go` (modified - added file:// resources)
-- โ
`pkg/git/server.go` (modified - added git:// resources)
-- โ
`pkg/memory/server.go` (modified - added memory:// resources and knowledge-query prompt)
-
-### Results:
-- **Complete MCP Resources capability** implemented with 3 URI schemes
-- **file:// resources**: Automatic discovery of files in allowed directories with MIME type detection
-- **git:// resources**: Repository files, branches, and commits accessible as resources
-- **memory:// resources**: Knowledge graph entities and relations exposed as JSON resources
-- **Thread-safe implementation** with proper mutex locking
-- **Security validation** for all resource access
-- **Comprehensive test coverage** including invalid URI handling
-
-### Resources Implemented:
-1. **file:// scheme**: Direct filesystem access with security validation
-2. **git:// scheme**: Repository browsing including files, branches, and commits
-3. **memory:// scheme**: Knowledge graph exploration with entity and relation access
-
----
-
-## Phase 4: Roots Support โ
-
-**Goal**: Enable root capability negotiation
-
-### Tasks:
-- [x] Create `pkg/mcp/roots.go` with Root structures
-- [x] Add roots support to BaseServer
-- [x] Implement `list_roots` method in BaseServer
-- [x] Add filesystem root configuration
-- [x] Add git repository root discovery
-- [x] Add memory graph root support
-- [x] Test root capability negotiation
-- [x] Update server initialization to announce roots
-
-### Files Created/Modified:
-- โ
`pkg/mcp/roots.go` (new - Root helper functions and interfaces)
-- โ
`pkg/mcp/roots_test.go` (new - comprehensive root tests)
-- โ
`pkg/mcp/roots_integration_test.go` (new - integration tests across all servers)
-- โ
`pkg/mcp/server_roots_test.go` (new - server root functionality tests)
-- โ
`pkg/mcp/types.go` (modified - added Roots to ServerCapabilities)
-- โ
`pkg/mcp/server.go` (modified - added complete roots infrastructure)
-- โ
`pkg/filesystem/server.go` (modified - added filesystem root registration)
-- โ
`pkg/git/server.go` (modified - added git repository root registration)
-- โ
`pkg/memory/server.go` (modified - added memory graph root registration with dynamic updates)
-
-### Results:
-- **Complete MCP Roots capability** implemented with automatic discovery
-- **Filesystem roots**: Each allowed directory registered as `file://` root with user-friendly names
-- **Git repository roots**: Repository path and current branch info exposed as `git://` root
-- **Memory graph roots**: Knowledge graph statistics exposed as `memory://` root with live updates
-- **Dynamic root updates**: Memory server automatically updates root info when graph changes
-- **Thread-safe implementation** with proper mutex locking for concurrent access
-- **Comprehensive testing** including integration tests and concurrency tests
-
-### Roots Implemented:
-1. **file:// scheme**: Directory-based roots for filesystem access points
-2. **git:// scheme**: Repository-based roots with branch information
-3. **memory:// scheme**: Knowledge graph roots with live entity/relation counts
-
----
-
-## Implementation Notes
-
-### Dependencies to Add:
-```bash
-go get github.com/PuerkitoBio/goquery
-go get github.com/JohannesKaufmann/html-to-markdown
-```
-
-### Key Design Principles:
-1. Maintain backward compatibility with existing tools
-2. Follow existing code patterns in the codebase
-3. Add proper error handling for all new features
-4. Keep JSON-RPC protocol compliance
-5. Test each phase independently
-
-### Testing Strategy:
-- Unit tests for each new package
-- Integration tests with sample MCP clients
-- Manual testing with reference implementations
-- Regression testing of existing functionality
-
----
-
-## Progress Tracking
-
-**Overall Progress**: 4/4 phases completed (100%) ๐
-
-**Last Updated**: Phase 4 completed - Roots Support fully implemented and tested
-**Status**: โ
COMPLETE - All planned MCP enhancements successfully implemented
-**Achievement**: Full feature parity with reference implementations for the requested capabilities (1.3, 2.1, 2.2, 2.3)
-
----
-
-## Notes for Continuation
-
-If Claude Code needs to be restarted:
-1. Check this PLAN.md for current progress status
-2. Review the "Last Updated" section for context
-3. Continue from the current phase's next unchecked task
-4. Update progress markers (โ/๐ก/โ
) as work completes
-5. Update "Last Updated" section with current status
-
-This plan focuses on the core MCP protocol enhancements that will make the Go implementation a powerful development tool, skipping enterprise features like robots.txt compliance and proxy support that aren't needed for personal development productivity.
\ No newline at end of file
README.md
@@ -4,30 +4,6 @@
A pure Go implementation of Model Context Protocol (MCP) servers, providing drop-in replacements for the Python MCP servers with zero dependencies and static linking.
-## Features
-
-- **Zero Dependencies**: Statically linked binaries with no runtime dependencies
-- **Drop-in Replacement**: Compatible with existing Python MCP server configurations
-- **Test-Driven Development**: Comprehensive test coverage for all servers
-- **Security First**: Built-in access controls and validation
-- **High Performance**: Native Go performance and concurrency
-
-## Available Servers
-
-| Server | Description | Status |
-| ------ | ----------- | ------ |
-| **fetch** | Web content fetching with HTML to Markdown conversion | Complete |
-| **filesystem** | Secure file operations with access controls | Complete |
-| **git** | Git repository operations (status, add, commit, log, etc.) | Complete |
-| **imap** | IMAP email server connectivity for Gmail, Migadu, etc. | Complete |
-| **maildir** | Email archive analysis for maildir format | Complete |
-| **memory** | Knowledge graph persistent memory system | Complete |
-| **packages** | Package management for Cargo, Homebrew, and more | Complete |
-| **sequential-thinking** | Dynamic problem-solving with thought sequences | Complete |
-| **signal** | Signal Desktop database access with encrypted SQLCipher | Complete |
-| **speech** | Text-to-speech synthesis using macOS say command | Complete |
-| **time** | Time and timezone conversion utilities | Complete |
-
## Quick Start
### Installation
@@ -44,279 +20,6 @@ make build
sudo make install
```
-### Binary Releases
-
-Pre-built binaries will be available from the [releases page](https://github.com/xlgmokha/mcp/releases) once the first release is published.
-
-### Claude Code Configuration
-
-Replace Python MCP servers in your `~/.claude.json` configuration:
-
-```json
-{
- "mcpServers": {
- "git": {
- "command": "mcp-git"
- },
- "filesystem": {
- "command": "mcp-filesystem"
- },
- "fetch": {
- "command": "mcp-fetch"
- },
- "imap": {
- "command": "mcp-imap",
- "args": ["--server", "imap.gmail.com", "--username", "user@gmail.com", "--password", "app-password"]
- },
- "memory": {
- "command": "mcp-memory"
- },
- "packages": {
- "command": "mcp-packages"
- },
- "sequential-thinking": {
- "command": "mcp-sequential-thinking"
- },
- "speech": {
- "command": "mcp-speech"
- },
- "time": {
- "command": "mcp-time"
- }
- }
-}
-```
-
-## Server Documentation
-
-### Git Server (`mcp-git`)
-
-Provides Git repository operations with safety checks.
-
-**Tools:**
-- `git_status` - Show repository status
-- `git_add` - Stage files for commit
-- `git_commit` - Create commits
-- `git_log` - View commit history
-- `git_diff` - Show differences
-- `git_show` - Show commit details
-
-**Usage:**
-```bash
-# Git server runs without additional arguments
-# Repository path is specified per-tool call
-mcp-git
-```
-
-### Filesystem Server (`mcp-filesystem`)
-
-Secure file operations with configurable access controls.
-
-**Tools:**
-- `read_file` - Read file contents
-- `write_file` - Write file contents
-- `edit_file` - Edit files with line-based operations
-- `list_directory` - List directory contents
-- `create_directory` - Create directories
-- `move_file` - Move/rename files
-- `search_files` - Search for files by pattern
-
-**Usage:**
-```bash
-# Filesystem server runs without additional arguments
-# Allowed paths are validated per-tool call
-mcp-filesystem
-```
-
-### Fetch Server (`mcp-fetch`)
-
-Web content fetching with intelligent HTML to Markdown conversion.
-
-**Tools:**
-- `fetch` - Fetch and convert web content
-
-**Features:**
-- Automatic HTML to Markdown conversion
-- Content truncation and pagination
-- Raw HTML mode
-- Custom User-Agent headers
-
-**Usage:**
-```bash
-mcp-fetch
-```
-
-### IMAP Server (`mcp-imap`)
-
-Connect to IMAP email servers like Gmail, Migadu, and other providers for email management.
-
-**Tools:**
-- `imap_list_folders` - List all IMAP folders (INBOX, Sent, Drafts, etc.)
-- `imap_list_messages` - List messages in folder with pagination
-- `imap_read_message` - Read full message content with headers and body
-- `imap_search_messages` - Search messages by content, sender, subject
-- `imap_get_folder_stats` - Get folder statistics (total, unread, recent)
-- `imap_mark_as_read` - Mark messages as read/unread
-- `imap_get_attachments` - List message attachments
-- `imap_get_connection_info` - Server connection status and capabilities
-- `imap_delete_message` - Delete single message (requires confirmation)
-- `imap_delete_messages` - Delete multiple messages (requires confirmation)
-- `imap_move_to_trash` - Move messages to trash folder (safe delete)
-- `imap_expunge_folder` - Permanently remove deleted messages
-
-**Prompts:**
-- `email-analysis` - AI-powered email content analysis
-- `email-search` - Contextual email search with AI insights
-
-**Features:**
-- Multi-provider support (Gmail, Migadu, generic IMAP)
-- Secure TLS/SSL encryption by default
-- Environment variable credential management
-- Thread-safe connection handling
-- Rich message parsing and formatting
-- Safe email deletion with confirmation prompts
-- Move to trash vs permanent delete options
-- Bulk operations for managing multiple messages
-
-**Usage:**
-```bash
-# Gmail with app password
-mcp-imap --server imap.gmail.com --username user@gmail.com --password app-password
-
-# Migadu
-mcp-imap --server mail.migadu.com --username user@domain.com --password password
-
-# Environment variables
-export IMAP_SERVER=imap.gmail.com
-export IMAP_USERNAME=user@gmail.com
-export IMAP_PASSWORD=app-password
-mcp-imap
-```
-
-### Memory Server (`mcp-memory`)
-
-Persistent knowledge graph for maintaining context across sessions.
-
-**Tools:**
-- `create_entities` - Create entities in the knowledge graph
-- `create_relations` - Create relationships between entities
-- `add_observations` - Add observations to entities
-- `read_graph` - Read the entire knowledge graph
-- `search_nodes` - Search entities and observations
-- `open_nodes` - Retrieve specific entities
-- `delete_entities` - Delete entities and their relations
-- `delete_observations` - Delete specific observations
-- `delete_relations` - Delete relationships
-
-**Usage:**
-```bash
-# Uses ~/.mcp_memory.json by default
-mcp-memory
-
-# Custom memory file
-MEMORY_FILE=/path/to/memory.json mcp-memory
-```
-
-### Sequential Thinking Server (`mcp-sequential-thinking`)
-
-Dynamic problem-solving through structured thought sequences with persistent session management.
-
-**Tools:**
-- `sequentialthinking` - Process thoughts with session persistence and branching
-- `get_session_history` - Retrieve complete session thought history
-- `list_sessions` - List all active thinking sessions
-- `get_branch_history` - Get thought history for specific branches
-- `clear_session` - Clear completed sessions and branches
-
-**Features:**
-- **Session Persistence**: Sessions survive server restarts with JSON storage
-- **Branch Tracking**: Create alternative reasoning paths from any thought
-- **Revision Support**: Mark and track thought revisions with context
-- **Progress Indicators**: Visual progress bars and status tracking
-- **Solution Extraction**: Automatic extraction from final thoughts
-- **Thread-Safe Operations**: Concurrent access to sessions and branches
-
-**Usage:**
-```bash
-# Without persistence (sessions lost on restart)
-mcp-sequential-thinking
-
-# With session persistence
-mcp-sequential-thinking --session-file /path/to/sessions.json
-```
-
-### Packages Server (`mcp-packages`)
-
-Package management tools for multiple ecosystems including Rust (Cargo) and macOS (Homebrew).
-
-**Cargo Tools:**
-- `cargo_build` - Build Rust projects with optional flags
-- `cargo_run` - Run Rust applications with arguments
-- `cargo_test` - Execute test suites with filtering
-- `cargo_add` - Add dependencies to Cargo.toml
-- `cargo_update` - Update dependencies to latest versions
-- `cargo_check` - Quick compile check without building
-- `cargo_clippy` - Run Rust linter with suggestions
-
-**Homebrew Tools:**
-- `brew_install` - Install packages or casks
-- `brew_uninstall` - Remove packages or casks
-- `brew_search` - Search for available packages
-- `brew_update` - Update Homebrew itself
-- `brew_upgrade` - Upgrade installed packages
-- `brew_doctor` - Check system health
-- `brew_list` - List installed packages
-
-**Cross-Platform Tools:**
-- `check_vulnerabilities` - Scan for security vulnerabilities
-- `outdated_packages` - Find packages needing updates
-- `package_info` - Get detailed package information
-
-**Usage:**
-```bash
-mcp-packages
-```
-
-### Speech Server (`mcp-speech`)
-
-Text-to-speech synthesis using the macOS `say` command. Enables LLMs to speak their responses with customizable voices and settings.
-
-**Tools:**
-- `say` - Speak text with customizable voice, rate, and volume
-- `list_voices` - List all available system voices (100+ voices)
-- `speak_file` - Read and speak the contents of a text file
-- `stop_speech` - Stop any currently playing speech synthesis
-- `speech_settings` - Get detailed information about speech options
-
-**Features:**
-- Voice selection from 100+ built-in voices (Alex, Samantha, Victoria, etc.)
-- Adjustable speech rate (80-500 words per minute)
-- Volume control (0.0 to 1.0)
-- Audio file output (.wav, .aiff, .m4a formats)
-- File reading with line limits
-- Instant speech control and interruption
-
-**Usage:**
-```bash
-mcp-speech
-```
-
-**Requirements:**
-- macOS (uses the built-in `say` command)
-
-### Time Server (`mcp-time`)
-
-Time and timezone utilities for temporal operations.
-
-**Tools:**
-- `get_current_time` - Get current time in specified timezone
-- `convert_time` - Convert between timezones
-
-**Usage:**
-```bash
-mcp-time
-```
-
## Development
### Prerequisites
@@ -328,14 +31,6 @@ mcp-time
```bash
# Build all servers
-go build -o bin/mcp-git ./cmd/git
-go build -o bin/mcp-filesystem ./cmd/filesystem
-go build -o bin/mcp-fetch ./cmd/fetch
-go build -o bin/mcp-memory ./cmd/memory
-go build -o bin/mcp-sequential-thinking ./cmd/sequential-thinking
-go build -o bin/mcp-time ./cmd/time
-
-# Or use make
make build
```
@@ -343,10 +38,10 @@ make build
```bash
# Run all tests
-go test ./...
+make test
# Run tests with coverage
-go test -cover ./...
+make test-coverage
# Run specific server tests
go test ./pkg/git/...
@@ -371,33 +66,9 @@ gofmt -l .
4. Update this README
5. Add build targets to Makefile
-## Architecture
-
-The project follows a clean, modular architecture with:
-
-- **`pkg/mcp/`**: Core MCP protocol implementation and shared utilities
-- **`pkg/*/`**: Individual MCP server implementations (reusable packages)
- - `pkg/git/`: Git operations server
- - `pkg/filesystem/`: Filesystem operations server
- - `pkg/fetch/`: Web content fetching server
- - `pkg/memory/`: Knowledge graph memory server
- - `pkg/thinking/`: Sequential thinking server
- - `pkg/time/`: Time and timezone server
-- **`cmd/*/`**: Minimal CLI entry points (thin wrappers around packages)
-- **Standard Go project layout** with clear separation of concerns
-- **Test-driven development** ensuring reliability
-
-Each server is a standalone binary that communicates via JSON-RPC over stdin/stdout, following the MCP specification. The modular package structure allows for easy reuse and testing of server implementations.
-
-## Performance
-
-Benchmarks comparing to Python implementations (approximate):
-
-| Metric | Python | Go | Improvement |
-|--------|--------|----| ----------- |
-| Memory Usage | ~50MB | ~8MB | 6x less |
-| Startup Time | ~200ms | ~5ms | 40x faster |
-| Binary Size | N/A | ~15MB | Single binary |
+Each server is a standalone binary that communicates via JSON-RPC over
+stdin/stdout, following the MCP specification. The modular package
+structure allows for easy reuse and testing of server implementations.
## License
@@ -417,5 +88,4 @@ Quick start:
## Acknowledgments
- Inspired by the [Python MCP servers](https://github.com/modelcontextprotocol/servers)
-- Built for compatibility with [Claude Code](https://claude.ai/code)
- Follows the [Model Context Protocol](https://modelcontextprotocol.io/) specification
SIGNAL_DEMO.md
@@ -1,160 +0,0 @@
-# Signal MCP Server Examples
-
-## ๐ฏ **What the Signal MCP Server Can Do**
-
-Based on your christine project's proven Signal database access, here are examples of what the Signal MCP server provides:
-
-### **1. Database Statistics**
-```bash
-echo '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "signal_get_stats", "arguments": {}}}' | mcp-signal
-```
-
-**Example Output:**
-```json
-{
- "jsonrpc": "2.0",
- "id": 1,
- "result": {
- "content": [{
- "type": "text",
- "text": "Signal Database Statistics:\n- Total Messages: 23,847\n- Total Conversations: 156\n- Database Path: /Users/xlgmokha/Library/Application Support/Signal/sql/db.sqlite"
- }],
- "isError": false
- }
-}
-```
-
-### **2. List Conversations**
-```bash
-echo '{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "signal_list_conversations", "arguments": {}}}' | mcp-signal
-```
-
-**Example Output:**
-```json
-{
- "jsonrpc": "2.0",
- "id": 2,
- "result": {
- "content": [{
- "type": "text",
- "text": "Found 156 conversations:\n\n1. Adia Khan (+15876643389)\n2. Allison Khan (+14036143389)\n3. Better ppl of the fam (Group)\n4. Christine Michaels-Igbokwe (+14035854029)\n5. Cyberdelia - Personal Disaster (Group)\n..."
- }],
- "isError": false
- }
-}
-```
-
-### **3. Search Messages**
-```bash
-echo '{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "signal_search_messages", "arguments": {"query": "dinner", "limit": "5"}}}' | mcp-signal
-```
-
-**Example Output:**
-```json
-{
- "jsonrpc": "2.0",
- "id": 3,
- "result": {
- "content": [{
- "type": "text",
- "text": "Found 23 messages containing 'dinner':\n\n**Adia Khan** (2024-06-15 18:30)\nWant to grab dinner tonight?\n---\n**Christine Michaels-Igbokwe** (2024-06-10 12:15)\nDinner at 7pm works for me\n---\n**Better ppl of the fam** (2024-06-08 16:45)\nFamily dinner this Sunday?\n---"
- }],
- "isError": false
- }
-}
-```
-
-### **4. Get Conversation History**
-```bash
-echo '{"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"name": "signal_get_conversation", "arguments": {"conversation_id": "uuid-of-adia-conversation", "limit": "10"}}}' | mcp-signal
-```
-
-**Example Output:**
-```json
-{
- "jsonrpc": "2.0",
- "id": 4,
- "result": {
- "content": [{
- "type": "text",
- "text": "Conversation with Adia Khan (10 recent messages):\n\n2024-06-23 14:22: Hey! How was your day?\n2024-06-23 14:18: Just finished work, heading home\n2024-06-22 19:45: Thanks for the photos! [1 attachment]\n2024-06-22 18:30: See you tomorrow\n2024-06-21 20:15: Movie was great, we should go again\n..."
- }],
- "isError": false
- }
-}
-```
-
-### **5. Contact Lookup**
-```bash
-echo '{"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {"name": "signal_search_messages", "arguments": {"query": "Adia", "limit": "1"}}}' | mcp-signal
-```
-
-## ๐ **Real Data from Your Signal Database**
-
-From your actual Signal contacts (based on christine project extraction):
-
-- **Adia Khan**: +15876643389
-- **Allison Khan**: +14036143389
-- **Christine Michaels-Igbokwe**: +14035854029
-- **Better ppl of the fam**: Group conversation
-- **Cyberdelia - Personal Disaster**: Group conversation
-- **GitLab Calgary lunch group**: Group conversation
-- And 150+ more contacts and groups
-
-## ๐ **Database Access Method**
-
-The Signal MCP server uses the same proven approach as your christine project:
-
-1. **Automatic Signal Detection**: Finds Signal installation across platforms
-2. **Key Decryption**: Handles macOS keychain and Linux credential stores
-3. **SQLCipher Integration**: Uses command-line sqlcipher for reliable database access
-4. **Schema Knowledge**: Direct access to conversations, messages, and attachments tables
-
-## ๐ **Schema Structure**
-
-The server understands Signal's database schema:
-
-```sql
--- Conversations table
-SELECT id, name, profileName, e164, type FROM conversations;
-
--- Messages table
-SELECT conversationId, type, json, body, sourceServiceId, sent_at
-FROM messages
-WHERE type NOT IN ('keychange', 'profile-change');
-
--- Rich message data in JSON column includes:
--- - attachments[]
--- - reactions[]
--- - quote{}
--- - sticker{}
-```
-
-## ๐ **Security Features**
-
-- **Read-only access**: No modification of Signal data
-- **Encrypted database support**: Full SQLCipher compatibility
-- **Keychain integration**: Secure key retrieval from system stores
-- **Signal-app-closed requirement**: Prevents database conflicts
-
-## ๐ **Integration Ready**
-
-Add to your Claude configuration:
-
-```json
-{
- "mcpServers": {
- "signal": {
- "command": "/usr/local/bin/mcp-signal"
- }
- }
-}
-```
-
-Then ask Claude things like:
-- "What was my last conversation with Adia?"
-- "Search my Signal messages for 'dinner plans'"
-- "How many Signal conversations do I have?"
-- "Show me all group conversations"
-
-The Signal MCP server provides the same comprehensive database access as your proven christine project, but exposed through the MCP protocol for seamless AI assistant integration!
\ No newline at end of file