diff --git a/pkg/github/__toolsnaps__/add_project_item.snap b/pkg/github/__toolsnaps__/add_project_item.snap index 143c04eb9..08f495370 100644 --- a/pkg/github/__toolsnaps__/add_project_item.snap +++ b/pkg/github/__toolsnaps__/add_project_item.snap @@ -1,48 +1,47 @@ { "annotations": { - "title": "Add project item", - "readOnlyHint": false + "title": "Add project item" }, "description": "Add a specific Project item for a user or org", "inputSchema": { + "type": "object", + "required": [ + "owner_type", + "owner", + "project_number", + "item_type", + "item_id" + ], "properties": { "item_id": { - "description": "The numeric ID of the issue or pull request to add to the project.", - "type": "number" + "type": "number", + "description": "The numeric ID of the issue or pull request to add to the project." }, "item_type": { + "type": "string", "description": "The item's type, either issue or pull_request.", "enum": [ "issue", "pull_request" - ], - "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" + "type": "string", + "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." }, "owner_type": { + "type": "string", "description": "Owner type", "enum": [ "user", "org" - ], - "type": "string" + ] }, "project_number": { - "description": "The project's number.", - "type": "number" + "type": "number", + "description": "The project's number." } - }, - "required": [ - "owner_type", - "owner", - "project_number", - "item_type", - "item_id" - ], - "type": "object" + } }, "name": "add_project_item" } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/delete_project_item.snap b/pkg/github/__toolsnaps__/delete_project_item.snap index 0de1336a0..d768df10f 100644 --- a/pkg/github/__toolsnaps__/delete_project_item.snap +++ b/pkg/github/__toolsnaps__/delete_project_item.snap @@ -1,39 +1,38 @@ { "annotations": { - "title": "Delete project item", - "readOnlyHint": false + "title": "Delete project item" }, "description": "Delete a specific Project item for a user or org", "inputSchema": { + "type": "object", + "required": [ + "owner_type", + "owner", + "project_number", + "item_id" + ], "properties": { "item_id": { - "description": "The internal project item ID to delete from the project (not the issue or pull request ID).", - "type": "number" + "type": "number", + "description": "The internal project item ID to delete from the project (not the issue or pull request ID)." }, "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" + "type": "string", + "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." }, "owner_type": { + "type": "string", "description": "Owner type", "enum": [ "user", "org" - ], - "type": "string" + ] }, "project_number": { - "description": "The project's number.", - "type": "number" + "type": "number", + "description": "The project's number." } - }, - "required": [ - "owner_type", - "owner", - "project_number", - "item_id" - ], - "type": "object" + } }, "name": "delete_project_item" } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_project.snap b/pkg/github/__toolsnaps__/get_project.snap index db060e427..8194b7358 100644 --- a/pkg/github/__toolsnaps__/get_project.snap +++ b/pkg/github/__toolsnaps__/get_project.snap @@ -1,34 +1,34 @@ { "annotations": { - "title": "Get project", - "readOnlyHint": true + "readOnlyHint": true, + "title": "Get project" }, "description": "Get Project for a user or org", "inputSchema": { + "type": "object", + "required": [ + "project_number", + "owner_type", + "owner" + ], "properties": { "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" + "type": "string", + "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." }, "owner_type": { + "type": "string", "description": "Owner type", "enum": [ "user", "org" - ], - "type": "string" + ] }, "project_number": { - "description": "The project's number", - "type": "number" + "type": "number", + "description": "The project's number" } - }, - "required": [ - "project_number", - "owner_type", - "owner" - ], - "type": "object" + } }, "name": "get_project" } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_project_field.snap b/pkg/github/__toolsnaps__/get_project_field.snap index 65d6f86f1..0df557a03 100644 --- a/pkg/github/__toolsnaps__/get_project_field.snap +++ b/pkg/github/__toolsnaps__/get_project_field.snap @@ -1,39 +1,39 @@ { "annotations": { - "title": "Get project field", - "readOnlyHint": true + "readOnlyHint": true, + "title": "Get project field" }, "description": "Get Project field for a user or org", "inputSchema": { + "type": "object", + "required": [ + "owner_type", + "owner", + "project_number", + "field_id" + ], "properties": { "field_id": { - "description": "The field's id.", - "type": "number" + "type": "number", + "description": "The field's id." }, "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" + "type": "string", + "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." }, "owner_type": { + "type": "string", "description": "Owner type", "enum": [ "user", "org" - ], - "type": "string" + ] }, "project_number": { - "description": "The project's number.", - "type": "number" + "type": "number", + "description": "The project's number." } - }, - "required": [ - "owner_type", - "owner", - "project_number", - "field_id" - ], - "type": "object" + } }, "name": "get_project_field" } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_project_item.snap b/pkg/github/__toolsnaps__/get_project_item.snap index 36eb7bb63..d77c49c1e 100644 --- a/pkg/github/__toolsnaps__/get_project_item.snap +++ b/pkg/github/__toolsnaps__/get_project_item.snap @@ -1,46 +1,46 @@ { "annotations": { - "title": "Get project item", - "readOnlyHint": true + "readOnlyHint": true, + "title": "Get project item" }, "description": "Get a specific Project item for a user or org", "inputSchema": { + "type": "object", + "required": [ + "owner_type", + "owner", + "project_number", + "item_id" + ], "properties": { "fields": { + "type": "array", "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.", "items": { "type": "string" - }, - "type": "array" + } }, "item_id": { - "description": "The item's ID.", - "type": "number" + "type": "number", + "description": "The item's ID." }, "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" + "type": "string", + "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." }, "owner_type": { + "type": "string", "description": "Owner type", "enum": [ "user", "org" - ], - "type": "string" + ] }, "project_number": { - "description": "The project's number.", - "type": "number" + "type": "number", + "description": "The project's number." } - }, - "required": [ - "owner_type", - "owner", - "project_number", - "item_id" - ], - "type": "object" + } }, "name": "get_project_item" } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_project_fields.snap b/pkg/github/__toolsnaps__/list_project_fields.snap index c543e69d7..6bef18507 100644 --- a/pkg/github/__toolsnaps__/list_project_fields.snap +++ b/pkg/github/__toolsnaps__/list_project_fields.snap @@ -1,46 +1,46 @@ { "annotations": { - "title": "List project fields", - "readOnlyHint": true + "readOnlyHint": true, + "title": "List project fields" }, "description": "List Project fields for a user or org", "inputSchema": { + "type": "object", + "required": [ + "owner_type", + "owner", + "project_number" + ], "properties": { "after": { - "description": "Forward pagination cursor from previous pageInfo.nextCursor.", - "type": "string" + "type": "string", + "description": "Forward pagination cursor from previous pageInfo.nextCursor." }, "before": { - "description": "Backward pagination cursor from previous pageInfo.prevCursor (rare).", - "type": "string" + "type": "string", + "description": "Backward pagination cursor from previous pageInfo.prevCursor (rare)." }, "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" + "type": "string", + "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." }, "owner_type": { + "type": "string", "description": "Owner type", "enum": [ "user", "org" - ], - "type": "string" + ] }, "per_page": { - "description": "Results per page (max 50)", - "type": "number" + "type": "number", + "description": "Results per page (max 50)" }, "project_number": { - "description": "The project's number.", - "type": "number" + "type": "number", + "description": "The project's number." } - }, - "required": [ - "owner_type", - "owner", - "project_number" - ], - "type": "object" + } }, "name": "list_project_fields" } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_project_items.snap b/pkg/github/__toolsnaps__/list_project_items.snap index 38d3cb509..bceb5d9eb 100644 --- a/pkg/github/__toolsnaps__/list_project_items.snap +++ b/pkg/github/__toolsnaps__/list_project_items.snap @@ -1,57 +1,57 @@ { "annotations": { - "title": "List project items", - "readOnlyHint": true + "readOnlyHint": true, + "title": "List project items" }, "description": "Search project items with advanced filtering", "inputSchema": { + "type": "object", + "required": [ + "owner_type", + "owner", + "project_number" + ], "properties": { "after": { - "description": "Forward pagination cursor from previous pageInfo.nextCursor.", - "type": "string" + "type": "string", + "description": "Forward pagination cursor from previous pageInfo.nextCursor." }, "before": { - "description": "Backward pagination cursor from previous pageInfo.prevCursor (rare).", - "type": "string" + "type": "string", + "description": "Backward pagination cursor from previous pageInfo.prevCursor (rare)." }, "fields": { + "type": "array", "description": "Field IDs to include (e.g. [\"102589\", \"985201\"]). CRITICAL: Always provide to get field values. Without this, only titles returned.", "items": { "type": "string" - }, - "type": "array" + } }, "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" + "type": "string", + "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." }, "owner_type": { + "type": "string", "description": "Owner type", "enum": [ "user", "org" - ], - "type": "string" + ] }, "per_page": { - "description": "Results per page (max 50)", - "type": "number" + "type": "number", + "description": "Results per page (max 50)" }, "project_number": { - "description": "The project's number.", - "type": "number" + "type": "number", + "description": "The project's number." }, "query": { - "description": "Query string for advanced filtering of project items using GitHub's project filtering syntax.", - "type": "string" + "type": "string", + "description": "Query string for advanced filtering of project items using GitHub's project filtering syntax." } - }, - "required": [ - "owner_type", - "owner", - "project_number" - ], - "type": "object" + } }, "name": "list_project_items" } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_projects.snap b/pkg/github/__toolsnaps__/list_projects.snap index 8a035271c..f48e26217 100644 --- a/pkg/github/__toolsnaps__/list_projects.snap +++ b/pkg/github/__toolsnaps__/list_projects.snap @@ -1,45 +1,45 @@ { "annotations": { - "title": "List projects", - "readOnlyHint": true + "readOnlyHint": true, + "title": "List projects" }, "description": "List Projects for a user or organization", "inputSchema": { + "type": "object", + "required": [ + "owner_type", + "owner" + ], "properties": { "after": { - "description": "Forward pagination cursor from previous pageInfo.nextCursor.", - "type": "string" + "type": "string", + "description": "Forward pagination cursor from previous pageInfo.nextCursor." }, "before": { - "description": "Backward pagination cursor from previous pageInfo.prevCursor (rare).", - "type": "string" + "type": "string", + "description": "Backward pagination cursor from previous pageInfo.prevCursor (rare)." }, "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" + "type": "string", + "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." }, "owner_type": { + "type": "string", "description": "Owner type", "enum": [ "user", "org" - ], - "type": "string" + ] }, "per_page": { - "description": "Results per page (max 50)", - "type": "number" + "type": "number", + "description": "Results per page (max 50)" }, "query": { - "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" + "type": "string", + "description": "Filter projects by title text and open/closed state; permitted qualifiers: is:open, is:closed; examples: \"roadmap is:open\", \"is:open feature planning\"." } - }, - "required": [ - "owner_type", - "owner" - ], - "type": "object" + } }, "name": "list_projects" } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/update_project_item.snap b/pkg/github/__toolsnaps__/update_project_item.snap index 6c8648503..8f5afaa58 100644 --- a/pkg/github/__toolsnaps__/update_project_item.snap +++ b/pkg/github/__toolsnaps__/update_project_item.snap @@ -1,45 +1,43 @@ { "annotations": { - "title": "Update project item", - "readOnlyHint": false + "title": "Update project item" }, "description": "Update a specific Project item for a user or org", "inputSchema": { + "type": "object", + "required": [ + "owner_type", + "owner", + "project_number", + "item_id", + "updated_field" + ], "properties": { "item_id": { - "description": "The unique identifier of the project item. This is not the issue or pull request ID.", - "type": "number" + "type": "number", + "description": "The unique identifier of the project item. This is not the issue or pull request ID." }, "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" + "type": "string", + "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." }, "owner_type": { + "type": "string", "description": "Owner type", "enum": [ "user", "org" - ], - "type": "string" + ] }, "project_number": { - "description": "The project's number.", - "type": "number" + "type": "number", + "description": "The project's number." }, "updated_field": { - "description": "Object consisting of the ID of the project field to update and the new value for the field. To clear the field, set value to null. Example: {\"id\": 123456, \"value\": \"New Value\"}", - "properties": {}, - "type": "object" + "type": "object", + "description": "Object consisting of the ID of the project field to update and the new value for the field. To clear the field, set value to null. Example: {\"id\": 123456, \"value\": \"New Value\"}" } - }, - "required": [ - "owner_type", - "owner", - "project_number", - "item_id", - "updated_field" - ], - "type": "object" + } }, "name": "update_project_item" } \ No newline at end of file diff --git a/pkg/github/projects.go b/pkg/github/projects.go index 6710a4f6f..79dfb25ce 100644 --- a/pkg/github/projects.go +++ b/pkg/github/projects.go @@ -1,5 +1,3 @@ -//go:build ignore - package github import ( @@ -12,9 +10,10 @@ import ( ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/translations" + "github.com/github/github-mcp-server/pkg/utils" "github.com/google/go-github/v79/github" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" ) const ( @@ -25,56 +24,69 @@ const ( 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 organization`)), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ +func ListProjects(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + return mcp.Tool{ + Name: "list_projects", + Description: t("TOOL_LIST_PROJECTS_DESCRIPTION", `List Projects for a user or organization`), + Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_PROJECTS_USER_TITLE", "List projects"), - ReadOnlyHint: ToBoolPtr(true), - }), - mcp.WithString("owner_type", - mcp.Required(), mcp.Description("Owner type"), mcp.Enum("user", "org"), - ), - mcp.WithString("owner", - mcp.Required(), - 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 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(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") + ReadOnlyHint: true, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner_type": { + Type: "string", + Description: "Owner type", + Enum: []any{"user", "org"}, + }, + "owner": { + Type: "string", + 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.", + }, + "query": { + Type: "string", + Description: `Filter projects by title text and open/closed state; permitted qualifiers: is:open, is:closed; examples: "roadmap is:open", "is:open feature planning".`, + }, + "per_page": { + Type: "number", + Description: fmt.Sprintf("Results per page (max %d)", MaxProjectsPerPage), + }, + "after": { + Type: "string", + Description: "Forward pagination cursor from previous pageInfo.nextCursor.", + }, + "before": { + Type: "string", + Description: "Backward pagination cursor from previous pageInfo.prevCursor (rare).", + }, + }, + Required: []string{"owner_type", "owner"}, + }, + }, func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - ownerType, err := RequiredParam[string](req, "owner_type") + ownerType, err := RequiredParam[string](args, "owner_type") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - queryStr, err := OptionalParam[string](req, "query") + queryStr, err := OptionalParam[string](args, "query") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - pagination, err := extractPaginationOptions(req) + pagination, err := extractPaginationOptionsFromArgs(args) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } client, err := getClient(ctx) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } var resp *github.Response @@ -102,7 +114,7 @@ func ListProjects(getClient GetClientFn, t translations.TranslationHelperFunc) ( "failed to list projects", resp, err, - ), nil + ), nil, nil } defer func() { _ = resp.Body.Close() }() @@ -117,53 +129,60 @@ func ListProjects(getClient GetClientFn, t translations.TranslationHelperFunc) ( r, err := json.Marshal(response) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil, nil } } -func GetProject(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool("get_project", - mcp.WithDescription(t("TOOL_GET_PROJECT_DESCRIPTION", "Get Project for a user or org")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ +func GetProject(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + return mcp.Tool{ + Name: "get_project", + Description: t("TOOL_GET_PROJECT_DESCRIPTION", "Get Project for a user or org"), + Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_PROJECT_USER_TITLE", "Get project"), - ReadOnlyHint: ToBoolPtr(true), - }), - mcp.WithNumber("project_number", - mcp.Required(), - mcp.Description("The project's number"), - ), - mcp.WithString("owner_type", - mcp.Required(), - mcp.Description("Owner type"), - mcp.Enum("user", "org"), - ), - mcp.WithString("owner", - mcp.Required(), - 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."), - ), - ), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + ReadOnlyHint: true, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "project_number": { + Type: "number", + Description: "The project's number", + }, + "owner_type": { + Type: "string", + Description: "Owner type", + Enum: []any{"user", "org"}, + }, + "owner": { + Type: "string", + 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.", + }, + }, + Required: []string{"project_number", "owner_type", "owner"}, + }, + }, func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - projectNumber, err := RequiredInt(req, "project_number") + projectNumber, err := RequiredInt(args, "project_number") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - owner, err := RequiredParam[string](req, "owner") + owner, err := RequiredParam[string](args, "owner") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - ownerType, err := RequiredParam[string](req, "owner_type") + ownerType, err := RequiredParam[string](args, "owner_type") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } client, err := getClient(ctx) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } var resp *github.Response @@ -179,80 +198,91 @@ func GetProject(getClient GetClientFn, t translations.TranslationHelperFunc) (to "failed to get project", resp, err, - ), nil + ), nil, nil } 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 nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("failed to get project: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to get project: %s", string(body))), nil, nil } minimalProject := convertToMinimalProject(project) r, err := json.Marshal(minimalProject) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil, nil } } -func ListProjectFields(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool("list_project_fields", - mcp.WithDescription(t("TOOL_LIST_PROJECT_FIELDS_DESCRIPTION", "List Project fields for a user or org")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ +func ListProjectFields(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + return mcp.Tool{ + Name: "list_project_fields", + Description: t("TOOL_LIST_PROJECT_FIELDS_DESCRIPTION", "List Project fields for a user or org"), + Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_PROJECT_FIELDS_USER_TITLE", "List project fields"), - ReadOnlyHint: ToBoolPtr(true), - }), - mcp.WithString("owner_type", - mcp.Required(), - mcp.Description("Owner type"), - mcp.Enum("user", "org")), - mcp.WithString("owner", - mcp.Required(), - 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.WithNumber("project_number", - mcp.Required(), - mcp.Description("The project's number."), - ), - mcp.WithNumber("per_page", - 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") + ReadOnlyHint: true, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner_type": { + Type: "string", + Description: "Owner type", + Enum: []any{"user", "org"}, + }, + "owner": { + Type: "string", + 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.", + }, + "project_number": { + Type: "number", + Description: "The project's number.", + }, + "per_page": { + Type: "number", + Description: fmt.Sprintf("Results per page (max %d)", MaxProjectsPerPage), + }, + "after": { + Type: "string", + Description: "Forward pagination cursor from previous pageInfo.nextCursor.", + }, + "before": { + Type: "string", + Description: "Backward pagination cursor from previous pageInfo.prevCursor (rare).", + }, + }, + Required: []string{"owner_type", "owner", "project_number"}, + }, + }, func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - ownerType, err := RequiredParam[string](req, "owner_type") + ownerType, err := RequiredParam[string](args, "owner_type") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - projectNumber, err := RequiredInt(req, "project_number") + projectNumber, err := RequiredInt(args, "project_number") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - pagination, err := extractPaginationOptions(req) + pagination, err := extractPaginationOptionsFromArgs(args) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } client, err := getClient(ctx) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } var resp *github.Response @@ -273,7 +303,7 @@ func ListProjectFields(getClient GetClientFn, t translations.TranslationHelperFu "failed to list project fields", resp, err, - ), nil + ), nil, nil } defer func() { _ = resp.Body.Close() }() @@ -284,54 +314,64 @@ func ListProjectFields(getClient GetClientFn, t translations.TranslationHelperFu r, err := json.Marshal(response) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil, nil } } -func GetProjectField(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool("get_project_field", - mcp.WithDescription(t("TOOL_GET_PROJECT_FIELD_DESCRIPTION", "Get Project field for a user or org")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ +func GetProjectField(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + return mcp.Tool{ + Name: "get_project_field", + Description: t("TOOL_GET_PROJECT_FIELD_DESCRIPTION", "Get Project field for a user or org"), + Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_PROJECT_FIELD_USER_TITLE", "Get project field"), - ReadOnlyHint: ToBoolPtr(true), - }), - mcp.WithString("owner_type", - mcp.Required(), - mcp.Description("Owner type"), mcp.Enum("user", "org")), - mcp.WithString("owner", - mcp.Required(), - 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.WithNumber("project_number", - mcp.Required(), - mcp.Description("The project's number.")), - mcp.WithNumber("field_id", - mcp.Required(), - mcp.Description("The field's id."), - ), - ), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := RequiredParam[string](req, "owner") + ReadOnlyHint: true, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner_type": { + Type: "string", + Description: "Owner type", + Enum: []any{"user", "org"}, + }, + "owner": { + Type: "string", + 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.", + }, + "project_number": { + Type: "number", + Description: "The project's number.", + }, + "field_id": { + Type: "number", + Description: "The field's id.", + }, + }, + Required: []string{"owner_type", "owner", "project_number", "field_id"}, + }, + }, func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - ownerType, err := RequiredParam[string](req, "owner_type") + ownerType, err := RequiredParam[string](args, "owner_type") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - projectNumber, err := RequiredInt(req, "project_number") + projectNumber, err := RequiredInt(args, "project_number") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - fieldID, err := RequiredBigInt(req, "field_id") + fieldID, err := RequiredBigInt(args, "field_id") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } client, err := getClient(ctx) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } var resp *github.Response @@ -348,95 +388,110 @@ func GetProjectField(getClient GetClientFn, t translations.TranslationHelperFunc "failed to get project field", resp, err, - ), nil + ), nil, nil } 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 nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("failed to get project field: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to get project field: %s", string(body))), nil, nil } r, err := json.Marshal(projectField) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil, nil } } -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", `Search project items with advanced filtering`)), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ +func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + return mcp.Tool{ + Name: "list_project_items", + Description: t("TOOL_LIST_PROJECT_ITEMS_DESCRIPTION", `Search project items with advanced filtering`), + Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_PROJECT_ITEMS_USER_TITLE", "List project items"), - ReadOnlyHint: ToBoolPtr(true), - }), - mcp.WithString("owner_type", - mcp.Required(), - mcp.Description("Owner type"), - mcp.Enum("user", "org"), - ), - mcp.WithString("owner", - mcp.Required(), - 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.WithNumber("project_number", mcp.Required(), - mcp.Description("The project's number."), - ), - mcp.WithString("query", - mcp.Description(`Query string for advanced filtering of project items using GitHub's project filtering syntax.`), - ), - mcp.WithNumber("per_page", - 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("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) { - owner, err := RequiredParam[string](req, "owner") + ReadOnlyHint: true, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner_type": { + Type: "string", + Description: "Owner type", + Enum: []any{"user", "org"}, + }, + "owner": { + Type: "string", + 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.", + }, + "project_number": { + Type: "number", + Description: "The project's number.", + }, + "query": { + Type: "string", + Description: `Query string for advanced filtering of project items using GitHub's project filtering syntax.`, + }, + "per_page": { + Type: "number", + Description: fmt.Sprintf("Results per page (max %d)", MaxProjectsPerPage), + }, + "after": { + Type: "string", + Description: "Forward pagination cursor from previous pageInfo.nextCursor.", + }, + "before": { + Type: "string", + Description: "Backward pagination cursor from previous pageInfo.prevCursor (rare).", + }, + "fields": { + Type: "array", + Description: "Field IDs to include (e.g. [\"102589\", \"985201\"]). CRITICAL: Always provide to get field values. Without this, only titles returned.", + Items: &jsonschema.Schema{ + Type: "string", + }, + }, + }, + Required: []string{"owner_type", "owner", "project_number"}, + }, + }, func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - ownerType, err := RequiredParam[string](req, "owner_type") + ownerType, err := RequiredParam[string](args, "owner_type") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - projectNumber, err := RequiredInt(req, "project_number") + projectNumber, err := RequiredInt(args, "project_number") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - queryStr, err := OptionalParam[string](req, "query") + queryStr, err := OptionalParam[string](args, "query") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - fields, err := OptionalBigIntArrayParam(req, "fields") + fields, err := OptionalBigIntArrayParam(args, "fields") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - pagination, err := extractPaginationOptions(req) + pagination, err := extractPaginationOptionsFromArgs(args) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } client, err := getClient(ctx) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } var resp *github.Response @@ -466,7 +521,7 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun ProjectListFailedError, resp, err, - ), nil + ), nil, nil } defer func() { _ = resp.Body.Close() }() @@ -477,68 +532,78 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun r, err := json.Marshal(response) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil, nil } } -func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool("get_project_item", - mcp.WithDescription(t("TOOL_GET_PROJECT_ITEM_DESCRIPTION", "Get a specific Project item for a user or org")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ +func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + return mcp.Tool{ + Name: "get_project_item", + Description: t("TOOL_GET_PROJECT_ITEM_DESCRIPTION", "Get a specific Project item for a user or org"), + Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_PROJECT_ITEM_USER_TITLE", "Get project item"), - ReadOnlyHint: ToBoolPtr(true), - }), - mcp.WithString("owner_type", - mcp.Required(), - mcp.Description("Owner type"), - mcp.Enum("user", "org"), - ), - mcp.WithString("owner", - mcp.Required(), - 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.WithNumber("project_number", - mcp.Required(), - mcp.Description("The project's number."), - ), - mcp.WithNumber("item_id", - mcp.Required(), - mcp.Description("The item's ID."), - ), - 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.WithStringItems(), - ), - ), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := RequiredParam[string](req, "owner") + ReadOnlyHint: true, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner_type": { + Type: "string", + Description: "Owner type", + Enum: []any{"user", "org"}, + }, + "owner": { + Type: "string", + 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.", + }, + "project_number": { + Type: "number", + Description: "The project's number.", + }, + "item_id": { + Type: "number", + Description: "The item's ID.", + }, + "fields": { + Type: "array", + 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.", + Items: &jsonschema.Schema{ + Type: "string", + }, + }, + }, + Required: []string{"owner_type", "owner", "project_number", "item_id"}, + }, + }, func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - ownerType, err := RequiredParam[string](req, "owner_type") + ownerType, err := RequiredParam[string](args, "owner_type") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - projectNumber, err := RequiredInt(req, "project_number") + projectNumber, err := RequiredInt(args, "project_number") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - itemID, err := RequiredBigInt(req, "item_id") + itemID, err := RequiredBigInt(args, "item_id") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - fields, err := OptionalBigIntArrayParam(req, "fields") + fields, err := OptionalBigIntArrayParam(args, "fields") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } client, err := getClient(ctx) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } var resp *github.Response @@ -562,76 +627,84 @@ func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) "failed to get project item", resp, err, - ), nil + ), nil, nil } defer func() { _ = resp.Body.Close() }() r, err := json.Marshal(projectItem) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil, nil } } -func AddProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool("add_project_item", - mcp.WithDescription(t("TOOL_ADD_PROJECT_ITEM_DESCRIPTION", "Add a specific Project item for a user or org")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ +func AddProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + return mcp.Tool{ + Name: "add_project_item", + Description: t("TOOL_ADD_PROJECT_ITEM_DESCRIPTION", "Add a specific Project item for a user or org"), + Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_ADD_PROJECT_ITEM_USER_TITLE", "Add project item"), - ReadOnlyHint: ToBoolPtr(false), - }), - mcp.WithString("owner_type", - mcp.Required(), - mcp.Description("Owner type"), mcp.Enum("user", "org"), - ), - mcp.WithString("owner", - mcp.Required(), - 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.WithNumber("project_number", - mcp.Required(), - mcp.Description("The project's number."), - ), - mcp.WithString("item_type", - mcp.Required(), - mcp.Description("The item's type, either issue or pull_request."), - mcp.Enum("issue", "pull_request"), - ), - mcp.WithNumber("item_id", - mcp.Required(), - mcp.Description("The numeric ID of the issue or pull request to add to the project."), - ), - ), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := RequiredParam[string](req, "owner") + ReadOnlyHint: false, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner_type": { + Type: "string", + Description: "Owner type", + Enum: []any{"user", "org"}, + }, + "owner": { + Type: "string", + 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.", + }, + "project_number": { + Type: "number", + Description: "The project's number.", + }, + "item_type": { + Type: "string", + Description: "The item's type, either issue or pull_request.", + Enum: []any{"issue", "pull_request"}, + }, + "item_id": { + Type: "number", + Description: "The numeric ID of the issue or pull request to add to the project.", + }, + }, + Required: []string{"owner_type", "owner", "project_number", "item_type", "item_id"}, + }, + }, func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - ownerType, err := RequiredParam[string](req, "owner_type") + ownerType, err := RequiredParam[string](args, "owner_type") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - projectNumber, err := RequiredInt(req, "project_number") + projectNumber, err := RequiredInt(args, "project_number") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - itemID, err := RequiredBigInt(req, "item_id") + itemID, err := RequiredBigInt(args, "item_id") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - itemType, err := RequiredParam[string](req, "item_type") + itemType, err := RequiredParam[string](args, "item_type") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } if itemType != "issue" && itemType != "pull_request" { - return mcp.NewToolResultError("item_type must be either 'issue' or 'pull_request'"), nil + return utils.NewToolResultError("item_type must be either 'issue' or 'pull_request'"), nil, nil } client, err := getClient(ctx) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } newItem := &github.AddProjectItemOptions{ @@ -653,89 +726,97 @@ func AddProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) ProjectAddFailedError, resp, err, - ), nil + ), nil, nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusCreated { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("%s: %s", ProjectAddFailedError, string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("%s: %s", ProjectAddFailedError, string(body))), nil, nil } r, err := json.Marshal(addedItem) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil, nil } } -func UpdateProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool("update_project_item", - mcp.WithDescription(t("TOOL_UPDATE_PROJECT_ITEM_DESCRIPTION", "Update a specific Project item for a user or org")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ +func UpdateProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + return mcp.Tool{ + Name: "update_project_item", + Description: t("TOOL_UPDATE_PROJECT_ITEM_DESCRIPTION", "Update a specific Project item for a user or org"), + Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_UPDATE_PROJECT_ITEM_USER_TITLE", "Update project item"), - ReadOnlyHint: ToBoolPtr(false), - }), - mcp.WithString("owner_type", - mcp.Required(), mcp.Description("Owner type"), - mcp.Enum("user", "org"), - ), - mcp.WithString("owner", - mcp.Required(), - 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.WithNumber("project_number", - mcp.Required(), - mcp.Description("The project's number."), - ), - mcp.WithNumber("item_id", - mcp.Required(), - mcp.Description("The unique identifier of the project item. This is not the issue or pull request ID."), - ), - mcp.WithObject("updated_field", - mcp.Required(), - mcp.Description("Object consisting of the ID of the project field to update and the new value for the field. To clear the field, set value to null. Example: {\"id\": 123456, \"value\": \"New Value\"}"), - ), - ), 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 - } - itemID, err := RequiredBigInt(req, "item_id") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - rawUpdatedField, exists := req.GetArguments()["updated_field"] + ReadOnlyHint: false, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner_type": { + Type: "string", + Description: "Owner type", + Enum: []any{"user", "org"}, + }, + "owner": { + Type: "string", + 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.", + }, + "project_number": { + Type: "number", + Description: "The project's number.", + }, + "item_id": { + Type: "number", + Description: "The unique identifier of the project item. This is not the issue or pull request ID.", + }, + "updated_field": { + Type: "object", + Description: "Object consisting of the ID of the project field to update and the new value for the field. To clear the field, set value to null. Example: {\"id\": 123456, \"value\": \"New Value\"}", + }, + }, + Required: []string{"owner_type", "owner", "project_number", "item_id", "updated_field"}, + }, + }, func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + ownerType, err := RequiredParam[string](args, "owner_type") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + projectNumber, err := RequiredInt(args, "project_number") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + itemID, err := RequiredBigInt(args, "item_id") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + rawUpdatedField, exists := args["updated_field"] if !exists { - return mcp.NewToolResultError("missing required parameter: updated_field"), nil + return utils.NewToolResultError("missing required parameter: updated_field"), nil, nil } fieldValue, ok := rawUpdatedField.(map[string]any) if !ok || fieldValue == nil { - return mcp.NewToolResultError("field_value must be an object"), nil + return utils.NewToolResultError("field_value must be an object"), nil, nil } updatePayload, err := buildUpdateProjectItem(fieldValue) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } client, err := getClient(ctx) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } var resp *github.Response @@ -752,70 +833,77 @@ func UpdateProjectItem(getClient GetClientFn, t translations.TranslationHelperFu ProjectUpdateFailedError, resp, err, - ), nil + ), nil, nil } 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 nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("%s: %s", ProjectUpdateFailedError, string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("%s: %s", ProjectUpdateFailedError, string(body))), nil, nil } r, err := json.Marshal(updatedItem) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil, nil } } -func DeleteProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool("delete_project_item", - mcp.WithDescription(t("TOOL_DELETE_PROJECT_ITEM_DESCRIPTION", "Delete a specific Project item for a user or org")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ +func DeleteProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + return mcp.Tool{ + Name: "delete_project_item", + Description: t("TOOL_DELETE_PROJECT_ITEM_DESCRIPTION", "Delete a specific Project item for a user or org"), + Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_DELETE_PROJECT_ITEM_USER_TITLE", "Delete project item"), - ReadOnlyHint: ToBoolPtr(false), - }), - mcp.WithString("owner_type", - mcp.Required(), - mcp.Description("Owner type"), - mcp.Enum("user", "org"), - ), - mcp.WithString("owner", - mcp.Required(), - 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.WithNumber("project_number", - mcp.Required(), - mcp.Description("The project's number."), - ), - mcp.WithNumber("item_id", - mcp.Required(), - mcp.Description("The internal project item ID to delete from the project (not the issue or pull request ID)."), - ), - ), 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 - } - itemID, err := RequiredBigInt(req, "item_id") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil + ReadOnlyHint: false, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner_type": { + Type: "string", + Description: "Owner type", + Enum: []any{"user", "org"}, + }, + "owner": { + Type: "string", + 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.", + }, + "project_number": { + Type: "number", + Description: "The project's number.", + }, + "item_id": { + Type: "number", + Description: "The internal project item ID to delete from the project (not the issue or pull request ID).", + }, + }, + Required: []string{"owner_type", "owner", "project_number", "item_id"}, + }, + }, func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + ownerType, err := RequiredParam[string](args, "owner_type") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + projectNumber, err := RequiredInt(args, "project_number") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + itemID, err := RequiredBigInt(args, "item_id") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil } client, err := getClient(ctx) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } var resp *github.Response @@ -830,18 +918,18 @@ func DeleteProjectItem(getClient GetClientFn, t translations.TranslationHelperFu ProjectDeleteFailedError, resp, err, - ), nil + ), nil, nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusNoContent { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("%s: %s", ProjectDeleteFailedError, string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("%s: %s", ProjectDeleteFailedError, string(body))), nil, nil } - return mcp.NewToolResultText("project item successfully deleted"), nil + return utils.NewToolResultText("project item successfully deleted"), nil, nil } } @@ -922,8 +1010,8 @@ func buildPageInfo(resp *github.Response) pageInfo { } } -func extractPaginationOptions(request mcp.CallToolRequest) (github.ListProjectsPaginationOptions, error) { - perPage, err := OptionalIntParamWithDefault(request, "per_page", MaxProjectsPerPage) +func extractPaginationOptionsFromArgs(args map[string]any) (github.ListProjectsPaginationOptions, error) { + perPage, err := OptionalIntParamWithDefault(args, "per_page", MaxProjectsPerPage) if err != nil { return github.ListProjectsPaginationOptions{}, err } @@ -931,12 +1019,12 @@ func extractPaginationOptions(request mcp.CallToolRequest) (github.ListProjectsP perPage = MaxProjectsPerPage } - after, err := OptionalParam[string](request, "after") + after, err := OptionalParam[string](args, "after") if err != nil { return github.ListProjectsPaginationOptions{}, err } - before, err := OptionalParam[string](request, "before") + before, err := OptionalParam[string](args, "before") if err != nil { return github.ListProjectsPaginationOptions{}, err } diff --git a/pkg/github/projects_test.go b/pkg/github/projects_test.go index 6cc4f6cc4..e2814c8f9 100644 --- a/pkg/github/projects_test.go +++ b/pkg/github/projects_test.go @@ -1,5 +1,3 @@ -//go:build ignore - package github import ( @@ -12,6 +10,7 @@ import ( "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" gh "github.com/google/go-github/v79/github" + "github.com/google/jsonschema-go/jsonschema" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -24,11 +23,13 @@ func Test_ListProjects(t *testing.T) { assert.Equal(t, "list_projects", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "owner_type") - assert.Contains(t, tool.InputSchema.Properties, "query") - assert.Contains(t, tool.InputSchema.Properties, "per_page") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "owner_type"}) + schema, ok := tool.InputSchema.(*jsonschema.Schema) + require.True(t, ok, "InputSchema should be a *jsonschema.Schema") + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "owner_type") + assert.Contains(t, schema.Properties, "query") + assert.Contains(t, schema.Properties, "per_page") + assert.ElementsMatch(t, schema.Required, []string{"owner", "owner_type"}) // API returns full ProjectV2 objects; we only need minimal fields for decoding. orgProjects := []map[string]any{{"id": 1, "node_id": "NODE1", "title": "Org Project"}} @@ -142,7 +143,7 @@ func Test_ListProjects(t *testing.T) { client := gh.NewClient(tc.mockedClient) _, handler := ListProjects(stubGetClientFn(client), translations.NullTranslationHelper) request := createMCPRequest(tc.requestArgs) - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) require.NoError(t, err) if tc.expectError { @@ -182,10 +183,12 @@ func Test_GetProject(t *testing.T) { assert.Equal(t, "get_project", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "project_number") - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "owner_type") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"project_number", "owner", "owner_type"}) + schema, ok := tool.InputSchema.(*jsonschema.Schema) + require.True(t, ok, "InputSchema should be a *jsonschema.Schema") + assert.Contains(t, schema.Properties, "project_number") + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "owner_type") + assert.ElementsMatch(t, schema.Required, []string{"project_number", "owner", "owner_type"}) project := map[string]any{"id": 123, "title": "Project Title"} @@ -276,7 +279,7 @@ func Test_GetProject(t *testing.T) { client := gh.NewClient(tc.mockedClient) _, handler := GetProject(stubGetClientFn(client), translations.NullTranslationHelper) request := createMCPRequest(tc.requestArgs) - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) require.NoError(t, err) if tc.expectError { @@ -313,11 +316,13 @@ func Test_ListProjectFields(t *testing.T) { assert.Equal(t, "list_project_fields", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "owner_type") - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "project_number") - assert.Contains(t, tool.InputSchema.Properties, "per_page") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner_type", "owner", "project_number"}) + schema, ok := tool.InputSchema.(*jsonschema.Schema) + require.True(t, ok, "InputSchema should be a *jsonschema.Schema") + assert.Contains(t, schema.Properties, "owner_type") + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "project_number") + assert.Contains(t, schema.Properties, "per_page") + assert.ElementsMatch(t, schema.Required, []string{"owner_type", "owner", "project_number"}) orgFields := []map[string]any{{"id": 101, "name": "Status", "data_type": "single_select"}} userFields := []map[string]any{{"id": 201, "name": "Priority", "data_type": "single_select"}} @@ -423,7 +428,7 @@ func Test_ListProjectFields(t *testing.T) { client := gh.NewClient(tc.mockedClient) _, handler := ListProjectFields(stubGetClientFn(client), translations.NullTranslationHelper) request := createMCPRequest(tc.requestArgs) - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) require.NoError(t, err) if tc.expectError { @@ -465,11 +470,13 @@ func Test_GetProjectField(t *testing.T) { assert.Equal(t, "get_project_field", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "owner_type") - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "project_number") - assert.Contains(t, tool.InputSchema.Properties, "field_id") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner_type", "owner", "project_number", "field_id"}) + schema, ok := tool.InputSchema.(*jsonschema.Schema) + require.True(t, ok, "InputSchema should be a *jsonschema.Schema") + assert.Contains(t, schema.Properties, "owner_type") + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "project_number") + assert.Contains(t, schema.Properties, "field_id") + assert.ElementsMatch(t, schema.Required, []string{"owner_type", "owner", "project_number", "field_id"}) orgField := map[string]any{"id": 101, "name": "Status", "dataType": "single_select"} userField := map[string]any{"id": 202, "name": "Priority", "dataType": "single_select"} @@ -578,7 +585,7 @@ func Test_GetProjectField(t *testing.T) { client := gh.NewClient(tc.mockedClient) _, handler := GetProjectField(stubGetClientFn(client), translations.NullTranslationHelper) request := createMCPRequest(tc.requestArgs) - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) require.NoError(t, err) if tc.expectError { @@ -621,13 +628,15 @@ func Test_ListProjectItems(t *testing.T) { assert.Equal(t, "list_project_items", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "owner_type") - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "project_number") - assert.Contains(t, tool.InputSchema.Properties, "query") - assert.Contains(t, tool.InputSchema.Properties, "per_page") - assert.Contains(t, tool.InputSchema.Properties, "fields") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner_type", "owner", "project_number"}) + schema, ok := tool.InputSchema.(*jsonschema.Schema) + require.True(t, ok, "InputSchema should be a *jsonschema.Schema") + assert.Contains(t, schema.Properties, "owner_type") + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "project_number") + assert.Contains(t, schema.Properties, "query") + assert.Contains(t, schema.Properties, "per_page") + assert.Contains(t, schema.Properties, "fields") + assert.ElementsMatch(t, schema.Required, []string{"owner_type", "owner", "project_number"}) orgItems := []map[string]any{ {"id": 301, "content_type": "Issue", "project_node_id": "PR_1", "fields": []map[string]any{ @@ -779,7 +788,7 @@ func Test_ListProjectItems(t *testing.T) { client := gh.NewClient(tc.mockedClient) _, handler := ListProjectItems(stubGetClientFn(client), translations.NullTranslationHelper) request := createMCPRequest(tc.requestArgs) - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) require.NoError(t, err) if tc.expectError { @@ -821,12 +830,14 @@ func Test_GetProjectItem(t *testing.T) { assert.Equal(t, "get_project_item", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "owner_type") - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "project_number") - assert.Contains(t, tool.InputSchema.Properties, "item_id") - assert.Contains(t, tool.InputSchema.Properties, "fields") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner_type", "owner", "project_number", "item_id"}) + schema, ok := tool.InputSchema.(*jsonschema.Schema) + require.True(t, ok, "InputSchema should be a *jsonschema.Schema") + assert.Contains(t, schema.Properties, "owner_type") + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "project_number") + assert.Contains(t, schema.Properties, "item_id") + assert.Contains(t, schema.Properties, "fields") + assert.ElementsMatch(t, schema.Required, []string{"owner_type", "owner", "project_number", "item_id"}) orgItem := map[string]any{ "id": 301, @@ -971,7 +982,7 @@ func Test_GetProjectItem(t *testing.T) { client := gh.NewClient(tc.mockedClient) _, handler := GetProjectItem(stubGetClientFn(client), translations.NullTranslationHelper) request := createMCPRequest(tc.requestArgs) - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) require.NoError(t, err) if tc.expectError { @@ -1014,12 +1025,14 @@ func Test_AddProjectItem(t *testing.T) { assert.Equal(t, "add_project_item", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "owner_type") - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "project_number") - assert.Contains(t, tool.InputSchema.Properties, "item_type") - assert.Contains(t, tool.InputSchema.Properties, "item_id") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner_type", "owner", "project_number", "item_type", "item_id"}) + schema, ok := tool.InputSchema.(*jsonschema.Schema) + require.True(t, ok, "InputSchema should be a *jsonschema.Schema") + assert.Contains(t, schema.Properties, "owner_type") + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "project_number") + assert.Contains(t, schema.Properties, "item_type") + assert.Contains(t, schema.Properties, "item_id") + assert.ElementsMatch(t, schema.Required, []string{"owner_type", "owner", "project_number", "item_type", "item_id"}) orgItem := map[string]any{ "id": 601, @@ -1196,7 +1209,7 @@ func Test_AddProjectItem(t *testing.T) { _, handler := AddProjectItem(stubGetClientFn(client), translations.NullTranslationHelper) request := createMCPRequest(tc.requestArgs) - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) require.NoError(t, err) if tc.expectError { @@ -1248,12 +1261,14 @@ func Test_UpdateProjectItem(t *testing.T) { assert.Equal(t, "update_project_item", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "owner_type") - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "project_number") - assert.Contains(t, tool.InputSchema.Properties, "item_id") - assert.Contains(t, tool.InputSchema.Properties, "updated_field") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner_type", "owner", "project_number", "item_id", "updated_field"}) + schema, ok := tool.InputSchema.(*jsonschema.Schema) + require.True(t, ok, "InputSchema should be a *jsonschema.Schema") + assert.Contains(t, schema.Properties, "owner_type") + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "project_number") + assert.Contains(t, schema.Properties, "item_id") + assert.Contains(t, schema.Properties, "updated_field") + assert.ElementsMatch(t, schema.Required, []string{"owner_type", "owner", "project_number", "item_id", "updated_field"}) orgUpdatedItem := map[string]any{ "id": 801, @@ -1475,7 +1490,7 @@ func Test_UpdateProjectItem(t *testing.T) { client := gh.NewClient(tc.mockedClient) _, handler := UpdateProjectItem(stubGetClientFn(client), translations.NullTranslationHelper) request := createMCPRequest(tc.requestArgs) - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) require.NoError(t, err) if tc.expectError { @@ -1523,11 +1538,13 @@ func Test_DeleteProjectItem(t *testing.T) { assert.Equal(t, "delete_project_item", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "owner_type") - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "project_number") - assert.Contains(t, tool.InputSchema.Properties, "item_id") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner_type", "owner", "project_number", "item_id"}) + schema, ok := tool.InputSchema.(*jsonschema.Schema) + require.True(t, ok, "InputSchema should be a *jsonschema.Schema") + assert.Contains(t, schema.Properties, "owner_type") + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "project_number") + assert.Contains(t, schema.Properties, "item_id") + assert.ElementsMatch(t, schema.Required, []string{"owner_type", "owner", "project_number", "item_id"}) tests := []struct { name string @@ -1637,7 +1654,7 @@ func Test_DeleteProjectItem(t *testing.T) { client := gh.NewClient(tc.mockedClient) _, handler := DeleteProjectItem(stubGetClientFn(client), translations.NullTranslationHelper) request := createMCPRequest(tc.requestArgs) - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) require.NoError(t, err) if tc.expectError { diff --git a/pkg/github/tools.go b/pkg/github/tools.go index 9ef0987b8..b6dd5695a 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -324,20 +324,20 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG toolsets.NewServerTool(UpdateGist(getClient, t)), ) - // projects := toolsets.NewToolset(ToolsetMetadataProjects.ID, ToolsetMetadataProjects.Description). - // AddReadTools( - // toolsets.NewServerTool(ListProjects(getClient, t)), - // toolsets.NewServerTool(GetProject(getClient, t)), - // toolsets.NewServerTool(ListProjectFields(getClient, t)), - // toolsets.NewServerTool(GetProjectField(getClient, t)), - // toolsets.NewServerTool(ListProjectItems(getClient, t)), - // toolsets.NewServerTool(GetProjectItem(getClient, t)), - // ). - // AddWriteTools( - // toolsets.NewServerTool(AddProjectItem(getClient, t)), - // toolsets.NewServerTool(DeleteProjectItem(getClient, t)), - // toolsets.NewServerTool(UpdateProjectItem(getClient, t)), - // ) + projects := toolsets.NewToolset(ToolsetMetadataProjects.ID, ToolsetMetadataProjects.Description). + AddReadTools( + toolsets.NewServerTool(ListProjects(getClient, t)), + toolsets.NewServerTool(GetProject(getClient, t)), + toolsets.NewServerTool(ListProjectFields(getClient, t)), + toolsets.NewServerTool(GetProjectField(getClient, t)), + toolsets.NewServerTool(ListProjectItems(getClient, t)), + toolsets.NewServerTool(GetProjectItem(getClient, t)), + ). + AddWriteTools( + toolsets.NewServerTool(AddProjectItem(getClient, t)), + toolsets.NewServerTool(DeleteProjectItem(getClient, t)), + toolsets.NewServerTool(UpdateProjectItem(getClient, t)), + ) // stargazers := toolsets.NewToolset(ToolsetMetadataStargazers.ID, ToolsetMetadataStargazers.Description). // AddReadTools( // toolsets.NewServerTool(ListStarredRepositories(getClient, t)), @@ -375,7 +375,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG tsg.AddToolset(discussions) tsg.AddToolset(gists) tsg.AddToolset(securityAdvisories) - // tsg.AddToolset(projects) + tsg.AddToolset(projects) // tsg.AddToolset(stargazers) tsg.AddToolset(labels)