Files
github--github-mcp-server/pkg/github/docs.go
2025-10-31 18:38:36 +00:00

145 lines
4.7 KiB
Go

package github
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
// DocsSearchResult represents a single search result from GitHub Docs
type DocsSearchResult struct {
Title string `json:"title"`
URL string `json:"url"`
Breadcrumbs string `json:"breadcrumbs"`
Content string `json:"content,omitempty"`
}
// DocsSearchResponse represents the response from GitHub Docs search API
type DocsSearchResponse struct {
Meta struct {
Found struct {
Value int `json:"value"`
} `json:"found"`
Took struct {
PrettyMs string `json:"pretty_ms"`
} `json:"took"`
} `json:"meta"`
Hits []DocsSearchResult `json:"hits"`
}
// SearchGitHubDocs creates a tool to search GitHub documentation.
func SearchGitHubDocs(t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("search_github_docs",
mcp.WithDescription(t("TOOL_SEARCH_GITHUB_DOCS_DESCRIPTION", "Search GitHub's official documentation at docs.github.com. Use this to find help articles, guides, and API documentation for GitHub features and products.")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_SEARCH_GITHUB_DOCS_USER_TITLE", "Search GitHub Docs"),
ReadOnlyHint: ToBoolPtr(true),
}),
mcp.WithString("query",
mcp.Required(),
mcp.Description("Search query for GitHub documentation. Examples: 'actions workflow syntax', 'pull request review', 'GitHub Pages'"),
),
mcp.WithString("version",
mcp.Description("GitHub version to search. Options: 'dotcom' (default, free/pro/team), 'ghec' (GitHub Enterprise Cloud), or a specific GHES version like '3.12'"),
),
mcp.WithString("language",
mcp.Description("Language code for documentation. Options: 'en' (default), 'es', 'ja', 'pt', 'zh', 'ru', 'fr', 'ko', 'de'"),
),
mcp.WithNumber("max_results",
mcp.Description("Maximum number of results to return (default: 10, max: 100)"),
),
),
func(_ context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
query, err := RequiredParam[string](request, "query")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
version, err := OptionalParam[string](request, "version")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
if version == "" {
version = "dotcom"
}
language, err := OptionalParam[string](request, "language")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
if language == "" {
language = "en"
}
maxResults, err := OptionalIntParam(request, "max_results")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
// Check if max_results was explicitly provided
_, maxResultsProvided := request.GetArguments()["max_results"]
if maxResultsProvided {
// Validate max_results only if it was provided
if maxResults < 1 || maxResults > 100 {
return mcp.NewToolResultError("max_results must be between 1 and 100"), nil
}
} else {
// Use default if not provided
maxResults = 10
}
// Build the search URL with client_name parameter
searchURL := fmt.Sprintf("https://docs.github.com/api/search/v1?version=%s&language=%s&query=%s&limit=%d&client_name=github-mcp-server",
url.QueryEscape(version),
url.QueryEscape(language),
url.QueryEscape(query),
maxResults,
)
// Make the HTTP request
// #nosec G107 - URL is constructed from validated parameters with proper escaping
resp, err := http.Get(searchURL)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to search GitHub Docs: %v", err)), nil
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return mcp.NewToolResultError(fmt.Sprintf("GitHub Docs API returned status %d: %s", resp.StatusCode, string(body))), nil
}
// Parse the response
body, err := io.ReadAll(resp.Body)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to read response body: %v", err)), nil
}
var searchResp DocsSearchResponse
if err := json.Unmarshal(body, &searchResp); err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to parse response: %v", err)), nil
}
// Format the results
result := map[string]interface{}{
"total_results": searchResp.Meta.Found.Value,
"search_time": searchResp.Meta.Took.PrettyMs,
"results": searchResp.Hits,
}
resultJSON, err := json.Marshal(result)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to format results: %v", err)), nil
}
return mcp.NewToolResultText(string(resultJSON)), nil
}
}