package fetch import ( "encoding/base64" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" "github.com/xlgmokha/mcp/pkg/mcp" ) type FetchResult struct { URL string `json:"url"` Content string `json:"content"` ContentType string `json:"content_type"` IsBinary bool `json:"is_binary"` } type FetchOperations struct { httpClient *http.Client userAgent string } func NewFetchOperations() *FetchOperations { return &FetchOperations{ httpClient: &http.Client{ Timeout: 30 * time.Second, }, userAgent: "ModelContextProtocol/1.0 (Fetch; +https://github.com/xlgmokha/mcp)", } } // New creates a new Fetch MCP server func New() *mcp.Server { fetch := NewFetchOperations() builder := mcp.NewServerBuilder("mcp-fetch", "1.0.0") 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{}{ "type": "object", "properties": map[string]interface{}{ "url": map[string]interface{}{ "type": "string", "description": "URL to fetch", "format": "uri", }, }, "required": []string{"url"}, }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) { urlStr, ok := req.Arguments["url"].(string) if !ok { return mcp.NewToolError("url is required"), nil } parsedURL, err := url.Parse(urlStr) if err != nil || parsedURL.Scheme == "" || parsedURL.Host == "" { return mcp.NewToolError("Invalid URL format"), nil } result, err := fetch.fetchContent(parsedURL.String()) 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 })) return builder.Build() } func (fetch *FetchOperations) fetchContent(urlStr string) (*FetchResult, error) { 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", fetch.userAgent) resp, err := fetch.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("Failed to fetch URL: %v", err) } defer resp.Body.Close() if resp.StatusCode >= 400 { return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("Failed to read response body: %v", err) } contentType := resp.Header.Get("Content-Type") isBinary := isBinaryContent(contentType) var content string if isBinary { content = base64.StdEncoding.EncodeToString(body) } else { content = string(body) } return &FetchResult{ URL: urlStr, Content: content, ContentType: contentType, IsBinary: isBinary, }, nil } func isBinaryContent(contentType string) bool { if contentType == "" { return false } contentType = strings.ToLower(strings.Split(contentType, ";")[0]) textTypes := []string{ "text/", "application/json", "application/xml", "application/javascript", "application/x-javascript", } for _, textType := range textTypes { if strings.HasPrefix(contentType, textType) { return false } } return true }