8658c22d34
Co-authored-by: colindembovsky <1932561+colindembovsky@users.noreply.github.com>
145 lines
4.7 KiB
Go
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
|
|
}
|
|
}
|