From 41090e5ed714c2cfc9254bd6099b29106d9c98b2 Mon Sep 17 00:00:00 2001 From: Tommaso Moro <37270480+tommaso-moro@users.noreply.github.com> Date: Thu, 13 Nov 2025 10:08:03 +0000 Subject: [PATCH 01/16] Update docs regarding MCP Server behaviour when bad toolsets are provided (#1398) * update docs * update --- docs/remote-server.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/remote-server.md b/docs/remote-server.md index ce506893e..b263d70aa 100644 --- a/docs/remote-server.md +++ b/docs/remote-server.md @@ -57,7 +57,7 @@ The Remote GitHub MCP server has optional headers equivalent to the Local server - `X-MCP-Toolsets`: Comma-separated list of toolsets to enable. E.g. "repos,issues". - Equivalent to `GITHUB_TOOLSETS` env var for Local server. - - If the list is empty, default toolsets will be used. If a bad toolset is provided, the server will fail to start and emit a 400 bad request status. Whitespace is ignored. + - If the list is empty, default toolsets will be used. Invalid or unknown toolsets are silently ignored without error and will not prevent the server from starting. Whitespace is ignored. - `X-MCP-Readonly`: Enables only "read" tools. - Equivalent to `GITHUB_READ_ONLY` env var for Local server. - If this header is empty, "false", "f", "no", "n", "0", or "off" (ignoring whitespace and case), it will be interpreted as false. All other values are interpreted as true. From 1d5603fa80b13ea236dd78afa4bc55890493a18d Mon Sep 17 00:00:00 2001 From: Tommaso Moro <37270480+tommaso-moro@users.noreply.github.com> Date: Thu, 13 Nov 2025 11:23:02 +0000 Subject: [PATCH 02/16] Update docs to state that dynamic mode is not available in Remote Server (#1399) * update * fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 70491f6fd..b03099e53 100644 --- a/README.md +++ b/README.md @@ -1218,7 +1218,7 @@ Possible options: ## Dynamic Tool Discovery -**Note**: This feature is currently in beta and may not be available in all environments. Please test it out and let us know if you encounter any issues. +**Note**: This feature is currently in beta and is not available in the Remote GitHub MCP Server. Please test it out and let us know if you encounter any issues. Instead of starting with all tools enabled, you can turn on dynamic toolset discovery. Dynamic toolsets allow the MCP host to list and enable toolsets in response to a user prompt. This should help to avoid situations where the model gets confused by the sheer number of tools available. From 4fcfaa513058dbbee83e788fc801421e58636e3a Mon Sep 17 00:00:00 2001 From: Thomas Sickert <80130182+thomas-sickert@users.noreply.github.com> Date: Thu, 13 Nov 2025 09:24:02 -0500 Subject: [PATCH 03/16] Set title for GitHub MCP Server in server.json (#1397) * Set title for GitHub MCP Server in server.json * Update server.json --- server.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server.json b/server.json index 127e4bd05..d0ab16035 100644 --- a/server.json +++ b/server.json @@ -2,6 +2,7 @@ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json", "name": "io.github.github/github-mcp-server", "description": "Connect AI assistants to GitHub - manage repos, issues, PRs, and workflows through natural language.", + "title": "GitHub", "status": "active", "repository": { "url": "https://github.com/github/github-mcp-server", @@ -61,4 +62,4 @@ ] } ] -} \ No newline at end of file +} From 3d6ce6881bd7ed37114f99028f52fe7fb8d7b661 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 09:33:57 +0100 Subject: [PATCH 04/16] build(deps): bump docker/metadata-action from 5.8.0 to 5.9.0 (#1380) Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5.8.0 to 5.9.0. - [Release notes](https://github.com/docker/metadata-action/releases) - [Commits](https://github.com/docker/metadata-action/compare/c1e51972afc2121e065aed6d45c65596fe445f3f...318604b99e75e41977312d83839a89be02ca4893) --- updated-dependencies: - dependency-name: docker/metadata-action dependency-version: 5.9.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: JoannaaKL --- .github/workflows/docker-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 28c7f00a0..baaf6c2f0 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -70,7 +70,7 @@ jobs: # https://github.com/docker/metadata-action - name: Extract Docker metadata id: meta - uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0 + uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | From e9033462e13376e1a68422d492911972ec93d6a4 Mon Sep 17 00:00:00 2001 From: Tom Elliott <13594679+tmelliottjr@users.noreply.github.com> Date: Fri, 14 Nov 2025 06:25:35 -0500 Subject: [PATCH 05/16] projects: serialization & pagination updates (#1390) * pagination, serialization updates * projects: add server instructions (#1393) * add server instructions * Update instructions.go --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 18 +- .../__toolsnaps__/list_project_fields.snap | 10 +- .../__toolsnaps__/list_project_items.snap | 16 +- pkg/github/__toolsnaps__/list_projects.snap | 14 +- pkg/github/instructions.go | 69 +++ pkg/github/projects.go | 402 +++++++----------- pkg/github/projects_test.go | 92 ++-- pkg/github/tools.go | 4 +- 8 files changed, 332 insertions(+), 293 deletions(-) diff --git a/README.md b/README.md index b03099e53..6dfa32ae5 100644 --- a/README.md +++ b/README.md @@ -844,24 +844,30 @@ Options are: - `project_number`: The project's number. (number, required) - **list_project_fields** - List project fields + - `after`: Forward pagination cursor from previous pageInfo.nextCursor. (string, optional) + - `before`: Backward pagination cursor from previous pageInfo.prevCursor (rare). (string, optional) - `owner`: If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive. (string, required) - `owner_type`: Owner type (string, required) - - `per_page`: Number of results per page (max 100, default: 30) (number, optional) + - `per_page`: Results per page (max 50) (number, optional) - `project_number`: The project's number. (number, required) - **list_project_items** - List project items - - `fields`: Specific list of field IDs to include in the response (e.g. ["102589", "985201", "169875"]). If not provided, only the title field is included. (string[], optional) + - `after`: Forward pagination cursor from previous pageInfo.nextCursor. (string, optional) + - `before`: Backward pagination cursor from previous pageInfo.prevCursor (rare). (string, optional) + - `fields`: Field IDs to include (e.g. ["102589", "985201"]). CRITICAL: Always provide to get field values. Without this, only titles returned. (string[], optional) - `owner`: If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive. (string, required) - `owner_type`: Owner type (string, required) - - `per_page`: Number of results per page (max 100, default: 30) (number, optional) + - `per_page`: Results per page (max 50) (number, optional) - `project_number`: The project's number. (number, required) - - `query`: Search query to filter items (string, optional) + - `query`: Query string for advanced filtering of project items using GitHub's project filtering syntax. (string, optional) - **list_projects** - List projects + - `after`: Forward pagination cursor from previous pageInfo.nextCursor. (string, optional) + - `before`: Backward pagination cursor from previous pageInfo.prevCursor (rare). (string, optional) - `owner`: If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive. (string, required) - `owner_type`: Owner type (string, required) - - `per_page`: Number of results per page (max 100, default: 30) (number, optional) - - `query`: Filter projects by a search query (matches title and description) (string, optional) + - `per_page`: Results per page (max 50) (number, optional) + - `query`: Filter projects by title text and open/closed state; permitted qualifiers: is:open, is:closed; examples: "roadmap is:open", "is:open feature planning". (string, optional) - **update_project_item** - Update project item - `item_id`: The unique identifier of the project item. This is not the issue or pull request ID. (number, required) diff --git a/pkg/github/__toolsnaps__/list_project_fields.snap b/pkg/github/__toolsnaps__/list_project_fields.snap index 0a2180e2b..c543e69d7 100644 --- a/pkg/github/__toolsnaps__/list_project_fields.snap +++ b/pkg/github/__toolsnaps__/list_project_fields.snap @@ -6,6 +6,14 @@ "description": "List Project fields for a user or org", "inputSchema": { "properties": { + "after": { + "description": "Forward pagination cursor from previous pageInfo.nextCursor.", + "type": "string" + }, + "before": { + "description": "Backward pagination cursor from previous pageInfo.prevCursor (rare).", + "type": "string" + }, "owner": { "description": "If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.", "type": "string" @@ -19,7 +27,7 @@ "type": "string" }, "per_page": { - "description": "Number of results per page (max 100, default: 30)", + "description": "Results per page (max 50)", "type": "number" }, "project_number": { diff --git a/pkg/github/__toolsnaps__/list_project_items.snap b/pkg/github/__toolsnaps__/list_project_items.snap index ebc7d17df..38d3cb509 100644 --- a/pkg/github/__toolsnaps__/list_project_items.snap +++ b/pkg/github/__toolsnaps__/list_project_items.snap @@ -3,11 +3,19 @@ "title": "List project items", "readOnlyHint": true }, - "description": "List Project items for a user or org", + "description": "Search project items with advanced filtering", "inputSchema": { "properties": { + "after": { + "description": "Forward pagination cursor from previous pageInfo.nextCursor.", + "type": "string" + }, + "before": { + "description": "Backward pagination cursor from previous pageInfo.prevCursor (rare).", + "type": "string" + }, "fields": { - "description": "Specific list of field IDs to include in the response (e.g. [\"102589\", \"985201\", \"169875\"]). If not provided, only the title field is included.", + "description": "Field IDs to include (e.g. [\"102589\", \"985201\"]). CRITICAL: Always provide to get field values. Without this, only titles returned.", "items": { "type": "string" }, @@ -26,7 +34,7 @@ "type": "string" }, "per_page": { - "description": "Number of results per page (max 100, default: 30)", + "description": "Results per page (max 50)", "type": "number" }, "project_number": { @@ -34,7 +42,7 @@ "type": "number" }, "query": { - "description": "Search query to filter items", + "description": "Query string for advanced filtering of project items using GitHub's project filtering syntax.", "type": "string" } }, diff --git a/pkg/github/__toolsnaps__/list_projects.snap b/pkg/github/__toolsnaps__/list_projects.snap index 8de28989a..8a035271c 100644 --- a/pkg/github/__toolsnaps__/list_projects.snap +++ b/pkg/github/__toolsnaps__/list_projects.snap @@ -3,9 +3,17 @@ "title": "List projects", "readOnlyHint": true }, - "description": "List Projects for a user or org", + "description": "List Projects for a user or organization", "inputSchema": { "properties": { + "after": { + "description": "Forward pagination cursor from previous pageInfo.nextCursor.", + "type": "string" + }, + "before": { + "description": "Backward pagination cursor from previous pageInfo.prevCursor (rare).", + "type": "string" + }, "owner": { "description": "If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.", "type": "string" @@ -19,11 +27,11 @@ "type": "string" }, "per_page": { - "description": "Number of results per page (max 100, default: 30)", + "description": "Results per page (max 50)", "type": "number" }, "query": { - "description": "Filter projects by a search query (matches title and description)", + "description": "Filter projects by title text and open/closed state; permitted qualifiers: is:open, is:closed; examples: \"roadmap is:open\", \"is:open feature planning\".", "type": "string" } }, diff --git a/pkg/github/instructions.go b/pkg/github/instructions.go index e783c6c08..338b8b987 100644 --- a/pkg/github/instructions.go +++ b/pkg/github/instructions.go @@ -62,6 +62,75 @@ Check 'list_issue_types' first for organizations to use proper issue types. Use return `## Discussions Use 'list_discussion_categories' to understand available categories before creating discussions. Filter by category for better organization.` + case "projects": + return `## Projects + +Workflow: 1) list_project_fields (get field IDs), 2) list_project_items (with pagination), 3) optional updates. + +Field usage: + - Call list_project_fields first to understand available fields and get IDs/types before filtering. + - Use EXACT returned field names (case-insensitive match). Don't invent names or IDs. + - Iteration synonyms (sprint/cycle) only if that field exists; map to the actual name (e.g. sprint:@current). + - Only include filters for fields that exist and are relevant. + +Pagination (mandatory): + - Loop while pageInfo.hasNextPage=true using after=pageInfo.nextCursor. + - Keep query, fields, per_page IDENTICAL on every page. + - Use before=pageInfo.prevCursor only when explicitly navigating to a previous page. + +Counting rules: + - Count items array length after full pagination. + - Never count field objects, content, or nested arrays as separate items. + +Summary vs list: + - Summaries ONLY if user uses verbs: analyze | summarize | summary | report | overview | insights. + - Listing verbs (list/show/get/fetch/display/enumerate) → enumerate + total. + +Self-check before returning: + - Paginated fully + - Correct IDs used + - Field names valid + - Summary only if requested. + +Return COMPLETE data or state what's missing (e.g. pages skipped). + +list_project_items query rules: +Query string - For advanced filtering of project items using GitHub's project filtering syntax: + +MUST reflect user intent; strongly prefer explicit content type if narrowed: + - "open issues" → state:open is:issue + - "merged PRs" → state:merged is:pr + - "items updated this week" → updated:>@today-7d (omit type only if mixed desired) + - "list all P1 priority items" → priority:p1 (omit state if user wants all, omit type if user specifies "items") + - "list all open P2 issues" → is:issue state:open priority:p2 (include state if user wants open or closed, include type if user specifies "issues" or "PRs") + - "all open issues I'm working on" → is:issue state:open assignee:@me + +Query Construction Heuristics: + a. Extract type nouns: issues → is:issue | PRs, Pulls, or Pull Requests → is:pr | tasks/tickets → is:issue (ask if ambiguity) + b. Map temporal phrases: "this week" → updated:>@today-7d + c. Map negations: "excluding wontfix" → -label:wontfix + d. Map priority adjectives: "high/sev1/p1" → priority:high OR priority:p1 (choose based on field presence) + e. When filtering by label, always use wildcard matching to account for cross-repository differences or emojis: (e.g. "bug 🐛" → label:*bug*) + f. When filtering by milestone, always use wildcard matching to account for cross-repository differences: (e.g. "v1.0" → milestone:*v1.0*) + +Syntax Essentials (items): + AND: space-separated. (label:bug priority:high). + OR: comma inside one qualifier (label:bug,critical). + NOT: leading '-' (-label:wontfix). + Hyphenate multi-word field names. (team-name:"Backend Team", story-points:>5). + Quote multi-word values. (status:"In Review" team-name:"Backend Team"). + Ranges: points:1..3, updated:<@today-30d. + Wildcards: title:*crash*, label:bug*. + Assigned to User: assignee:@me | assignee:username | no:assignee + +Common Qualifier Glossary (items): + is:issue | is:pr | state:open|closed|merged | assignee:@me|username | label:NAME | status:VALUE | + priority:p1|high | sprint-name:@current | team-name:"Backend Team" | parent-issue:"org/repo#123" | + updated:>@today-7d | title:*text* | -label:wontfix | label:bug,critical | no:assignee | has:label + +Never: + - Infer field IDs; fetch via list_project_fields. + - Drop 'fields' param on subsequent pages if field values are needed.` default: return "" } diff --git a/pkg/github/projects.go b/pkg/github/projects.go index 21d4c1103..c961c8678 100644 --- a/pkg/github/projects.go +++ b/pkg/github/projects.go @@ -23,11 +23,12 @@ const ( ProjectAddFailedError = "failed to add a project item" ProjectDeleteFailedError = "failed to delete a project item" ProjectListFailedError = "failed to list project items" + MaxProjectsPerPage = 50 ) func ListProjects(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("list_projects", - mcp.WithDescription(t("TOOL_LIST_PROJECTS_DESCRIPTION", "List Projects for a user or org")), + mcp.WithDescription(t("TOOL_LIST_PROJECTS_DESCRIPTION", `List Projects for a user or organization`)), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_LIST_PROJECTS_USER_TITLE", "List projects"), ReadOnlyHint: ToBoolPtr(true), @@ -40,28 +41,38 @@ func ListProjects(getClient GetClientFn, t translations.TranslationHelperFunc) ( mcp.Description("If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive."), ), mcp.WithString("query", - mcp.Description("Filter projects by a search query (matches title and description)"), + mcp.Description(`Filter projects by title text and open/closed state; permitted qualifiers: is:open, is:closed; examples: "roadmap is:open", "is:open feature planning".`), ), mcp.WithNumber("per_page", - mcp.Description("Number of results per page (max 100, default: 30)"), + mcp.Description(fmt.Sprintf("Results per page (max %d)", MaxProjectsPerPage)), + ), + mcp.WithString("after", + mcp.Description("Forward pagination cursor from previous pageInfo.nextCursor."), + ), + mcp.WithString("before", + mcp.Description("Backward pagination cursor from previous pageInfo.prevCursor (rare)."), ), ), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := RequiredParam[string](req, "owner") if err != nil { return mcp.NewToolResultError(err.Error()), nil } + ownerType, err := RequiredParam[string](req, "owner_type") if err != nil { return mcp.NewToolResultError(err.Error()), nil } + queryStr, err := OptionalParam[string](req, "query") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - perPage, err := OptionalIntParamWithDefault(req, "per_page", 30) + + pagination, err := extractPaginationOptions(req) if err != nil { return mcp.NewToolResultError(err.Error()), nil } + client, err := getClient(ctx) if err != nil { return mcp.NewToolResultError(err.Error()), nil @@ -77,7 +88,7 @@ func ListProjects(getClient GetClientFn, t translations.TranslationHelperFunc) ( minimalProjects := []MinimalProject{} opts := &github.ListProjectsOptions{ - ListProjectsPaginationOptions: github.ListProjectsPaginationOptions{PerPage: &perPage}, + ListProjectsPaginationOptions: pagination, Query: queryPtr, } @@ -100,14 +111,12 @@ func ListProjects(getClient GetClientFn, t translations.TranslationHelperFunc) ( minimalProjects = append(minimalProjects, *convertToMinimalProject(project)) } - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to list projects: %s", string(body))), nil + response := map[string]any{ + "projects": minimalProjects, + "pageInfo": buildPageInfo(resp), } - r, err := json.Marshal(minimalProjects) + + r, err := json.Marshal(response) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } @@ -213,43 +222,64 @@ func ListProjectFields(getClient GetClientFn, t translations.TranslationHelperFu mcp.Description("The project's number."), ), mcp.WithNumber("per_page", - mcp.Description("Number of results per page (max 100, default: 30)"), + mcp.Description(fmt.Sprintf("Results per page (max %d)", MaxProjectsPerPage)), + ), + mcp.WithString("after", + mcp.Description("Forward pagination cursor from previous pageInfo.nextCursor."), + ), + mcp.WithString("before", + mcp.Description("Backward pagination cursor from previous pageInfo.prevCursor (rare)."), ), ), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := RequiredParam[string](req, "owner") if err != nil { return mcp.NewToolResultError(err.Error()), nil } + ownerType, err := RequiredParam[string](req, "owner_type") if err != nil { return mcp.NewToolResultError(err.Error()), nil } + projectNumber, err := RequiredInt(req, "project_number") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - perPage, err := OptionalIntParamWithDefault(req, "per_page", 30) + + pagination, err := extractPaginationOptions(req) if err != nil { return mcp.NewToolResultError(err.Error()), nil } + client, err := getClient(ctx) if err != nil { return mcp.NewToolResultError(err.Error()), nil } - var resp *github.Response - var projectFields []*github.ProjectV2Field - opts := &github.ListProjectsOptions{ - ListProjectsPaginationOptions: github.ListProjectsPaginationOptions{PerPage: &perPage}, + ListProjectsPaginationOptions: pagination, } + var url string if ownerType == "org" { - projectFields, resp, err = client.Projects.ListOrganizationProjectFields(ctx, owner, projectNumber, opts) + url = fmt.Sprintf("orgs/%s/projectsV2/%d/fields", owner, projectNumber) } else { - projectFields, resp, err = client.Projects.ListUserProjectFields(ctx, owner, projectNumber, opts) + url = fmt.Sprintf("users/%s/projectsV2/%d/fields", owner, projectNumber) + } + + url, err = addOptions(url, opts) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + httpRequest, err := client.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) } + var projectFields []projectV2Field + resp, err := client.Do(ctx, httpRequest, &projectFields) + if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list project fields", @@ -259,14 +289,12 @@ func ListProjectFields(getClient GetClientFn, t translations.TranslationHelperFu } defer func() { _ = resp.Body.Close() }() - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to list project fields: %s", string(body))), nil + response := map[string]any{ + "fields": projectFields, + "pageInfo": buildPageInfo(resp), } - r, err := json.Marshal(projectFields) + + r, err := json.Marshal(response) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } @@ -354,7 +382,7 @@ func GetProjectField(getClient GetClientFn, t translations.TranslationHelperFunc func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("list_project_items", - mcp.WithDescription(t("TOOL_LIST_PROJECT_ITEMS_DESCRIPTION", "List Project items for a user or org")), + mcp.WithDescription(t("TOOL_LIST_PROJECT_ITEMS_DESCRIPTION", `Search project items with advanced filtering`)), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_LIST_PROJECT_ITEMS_USER_TITLE", "List project items"), ReadOnlyHint: ToBoolPtr(true), @@ -372,13 +400,19 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun mcp.Description("The project's number."), ), mcp.WithString("query", - mcp.Description("Search query to filter items"), + mcp.Description(`Query string for advanced filtering of project items using GitHub's project filtering syntax.`), ), mcp.WithNumber("per_page", - mcp.Description("Number of results per page (max 100, default: 30)"), + mcp.Description(fmt.Sprintf("Results per page (max %d)", MaxProjectsPerPage)), + ), + mcp.WithString("after", + mcp.Description("Forward pagination cursor from previous pageInfo.nextCursor."), + ), + mcp.WithString("before", + mcp.Description("Backward pagination cursor from previous pageInfo.prevCursor (rare)."), ), mcp.WithArray("fields", - mcp.Description("Specific list of field IDs to include in the response (e.g. [\"102589\", \"985201\", \"169875\"]). If not provided, only the title field is included."), + mcp.Description("Field IDs to include (e.g. [\"102589\", \"985201\"]). CRITICAL: Always provide to get field values. Without this, only titles returned."), mcp.WithStringItems(), ), ), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -386,33 +420,39 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun if err != nil { return mcp.NewToolResultError(err.Error()), nil } + ownerType, err := RequiredParam[string](req, "owner_type") if err != nil { return mcp.NewToolResultError(err.Error()), nil } + projectNumber, err := RequiredInt(req, "project_number") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - perPage, err := OptionalIntParamWithDefault(req, "per_page", 30) + + queryStr, err := OptionalParam[string](req, "query") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - queryStr, err := OptionalParam[string](req, "query") + + fields, err := OptionalBigIntArrayParam(req, "fields") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - fields, err := OptionalBigIntArrayParam(req, "fields") + + pagination, err := extractPaginationOptions(req) if err != nil { return mcp.NewToolResultError(err.Error()), nil } + client, err := getClient(ctx) if err != nil { return mcp.NewToolResultError(err.Error()), nil } var resp *github.Response - var projectItems []*github.ProjectV2Item + var projectItems []projectV2Item var queryPtr *string if queryStr != "" { @@ -422,17 +462,30 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun opts := &github.ListProjectItemsOptions{ Fields: fields, ListProjectsOptions: github.ListProjectsOptions{ - ListProjectsPaginationOptions: github.ListProjectsPaginationOptions{PerPage: &perPage}, + ListProjectsPaginationOptions: pagination, Query: queryPtr, }, } + var url string if ownerType == "org" { - projectItems, resp, err = client.Projects.ListOrganizationProjectItems(ctx, owner, projectNumber, opts) + url = fmt.Sprintf("orgs/%s/projectsV2/%d/items", owner, projectNumber) } else { - projectItems, resp, err = client.Projects.ListUserProjectItems(ctx, owner, projectNumber, opts) + url = fmt.Sprintf("users/%s/projectsV2/%d/items", owner, projectNumber) + } + + url, err = addOptions(url, opts) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + httpRequest, err := client.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) } + resp, err = client.Do(ctx, httpRequest, &projectItems) + if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, ProjectListFailedError, @@ -442,15 +495,12 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun } defer func() { _ = resp.Body.Close() }() - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("%s: %s", ProjectListFailedError, string(body))), nil + response := map[string]any{ + "items": projectItems, + "pageInfo": buildPageInfo(resp), } - r, err := json.Marshal(projectItems) + r, err := json.Marshal(response) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } @@ -492,10 +542,12 @@ func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) if err != nil { return mcp.NewToolResultError(err.Error()), nil } + ownerType, err := RequiredParam[string](req, "owner_type") if err != nil { return mcp.NewToolResultError(err.Error()), nil } + projectNumber, err := RequiredInt(req, "project_number") if err != nil { return mcp.NewToolResultError(err.Error()), nil @@ -549,13 +601,6 @@ func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) } defer func() { _ = resp.Body.Close() }() - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to get project item: %s", string(body))), nil - } r, err := json.Marshal(projectItem) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) @@ -857,10 +902,10 @@ type updateProjectItem struct { } type projectV2ItemFieldValue struct { - ID *int64 `json:"id,omitempty"` // The unique identifier for this field. - Name string `json:"name,omitempty"` // The display name of the field. - DataType string `json:"data_type,omitempty"` // The data type of the field (e.g., "text", "number", "date", "single_select", "multi_select"). - Value interface{} `json:"value,omitempty"` // The value of the field for a specific project item. + ID *int64 `json:"id,omitempty"` + Name string `json:"name,omitempty"` + DataType string `json:"data_type,omitempty"` + Value any `json:"value,omitempty"` } type projectV2Item struct { @@ -885,7 +930,6 @@ type projectV2ItemContent struct { CreatedAt *github.Timestamp `json:"created_at,omitempty"` ID *int64 `json:"id,omitempty"` Number *int `json:"number,omitempty"` - Repository MinimalRepository `json:"repository,omitempty"` State *string `json:"state,omitempty"` StateReason *string `json:"stateReason,omitempty"` Title *string `json:"title,omitempty"` @@ -893,6 +937,25 @@ type projectV2ItemContent struct { URL *string `json:"url,omitempty"` } +type pageInfo struct { + HasNextPage bool `json:"hasNextPage"` + HasPreviousPage bool `json:"hasPreviousPage"` + NextCursor string `json:"nextCursor,omitempty"` + PrevCursor string `json:"prevCursor,omitempty"` +} + +type projectV2Field struct { + ID *int64 `json:"id,omitempty"` + NodeID *string `json:"node_id,omitempty"` + Name *string `json:"name,omitempty"` + DataType *string `json:"data_type,omitempty"` + ProjectURL *string `json:"project_url,omitempty"` + Options []any `json:"options,omitempty"` + Configuration any `json:"configuration,omitempty"` + CreatedAt *github.Timestamp `json:"created_at,omitempty"` + UpdatedAt *github.Timestamp `json:"updated_at,omitempty"` +} + func toNewProjectType(projType string) string { switch strings.ToLower(projType) { case "issue": @@ -928,6 +991,50 @@ func buildUpdateProjectItem(input map[string]any) (*updateProjectItem, error) { return payload, nil } +func buildPageInfo(resp *github.Response) pageInfo { + return pageInfo{ + HasNextPage: resp.After != "", + HasPreviousPage: resp.Before != "", + NextCursor: resp.After, + PrevCursor: resp.Before, + } +} + +func extractPaginationOptions(request mcp.CallToolRequest) (github.ListProjectsPaginationOptions, error) { + perPage, err := OptionalIntParamWithDefault(request, "per_page", MaxProjectsPerPage) + if err != nil { + return github.ListProjectsPaginationOptions{}, err + } + if perPage > MaxProjectsPerPage { + perPage = MaxProjectsPerPage + } + + after, err := OptionalParam[string](request, "after") + if err != nil { + return github.ListProjectsPaginationOptions{}, err + } + + before, err := OptionalParam[string](request, "before") + if err != nil { + return github.ListProjectsPaginationOptions{}, err + } + + opts := github.ListProjectsPaginationOptions{ + PerPage: &perPage, + } + + // Only set After/Before if they have non-empty values + if after != "" { + opts.After = &after + } + + if before != "" { + opts.Before = &before + } + + return opts, nil +} + // addOptions adds the parameters in opts as URL query parameters to s. opts // must be a struct whose fields may contain "url" tags. func addOptions(s string, opts any) (string, error) { @@ -959,184 +1066,3 @@ func addOptions(s string, opts any) (string, error) { origURL.RawQuery = origValues.Encode() return origURL.String(), nil } - -func ManageProjectItemsPrompt(t translations.TranslationHelperFunc) (tool mcp.Prompt, handler server.PromptHandlerFunc) { - return mcp.NewPrompt("ManageProjectItems", - mcp.WithPromptDescription(t("PROMPT_MANAGE_PROJECT_ITEMS_DESCRIPTION", "Interactive guide for managing GitHub Projects V2, including discovery, field management, querying, and updates.")), - mcp.WithArgument("owner", mcp.ArgumentDescription("The owner of the project (user or organization name)"), mcp.RequiredArgument()), - mcp.WithArgument("owner_type", mcp.ArgumentDescription("Type of owner: 'user' or 'org'"), mcp.RequiredArgument()), - mcp.WithArgument("task", mcp.ArgumentDescription("Optional: specific task to focus on (e.g., 'discover_projects', 'update_items', 'create_reports')")), - ), func(_ context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { - owner := request.Params.Arguments["owner"] - ownerType := request.Params.Arguments["owner_type"] - - task := "" - if t, exists := request.Params.Arguments["task"]; exists { - task = fmt.Sprintf("%v", t) - } - - messages := []mcp.PromptMessage{ - { - Role: "system", - Content: mcp.NewTextContent("You are a GitHub Projects V2 management assistant. Your expertise includes:\n\n" + - "**Core Capabilities:**\n" + - "- Project discovery and field analysis\n" + - "- Item querying with advanced filters\n" + - "- Field value updates and management\n" + - "- Progress reporting and insights\n\n" + - "**Key Rules:**\n" + - "- ALWAYS use the 'query' parameter in **list_project_items** to filter results effectively\n" + - "- ALWAYS include 'fields' parameter with specific field IDs to retrieve field values\n" + - "- Use proper field IDs (not names) when updating items\n" + - "- Provide step-by-step workflows with concrete examples\n\n" + - "**Understanding Project Items:**\n" + - "- Project items reference underlying content (issues or pull requests)\n" + - "- Project tools provide: project fields, item metadata, and basic content info\n" + - "- For detailed information about an issue or pull request (comments, events, etc.), use issue/PR specific tools\n" + - "- The 'content' field in project items includes: repository, issue/PR number, title, state\n" + - "- Use this info to fetch full details: **get_issue**, **list_comments**, **list_issue_events**\n\n" + - "**Available Tools:**\n" + - "- **list_projects**: Discover available projects\n" + - "- **get_project**: Get detailed project information\n" + - "- **list_project_fields**: Get field definitions and IDs\n" + - "- **list_project_items**: Query items with filters and field selection\n" + - "- **get_project_item**: Get specific item details\n" + - "- **add_project_item**: Add issues/PRs to projects\n" + - "- **update_project_item**: Update field values\n" + - "- **delete_project_item**: Remove items from projects"), - }, - { - Role: "user", - Content: mcp.NewTextContent(fmt.Sprintf("I want to work with GitHub Projects for %s (owner_type: %s).%s\n\n"+ - "Help me get started with project management tasks.", - owner, - ownerType, - func() string { - if task != "" { - return fmt.Sprintf(" I'm specifically interested in: %s.", task) - } - return "" - }())), - }, - { - Role: "assistant", - Content: mcp.NewTextContent(fmt.Sprintf("Perfect! I'll help you manage GitHub Projects for %s. Let me guide you through the essential workflows.\n\n"+ - "**🔍 Step 1: Project Discovery**\n"+ - "First, let's see what projects are available using **list_projects**.", owner)), - }, - { - Role: "user", - Content: mcp.NewTextContent("Great! After seeing the projects, I want to understand how to work with project fields and items."), - }, - { - Role: "assistant", - Content: mcp.NewTextContent("**📋 Step 2: Understanding Project Structure**\n\n" + - "Once you select a project, I'll help you:\n\n" + - "1. **Get field information** using **list_project_fields**\n" + - " - Find field IDs, names, and data types\n" + - " - Understand available options for select fields\n" + - " - Identify required vs. optional fields\n\n" + - "2. **Query project items** using **list_project_items**\n" + - " - Filter by assignees: query=\"assignee:@me\"\n" + - " - Filter by status: query=\"status:In Progress\"\n" + - " - Filter by labels: query=\"label:bug\"\n" + - " - Include specific fields: fields=[\"198354254\", \"198354255\"]\n\n" + - "**💡 Pro Tip:** Always specify the 'fields' parameter to get field values, not just titles!"), - }, - { - Role: "user", - Content: mcp.NewTextContent("How do I update field values? What about the different field types?"), - }, - { - Role: "assistant", - Content: mcp.NewTextContent("**✏️ Step 3: Updating Field Values**\n\n" + - "Use **update_project_item** with the updated_field parameter. The format varies by field type:\n\n" + - "**Text fields:**\n" + - "```json\n" + - "{\"id\": 123456, \"value\": \"Updated text content\"}\n" + - "```\n\n" + - "**Single-select fields:**\n" + - "```json\n" + - "{\"id\": 198354254, \"value\": 18498754}\n" + - "```\n" + - "*(Use option ID, not option name)*\n\n" + - "**Date fields:**\n" + - "```json\n" + - "{\"id\": 789012, \"value\": \"2024-03-15\"}\n" + - "```\n\n" + - "**Number fields:**\n" + - "```json\n" + - "{\"id\": 345678, \"value\": 5}\n" + - "```\n\n" + - "**Clear a field:**\n" + - "```json\n" + - "{\"id\": 123456, \"value\": null}\n" + - "```\n\n" + - "**⚠️ Important:** Use the internal project item_id (not issue/PR number) for updates!"), - }, - { - Role: "user", - Content: mcp.NewTextContent("Can you show me a complete workflow example?"), - }, - { - Role: "assistant", - Content: mcp.NewTextContent(fmt.Sprintf("**🔄 Complete Workflow Example**\n\n"+ - "Here's how to find and update your assigned items:\n\n"+ - "**Step 1:** Discover projects\n\n"+ - "**list_projects** owner=\"%s\" owner_type=\"%s\"\n\n\n"+ - "**Step 2:** Get project fields (using project #123)\n\n"+ - "**list_project_fields** owner=\"%s\" owner_type=\"%s\" project_number=123\n\n"+ - "*(Note the Status field ID, e.g., 198354254)*\n\n"+ - "**Step 3:** Query your assigned items\n\n"+ - "**list_project_items**\n"+ - " owner=\"%s\"\n"+ - " owner_type=\"%s\"\n"+ - " project_number=123\n"+ - " query=\"assignee:@me\"\n"+ - " fields=[\"198354254\", \"other_field_ids\"]\n\n\n"+ - "**Step 4:** Update item status\n\n"+ - "**update_project_item**\n"+ - " owner=\"%s\"\n"+ - " owner_type=\"%s\"\n"+ - " project_number=123\n"+ - " item_id=789123\n"+ - " updated_field={\"id\": 198354254, \"value\": 18498754}\n\n\n"+ - "Let me start by listing your projects now!", owner, ownerType, owner, ownerType, owner, ownerType, owner, ownerType)), - }, - { - Role: "user", - Content: mcp.NewTextContent("What if I need more details about the items, like recent comments or linked pull requests?"), - }, - { - Role: "assistant", - Content: mcp.NewTextContent("**📝 Accessing Underlying Issue/PR Details**\n\n" + - "Project items contain basic content info, but for detailed information you need to use issue/PR tools:\n\n" + - "**From project items, extract:**\n" + - "- content.repository.name and content.repository.owner.login\n" + - "- content.number (the issue/PR number)\n" + - "- content_type (\"Issue\" or \"PullRequest\")\n\n" + - "**Then use these tools for details:**\n\n" + - "1. **Get full issue/PR details:**\n" + - " - **get_issue** owner=repo_owner repo=repo_name issue_number=123\n" + - " - Returns: full body, labels, assignees, milestone, etc.\n\n" + - "2. **Get recent comments:**\n" + - " - **list_comments** owner=repo_owner repo=repo_name issue_number=123\n" + - " - Add since parameter to filter recent comments\n\n" + - "3. **Get issue events:**\n" + - " - **list_issue_events** owner=repo_owner repo=repo_name issue_number=123\n" + - " - Shows timeline: assignments, label changes, status updates\n\n" + - "4. **For pull requests specifically:**\n" + - " - **get_pull_request** owner=repo_owner repo=repo_name pull_number=123\n" + - " - **list_pull_request_reviews** for review status\n\n" + - "**💡 Example:** To check for blockers in comments:\n" + - "1. Get project items with query=\"assignee:@me is:open\"\n" + - "2. For each item, extract repository and issue number from content\n" + - "3. Use **list_comments** to get recent comments\n" + - "4. Search comments for keywords like \"blocked\", \"blocker\", \"waiting\""), - }, - } - return &mcp.GetPromptResult{ - Messages: messages, - }, nil - } -} diff --git a/pkg/github/projects_test.go b/pkg/github/projects_test.go index ed198a97a..c43853a3d 100644 --- a/pkg/github/projects_test.go +++ b/pkg/github/projects_test.go @@ -28,8 +28,9 @@ func Test_ListProjects(t *testing.T) { assert.Contains(t, tool.InputSchema.Properties, "per_page") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "owner_type"}) - orgProjects := []map[string]any{{"id": 1, "title": "Org Project"}} - userProjects := []map[string]any{{"id": 2, "title": "User Project"}} + // API returns full ProjectV2 objects; we only need minimal fields for decoding. + orgProjects := []map[string]any{{"id": 1, "node_id": "NODE1", "title": "Org Project"}} + userProjects := []map[string]any{{"id": 2, "node_id": "NODE2", "title": "User Project"}} tests := []struct { name string @@ -44,7 +45,10 @@ func Test_ListProjects(t *testing.T) { mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.EndpointPattern{Pattern: "/orgs/{org}/projectsV2", Method: http.MethodGet}, - mockResponse(t, http.StatusOK, orgProjects), + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write(mock.MustMarshal(orgProjects)) + }), ), ), requestArgs: map[string]interface{}{ @@ -59,7 +63,10 @@ func Test_ListProjects(t *testing.T) { mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.EndpointPattern{Pattern: "/users/{username}/projectsV2", Method: http.MethodGet}, - mockResponse(t, http.StatusOK, userProjects), + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write(mock.MustMarshal(userProjects)) + }), ), ), requestArgs: map[string]interface{}{ @@ -153,10 +160,15 @@ func Test_ListProjects(t *testing.T) { require.False(t, result.IsError) textContent := getTextResult(t, result) - var arr []map[string]any - err = json.Unmarshal([]byte(textContent.Text), &arr) + var response map[string]any + err = json.Unmarshal([]byte(textContent.Text), &response) require.NoError(t, err) - assert.Equal(t, tc.expectedLength, len(arr)) + projects, ok := response["projects"].([]interface{}) + require.True(t, ok) + assert.Equal(t, tc.expectedLength, len(projects)) + // pageInfo should exist + _, hasPageInfo := response["pageInfo"].(map[string]interface{}) + assert.True(t, hasPageInfo) }) } } @@ -305,12 +317,8 @@ func Test_ListProjectFields(t *testing.T) { assert.Contains(t, tool.InputSchema.Properties, "per_page") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner_type", "owner", "project_number"}) - orgFields := []map[string]any{ - {"id": 101, "name": "Status", "dataType": "single_select"}, - } - userFields := []map[string]any{ - {"id": 201, "name": "Priority", "dataType": "single_select"}, - } + orgFields := []map[string]any{{"id": 101, "name": "Status", "data_type": "single_select"}} + userFields := []map[string]any{{"id": 201, "name": "Priority", "data_type": "single_select"}} tests := []struct { name string @@ -325,7 +333,10 @@ func Test_ListProjectFields(t *testing.T) { mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.EndpointPattern{Pattern: "/orgs/{org}/projectsV2/{project}/fields", Method: http.MethodGet}, - mockResponse(t, http.StatusOK, orgFields), + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write(mock.MustMarshal(orgFields)) + }), ), ), requestArgs: map[string]interface{}{ @@ -433,10 +444,14 @@ func Test_ListProjectFields(t *testing.T) { require.False(t, result.IsError) textContent := getTextResult(t, result) - var fields []map[string]any - err = json.Unmarshal([]byte(textContent.Text), &fields) + var response map[string]any + err = json.Unmarshal([]byte(textContent.Text), &response) require.NoError(t, err) + fields, ok := response["fields"].([]interface{}) + require.True(t, ok) assert.Equal(t, tc.expectedLength, len(fields)) + _, hasPageInfo := response["pageInfo"].(map[string]interface{}) + assert.True(t, hasPageInfo) }) } } @@ -653,8 +668,7 @@ func Test_ListProjectItems(t *testing.T) { mock.EndpointPattern{Pattern: "/orgs/{org}/projectsV2/{project}/items", Method: http.MethodGet}, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() - fieldParams := q.Get("fields") - if fieldParams == "123,456,789" { + if q.Get("fields") == "123,456,789" { w.WriteHeader(http.StatusOK) _, _ = w.Write(mock.MustMarshal(orgItems)) return @@ -786,10 +800,14 @@ func Test_ListProjectItems(t *testing.T) { require.False(t, result.IsError) textContent := getTextResult(t, result) - var items []map[string]any - err = json.Unmarshal([]byte(textContent.Text), &items) + var response map[string]any + err = json.Unmarshal([]byte(textContent.Text), &response) require.NoError(t, err) + items, ok := response["items"].([]interface{}) + require.True(t, ok) assert.Equal(t, tc.expectedLength, len(items)) + _, hasPageInfo := response["pageInfo"].(map[string]interface{}) + assert.True(t, hasPageInfo) }) } } @@ -852,8 +870,7 @@ func Test_GetProjectItem(t *testing.T) { mock.EndpointPattern{Pattern: "/orgs/{org}/projectsV2/{project}/items/{item_id}", Method: http.MethodGet}, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() - fieldParams := q.Get("fields") - if fieldParams == "123,456" { + if q.Get("fields") == "123,456" { w.WriteHeader(http.StatusOK) _, _ = w.Write(mock.MustMarshal(orgItem)) return @@ -1351,8 +1368,8 @@ func Test_UpdateProjectItem(t *testing.T) { "owner_type": "org", "project_number": float64(1), "item_id": float64(2), - "field_id": float64(1), - "new_field": map[string]any{ + "updated_field": map[string]any{ + "id": float64(1), "value": "X", }, }, @@ -1365,7 +1382,7 @@ func Test_UpdateProjectItem(t *testing.T) { "owner": "octo-org", "project_number": float64(1), "item_id": float64(2), - "new_field": map[string]any{ + "updated_field": map[string]any{ "id": float64(1), "value": "X", }, @@ -1379,7 +1396,7 @@ func Test_UpdateProjectItem(t *testing.T) { "owner": "octo-org", "owner_type": "org", "item_id": float64(2), - "new_field": map[string]any{ + "updated_field": map[string]any{ "id": float64(1), "value": "X", }, @@ -1393,7 +1410,7 @@ func Test_UpdateProjectItem(t *testing.T) { "owner": "octo-org", "owner_type": "org", "project_number": float64(1), - "new_field": map[string]any{ + "updated_field": map[string]any{ "id": float64(1), "value": "X", }, @@ -1401,19 +1418,18 @@ func Test_UpdateProjectItem(t *testing.T) { expectError: true, }, { - name: "missing field_value", + name: "missing updated_field", mockedClient: mock.NewMockedHTTPClient(), requestArgs: map[string]any{ "owner": "octo-org", "owner_type": "org", "project_number": float64(1), "item_id": float64(2), - "field_id": float64(2), }, expectError: true, }, { - name: "new_field not object", + name: "updated_field not object", mockedClient: mock.NewMockedHTTPClient(), requestArgs: map[string]any{ "owner": "octo-org", @@ -1425,7 +1441,7 @@ func Test_UpdateProjectItem(t *testing.T) { expectError: true, }, { - name: "new_field missing id", + name: "updated_field missing id", mockedClient: mock.NewMockedHTTPClient(), requestArgs: map[string]any{ "owner": "octo-org", @@ -1437,7 +1453,7 @@ func Test_UpdateProjectItem(t *testing.T) { expectError: true, }, { - name: "new_field missing value", + name: "updated_field missing value", mockedClient: mock.NewMockedHTTPClient(), requestArgs: map[string]any{ "owner": "octo-org", @@ -1475,14 +1491,14 @@ func Test_UpdateProjectItem(t *testing.T) { assert.Contains(t, text, "missing required parameter: project_number") case "missing item_id": assert.Contains(t, text, "missing required parameter: item_id") - case "missing field_value": + case "missing updated_field": assert.Contains(t, text, "missing required parameter: updated_field") - case "field_value not object": + case "updated_field not object": assert.Contains(t, text, "field_value must be an object") - case "field_value missing id": - assert.Contains(t, text, "missing required parameter: field_id") - case "field_value missing value": - assert.Contains(t, text, "field_value.value is required") + case "updated_field missing id": + assert.Contains(t, text, "updated_field.id is required") + case "updated_field missing value": + assert.Contains(t, text, "updated_field.value is required") } return } diff --git a/pkg/github/tools.go b/pkg/github/tools.go index 36c22e7a8..989999da4 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -336,9 +336,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG toolsets.NewServerTool(AddProjectItem(getClient, t)), toolsets.NewServerTool(DeleteProjectItem(getClient, t)), toolsets.NewServerTool(UpdateProjectItem(getClient, t)), - ).AddPrompts( - toolsets.NewServerPrompt(ManageProjectItemsPrompt(t)), - ) + ) stargazers := toolsets.NewToolset(ToolsetMetadataStargazers.ID, ToolsetMetadataStargazers.Description). AddReadTools( toolsets.NewServerTool(ListStarredRepositories(getClient, t)), From be6161d14523d57df22e56671969df0b7850fc94 Mon Sep 17 00:00:00 2001 From: Neno Loje Date: Sat, 1 Nov 2025 11:11:43 +0100 Subject: [PATCH 06/16] Fix capitalization in GitHub MCP Server section --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6dfa32ae5..7c4884074 100644 --- a/README.md +++ b/README.md @@ -414,7 +414,7 @@ The following sets of tools are available: | `users` | GitHub User related tools | -### Additional Toolsets in Remote Github MCP Server +### Additional Toolsets in Remote GitHub MCP Server | Toolset | Description | | ----------------------- | ------------------------------------------------------------- | @@ -1188,7 +1188,7 @@ Possible options: -### Additional Tools in Remote Github MCP Server +### Additional Tools in Remote GitHub MCP Server
From c73f06fa299648e7131c586051a03dbc7261fed0 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Fri, 14 Nov 2025 16:20:05 +0000 Subject: [PATCH 07/16] bumps google/go-github to v79 --- cmd/github-mcp-server/generate_docs.go | 2 +- e2e/e2e_test.go | 2 +- go.mod | 2 +- go.sum | 4 ++-- internal/ghmcp/server.go | 2 +- pkg/errors/error.go | 2 +- pkg/errors/error_test.go | 2 +- pkg/github/actions.go | 2 +- pkg/github/actions_test.go | 2 +- pkg/github/code_scanning.go | 2 +- pkg/github/code_scanning_test.go | 2 +- pkg/github/context_tools_test.go | 2 +- pkg/github/dependabot.go | 2 +- pkg/github/dependabot_test.go | 2 +- pkg/github/discussions.go | 2 +- pkg/github/discussions_test.go | 2 +- pkg/github/gists.go | 2 +- pkg/github/gists_test.go | 2 +- pkg/github/git.go | 2 +- pkg/github/issues.go | 2 +- pkg/github/issues_test.go | 2 +- pkg/github/minimal_types.go | 2 +- pkg/github/notifications.go | 2 +- pkg/github/notifications_test.go | 2 +- pkg/github/projects.go | 2 +- pkg/github/projects_test.go | 2 +- pkg/github/pullrequests.go | 2 +- pkg/github/pullrequests_test.go | 2 +- pkg/github/repositories.go | 2 +- pkg/github/repositories_test.go | 2 +- pkg/github/repository_resource.go | 2 +- pkg/github/repository_resource_test.go | 2 +- pkg/github/search.go | 2 +- pkg/github/search_test.go | 2 +- pkg/github/search_utils.go | 2 +- pkg/github/secret_scanning.go | 2 +- pkg/github/secret_scanning_test.go | 2 +- pkg/github/security_advisories.go | 2 +- pkg/github/security_advisories_test.go | 2 +- pkg/github/server.go | 2 +- pkg/github/server_test.go | 2 +- pkg/github/tools.go | 2 +- pkg/raw/raw.go | 2 +- pkg/raw/raw_test.go | 2 +- third-party-licenses.darwin.md | 2 +- third-party-licenses.linux.md | 2 +- third-party-licenses.windows.md | 2 +- 47 files changed, 48 insertions(+), 48 deletions(-) diff --git a/cmd/github-mcp-server/generate_docs.go b/cmd/github-mcp-server/generate_docs.go index 478b35bd4..359370760 100644 --- a/cmd/github-mcp-server/generate_docs.go +++ b/cmd/github-mcp-server/generate_docs.go @@ -13,7 +13,7 @@ import ( "github.com/github/github-mcp-server/pkg/raw" "github.com/github/github-mcp-server/pkg/toolsets" "github.com/github/github-mcp-server/pkg/translations" - gogithub "github.com/google/go-github/v77/github" + gogithub "github.com/google/go-github/v79/github" "github.com/mark3labs/mcp-go/mcp" "github.com/shurcooL/githubv4" "github.com/spf13/cobra" diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index 8c713649a..49dc3e6ee 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -18,7 +18,7 @@ import ( "github.com/github/github-mcp-server/internal/ghmcp" "github.com/github/github-mcp-server/pkg/github" "github.com/github/github-mcp-server/pkg/translations" - gogithub "github.com/google/go-github/v77/github" + gogithub "github.com/google/go-github/v79/github" mcpClient "github.com/mark3labs/mcp-go/client" "github.com/mark3labs/mcp-go/mcp" "github.com/stretchr/testify/require" diff --git a/go.mod b/go.mod index eea55c143..02b9ad252 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/github/github-mcp-server go 1.24.0 require ( - github.com/google/go-github/v77 v77.0.0 + github.com/google/go-github/v79 v79.0.0 github.com/josephburnett/jd v1.9.2 github.com/mark3labs/mcp-go v0.36.0 github.com/microcosm-cc/bluemonday v1.0.27 diff --git a/go.sum b/go.sum index 72ef812df..1ac8b7606 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github/v71 v71.0.0 h1:Zi16OymGKZZMm8ZliffVVJ/Q9YZreDKONCr+WUd0Z30= github.com/google/go-github/v71 v71.0.0/go.mod h1:URZXObp2BLlMjwu0O8g4y6VBneUj2bCHgnI8FfgZ51M= -github.com/google/go-github/v77 v77.0.0 h1:9DsKKbZqil5y/4Z9mNpZDQnpli6PJbqipSuuNdcbjwI= -github.com/google/go-github/v77 v77.0.0/go.mod h1:c8VmGXRUmaZUqbctUcGEDWYnMrtzZfJhDSylEf1wfmA= +github.com/google/go-github/v79 v79.0.0 h1:MdodQojuFPBhmtwHiBcIGLw/e/wei2PvFX9ndxK0X4Y= +github.com/google/go-github/v79 v79.0.0/go.mod h1:OAFbNhq7fQwohojb06iIIQAB9CBGYLq999myfUFnrS4= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= diff --git a/internal/ghmcp/server.go b/internal/ghmcp/server.go index 0e338cfd9..1067a222f 100644 --- a/internal/ghmcp/server.go +++ b/internal/ghmcp/server.go @@ -19,7 +19,7 @@ import ( mcplog "github.com/github/github-mcp-server/pkg/log" "github.com/github/github-mcp-server/pkg/raw" "github.com/github/github-mcp-server/pkg/translations" - gogithub "github.com/google/go-github/v77/github" + gogithub "github.com/google/go-github/v79/github" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/shurcooL/githubv4" diff --git a/pkg/errors/error.go b/pkg/errors/error.go index 72bbeed53..57e4a0d97 100644 --- a/pkg/errors/error.go +++ b/pkg/errors/error.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/mark3labs/mcp-go/mcp" ) diff --git a/pkg/errors/error_test.go b/pkg/errors/error_test.go index 654be569b..0d7aa6afa 100644 --- a/pkg/errors/error_test.go +++ b/pkg/errors/error_test.go @@ -6,7 +6,7 @@ import ( "net/http" "testing" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/github/actions.go b/pkg/github/actions.go index ecf538323..ecf00021a 100644 --- a/pkg/github/actions.go +++ b/pkg/github/actions.go @@ -12,7 +12,7 @@ import ( buffer "github.com/github/github-mcp-server/pkg/buffer" ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) diff --git a/pkg/github/actions_test.go b/pkg/github/actions_test.go index 1738bc8e5..2f82ceafd 100644 --- a/pkg/github/actions_test.go +++ b/pkg/github/actions_test.go @@ -15,7 +15,7 @@ import ( "github.com/github/github-mcp-server/internal/profiler" buffer "github.com/github/github-mcp-server/pkg/buffer" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/github/code_scanning.go b/pkg/github/code_scanning.go index aa39cfc35..979d98ff6 100644 --- a/pkg/github/code_scanning.go +++ b/pkg/github/code_scanning.go @@ -9,7 +9,7 @@ import ( ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) diff --git a/pkg/github/code_scanning_test.go b/pkg/github/code_scanning_test.go index 874d1eeda..3c6a8325f 100644 --- a/pkg/github/code_scanning_test.go +++ b/pkg/github/code_scanning_test.go @@ -8,7 +8,7 @@ import ( "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/github/context_tools_test.go b/pkg/github/context_tools_test.go index 880d9d98c..d3d5d0797 100644 --- a/pkg/github/context_tools_test.go +++ b/pkg/github/context_tools_test.go @@ -10,7 +10,7 @@ import ( "github.com/github/github-mcp-server/internal/githubv4mock" "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/shurcooL/githubv4" "github.com/stretchr/testify/assert" diff --git a/pkg/github/dependabot.go b/pkg/github/dependabot.go index e21562c02..ebd295aad 100644 --- a/pkg/github/dependabot.go +++ b/pkg/github/dependabot.go @@ -9,7 +9,7 @@ import ( ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) diff --git a/pkg/github/dependabot_test.go b/pkg/github/dependabot_test.go index 302692a3a..57b421db3 100644 --- a/pkg/github/dependabot_test.go +++ b/pkg/github/dependabot_test.go @@ -8,7 +8,7 @@ import ( "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/github/discussions.go b/pkg/github/discussions.go index 3aa92f05c..3c3e42c63 100644 --- a/pkg/github/discussions.go +++ b/pkg/github/discussions.go @@ -7,7 +7,7 @@ import ( "github.com/github/github-mcp-server/pkg/translations" "github.com/go-viper/mapstructure/v2" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/shurcooL/githubv4" diff --git a/pkg/github/discussions_test.go b/pkg/github/discussions_test.go index 0930b1421..702ac4ff8 100644 --- a/pkg/github/discussions_test.go +++ b/pkg/github/discussions_test.go @@ -9,7 +9,7 @@ import ( "github.com/github/github-mcp-server/internal/githubv4mock" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/shurcooL/githubv4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/github/gists.go b/pkg/github/gists.go index 7168f8c0e..5183f353e 100644 --- a/pkg/github/gists.go +++ b/pkg/github/gists.go @@ -8,7 +8,7 @@ import ( "net/http" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) diff --git a/pkg/github/gists_test.go b/pkg/github/gists_test.go index e8eb6d7f4..fc4a2c692 100644 --- a/pkg/github/gists_test.go +++ b/pkg/github/gists_test.go @@ -8,7 +8,7 @@ import ( "time" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/github/git.go b/pkg/github/git.go index 5dfc8e0e8..e0207ac8d 100644 --- a/pkg/github/git.go +++ b/pkg/github/git.go @@ -8,7 +8,7 @@ import ( ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 1032d4d04..1c4f9514c 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -14,7 +14,7 @@ import ( "github.com/github/github-mcp-server/pkg/sanitize" "github.com/github/github-mcp-server/pkg/translations" "github.com/go-viper/mapstructure/v2" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/shurcooL/githubv4" diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index d13b93e4b..4cc3a1302 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -12,7 +12,7 @@ import ( "github.com/github/github-mcp-server/internal/githubv4mock" "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/shurcooL/githubv4" "github.com/stretchr/testify/assert" diff --git a/pkg/github/minimal_types.go b/pkg/github/minimal_types.go index dd3b25af3..b06b333bc 100644 --- a/pkg/github/minimal_types.go +++ b/pkg/github/minimal_types.go @@ -1,6 +1,6 @@ package github -import "github.com/google/go-github/v77/github" +import "github.com/google/go-github/v79/github" // MinimalUser is the output type for user and organization search results. type MinimalUser struct { diff --git a/pkg/github/notifications.go b/pkg/github/notifications.go index 6dca53cca..8bf862006 100644 --- a/pkg/github/notifications.go +++ b/pkg/github/notifications.go @@ -11,7 +11,7 @@ import ( ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) diff --git a/pkg/github/notifications_test.go b/pkg/github/notifications_test.go index 034d8d4e2..53a25076b 100644 --- a/pkg/github/notifications_test.go +++ b/pkg/github/notifications_test.go @@ -8,7 +8,7 @@ import ( "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/github/projects.go b/pkg/github/projects.go index c961c8678..9ecd18fe0 100644 --- a/pkg/github/projects.go +++ b/pkg/github/projects.go @@ -12,7 +12,7 @@ import ( ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/google/go-querystring/query" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" diff --git a/pkg/github/projects_test.go b/pkg/github/projects_test.go index c43853a3d..ac0019ac0 100644 --- a/pkg/github/projects_test.go +++ b/pkg/github/projects_test.go @@ -9,7 +9,7 @@ import ( "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" - gh "github.com/google/go-github/v77/github" + gh "github.com/google/go-github/v79/github" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index 117f92ecf..e64ae03e4 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -8,7 +8,7 @@ import ( "net/http" "github.com/go-viper/mapstructure/v2" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/shurcooL/githubv4" diff --git a/pkg/github/pullrequests_test.go b/pkg/github/pullrequests_test.go index 4cc4480e9..347bce672 100644 --- a/pkg/github/pullrequests_test.go +++ b/pkg/github/pullrequests_test.go @@ -10,7 +10,7 @@ import ( "github.com/github/github-mcp-server/internal/githubv4mock" "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/shurcooL/githubv4" "github.com/migueleliasweb/go-github-mock/src/mock" diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index 0d4d11bbf..6c7c65a5f 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -13,7 +13,7 @@ import ( ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/raw" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) diff --git a/pkg/github/repositories_test.go b/pkg/github/repositories_test.go index 665af6b0a..b9628eee5 100644 --- a/pkg/github/repositories_test.go +++ b/pkg/github/repositories_test.go @@ -13,7 +13,7 @@ import ( "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/raw" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/mark3labs/mcp-go/mcp" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go index a159111af..8fb1a52ed 100644 --- a/pkg/github/repository_resource.go +++ b/pkg/github/repository_resource.go @@ -14,7 +14,7 @@ import ( "github.com/github/github-mcp-server/pkg/raw" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) diff --git a/pkg/github/repository_resource_test.go b/pkg/github/repository_resource_test.go index f452912a0..96bf33b72 100644 --- a/pkg/github/repository_resource_test.go +++ b/pkg/github/repository_resource_test.go @@ -8,7 +8,7 @@ import ( "github.com/github/github-mcp-server/pkg/raw" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/mark3labs/mcp-go/mcp" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/require" diff --git a/pkg/github/search.go b/pkg/github/search.go index 5084773b2..147b16402 100644 --- a/pkg/github/search.go +++ b/pkg/github/search.go @@ -8,7 +8,7 @@ import ( ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) diff --git a/pkg/github/search_test.go b/pkg/github/search_test.go index e14ba023f..d31abc154 100644 --- a/pkg/github/search_test.go +++ b/pkg/github/search_test.go @@ -8,7 +8,7 @@ import ( "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/github/search_utils.go b/pkg/github/search_utils.go index 04cb2224f..9f2b1f5c3 100644 --- a/pkg/github/search_utils.go +++ b/pkg/github/search_utils.go @@ -8,7 +8,7 @@ import ( "net/http" "regexp" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/mark3labs/mcp-go/mcp" ) diff --git a/pkg/github/secret_scanning.go b/pkg/github/secret_scanning.go index 866c54617..1c5da12f9 100644 --- a/pkg/github/secret_scanning.go +++ b/pkg/github/secret_scanning.go @@ -9,7 +9,7 @@ import ( ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) diff --git a/pkg/github/secret_scanning_test.go b/pkg/github/secret_scanning_test.go index 4a9d50ab9..74d0d382b 100644 --- a/pkg/github/secret_scanning_test.go +++ b/pkg/github/secret_scanning_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/github/security_advisories.go b/pkg/github/security_advisories.go index 316b5d58c..027203687 100644 --- a/pkg/github/security_advisories.go +++ b/pkg/github/security_advisories.go @@ -8,7 +8,7 @@ import ( "net/http" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) diff --git a/pkg/github/security_advisories_test.go b/pkg/github/security_advisories_test.go index e083cb166..7975dc145 100644 --- a/pkg/github/security_advisories_test.go +++ b/pkg/github/security_advisories_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/github/server.go b/pkg/github/server.go index ddf3b0f86..439f93346 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -6,7 +6,7 @@ import ( "fmt" "strconv" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) diff --git a/pkg/github/server_test.go b/pkg/github/server_test.go index 77752d090..3bfc1ef94 100644 --- a/pkg/github/server_test.go +++ b/pkg/github/server_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/github/github-mcp-server/pkg/raw" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/shurcooL/githubv4" "github.com/stretchr/testify/assert" ) diff --git a/pkg/github/tools.go b/pkg/github/tools.go index 989999da4..0594f2f94 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -8,7 +8,7 @@ import ( "github.com/github/github-mcp-server/pkg/raw" "github.com/github/github-mcp-server/pkg/toolsets" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/mark3labs/mcp-go/server" "github.com/shurcooL/githubv4" ) diff --git a/pkg/raw/raw.go b/pkg/raw/raw.go index aee8a9313..10bade5eb 100644 --- a/pkg/raw/raw.go +++ b/pkg/raw/raw.go @@ -6,7 +6,7 @@ import ( "net/http" "net/url" - gogithub "github.com/google/go-github/v77/github" + gogithub "github.com/google/go-github/v79/github" ) // GetRawClientFn is a function type that returns a RawClient instance. diff --git a/pkg/raw/raw_test.go b/pkg/raw/raw_test.go index 18dafe3e1..242029c8b 100644 --- a/pkg/raw/raw_test.go +++ b/pkg/raw/raw_test.go @@ -6,7 +6,7 @@ import ( "net/url" "testing" - "github.com/google/go-github/v77/github" + "github.com/google/go-github/v79/github" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/require" ) diff --git a/third-party-licenses.darwin.md b/third-party-licenses.darwin.md index d4d742c6e..329db1c3d 100644 --- a/third-party-licenses.darwin.md +++ b/third-party-licenses.darwin.md @@ -16,7 +16,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/go-openapi/swag](https://pkg.go.dev/github.com/go-openapi/swag) ([Apache-2.0](https://github.com/go-openapi/swag/blob/v0.21.1/LICENSE)) - [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.4.0/LICENSE)) - [github.com/google/go-github/v71/github](https://pkg.go.dev/github.com/google/go-github/v71/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v71.0.0/LICENSE)) - - [github.com/google/go-github/v77/github](https://pkg.go.dev/github.com/google/go-github/v77/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v77.0.0/LICENSE)) + - [github.com/google/go-github/v79/github](https://pkg.go.dev/github.com/google/go-github/v79/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v77.0.0/LICENSE)) - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) - [github.com/gorilla/css/scanner](https://pkg.go.dev/github.com/gorilla/css/scanner) ([BSD-3-Clause](https://github.com/gorilla/css/blob/v1.0.1/LICENSE)) diff --git a/third-party-licenses.linux.md b/third-party-licenses.linux.md index d4d742c6e..329db1c3d 100644 --- a/third-party-licenses.linux.md +++ b/third-party-licenses.linux.md @@ -16,7 +16,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/go-openapi/swag](https://pkg.go.dev/github.com/go-openapi/swag) ([Apache-2.0](https://github.com/go-openapi/swag/blob/v0.21.1/LICENSE)) - [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.4.0/LICENSE)) - [github.com/google/go-github/v71/github](https://pkg.go.dev/github.com/google/go-github/v71/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v71.0.0/LICENSE)) - - [github.com/google/go-github/v77/github](https://pkg.go.dev/github.com/google/go-github/v77/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v77.0.0/LICENSE)) + - [github.com/google/go-github/v79/github](https://pkg.go.dev/github.com/google/go-github/v79/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v77.0.0/LICENSE)) - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) - [github.com/gorilla/css/scanner](https://pkg.go.dev/github.com/gorilla/css/scanner) ([BSD-3-Clause](https://github.com/gorilla/css/blob/v1.0.1/LICENSE)) diff --git a/third-party-licenses.windows.md b/third-party-licenses.windows.md index e7117d82c..0c60157aa 100644 --- a/third-party-licenses.windows.md +++ b/third-party-licenses.windows.md @@ -16,7 +16,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/go-openapi/swag](https://pkg.go.dev/github.com/go-openapi/swag) ([Apache-2.0](https://github.com/go-openapi/swag/blob/v0.21.1/LICENSE)) - [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.4.0/LICENSE)) - [github.com/google/go-github/v71/github](https://pkg.go.dev/github.com/google/go-github/v71/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v71.0.0/LICENSE)) - - [github.com/google/go-github/v77/github](https://pkg.go.dev/github.com/google/go-github/v77/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v77.0.0/LICENSE)) + - [github.com/google/go-github/v79/github](https://pkg.go.dev/github.com/google/go-github/v79/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v77.0.0/LICENSE)) - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) - [github.com/gorilla/css/scanner](https://pkg.go.dev/github.com/gorilla/css/scanner) ([BSD-3-Clause](https://github.com/gorilla/css/blob/v1.0.1/LICENSE)) From 649087cbf49e9f67d587463591183fc4a506497f Mon Sep 17 00:00:00 2001 From: Jonathan Date: Fri, 14 Nov 2025 16:21:24 +0000 Subject: [PATCH 08/16] updates generated licenses --- third-party-licenses.darwin.md | 2 +- third-party-licenses.linux.md | 2 +- third-party-licenses.windows.md | 2 +- .../github.com/google/go-github/{v77 => v79}/github/LICENSE | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename third-party/github.com/google/go-github/{v77 => v79}/github/LICENSE (100%) diff --git a/third-party-licenses.darwin.md b/third-party-licenses.darwin.md index 329db1c3d..eecc6faa8 100644 --- a/third-party-licenses.darwin.md +++ b/third-party-licenses.darwin.md @@ -16,7 +16,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/go-openapi/swag](https://pkg.go.dev/github.com/go-openapi/swag) ([Apache-2.0](https://github.com/go-openapi/swag/blob/v0.21.1/LICENSE)) - [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.4.0/LICENSE)) - [github.com/google/go-github/v71/github](https://pkg.go.dev/github.com/google/go-github/v71/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v71.0.0/LICENSE)) - - [github.com/google/go-github/v79/github](https://pkg.go.dev/github.com/google/go-github/v79/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v77.0.0/LICENSE)) + - [github.com/google/go-github/v79/github](https://pkg.go.dev/github.com/google/go-github/v79/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v79.0.0/LICENSE)) - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) - [github.com/gorilla/css/scanner](https://pkg.go.dev/github.com/gorilla/css/scanner) ([BSD-3-Clause](https://github.com/gorilla/css/blob/v1.0.1/LICENSE)) diff --git a/third-party-licenses.linux.md b/third-party-licenses.linux.md index 329db1c3d..eecc6faa8 100644 --- a/third-party-licenses.linux.md +++ b/third-party-licenses.linux.md @@ -16,7 +16,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/go-openapi/swag](https://pkg.go.dev/github.com/go-openapi/swag) ([Apache-2.0](https://github.com/go-openapi/swag/blob/v0.21.1/LICENSE)) - [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.4.0/LICENSE)) - [github.com/google/go-github/v71/github](https://pkg.go.dev/github.com/google/go-github/v71/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v71.0.0/LICENSE)) - - [github.com/google/go-github/v79/github](https://pkg.go.dev/github.com/google/go-github/v79/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v77.0.0/LICENSE)) + - [github.com/google/go-github/v79/github](https://pkg.go.dev/github.com/google/go-github/v79/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v79.0.0/LICENSE)) - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) - [github.com/gorilla/css/scanner](https://pkg.go.dev/github.com/gorilla/css/scanner) ([BSD-3-Clause](https://github.com/gorilla/css/blob/v1.0.1/LICENSE)) diff --git a/third-party-licenses.windows.md b/third-party-licenses.windows.md index 0c60157aa..75fe8172a 100644 --- a/third-party-licenses.windows.md +++ b/third-party-licenses.windows.md @@ -16,7 +16,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/go-openapi/swag](https://pkg.go.dev/github.com/go-openapi/swag) ([Apache-2.0](https://github.com/go-openapi/swag/blob/v0.21.1/LICENSE)) - [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.4.0/LICENSE)) - [github.com/google/go-github/v71/github](https://pkg.go.dev/github.com/google/go-github/v71/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v71.0.0/LICENSE)) - - [github.com/google/go-github/v79/github](https://pkg.go.dev/github.com/google/go-github/v79/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v77.0.0/LICENSE)) + - [github.com/google/go-github/v79/github](https://pkg.go.dev/github.com/google/go-github/v79/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v79.0.0/LICENSE)) - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) - [github.com/gorilla/css/scanner](https://pkg.go.dev/github.com/gorilla/css/scanner) ([BSD-3-Clause](https://github.com/gorilla/css/blob/v1.0.1/LICENSE)) diff --git a/third-party/github.com/google/go-github/v77/github/LICENSE b/third-party/github.com/google/go-github/v79/github/LICENSE similarity index 100% rename from third-party/github.com/google/go-github/v77/github/LICENSE rename to third-party/github.com/google/go-github/v79/github/LICENSE From 4852f5a3df7c65c5e32ad5e0a1eabc395549a5e4 Mon Sep 17 00:00:00 2001 From: Ksenia Bobrova Date: Thu, 13 Nov 2025 11:17:30 +0000 Subject: [PATCH 09/16] Add remote server to registry endtry --- server.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/server.json b/server.json index d0ab16035..a8751b7f0 100644 --- a/server.json +++ b/server.json @@ -61,5 +61,19 @@ } ] } + ], + "remotes": [ + { + "type": "streamable-http", + "url": "https://api.githubcopilot.com/mcp", + "headers": [ + { + "name": "Authorization", + "description": "Authentication token (PAT or App token)", + "isRequired": true, + "isSecret": true + } + ] + } ] } From df347e3cb4734a85f55ba06c882542f284622577 Mon Sep 17 00:00:00 2001 From: Ksenia Bobrova Date: Thu, 13 Nov 2025 12:34:07 +0100 Subject: [PATCH 10/16] Update server.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- server.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.json b/server.json index a8751b7f0..544f1fbc4 100644 --- a/server.json +++ b/server.json @@ -65,7 +65,7 @@ "remotes": [ { "type": "streamable-http", - "url": "https://api.githubcopilot.com/mcp", + "url": "https://api.githubcopilot.com/mcp/", "headers": [ { "name": "Authorization", From bd7ca66505f00f83d3e7704e85d8085464bcefbc Mon Sep 17 00:00:00 2001 From: Jonathan Otalora Date: Fri, 14 Nov 2025 10:24:05 -0700 Subject: [PATCH 11/16] updates mcp-server with latest google-go-github APIs --- pkg/github/projects.go | 106 ++++++++++++----------------------------- 1 file changed, 31 insertions(+), 75 deletions(-) diff --git a/pkg/github/projects.go b/pkg/github/projects.go index 9ecd18fe0..1e40be70f 100644 --- a/pkg/github/projects.go +++ b/pkg/github/projects.go @@ -256,30 +256,19 @@ func ListProjectFields(getClient GetClientFn, t translations.TranslationHelperFu return mcp.NewToolResultError(err.Error()), nil } + var resp *github.Response + var projectFields []*github.ProjectV2Field + opts := &github.ListProjectsOptions{ ListProjectsPaginationOptions: pagination, } - var url string if ownerType == "org" { - url = fmt.Sprintf("orgs/%s/projectsV2/%d/fields", owner, projectNumber) + projectFields, resp, err = client.Projects.ListOrganizationProjectFields(ctx, owner, projectNumber, opts) } else { - url = fmt.Sprintf("users/%s/projectsV2/%d/fields", owner, projectNumber) - } - - url, err = addOptions(url, opts) - if err != nil { - return mcp.NewToolResultError(err.Error()), nil + projectFields, resp, err = client.Projects.ListUserProjectFields(ctx, owner, projectNumber, opts) } - httpRequest, err := client.NewRequest("GET", url, nil) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - - var projectFields []projectV2Field - resp, err := client.Do(ctx, httpRequest, &projectFields) - if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list project fields", @@ -452,7 +441,7 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun } var resp *github.Response - var projectItems []projectV2Item + var projectItems []*github.ProjectV2Item var queryPtr *string if queryStr != "" { @@ -467,25 +456,12 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun }, } - var url string if ownerType == "org" { - url = fmt.Sprintf("orgs/%s/projectsV2/%d/items", owner, projectNumber) + projectItems, resp, err = client.Projects.ListOrganizationProjectItems(ctx, owner, projectNumber, opts) } else { - url = fmt.Sprintf("users/%s/projectsV2/%d/items", owner, projectNumber) + projectItems, resp, err = client.Projects.ListUserProjectItems(ctx, owner, projectNumber, opts) } - url, err = addOptions(url, opts) - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - httpRequest, err := client.NewRequest("GET", url, nil) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - - resp, err = client.Do(ctx, httpRequest, &projectItems) - if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, ProjectListFailedError, @@ -566,32 +542,22 @@ func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) return mcp.NewToolResultError(err.Error()), nil } - var url string - if ownerType == "org" { - url = fmt.Sprintf("orgs/%s/projectsV2/%d/items/%d", owner, projectNumber, itemID) - } else { - url = fmt.Sprintf("users/%s/projectsV2/%d/items/%d", owner, projectNumber, itemID) - } - - opts := fieldSelectionOptions{} + resp := &github.Response{} + projectItem := &github.ProjectV2Item{} + var opts *github.GetProjectItemOptions if len(fields) > 0 { - opts.Fields = fields - } - - url, err = addOptions(url, opts) - if err != nil { - return mcp.NewToolResultError(err.Error()), nil + opts = &github.GetProjectItemOptions{ + Fields: fields, + } } - projectItem := projectV2Item{} - - httpRequest, err := client.NewRequest("GET", url, nil) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) + if ownerType == "org" { + projectItem, resp, err = client.Projects.GetOrganizationProjectItem(ctx, owner, projectNumber, itemID, opts) + } else { + projectItem, resp, err = client.Projects.GetUserProjectItem(ctx, owner, projectNumber, itemID, opts) } - resp, err := client.Do(ctx, httpRequest, &projectItem) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get project item", @@ -748,7 +714,7 @@ func UpdateProjectItem(getClient GetClientFn, t translations.TranslationHelperFu if err != nil { return mcp.NewToolResultError(err.Error()), nil } - itemID, err := RequiredInt(req, "item_id") + itemID, err := RequiredBigInt(req, "item_id") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -773,21 +739,15 @@ func UpdateProjectItem(getClient GetClientFn, t translations.TranslationHelperFu return mcp.NewToolResultError(err.Error()), nil } - var projectsURL string + var resp *github.Response + var updatedItem *github.ProjectV2Item + if ownerType == "org" { - projectsURL = fmt.Sprintf("orgs/%s/projectsV2/%d/items/%d", owner, projectNumber, itemID) + updatedItem, resp, err = client.Projects.UpdateOrganizationProjectItem(ctx, owner, projectNumber, itemID, updatePayload) } else { - projectsURL = fmt.Sprintf("users/%s/projectsV2/%d/items/%d", owner, projectNumber, itemID) - } - httpRequest, err := client.NewRequest("PATCH", projectsURL, updateProjectItemPayload{ - Fields: []updateProjectItem{*updatePayload}, - }) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) + updatedItem, resp, err = client.Projects.UpdateUserProjectItem(ctx, owner, projectNumber, itemID, updatePayload) } - updatedItem := projectV2Item{} - resp, err := client.Do(ctx, httpRequest, &updatedItem) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, ProjectUpdateFailedError, @@ -892,15 +852,6 @@ type fieldSelectionOptions struct { Fields []int64 `url:"fields,omitempty,comma"` } -type updateProjectItemPayload struct { - Fields []updateProjectItem `json:"fields"` -} - -type updateProjectItem struct { - ID int `json:"id"` - Value any `json:"value"` -} - type projectV2ItemFieldValue struct { ID *int64 `json:"id,omitempty"` Name string `json:"name,omitempty"` @@ -967,7 +918,7 @@ func toNewProjectType(projType string) string { } } -func buildUpdateProjectItem(input map[string]any) (*updateProjectItem, error) { +func buildUpdateProjectItem(input map[string]any) (*github.UpdateProjectItemOptions, error) { if input == nil { return nil, fmt.Errorf("updated_field must be an object") } @@ -986,7 +937,12 @@ func buildUpdateProjectItem(input map[string]any) (*updateProjectItem, error) { if !ok { return nil, fmt.Errorf("updated_field.value is required") } - payload := &updateProjectItem{ID: int(idFieldAsFloat64), Value: valueField} + payload := &github.UpdateProjectItemOptions{ + Fields: []*github.UpdateProjectV2Field{{ + ID: int64(idFieldAsFloat64), + Value: valueField, + }}, + } return payload, nil } From 1d6499d8755d2f10aa992d90cd2775f8d61bf5c4 Mon Sep 17 00:00:00 2001 From: Jonathan Otalora Date: Fri, 14 Nov 2025 10:37:04 -0700 Subject: [PATCH 12/16] removes unused code --- pkg/github/projects.go | 93 +----------------------------------------- 1 file changed, 2 insertions(+), 91 deletions(-) diff --git a/pkg/github/projects.go b/pkg/github/projects.go index 1e40be70f..e43c514c0 100644 --- a/pkg/github/projects.go +++ b/pkg/github/projects.go @@ -6,14 +6,11 @@ import ( "fmt" "io" "net/http" - "net/url" - "reflect" "strings" ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v79/github" - "github.com/google/go-querystring/query" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) @@ -542,8 +539,8 @@ func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) return mcp.NewToolResultError(err.Error()), nil } - resp := &github.Response{} - projectItem := &github.ProjectV2Item{} + var resp *github.Response + var projectItem *github.ProjectV2Item var opts *github.GetProjectItemOptions if len(fields) > 0 { @@ -846,48 +843,6 @@ func DeleteProjectItem(getClient GetClientFn, t translations.TranslationHelperFu } } -type fieldSelectionOptions struct { - // Specific list of field IDs to include in the response. If not provided, only the title field is included. - // The comma tag encodes the slice as comma-separated values: fields=102589,985201,169875 - Fields []int64 `url:"fields,omitempty,comma"` -} - -type projectV2ItemFieldValue struct { - ID *int64 `json:"id,omitempty"` - Name string `json:"name,omitempty"` - DataType string `json:"data_type,omitempty"` - Value any `json:"value,omitempty"` -} - -type projectV2Item struct { - ArchivedAt *github.Timestamp `json:"archived_at,omitempty"` - Content *projectV2ItemContent `json:"content,omitempty"` - ContentType *string `json:"content_type,omitempty"` - CreatedAt *github.Timestamp `json:"created_at,omitempty"` - Creator *github.User `json:"creator,omitempty"` - Description *string `json:"description,omitempty"` - Fields []*projectV2ItemFieldValue `json:"fields,omitempty"` - ID *int64 `json:"id,omitempty"` - ItemURL *string `json:"item_url,omitempty"` - NodeID *string `json:"node_id,omitempty"` - ProjectURL *string `json:"project_url,omitempty"` - Title *string `json:"title,omitempty"` - UpdatedAt *github.Timestamp `json:"updated_at,omitempty"` -} - -type projectV2ItemContent struct { - Body *string `json:"body,omitempty"` - ClosedAt *github.Timestamp `json:"closed_at,omitempty"` - CreatedAt *github.Timestamp `json:"created_at,omitempty"` - ID *int64 `json:"id,omitempty"` - Number *int `json:"number,omitempty"` - State *string `json:"state,omitempty"` - StateReason *string `json:"stateReason,omitempty"` - Title *string `json:"title,omitempty"` - UpdatedAt *github.Timestamp `json:"updated_at,omitempty"` - URL *string `json:"url,omitempty"` -} - type pageInfo struct { HasNextPage bool `json:"hasNextPage"` HasPreviousPage bool `json:"hasPreviousPage"` @@ -895,18 +850,6 @@ type pageInfo struct { PrevCursor string `json:"prevCursor,omitempty"` } -type projectV2Field struct { - ID *int64 `json:"id,omitempty"` - NodeID *string `json:"node_id,omitempty"` - Name *string `json:"name,omitempty"` - DataType *string `json:"data_type,omitempty"` - ProjectURL *string `json:"project_url,omitempty"` - Options []any `json:"options,omitempty"` - Configuration any `json:"configuration,omitempty"` - CreatedAt *github.Timestamp `json:"created_at,omitempty"` - UpdatedAt *github.Timestamp `json:"updated_at,omitempty"` -} - func toNewProjectType(projType string) string { switch strings.ToLower(projType) { case "issue": @@ -990,35 +933,3 @@ func extractPaginationOptions(request mcp.CallToolRequest) (github.ListProjectsP return opts, nil } - -// addOptions adds the parameters in opts as URL query parameters to s. opts -// must be a struct whose fields may contain "url" tags. -func addOptions(s string, opts any) (string, error) { - v := reflect.ValueOf(opts) - if v.Kind() == reflect.Ptr && v.IsNil() { - return s, nil - } - - origURL, err := url.Parse(s) - if err != nil { - return s, err - } - - origValues := origURL.Query() - - // Use the github.com/google/go-querystring library to parse the struct - newValues, err := query.Values(opts) - if err != nil { - return s, err - } - - // Merge the values - for key, values := range newValues { - for _, value := range values { - origValues.Add(key, value) - } - } - - origURL.RawQuery = origValues.Encode() - return origURL.String(), nil -} From e95d8ee6fe241e797eda82da13b3d7809bd9187a Mon Sep 17 00:00:00 2001 From: Jonathan Otalora Date: Fri, 14 Nov 2025 14:00:28 -0700 Subject: [PATCH 13/16] addresses float64 to in64 feedback from copilot --- pkg/github/projects.go | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/pkg/github/projects.go b/pkg/github/projects.go index e43c514c0..4a2a68bf2 100644 --- a/pkg/github/projects.go +++ b/pkg/github/projects.go @@ -861,6 +861,26 @@ func toNewProjectType(projType string) string { } } +// validateAndConvertToInt64 ensures the value is a number and converts it to int64. +func validateAndConvertToInt64(value any) (int64, error) { + switch v := value.(type) { + case float64: + // Validate that the float64 can be safely converted to int64 + intVal := int64(v) + if float64(intVal) != v { + return 0, fmt.Errorf("value must be a valid integer (got %v)", v) + } + return intVal, nil + case int64: + return v, nil + case int: + return int64(v), nil + default: + return 0, fmt.Errorf("value must be a number (got %T)", v) + } +} + +// buildUpdateProjectItem constructs UpdateProjectItemOptions from the input map. func buildUpdateProjectItem(input map[string]any) (*github.UpdateProjectItemOptions, error) { if input == nil { return nil, fmt.Errorf("updated_field must be an object") @@ -871,18 +891,19 @@ func buildUpdateProjectItem(input map[string]any) (*github.UpdateProjectItemOpti return nil, fmt.Errorf("updated_field.id is required") } - idFieldAsFloat64, ok := idField.(float64) // JSON numbers are float64 - if !ok { - return nil, fmt.Errorf("updated_field.id must be a number") + fieldID, err := validateAndConvertToInt64(idField) + if err != nil { + return nil, fmt.Errorf("updated_field.id: %w", err) } valueField, ok := input["value"] if !ok { return nil, fmt.Errorf("updated_field.value is required") } + payload := &github.UpdateProjectItemOptions{ Fields: []*github.UpdateProjectV2Field{{ - ID: int64(idFieldAsFloat64), + ID: fieldID, Value: valueField, }}, } From 88a594e76bb5ab534d533d99cfc33a167856ac88 Mon Sep 17 00:00:00 2001 From: Tony Truong Date: Mon, 17 Nov 2025 10:19:42 +0100 Subject: [PATCH 14/16] improve response/feedback loop with better error messages (#1414) --- pkg/github/repositories.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index 6c7c65a5f..b1fe5bf72 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -560,7 +560,7 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t ), nil } if fileContent == nil || fileContent.SHA == nil { - return mcp.NewToolResultError("file content SHA is nil"), nil + return mcp.NewToolResultError("file content SHA is nil, if a directory was requested, path parameters should end with a trailing slash '/'"), nil } fileSHA = *fileContent.SHA From 2f64ac0c2155336852713cbf201fee184928e0b4 Mon Sep 17 00:00:00 2001 From: SangheeSon Date: Mon, 17 Nov 2025 19:20:39 +0900 Subject: [PATCH 15/16] Add discussion metadata fields to get_discussion tool (#1305) * Add state metadata fields to get_discussion tool Fixes #1303 The get_discussion tool was missing important state metadata that's already available in get_issue. Added four fields to provide complete discussion status information: - state: Current discussion state (OPEN/CLOSED) - isAnswered: Whether the discussion has an accepted answer - answeredAt: Timestamp when answer was provided - answerChosenAt: Timestamp when answer was selected Changed GetDiscussion to return a map instead of github.Discussion struct since the go-github library doesn't include all these fields in its type definition. This approach is consistent with other functions in this codebase (ListDiscussions, GetDiscussionComments). All tests pass and linter checks pass. * Fix Discussion field mappings based on GitHub GraphQL API Changes: - Replace 'State' (doesn't exist) with 'Closed' (Boolean) - Remove 'AnsweredAt' (doesn't exist) - Keep 'IsAnswered' (verified to exist in GitHub GraphQL API) - Use 'AnswerChosenAt' for answer timestamp Updated both implementation and tests to match actual GitHub GraphQL schema. All tests passing. --------- Co-authored-by: tommaso-moro --- pkg/github/discussions.go | 59 +++++++---- pkg/github/discussions_test.go | 177 ++++++++++++++++++--------------- 2 files changed, 136 insertions(+), 100 deletions(-) diff --git a/pkg/github/discussions.go b/pkg/github/discussions.go index 3c3e42c63..d37794db4 100644 --- a/pkg/github/discussions.go +++ b/pkg/github/discussions.go @@ -44,11 +44,14 @@ type DiscussionFragment struct { } type NodeFragment struct { - Number githubv4.Int - Title githubv4.String - CreatedAt githubv4.DateTime - UpdatedAt githubv4.DateTime - Author struct { + Number githubv4.Int + Title githubv4.String + CreatedAt githubv4.DateTime + UpdatedAt githubv4.DateTime + Closed githubv4.Boolean + IsAnswered githubv4.Boolean + AnswerChosenAt *githubv4.DateTime + Author struct { Login githubv4.String } Category struct { @@ -294,12 +297,15 @@ func GetDiscussion(getGQLClient GetGQLClientFn, t translations.TranslationHelper var q struct { Repository struct { Discussion struct { - Number githubv4.Int - Title githubv4.String - Body githubv4.String - CreatedAt githubv4.DateTime - URL githubv4.String `graphql:"url"` - Category struct { + Number githubv4.Int + Title githubv4.String + Body githubv4.String + CreatedAt githubv4.DateTime + Closed githubv4.Boolean + IsAnswered githubv4.Boolean + AnswerChosenAt *githubv4.DateTime + URL githubv4.String `graphql:"url"` + Category struct { Name githubv4.String } `graphql:"category"` } `graphql:"discussion(number: $discussionNumber)"` @@ -314,17 +320,30 @@ func GetDiscussion(getGQLClient GetGQLClientFn, t translations.TranslationHelper return mcp.NewToolResultError(err.Error()), nil } d := q.Repository.Discussion - discussion := &github.Discussion{ - Number: github.Ptr(int(d.Number)), - Title: github.Ptr(string(d.Title)), - Body: github.Ptr(string(d.Body)), - HTMLURL: github.Ptr(string(d.URL)), - CreatedAt: &github.Timestamp{Time: d.CreatedAt.Time}, - DiscussionCategory: &github.DiscussionCategory{ - Name: github.Ptr(string(d.Category.Name)), + + // Build response as map to include fields not present in go-github's Discussion struct. + // The go-github library's Discussion type lacks isAnswered and answerChosenAt fields, + // so we use map[string]interface{} for the response (consistent with other functions + // like ListDiscussions and GetDiscussionComments). + response := map[string]interface{}{ + "number": int(d.Number), + "title": string(d.Title), + "body": string(d.Body), + "url": string(d.URL), + "closed": bool(d.Closed), + "isAnswered": bool(d.IsAnswered), + "createdAt": d.CreatedAt.Time, + "category": map[string]interface{}{ + "name": string(d.Category.Name), }, } - out, err := json.Marshal(discussion) + + // Add optional timestamp fields if present + if d.AnswerChosenAt != nil { + response["answerChosenAt"] = d.AnswerChosenAt.Time + } + + out, err := json.Marshal(response) if err != nil { return nil, fmt.Errorf("failed to marshal discussion: %w", err) } diff --git a/pkg/github/discussions_test.go b/pkg/github/discussions_test.go index 702ac4ff8..05789b606 100644 --- a/pkg/github/discussions_test.go +++ b/pkg/github/discussions_test.go @@ -5,7 +5,6 @@ import ( "encoding/json" "net/http" "testing" - "time" "github.com/github/github-mcp-server/internal/githubv4mock" "github.com/github/github-mcp-server/pkg/translations" @@ -17,75 +16,89 @@ import ( var ( discussionsGeneral = []map[string]any{ - {"number": 1, "title": "Discussion 1 title", "createdAt": "2023-01-01T00:00:00Z", "updatedAt": "2023-01-01T00:00:00Z", "author": map[string]any{"login": "user1"}, "url": "https://github.com/owner/repo/discussions/1", "category": map[string]any{"name": "General"}}, - {"number": 3, "title": "Discussion 3 title", "createdAt": "2023-03-01T00:00:00Z", "updatedAt": "2023-02-01T00:00:00Z", "author": map[string]any{"login": "user1"}, "url": "https://github.com/owner/repo/discussions/3", "category": map[string]any{"name": "General"}}, + {"number": 1, "title": "Discussion 1 title", "createdAt": "2023-01-01T00:00:00Z", "updatedAt": "2023-01-01T00:00:00Z", "closed": false, "isAnswered": false, "author": map[string]any{"login": "user1"}, "url": "https://github.com/owner/repo/discussions/1", "category": map[string]any{"name": "General"}}, + {"number": 3, "title": "Discussion 3 title", "createdAt": "2023-03-01T00:00:00Z", "updatedAt": "2023-02-01T00:00:00Z", "closed": false, "isAnswered": false, "author": map[string]any{"login": "user1"}, "url": "https://github.com/owner/repo/discussions/3", "category": map[string]any{"name": "General"}}, } discussionsAll = []map[string]any{ { - "number": 1, - "title": "Discussion 1 title", - "createdAt": "2023-01-01T00:00:00Z", - "updatedAt": "2023-01-01T00:00:00Z", - "author": map[string]any{"login": "user1"}, - "url": "https://github.com/owner/repo/discussions/1", - "category": map[string]any{"name": "General"}, + "number": 1, + "title": "Discussion 1 title", + "createdAt": "2023-01-01T00:00:00Z", + "updatedAt": "2023-01-01T00:00:00Z", + "closed": false, + "isAnswered": false, + "author": map[string]any{"login": "user1"}, + "url": "https://github.com/owner/repo/discussions/1", + "category": map[string]any{"name": "General"}, }, { - "number": 2, - "title": "Discussion 2 title", - "createdAt": "2023-02-01T00:00:00Z", - "updatedAt": "2023-02-01T00:00:00Z", - "author": map[string]any{"login": "user2"}, - "url": "https://github.com/owner/repo/discussions/2", - "category": map[string]any{"name": "Questions"}, + "number": 2, + "title": "Discussion 2 title", + "createdAt": "2023-02-01T00:00:00Z", + "updatedAt": "2023-02-01T00:00:00Z", + "closed": false, + "isAnswered": false, + "author": map[string]any{"login": "user2"}, + "url": "https://github.com/owner/repo/discussions/2", + "category": map[string]any{"name": "Questions"}, }, { - "number": 3, - "title": "Discussion 3 title", - "createdAt": "2023-03-01T00:00:00Z", - "updatedAt": "2023-03-01T00:00:00Z", - "author": map[string]any{"login": "user3"}, - "url": "https://github.com/owner/repo/discussions/3", - "category": map[string]any{"name": "General"}, + "number": 3, + "title": "Discussion 3 title", + "createdAt": "2023-03-01T00:00:00Z", + "updatedAt": "2023-03-01T00:00:00Z", + "closed": false, + "isAnswered": false, + "author": map[string]any{"login": "user3"}, + "url": "https://github.com/owner/repo/discussions/3", + "category": map[string]any{"name": "General"}, }, } discussionsOrgLevel = []map[string]any{ { - "number": 1, - "title": "Org Discussion 1 - Community Guidelines", - "createdAt": "2023-01-15T00:00:00Z", - "updatedAt": "2023-01-15T00:00:00Z", - "author": map[string]any{"login": "org-admin"}, - "url": "https://github.com/owner/.github/discussions/1", - "category": map[string]any{"name": "Announcements"}, + "number": 1, + "title": "Org Discussion 1 - Community Guidelines", + "createdAt": "2023-01-15T00:00:00Z", + "updatedAt": "2023-01-15T00:00:00Z", + "closed": false, + "isAnswered": false, + "author": map[string]any{"login": "org-admin"}, + "url": "https://github.com/owner/.github/discussions/1", + "category": map[string]any{"name": "Announcements"}, }, { - "number": 2, - "title": "Org Discussion 2 - Roadmap 2023", - "createdAt": "2023-02-20T00:00:00Z", - "updatedAt": "2023-02-20T00:00:00Z", - "author": map[string]any{"login": "org-admin"}, - "url": "https://github.com/owner/.github/discussions/2", - "category": map[string]any{"name": "General"}, + "number": 2, + "title": "Org Discussion 2 - Roadmap 2023", + "createdAt": "2023-02-20T00:00:00Z", + "updatedAt": "2023-02-20T00:00:00Z", + "closed": false, + "isAnswered": false, + "author": map[string]any{"login": "org-admin"}, + "url": "https://github.com/owner/.github/discussions/2", + "category": map[string]any{"name": "General"}, }, { - "number": 3, - "title": "Org Discussion 3 - Roadmap 2024", - "createdAt": "2023-02-20T00:00:00Z", - "updatedAt": "2023-02-20T00:00:00Z", - "author": map[string]any{"login": "org-admin"}, - "url": "https://github.com/owner/.github/discussions/3", - "category": map[string]any{"name": "General"}, + "number": 3, + "title": "Org Discussion 3 - Roadmap 2024", + "createdAt": "2023-02-20T00:00:00Z", + "updatedAt": "2023-02-20T00:00:00Z", + "closed": false, + "isAnswered": false, + "author": map[string]any{"login": "org-admin"}, + "url": "https://github.com/owner/.github/discussions/3", + "category": map[string]any{"name": "General"}, }, { - "number": 4, - "title": "Org Discussion 4 - Roadmap 2025", - "createdAt": "2023-02-20T00:00:00Z", - "updatedAt": "2023-02-20T00:00:00Z", - "author": map[string]any{"login": "org-admin"}, - "url": "https://github.com/owner/.github/discussions/4", - "category": map[string]any{"name": "General"}, + "number": 4, + "title": "Org Discussion 4 - Roadmap 2025", + "createdAt": "2023-02-20T00:00:00Z", + "updatedAt": "2023-02-20T00:00:00Z", + "closed": false, + "isAnswered": false, + "author": map[string]any{"login": "org-admin"}, + "url": "https://github.com/owner/.github/discussions/4", + "category": map[string]any{"name": "General"}, }, } @@ -388,10 +401,10 @@ func Test_ListDiscussions(t *testing.T) { } // Define the actual query strings that match the implementation - qBasicNoOrder := "query($after:String$first:Int!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussions(first: $first, after: $after){nodes{number,title,createdAt,updatedAt,author{login},category{name},url},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}" - qWithCategoryNoOrder := "query($after:String$categoryId:ID!$first:Int!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussions(first: $first, after: $after, categoryId: $categoryId){nodes{number,title,createdAt,updatedAt,author{login},category{name},url},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}" - qBasicWithOrder := "query($after:String$first:Int!$orderByDirection:OrderDirection!$orderByField:DiscussionOrderField!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussions(first: $first, after: $after, orderBy: { field: $orderByField, direction: $orderByDirection }){nodes{number,title,createdAt,updatedAt,author{login},category{name},url},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}" - qWithCategoryAndOrder := "query($after:String$categoryId:ID!$first:Int!$orderByDirection:OrderDirection!$orderByField:DiscussionOrderField!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussions(first: $first, after: $after, categoryId: $categoryId, orderBy: { field: $orderByField, direction: $orderByDirection }){nodes{number,title,createdAt,updatedAt,author{login},category{name},url},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}" + qBasicNoOrder := "query($after:String$first:Int!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussions(first: $first, after: $after){nodes{number,title,createdAt,updatedAt,closed,isAnswered,answerChosenAt,author{login},category{name},url},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}" + qWithCategoryNoOrder := "query($after:String$categoryId:ID!$first:Int!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussions(first: $first, after: $after, categoryId: $categoryId){nodes{number,title,createdAt,updatedAt,closed,isAnswered,answerChosenAt,author{login},category{name},url},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}" + qBasicWithOrder := "query($after:String$first:Int!$orderByDirection:OrderDirection!$orderByField:DiscussionOrderField!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussions(first: $first, after: $after, orderBy: { field: $orderByField, direction: $orderByDirection }){nodes{number,title,createdAt,updatedAt,closed,isAnswered,answerChosenAt,author{login},category{name},url},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}" + qWithCategoryAndOrder := "query($after:String$categoryId:ID!$first:Int!$orderByDirection:OrderDirection!$orderByField:DiscussionOrderField!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussions(first: $first, after: $after, categoryId: $categoryId, orderBy: { field: $orderByField, direction: $orderByDirection }){nodes{number,title,createdAt,updatedAt,closed,isAnswered,answerChosenAt,author{login},category{name},url},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}" for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { @@ -484,7 +497,7 @@ func Test_GetDiscussion(t *testing.T) { assert.ElementsMatch(t, toolDef.InputSchema.Required, []string{"owner", "repo", "discussionNumber"}) // Use exact string query that matches implementation output - qGetDiscussion := "query($discussionNumber:Int!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussion(number: $discussionNumber){number,title,body,createdAt,url,category{name}}}}" + qGetDiscussion := "query($discussionNumber:Int!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussion(number: $discussionNumber){number,title,body,createdAt,closed,isAnswered,answerChosenAt,url,category{name}}}}" vars := map[string]interface{}{ "owner": "owner", @@ -495,31 +508,31 @@ func Test_GetDiscussion(t *testing.T) { name string response githubv4mock.GQLResponse expectError bool - expected *github.Discussion + expected map[string]interface{} errContains string }{ { name: "successful retrieval", response: githubv4mock.DataResponse(map[string]any{ "repository": map[string]any{"discussion": map[string]any{ - "number": 1, - "title": "Test Discussion Title", - "body": "This is a test discussion", - "url": "https://github.com/owner/repo/discussions/1", - "createdAt": "2025-04-25T12:00:00Z", - "category": map[string]any{"name": "General"}, + "number": 1, + "title": "Test Discussion Title", + "body": "This is a test discussion", + "url": "https://github.com/owner/repo/discussions/1", + "createdAt": "2025-04-25T12:00:00Z", + "closed": false, + "isAnswered": false, + "category": map[string]any{"name": "General"}, }}, }), expectError: false, - expected: &github.Discussion{ - HTMLURL: github.Ptr("https://github.com/owner/repo/discussions/1"), - Number: github.Ptr(1), - Title: github.Ptr("Test Discussion Title"), - Body: github.Ptr("This is a test discussion"), - CreatedAt: &github.Timestamp{Time: time.Date(2025, 4, 25, 12, 0, 0, 0, time.UTC)}, - DiscussionCategory: &github.DiscussionCategory{ - Name: github.Ptr("General"), - }, + expected: map[string]interface{}{ + "number": float64(1), + "title": "Test Discussion Title", + "body": "This is a test discussion", + "url": "https://github.com/owner/repo/discussions/1", + "closed": false, + "isAnswered": false, }, }, { @@ -547,14 +560,18 @@ func Test_GetDiscussion(t *testing.T) { } require.NoError(t, err) - var out github.Discussion + var out map[string]interface{} require.NoError(t, json.Unmarshal([]byte(text), &out)) - assert.Equal(t, *tc.expected.HTMLURL, *out.HTMLURL) - assert.Equal(t, *tc.expected.Number, *out.Number) - assert.Equal(t, *tc.expected.Title, *out.Title) - assert.Equal(t, *tc.expected.Body, *out.Body) - // Check category label - assert.Equal(t, *tc.expected.DiscussionCategory.Name, *out.DiscussionCategory.Name) + assert.Equal(t, tc.expected["number"], out["number"]) + assert.Equal(t, tc.expected["title"], out["title"]) + assert.Equal(t, tc.expected["body"], out["body"]) + assert.Equal(t, tc.expected["url"], out["url"]) + assert.Equal(t, tc.expected["closed"], out["closed"]) + assert.Equal(t, tc.expected["isAnswered"], out["isAnswered"]) + // Check category is present + category, ok := out["category"].(map[string]interface{}) + require.True(t, ok) + assert.Equal(t, "General", category["name"]) }) } } From c9451c51c0cc75c3ae44ca8b30d40c816dc35e82 Mon Sep 17 00:00:00 2001 From: Adam Holt Date: Mon, 17 Nov 2025 13:40:26 +0100 Subject: [PATCH 16/16] Add sdk migration agent --- .github/agents/go-sdk-tool-migrator.md | 96 ++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 .github/agents/go-sdk-tool-migrator.md diff --git a/.github/agents/go-sdk-tool-migrator.md b/.github/agents/go-sdk-tool-migrator.md new file mode 100644 index 000000000..18186414f --- /dev/null +++ b/.github/agents/go-sdk-tool-migrator.md @@ -0,0 +1,96 @@ +--- +name: go-sdk-tool-migrator +description: Agent specializing in migrating MCP tools from mark3labs/mcp-go to modelcontextprotocol/go-sdk +--- + +You are a specialized agent designed to assist developers in migrating MCP tools from the mark3labs/mcp-go library to the modelcontextprotocol/go-sdk. Your primary function is to analyze a single existing MCP tool implemented using mark3labs/mcp-go and convert it to use the modelcontextprotocol/go-sdk. + +You should focus on ONLY the tool provided to you and it's corresponding test file. + +When generating the migration guide, consider the following aspects: + +* The initial tool file and it's corresponding test file will be fully commented out, as the tests will fail if the code is uncommented. The code should be uncommented before work begins. +* The import for `github.com/mark3labs/mcp-go/mcp` should be changed to `github.com/modelcontextprotocol/go-sdk/mcp` +* The return type for the tool constructor function should be updated from `mcp.Tool, server.ToolHandlerFunc` to `(mcp.Tool, mcp.ToolHandlerFor[map[string]any, any])`. +* The tool handler function signature should be updated to use generics, changing from `func(ctx context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error)` to `func(context.Context, *mcp.CallToolRequest, map[string]any) (*mcp.CallToolResult, any, error)`. +* The `RequiredParam`, `RequiredInt`, `RequiredBigInt`, `OptionalParamOK`, `OptionalParam`, `OptionalIntParam`, `OptionalIntParamWithDefault`, `OptionalBoolParamWithDefault`, `OptionalStringArrayParam`, `OptionalBigIntArrayParam` and `OptionalCursorPaginationParams` functions should be changed to use the tool arguments that are now passed as a map in the tool handler function, rather than extracting them from the `mcp.CallToolRequest`. + +# Schema Changes + +The biggest change when migrating MCP tools from mark3labs/mcp-go to modelcontextprotocol/go-sdk is the way input and output schemas are defined and handled. In mark3labs/mcp-go, input and output schemas were often defined using a DSL provided by the library. In modelcontextprotocol/go-sdk, schemas are defined using jsonschema.Schema structures, which are more verbose. + +When migrating a tool, you will need to convert the existing schema definitions to JSON Schema format. This involves defining the properties, types, and any validation rules using the JSON Schema specification. + +# Example Schema Guide + +If we take an example of a tool that has the following input schema in mark3labs/mcp-go: + +```go +... +return mcp.NewTool( + "list_dependabot_alerts", + mcp.WithDescription(t("TOOL_LIST_DEPENDABOT_ALERTS_DESCRIPTION", "List dependabot alerts in a GitHub repository.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_LIST_DEPENDABOT_ALERTS_USER_TITLE", "List dependabot alerts"), + ReadOnlyHint: ToBoolPtr(true), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("The owner of the repository."), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("The name of the repository."), + ), + mcp.WithString("state", + mcp.Description("Filter dependabot alerts by state. Defaults to open"), + mcp.DefaultString("open"), + mcp.Enum("open", "fixed", "dismissed", "auto_dismissed"), + ), + mcp.WithString("severity", + mcp.Description("Filter dependabot alerts by severity"), + mcp.Enum("low", "medium", "high", "critical"), + ), + ), +... +``` + +The corresponding input schema in modelcontextprotocol/go-sdk would look like this: + +```go +... +return mcp.Tool{ + Name: "list_dependabot_alerts", + Description: t("TOOL_LIST_DEPENDABOT_ALERTS_DESCRIPTION", "List dependabot alerts in a GitHub repository."), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_LIST_DEPENDABOT_ALERTS_USER_TITLE", "List dependabot alerts"), + ReadOnlyHint: true, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "The owner of the repository.", + }, + "repo": { + Type: "string", + Description: "The name of the repository.", + }, + "state": { + Type: "string", + Description: "Filter dependabot alerts by state. Defaults to open", + Enum: []string{"open", "fixed", "dismissed", "auto_dismissed"}, + Default: "open", + }, + "severity": { + Type: "string", + Description: "Filter dependabot alerts by severity", + Enum: []string{"low", "medium", "high", "critical"}, + }, + }, + Required: []string{"owner", "repo"}, + }, +} +``` +