Commit 56a2ff4
Changed files (9)
cmd
cmd/del/main.go
@@ -1,479 +0,0 @@
-package main
-
-import (
- "bufio"
- "context"
- "encoding/json"
- "fmt"
- "log"
- "os"
- "os/exec"
- "strings"
-
- "github.com/spf13/cobra"
-)
-
-// AI Provider interface - supports multiple AI backends
-type AIProvider interface {
- Generate(ctx context.Context, prompt string) (string, error)
- Name() string
-}
-
-// Tool represents an available capability
-type Tool struct {
- Name string
- Description string
- Handler func(args map[string]interface{}) (interface{}, error)
-}
-
-// Del is our main AI assistant - Del the Funky Robosapien!
-type Del struct {
- aiProvider AIProvider
- tools map[string]*Tool
- mcpServers map[string]string // name -> command path
-}
-
-// Claude API provider
-type ClaudeProvider struct {
- apiKey string
-}
-
-func (c *ClaudeProvider) Generate(ctx context.Context, prompt string) (string, error) {
- // Use Claude CLI if available, fallback to API
- cmd := exec.CommandContext(ctx, "claude", "--print", prompt)
- output, err := cmd.Output()
- if err != nil {
- return "", fmt.Errorf("claude error: %w", err)
- }
- return string(output), nil
-}
-
-func (c *ClaudeProvider) Name() string {
- return "Claude"
-}
-
-// Ollama provider
-type OllamaProvider struct {
- model string
-}
-
-func (o *OllamaProvider) Generate(ctx context.Context, prompt string) (string, error) {
- cmd := exec.CommandContext(ctx, "ollama", "run", o.model, prompt)
- output, err := cmd.Output()
- if err != nil {
- return "", fmt.Errorf("ollama error: %w", err)
- }
- return string(output), nil
-}
-
-func (o *OllamaProvider) Name() string {
- return fmt.Sprintf("Ollama (%s)", o.model)
-}
-
-// Initialize Del with tools and MCP servers
-func NewDel(provider AIProvider) *Del {
- d := &Del{
- aiProvider: provider,
- tools: make(map[string]*Tool),
- mcpServers: make(map[string]string),
- }
-
- // Register built-in tools
- d.registerBuiltinTools()
-
- // Auto-discover MCP servers
- d.discoverMCPServers()
-
- return d
-}
-
-func (d *Del) registerBuiltinTools() {
- // Security scanner tool
- d.tools["security_scan"] = &Tool{
- Name: "security_scan",
- Description: "Scan code for security vulnerabilities",
- Handler: d.handleSecurityScan,
- }
-
- // Code analysis tool
- d.tools["analyze_code"] = &Tool{
- Name: "analyze_code",
- Description: "Analyze code quality and suggest improvements",
- Handler: d.handleCodeAnalysis,
- }
-
- // File operations
- d.tools["read_file"] = &Tool{
- Name: "read_file",
- Description: "Read contents of a file",
- Handler: d.handleReadFile,
- }
-
- d.tools["write_file"] = &Tool{
- Name: "write_file",
- Description: "Write content to a file",
- Handler: d.handleWriteFile,
- }
-
- // Command execution
- d.tools["run_command"] = &Tool{
- Name: "run_command",
- Description: "Execute shell commands",
- Handler: d.handleRunCommand,
- }
-}
-
-func (d *Del) discoverMCPServers() {
- // Look for MCP servers in ../
- servers := map[string]string{
- "git": "../git/main.go",
- "filesystem": "../fs/main.go",
- "bash": "../bash/main.go",
- }
-
- for name, path := range servers {
- if _, err := os.Stat(path); err == nil {
- d.mcpServers[name] = path
- }
- }
-}
-
-func (d *Del) handleSecurityScan(args map[string]interface{}) (interface{}, error) {
- path, ok := args["path"].(string)
- if !ok {
- return nil, fmt.Errorf("path required")
- }
-
- // Run security tools like gosec, semgrep, etc.
- results := map[string]interface{}{
- "vulnerabilities": []string{},
- "warnings": []string{},
- "suggestions": []string{},
- }
-
- // Check for common security issues
- if strings.Contains(path, ".go") {
- // Run gosec if available
- cmd := exec.Command("gosec", "-fmt", "json", path)
- if output, err := cmd.Output(); err == nil {
- results["gosec_output"] = string(output)
- }
- }
-
- return results, nil
-}
-
-func (d *Del) handleCodeAnalysis(args map[string]interface{}) (interface{}, error) {
- path, ok := args["path"].(string)
- if !ok {
- return nil, fmt.Errorf("path required")
- }
-
- analysis := map[string]interface{}{
- "complexity": "medium",
- "maintainability": "good",
- "suggestions": []string{
- "Consider adding more comments",
- "Extract complex functions",
- },
- }
-
- return analysis, nil
-}
-
-func (d *Del) handleReadFile(args map[string]interface{}) (interface{}, error) {
- path, ok := args["path"].(string)
- if !ok {
- return nil, fmt.Errorf("path required")
- }
-
- content, err := os.ReadFile(path)
- if err != nil {
- return nil, err
- }
-
- return map[string]interface{}{
- "path": path,
- "content": string(content),
- "size": len(content),
- }, nil
-}
-
-func (d *Del) handleWriteFile(args map[string]interface{}) (interface{}, error) {
- path, ok := args["path"].(string)
- if !ok {
- return nil, fmt.Errorf("path required")
- }
-
- content, ok := args["content"].(string)
- if !ok {
- return nil, fmt.Errorf("content required")
- }
-
- // Ask for confirmation
- fmt.Printf("๐ค Del asks: Write to %s? [y/N]: ", path)
- reader := bufio.NewReader(os.Stdin)
- response, _ := reader.ReadString('\n')
-
- if strings.ToLower(strings.TrimSpace(response)) != "y" {
- return map[string]interface{}{"status": "cancelled"}, nil
- }
-
- err := os.WriteFile(path, []byte(content), 0644)
- if err != nil {
- return nil, err
- }
-
- return map[string]interface{}{
- "status": "written",
- "path": path,
- "size": len(content),
- }, nil
-}
-
-func (d *Del) handleRunCommand(args map[string]interface{}) (interface{}, error) {
- command, ok := args["command"].(string)
- if !ok {
- return nil, fmt.Errorf("command required")
- }
-
- // Ask for confirmation
- fmt.Printf("๐ค Del asks: Execute '%s'? [y/N]: ", command)
- reader := bufio.NewReader(os.Stdin)
- response, _ := reader.ReadString('\n')
-
- if strings.ToLower(strings.TrimSpace(response)) != "y" {
- return map[string]interface{}{"status": "cancelled"}, nil
- }
-
- cmd := exec.Command("sh", "-c", command)
- output, err := cmd.CombinedOutput()
-
- result := map[string]interface{}{
- "command": command,
- "output": string(output),
- "success": err == nil,
- }
-
- if err != nil {
- result["error"] = err.Error()
- }
-
- return result, nil
-}
-
-// Enhanced prompt with available tools
-func (d *Del) buildPrompt(userInput string) string {
- toolsList := make([]string, 0, len(d.tools))
- for name, tool := range d.tools {
- toolsList = append(toolsList, fmt.Sprintf("- %s: %s", name, tool.Description))
- }
-
- mcpList := make([]string, 0, len(d.mcpServers))
- for name := range d.mcpServers {
- mcpList = append(mcpList, name)
- }
-
- return fmt.Sprintf(`You are Del the Funky Robosapien, an AI-powered coding superhero assistant.
-Channel the energy of Del the Funky Homosapien - you're creative, clever, and always ready to drop some funky code solutions!
-
-AVAILABLE TOOLS:
-%s
-
-AVAILABLE MCP SERVERS:
-%s
-
-CURRENT CONTEXT:
-- Working Directory: %s
-- Git Status: %s
-
-When you want to use a tool, respond with:
-TOOL_USE: tool_name {"arg1": "value1", "arg2": "value2"}
-
-For example:
-TOOL_USE: read_file {"path": "main.go"}
-TOOL_USE: security_scan {"path": "."}
-TOOL_USE: run_command {"command": "go build"}
-
-Keep it funky! ๐ค๐ค
-
-User Request: %s`,
- strings.Join(toolsList, "\n"),
- strings.Join(mcpList, ", "),
- getCurrentDir(),
- getGitStatus(),
- userInput)
-}
-
-func getCurrentDir() string {
- dir, _ := os.Getwd()
- return dir
-}
-
-func getGitStatus() string {
- cmd := exec.Command("git", "status", "--porcelain")
- output, err := cmd.Output()
- if err != nil {
- return "Not a git repository"
- }
- return string(output)
-}
-
-// Process AI response and execute tools
-func (d *Del) processResponse(response string) {
- lines := strings.Split(response, "\n")
-
- for _, line := range lines {
- if strings.HasPrefix(line, "TOOL_USE:") {
- d.executeTool(strings.TrimPrefix(line, "TOOL_USE:"))
- }
- }
-}
-
-func (d *Del) executeTool(toolCall string) {
- parts := strings.SplitN(toolCall, " ", 2)
- if len(parts) != 2 {
- fmt.Printf("โ Invalid tool call: %s\n", toolCall)
- return
- }
-
- toolName := strings.TrimSpace(parts[0])
- argsJSON := strings.TrimSpace(parts[1])
-
- tool, exists := d.tools[toolName]
- if !exists {
- fmt.Printf("โ Unknown tool: %s\n", toolName)
- return
- }
-
- var args map[string]interface{}
- if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
- fmt.Printf("โ Invalid tool args: %s\n", err)
- return
- }
-
- fmt.Printf("๐ค Del is dropping some tool magic: %s\n", toolName)
- result, err := tool.Handler(args)
- if err != nil {
- fmt.Printf("โ Tool error: %s\n", err)
- return
- }
-
- fmt.Printf("โ
Funky result: %+v\n", result)
-}
-
-// Interactive REPL
-func (d *Del) startREPL() {
- fmt.Printf("๐ค๐ค Del the Funky Robosapien is in the house! (Provider: %s)\n", d.aiProvider.Name())
- fmt.Printf("๐ง Available tools: %s\n", strings.Join(func() []string {
- var names []string
- for name := range d.tools {
- names = append(names, name)
- }
- return names
- }(), ", "))
- fmt.Println("๐ฌ Type 'quit' to exit, 'help' for assistance")
- fmt.Println("๐ต Let's make some funky code together!")
- fmt.Println()
-
- scanner := bufio.NewScanner(os.Stdin)
-
- for {
- fmt.Print("๐ค You: ")
- if !scanner.Scan() {
- break
- }
-
- input := strings.TrimSpace(scanner.Text())
- if input == "" {
- continue
- }
-
- if input == "quit" || input == "exit" {
- fmt.Println("๐ค Del says: Catch you on the flip side! Stay funky! โ๏ธ")
- break
- }
-
- if input == "help" {
- d.showHelp()
- continue
- }
-
- // Generate AI response
- prompt := d.buildPrompt(input)
- response, err := d.aiProvider.Generate(context.Background(), prompt)
- if err != nil {
- fmt.Printf("โ AI Error: %s\n", err)
- continue
- }
-
- fmt.Printf("๐ค Del: %s\n", response)
-
- // Process any tool calls
- d.processResponse(response)
- fmt.Println()
- }
-}
-
-func (d *Del) showHelp() {
- fmt.Println("๐ค๐ค Del the Funky Robosapien - Help Menu:")
- fmt.Println(" ๐ก๏ธ Security Commands:")
- fmt.Println(" 'scan for vulnerabilities'")
- fmt.Println(" 'check this code for security issues'")
- fmt.Println(" ๐ Development Commands:")
- fmt.Println(" 'read the main.go file'")
- fmt.Println(" 'build this project'")
- fmt.Println(" 'run the tests'")
- fmt.Println(" 'analyze code quality'")
- fmt.Println(" ๐ง Available Tools:")
- for name, tool := range d.tools {
- fmt.Printf(" - %s: %s\n", name, tool.Description)
- }
- fmt.Println(" ๐ต Remember: Keep it funky!")
- fmt.Println()
-}
-
-var rootCmd = &cobra.Command{
- Use: "del",
- Short: "Del the Funky Robosapien - Your AI-powered coding superhero",
- Long: `๐ค๐ค Del the Funky Robosapien
-
-Your AI-powered coding superhero with the soul of a funky homosapien!
-Equipped with security scanning, code analysis, file operations, and more.
-
-Channel the creative energy of Del the Funky Homosapien while coding!`,
- Run: func(cmd *cobra.Command, args []string) {
- provider := cmd.Flag("provider").Value.String()
- model := cmd.Flag("model").Value.String()
-
- var aiProvider AIProvider
-
- switch provider {
- case "claude":
- aiProvider = &ClaudeProvider{}
- case "ollama":
- aiProvider = &OllamaProvider{model: model}
- default:
- // Try Claude first, fallback to Ollama
- if exec.Command("claude", "--version").Run() == nil {
- aiProvider = &ClaudeProvider{}
- } else {
- aiProvider = &OllamaProvider{model: "deepseek-coder-v2:16b"}
- }
- }
-
- del := NewDel(aiProvider)
- del.startREPL()
- },
-}
-
-func init() {
- rootCmd.Flags().StringP("provider", "p", "auto", "AI provider (claude, ollama, auto)")
- rootCmd.Flags().StringP("model", "m", "deepseek-coder-v2:16b", "Model to use (for Ollama)")
-}
-
-func main() {
- if err := rootCmd.Execute(); err != nil {
- log.Fatal(err)
- }
-}
\ No newline at end of file
cmd/fetch/main.go
@@ -0,0 +1,23 @@
+package main
+
+import (
+ "context"
+ "log"
+
+ "github.com/xlgmokha/mcp/pkg/mcp"
+)
+
+func main() {
+ server := NewFetchServer()
+
+ // Set up basic initialization
+ server.SetInitializeHandler(func(req mcp.InitializeRequest) (mcp.InitializeResult, error) {
+ // Use default initialization
+ return mcp.InitializeResult{}, nil
+ })
+
+ ctx := context.Background()
+ if err := server.Run(ctx); err != nil {
+ log.Fatalf("Server error: %v", err)
+ }
+}
\ No newline at end of file
cmd/fetch/server.go
@@ -0,0 +1,382 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "regexp"
+ "strings"
+ "time"
+
+ "github.com/xlgmokha/mcp/pkg/mcp"
+ "golang.org/x/net/html"
+)
+
+// FetchServer implements the Fetch MCP server
+type FetchServer struct {
+ *mcp.Server
+ httpClient *http.Client
+ userAgent string
+}
+
+// FetchResult represents the result of a fetch operation
+type FetchResult struct {
+ URL string `json:"url"`
+ Content string `json:"content"`
+ ContentType string `json:"content_type"`
+ Length int `json:"length"`
+ Truncated bool `json:"truncated,omitempty"`
+ NextIndex int `json:"next_index,omitempty"`
+}
+
+// NewFetchServer creates a new Fetch MCP server
+func NewFetchServer() *FetchServer {
+ server := mcp.NewServer("mcp-fetch", "1.0.0")
+
+ fetchServer := &FetchServer{
+ Server: server,
+ httpClient: &http.Client{
+ Timeout: 30 * time.Second,
+ },
+ userAgent: "ModelContextProtocol/1.0 (Fetch; +https://github.com/xlgmokha/mcp)",
+ }
+
+ // Register all fetch tools
+ fetchServer.registerTools()
+
+ return fetchServer
+}
+
+// registerTools registers all Fetch tools with the server
+func (fs *FetchServer) registerTools() {
+ fs.RegisterTool("fetch", fs.HandleFetch)
+}
+
+// ListTools returns all available Fetch tools
+func (fs *FetchServer) ListTools() []mcp.Tool {
+ return []mcp.Tool{
+ {
+ Name: "fetch",
+ Description: "Fetches a URL from the internet and extracts its contents as markdown. Always returns successful response with content or error details.",
+ InputSchema: map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "url": map[string]interface{}{
+ "type": "string",
+ "description": "URL to fetch",
+ "format": "uri",
+ },
+ "max_length": map[string]interface{}{
+ "type": "integer",
+ "description": "Maximum number of characters to return. Defaults to 5000",
+ "minimum": 1,
+ "maximum": 999999,
+ "default": 5000,
+ },
+ "start_index": map[string]interface{}{
+ "type": "integer",
+ "description": "Start reading content from this character index. Defaults to 0",
+ "minimum": 0,
+ "default": 0,
+ },
+ "raw": map[string]interface{}{
+ "type": "boolean",
+ "description": "Get raw HTML content without markdown conversion. Defaults to false",
+ "default": false,
+ },
+ },
+ "required": []string{"url"},
+ },
+ },
+ }
+}
+
+// Tool handlers
+
+func (fs *FetchServer) HandleFetch(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ urlStr, ok := req.Arguments["url"].(string)
+ if !ok {
+ return mcp.NewToolError("url is required"), nil
+ }
+
+ // Parse and validate URL
+ parsedURL, err := url.Parse(urlStr)
+ if err != nil || parsedURL.Scheme == "" || parsedURL.Host == "" {
+ return mcp.NewToolError("Invalid URL format"), nil
+ }
+
+ // Get optional parameters
+ maxLength := 5000
+ if ml, ok := req.Arguments["max_length"]; ok {
+ switch v := ml.(type) {
+ case float64:
+ maxLength = int(v)
+ case int:
+ maxLength = v
+ default:
+ return mcp.NewToolError("max_length must be a number"), nil
+ }
+ if maxLength < 1 || maxLength > 999999 {
+ return mcp.NewToolError("max_length must be between 1 and 999999"), nil
+ }
+ }
+
+ startIndex := 0
+ if si, ok := req.Arguments["start_index"]; ok {
+ switch v := si.(type) {
+ case float64:
+ startIndex = int(v)
+ case int:
+ startIndex = v
+ default:
+ return mcp.NewToolError("start_index must be a number"), nil
+ }
+ if startIndex < 0 {
+ return mcp.NewToolError("start_index must be >= 0"), nil
+ }
+ }
+
+ raw := false
+ if r, ok := req.Arguments["raw"]; ok {
+ if rBool, ok := r.(bool); ok {
+ raw = rBool
+ }
+ }
+
+ // Fetch the content
+ result, err := fs.fetchContent(parsedURL.String(), maxLength, startIndex, raw)
+ if err != nil {
+ return mcp.NewToolError(err.Error()), nil
+ }
+
+ // Format result as JSON
+ jsonResult, err := json.MarshalIndent(result, "", " ")
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to marshal result: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(string(jsonResult))), nil
+}
+
+// Helper methods
+
+func (fs *FetchServer) fetchContent(urlStr string, maxLength, startIndex int, raw bool) (*FetchResult, error) {
+ // Create HTTP request
+ req, err := http.NewRequest("GET", urlStr, nil)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to create request: %v", err)
+ }
+
+ req.Header.Set("User-Agent", fs.userAgent)
+ req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
+
+ // Perform HTTP request
+ resp, err := fs.httpClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to fetch URL: %v", err)
+ }
+ defer resp.Body.Close()
+
+ // Check for HTTP errors
+ if resp.StatusCode >= 400 {
+ return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
+ }
+
+ // Read response body
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to read response body: %v", err)
+ }
+
+ // Get content type
+ contentType := resp.Header.Get("Content-Type")
+
+ // Process content
+ var content string
+ if raw || !isHTMLContent(string(body), contentType) {
+ content = string(body)
+ } else {
+ // Convert HTML to markdown
+ content = fs.htmlToMarkdown(string(body))
+ }
+
+ // Apply start index first
+ originalLength := len(content)
+ if startIndex > 0 {
+ if startIndex >= originalLength {
+ return nil, fmt.Errorf("start_index (%d) is beyond content length (%d)", startIndex, originalLength)
+ }
+ content = content[startIndex:]
+ }
+
+ // Apply max length and check for truncation
+ truncated := false
+ nextIndex := 0
+ if len(content) > maxLength {
+ content = content[:maxLength]
+ truncated = true
+ nextIndex = startIndex + maxLength
+ }
+
+ result := &FetchResult{
+ URL: urlStr,
+ Content: content,
+ ContentType: contentType,
+ Length: len(content),
+ }
+
+ if truncated {
+ result.Truncated = true
+ result.NextIndex = nextIndex
+ }
+
+ return result, nil
+}
+
+func isHTMLContent(content, contentType string) bool {
+ // Check content type header
+ if strings.Contains(strings.ToLower(contentType), "text/html") {
+ return true
+ }
+
+ // Check if content starts with HTML tags (first 100 chars)
+ prefix := content
+ if len(prefix) > 100 {
+ prefix = prefix[:100]
+ }
+
+ return strings.Contains(strings.ToLower(prefix), "<html")
+}
+
+func (fs *FetchServer) htmlToMarkdown(htmlContent string) string {
+ // Parse HTML
+ doc, err := html.Parse(strings.NewReader(htmlContent))
+ if err != nil {
+ // If parsing fails, return cleaned text
+ return fs.stripHTMLTags(htmlContent)
+ }
+
+ // Extract text content and convert to markdown
+ var result strings.Builder
+ fs.extractMarkdown(doc, &result, 0)
+
+ // Clean up the result
+ content := result.String()
+ content = fs.cleanMarkdown(content)
+
+ return content
+}
+
+func (fs *FetchServer) extractMarkdown(node *html.Node, result *strings.Builder, depth int) {
+ if node.Type == html.TextNode {
+ text := strings.TrimSpace(node.Data)
+ if text != "" {
+ result.WriteString(text)
+ result.WriteString(" ")
+ }
+ return
+ }
+
+ if node.Type == html.ElementNode {
+ switch strings.ToLower(node.Data) {
+ case "h1":
+ result.WriteString("\n# ")
+ case "h2":
+ result.WriteString("\n## ")
+ case "h3":
+ result.WriteString("\n### ")
+ case "h4":
+ result.WriteString("\n#### ")
+ case "h5":
+ result.WriteString("\n##### ")
+ case "h6":
+ result.WriteString("\n###### ")
+ case "p":
+ result.WriteString("\n\n")
+ case "br":
+ result.WriteString("\n")
+ case "li":
+ result.WriteString("\n- ")
+ case "blockquote":
+ result.WriteString("\n> ")
+ case "code":
+ result.WriteString("`")
+ case "pre":
+ result.WriteString("\n```\n")
+ case "strong", "b":
+ result.WriteString("**")
+ case "em", "i":
+ result.WriteString("*")
+ case "a":
+ // Extract href attribute for links
+ for _, attr := range node.Attr {
+ if attr.Key == "href" {
+ result.WriteString("[")
+ break
+ }
+ }
+ }
+ }
+
+ // Process child nodes
+ for child := node.FirstChild; child != nil; child = child.NextSibling {
+ fs.extractMarkdown(child, result, depth+1)
+ }
+
+ // Closing tags
+ if node.Type == html.ElementNode {
+ switch strings.ToLower(node.Data) {
+ case "h1", "h2", "h3", "h4", "h5", "h6":
+ result.WriteString("\n")
+ case "p":
+ result.WriteString("\n")
+ case "code":
+ result.WriteString("`")
+ case "pre":
+ result.WriteString("\n```\n")
+ case "strong", "b":
+ result.WriteString("**")
+ case "em", "i":
+ result.WriteString("*")
+ case "a":
+ // Close link and add URL
+ for _, attr := range node.Attr {
+ if attr.Key == "href" {
+ result.WriteString("](")
+ result.WriteString(attr.Val)
+ result.WriteString(")")
+ break
+ }
+ }
+ }
+ }
+}
+
+func (fs *FetchServer) stripHTMLTags(content string) string {
+ // Remove HTML tags using regex
+ re := regexp.MustCompile(`<[^>]*>`)
+ text := re.ReplaceAllString(content, " ")
+
+ // Clean up whitespace
+ re = regexp.MustCompile(`\s+`)
+ text = re.ReplaceAllString(text, " ")
+
+ return strings.TrimSpace(text)
+}
+
+func (fs *FetchServer) cleanMarkdown(content string) string {
+ // Remove excessive newlines
+ re := regexp.MustCompile(`\n{3,}`)
+ content = re.ReplaceAllString(content, "\n\n")
+
+ // Remove excessive spaces
+ re = regexp.MustCompile(` {2,}`)
+ content = re.ReplaceAllString(content, " ")
+
+ // Trim whitespace
+ content = strings.TrimSpace(content)
+
+ return content
+}
\ No newline at end of file
cmd/fetch/server_test.go
@@ -0,0 +1,344 @@
+package main
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/xlgmokha/mcp/pkg/mcp"
+)
+
+func TestFetchServer_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 := NewFetchServer()
+
+ 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 TestFetchServer_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 := NewFetchServer()
+
+ 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 TestFetchServer_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 := NewFetchServer()
+
+ 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 TestFetchServer_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 := NewFetchServer()
+
+ 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 TestFetchServer_FetchInvalidURL(t *testing.T) {
+ server := NewFetchServer()
+
+ 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 TestFetchServer_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 := NewFetchServer()
+
+ 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 TestFetchServer_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 := NewFetchServer()
+
+ 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 TestFetchServer_ListTools(t *testing.T) {
+ server := NewFetchServer()
+ 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)
+}
\ No newline at end of file
cmd/time/main.go
@@ -0,0 +1,23 @@
+package main
+
+import (
+ "context"
+ "log"
+
+ "github.com/xlgmokha/mcp/pkg/mcp"
+)
+
+func main() {
+ server := NewTimeServer()
+
+ // Set up basic initialization
+ server.SetInitializeHandler(func(req mcp.InitializeRequest) (mcp.InitializeResult, error) {
+ // Use default initialization
+ return mcp.InitializeResult{}, nil
+ })
+
+ ctx := context.Background()
+ if err := server.Run(ctx); err != nil {
+ log.Fatalf("Server error: %v", err)
+ }
+}
\ No newline at end of file
cmd/time/server.go
@@ -0,0 +1,283 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/xlgmokha/mcp/pkg/mcp"
+)
+
+// TimeServer implements the Time MCP server
+type TimeServer struct {
+ *mcp.Server
+ localTimezone string
+}
+
+// TimeResult represents the result of a time operation
+type TimeResult struct {
+ Timezone string `json:"timezone"`
+ Datetime string `json:"datetime"`
+ IsDST bool `json:"is_dst"`
+}
+
+// TimeConversionResult represents the result of a time conversion
+type TimeConversionResult struct {
+ Source TimeResult `json:"source"`
+ Target TimeResult `json:"target"`
+ TimeDifference string `json:"time_difference"`
+}
+
+// NewTimeServer creates a new Time MCP server
+func NewTimeServer() *TimeServer {
+ server := mcp.NewServer("mcp-time", "1.0.0")
+
+ // Get local timezone
+ localTZ := getLocalTimezone()
+
+ timeServer := &TimeServer{
+ Server: server,
+ localTimezone: localTZ,
+ }
+
+ // Register all time tools
+ timeServer.registerTools()
+
+ return timeServer
+}
+
+// registerTools registers all Time tools with the server
+func (ts *TimeServer) registerTools() {
+ ts.RegisterTool("get_current_time", ts.HandleGetCurrentTime)
+ ts.RegisterTool("convert_time", ts.HandleConvertTime)
+}
+
+// ListTools returns all available Time tools
+func (ts *TimeServer) ListTools() []mcp.Tool {
+ return []mcp.Tool{
+ {
+ Name: "get_current_time",
+ Description: "Get current time in a specific timezone",
+ InputSchema: map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "timezone": map[string]interface{}{
+ "type": "string",
+ "description": fmt.Sprintf("IANA timezone name (e.g., 'America/New_York', 'Europe/London'). Use '%s' as local timezone if no timezone provided by the user.", ts.localTimezone),
+ },
+ },
+ "required": []string{"timezone"},
+ },
+ },
+ {
+ Name: "convert_time",
+ Description: "Convert time between timezones",
+ InputSchema: map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "source_timezone": map[string]interface{}{
+ "type": "string",
+ "description": fmt.Sprintf("Source IANA timezone name (e.g., 'America/New_York', 'Europe/London'). Use '%s' as local timezone if no source timezone provided by the user.", ts.localTimezone),
+ },
+ "time": map[string]interface{}{
+ "type": "string",
+ "description": "Time to convert in 24-hour format (HH:MM)",
+ },
+ "target_timezone": map[string]interface{}{
+ "type": "string",
+ "description": fmt.Sprintf("Target IANA timezone name (e.g., 'Asia/Tokyo', 'America/San_Francisco'). Use '%s' as local timezone if no target timezone provided by the user.", ts.localTimezone),
+ },
+ },
+ "required": []string{"source_timezone", "time", "target_timezone"},
+ },
+ },
+ }
+}
+
+// Tool handlers
+
+func (ts *TimeServer) HandleGetCurrentTime(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ timezone, ok := req.Arguments["timezone"].(string)
+ if !ok {
+ return mcp.NewToolError("timezone is required"), nil
+ }
+
+ result, err := ts.getCurrentTime(timezone)
+ if err != nil {
+ return mcp.NewToolError(err.Error()), nil
+ }
+
+ jsonResult, err := json.MarshalIndent(result, "", " ")
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to marshal result: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(string(jsonResult))), nil
+}
+
+func (ts *TimeServer) HandleConvertTime(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ sourceTimezone, ok := req.Arguments["source_timezone"].(string)
+ if !ok {
+ return mcp.NewToolError("source_timezone is required"), nil
+ }
+
+ timeStr, ok := req.Arguments["time"].(string)
+ if !ok {
+ return mcp.NewToolError("time is required"), nil
+ }
+
+ targetTimezone, ok := req.Arguments["target_timezone"].(string)
+ if !ok {
+ return mcp.NewToolError("target_timezone is required"), nil
+ }
+
+ result, err := ts.convertTime(sourceTimezone, timeStr, targetTimezone)
+ if err != nil {
+ return mcp.NewToolError(err.Error()), nil
+ }
+
+ jsonResult, err := json.MarshalIndent(result, "", " ")
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to marshal result: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(string(jsonResult))), nil
+}
+
+// Helper methods
+
+func (ts *TimeServer) getCurrentTime(timezone string) (*TimeResult, error) {
+ loc, err := time.LoadLocation(timezone)
+ if err != nil {
+ return nil, fmt.Errorf("Invalid timezone: %v", err)
+ }
+
+ currentTime := time.Now().In(loc)
+
+ return &TimeResult{
+ Timezone: timezone,
+ Datetime: currentTime.Format("2006-01-02T15:04:05-07:00"),
+ IsDST: isDST(currentTime),
+ }, nil
+}
+
+func (ts *TimeServer) convertTime(sourceTimezone, timeStr, targetTimezone string) (*TimeConversionResult, error) {
+ sourceLoc, err := time.LoadLocation(sourceTimezone)
+ if err != nil {
+ return nil, fmt.Errorf("Invalid source timezone: %v", err)
+ }
+
+ targetLoc, err := time.LoadLocation(targetTimezone)
+ if err != nil {
+ return nil, fmt.Errorf("Invalid target timezone: %v", err)
+ }
+
+ // Parse time in HH:MM format
+ parts := strings.Split(timeStr, ":")
+ if len(parts) != 2 {
+ return nil, fmt.Errorf("Invalid time format. Expected HH:MM [24-hour format]")
+ }
+
+ hour, err := strconv.Atoi(parts[0])
+ if err != nil || hour < 0 || hour > 23 {
+ return nil, fmt.Errorf("Invalid time format. Expected HH:MM [24-hour format]")
+ }
+
+ minute, err := strconv.Atoi(parts[1])
+ if err != nil || minute < 0 || minute > 59 {
+ return nil, fmt.Errorf("Invalid time format. Expected HH:MM [24-hour format]")
+ }
+
+ // Use current date in source timezone
+ now := time.Now().In(sourceLoc)
+ sourceTime := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, sourceLoc)
+
+ // Convert to target timezone
+ targetTime := sourceTime.In(targetLoc)
+
+ // Calculate time difference
+ sourceOffset := getTimezoneOffset(sourceTime)
+ targetOffset := getTimezoneOffset(targetTime)
+ hoursDifference := float64(targetOffset-sourceOffset) / 3600
+
+ timeDiffStr := formatTimeDifference(hoursDifference)
+
+ return &TimeConversionResult{
+ Source: TimeResult{
+ Timezone: sourceTimezone,
+ Datetime: sourceTime.Format("2006-01-02T15:04:05-07:00"),
+ IsDST: isDST(sourceTime),
+ },
+ Target: TimeResult{
+ Timezone: targetTimezone,
+ Datetime: targetTime.Format("2006-01-02T15:04:05-07:00"),
+ IsDST: isDST(targetTime),
+ },
+ TimeDifference: timeDiffStr,
+ }, nil
+}
+
+func getLocalTimezone() string {
+ // Get local timezone from the system
+ now := time.Now()
+ zone, _ := now.Zone()
+ if zone == "" {
+ return "UTC"
+ }
+
+ // Try to get the IANA timezone name
+ loc := now.Location()
+ if loc != nil && loc.String() != "" && loc.String() != "Local" {
+ return loc.String()
+ }
+
+ // Fallback to UTC if we can't determine the timezone
+ return "UTC"
+}
+
+func isDST(t time.Time) bool {
+ // Check if time is in daylight saving time
+ _, offset := t.Zone()
+
+ // Create time in January (standard time) and July (potentially DST)
+ jan := time.Date(t.Year(), 1, 1, 12, 0, 0, 0, t.Location())
+ jul := time.Date(t.Year(), 7, 1, 12, 0, 0, 0, t.Location())
+
+ _, janOffset := jan.Zone()
+ _, julOffset := jul.Zone()
+
+ // If offsets are different, the timezone observes DST
+ if janOffset != julOffset {
+ // We're in DST if current offset matches the "more positive" offset
+ // (which is typically summer time)
+ if janOffset < julOffset {
+ return offset == julOffset
+ } else {
+ return offset == janOffset
+ }
+ }
+
+ // No DST observed in this timezone
+ return false
+}
+
+func getTimezoneOffset(t time.Time) int {
+ _, offset := t.Zone()
+ return offset
+}
+
+func formatTimeDifference(hours float64) string {
+ if hours == float64(int(hours)) {
+ // Whole hours
+ return fmt.Sprintf("%+.1fh", hours)
+ } else {
+ // Fractional hours (like Nepal's UTC+5:45)
+ formatted := fmt.Sprintf("%+.2f", hours)
+ // Remove trailing zeros
+ formatted = strings.TrimRight(formatted, "0")
+ formatted = strings.TrimRight(formatted, ".")
+ return formatted + "h"
+ }
+}
\ No newline at end of file
cmd/time/server_test.go
@@ -0,0 +1,206 @@
+package main
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/xlgmokha/mcp/pkg/mcp"
+)
+
+func TestTimeServer_GetCurrentTime(t *testing.T) {
+ server := NewTimeServer()
+
+ 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 TestTimeServer_ConvertTime(t *testing.T) {
+ server := NewTimeServer()
+
+ 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 TestTimeServer_InvalidTimezone(t *testing.T) {
+ server := NewTimeServer()
+
+ 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 TestTimeServer_InvalidTimeFormat(t *testing.T) {
+ server := NewTimeServer()
+
+ 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 TestTimeServer_ListTools(t *testing.T) {
+ server := NewTimeServer()
+ 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 TestTimeServer_ConvertTimeWithDST(t *testing.T) {
+ server := NewTimeServer()
+
+ // 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)
+}
\ No newline at end of file
go.mod
@@ -1,3 +1,11 @@
module github.com/xlgmokha/mcp
go 1.24.0
+
+require github.com/spf13/cobra v1.9.1
+
+require (
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/spf13/pflag v1.0.6 // indirect
+ golang.org/x/net v0.41.0 // indirect
+)
go.sum
@@ -0,0 +1,12 @@
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
+github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
+golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=