main
  1package fetch
  2
  3import (
  4	"encoding/base64"
  5	"encoding/json"
  6	"fmt"
  7	"io"
  8	"net/http"
  9	"net/url"
 10	"strings"
 11	"time"
 12
 13	"github.com/xlgmokha/mcp/pkg/mcp"
 14)
 15
 16type FetchResult struct {
 17	URL         string `json:"url"`
 18	Content     string `json:"content"`
 19	ContentType string `json:"content_type"`
 20	IsBinary    bool   `json:"is_binary"`
 21}
 22
 23type FetchOperations struct {
 24	httpClient *http.Client
 25	userAgent  string
 26}
 27
 28func NewFetchOperations() *FetchOperations {
 29	return &FetchOperations{
 30		httpClient: &http.Client{
 31			Timeout: 30 * time.Second,
 32		},
 33		userAgent: "ModelContextProtocol/1.0 (Fetch; +https://github.com/xlgmokha/mcp)",
 34	}
 35}
 36
 37// New creates a new Fetch MCP server
 38func New() *mcp.Server {
 39	fetch := NewFetchOperations()
 40	builder := mcp.NewServerBuilder("mcp-fetch", "1.0.0")
 41
 42	builder.AddTool(mcp.NewTool("fetch", "Fetches a URL and returns the content. Text content is returned as-is, binary content is base64 encoded.", map[string]interface{}{
 43		"type": "object",
 44		"properties": map[string]interface{}{
 45			"url": map[string]interface{}{
 46				"type":        "string",
 47				"description": "URL to fetch",
 48				"format":      "uri",
 49			},
 50		},
 51		"required": []string{"url"},
 52	}, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 53		urlStr, ok := req.Arguments["url"].(string)
 54		if !ok {
 55			return mcp.NewToolError("url is required"), nil
 56		}
 57
 58		parsedURL, err := url.Parse(urlStr)
 59		if err != nil || parsedURL.Scheme == "" || parsedURL.Host == "" {
 60			return mcp.NewToolError("Invalid URL format"), nil
 61		}
 62
 63		result, err := fetch.fetchContent(parsedURL.String())
 64		if err != nil {
 65			return mcp.NewToolError(err.Error()), nil
 66		}
 67
 68		jsonResult, err := json.MarshalIndent(result, "", "  ")
 69		if err != nil {
 70			return mcp.NewToolError(fmt.Sprintf("Failed to marshal result: %v", err)), nil
 71		}
 72
 73		return mcp.NewToolResult(mcp.NewTextContent(string(jsonResult))), nil
 74	}))
 75
 76	return builder.Build()
 77}
 78
 79func (fetch *FetchOperations) fetchContent(urlStr string) (*FetchResult, error) {
 80	req, err := http.NewRequest("GET", urlStr, nil)
 81	if err != nil {
 82		return nil, fmt.Errorf("Failed to create request: %v", err)
 83	}
 84
 85	req.Header.Set("User-Agent", fetch.userAgent)
 86
 87	resp, err := fetch.httpClient.Do(req)
 88	if err != nil {
 89		return nil, fmt.Errorf("Failed to fetch URL: %v", err)
 90	}
 91	defer resp.Body.Close()
 92
 93	if resp.StatusCode >= 400 {
 94		return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
 95	}
 96
 97	body, err := io.ReadAll(resp.Body)
 98	if err != nil {
 99		return nil, fmt.Errorf("Failed to read response body: %v", err)
100	}
101
102	contentType := resp.Header.Get("Content-Type")
103	isBinary := isBinaryContent(contentType)
104	
105	var content string
106	if isBinary {
107		content = base64.StdEncoding.EncodeToString(body)
108	} else {
109		content = string(body)
110	}
111
112	return &FetchResult{
113		URL:         urlStr,
114		Content:     content,
115		ContentType: contentType,
116		IsBinary:    isBinary,
117	}, nil
118}
119
120func isBinaryContent(contentType string) bool {
121	if contentType == "" {
122		return false
123	}
124	
125	contentType = strings.ToLower(strings.Split(contentType, ";")[0])
126	
127	textTypes := []string{
128		"text/",
129		"application/json",
130		"application/xml",
131		"application/javascript",
132		"application/x-javascript",
133	}
134	
135	for _, textType := range textTypes {
136		if strings.HasPrefix(contentType, textType) {
137			return false
138		}
139	}
140	
141	return true
142}