- Notifications
You must be signed in to change notification settings - Fork3k
Add GitHub Docs search MCP endpoint#1338
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
base:main
Are you sure you want to change the base?
Changes fromall commits
774cc7616e019a184615cb7ba52b51acc59145a8742f846ed93a13ee8658c22ee35ce1File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| { | ||
| "annotations": { | ||
| "title": "Search GitHub Docs", | ||
| "readOnlyHint": true | ||
| }, | ||
| "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.", | ||
| "inputSchema": { | ||
| "properties": { | ||
| "language": { | ||
| "description": "Language code for documentation. Options: 'en' (default), 'es', 'ja', 'pt', 'zh', 'ru', 'fr', 'ko', 'de'", | ||
| "type": "string" | ||
| }, | ||
| "max_results": { | ||
| "description": "Maximum number of results to return (default: 10, max: 100)", | ||
| "type": "number" | ||
| }, | ||
| "query": { | ||
| "description": "Search query for GitHub documentation. Examples: 'actions workflow syntax', 'pull request review', 'GitHub Pages'", | ||
| "type": "string" | ||
| }, | ||
| "version": { | ||
| "description": "GitHub version to search. Options: 'dotcom' (default, free/pro/team), 'ghec' (GitHub Enterprise Cloud), or a specific GHES version like '3.12'", | ||
| "type": "string" | ||
| } | ||
| }, | ||
| "required": [ | ||
| "query" | ||
| ], | ||
| "type": "object" | ||
| }, | ||
| "name": "search_github_docs" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| 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) | ||
Comment on lines +107 to +108 CopilotAI | ||
| 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 | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.