Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1121,6 +1121,7 @@ The following sets of tools are available:
- `item_owner`: The owner (user or organization) of the repository containing the issue or pull request. Required for 'add_project_item' method. Also accepted by 'update_project_item' when resolving the item by issue number. (string, optional)
- `item_repo`: The name of the repository containing the issue or pull request. Required for 'add_project_item' method. Also accepted by 'update_project_item' when resolving the item by issue number. (string, optional)
- `item_type`: The item's type, either issue or pull_request. Required for 'add_project_item' method. (string, optional)
- `items`: The items to update with the top-level 'updated_field'. Required for 'update_project_items'; prefer it over calling 'update_project_item' in a loop. Each entry must match exactly one reference variant: 'node_id', numeric 'item_id', or 'item_owner' + 'item_repo' + 'issue_number'. Limit: 50 items per call. (object[], optional)
- `iteration_duration`: Duration in days for iterations of the field (e.g. 7 for weekly, 14 for bi-weekly). Required for 'create_iteration_field' method. (number, optional)
- `iterations`: Custom iterations for 'create_iteration_field' method. Only set this when you need iterations with varying durations, breaks between them, or specific titles. Otherwise omit it: GitHub auto-creates three iterations of 'iteration_duration' days starting on 'start_date', which is the right choice for most cases. (object[], optional)
- `method`: The method to execute (string, required)
Expand All @@ -1132,7 +1133,7 @@ The following sets of tools are available:
- `status`: The status of the project. Used for 'create_project_status_update' method. (string, optional)
- `target_date`: The target date of the status update in YYYY-MM-DD format. Used for 'create_project_status_update' method. (string, optional)
- `title`: The project title. Required for 'create_project' method. (string, optional)
- `updated_field`: Object describing the field to update and its new value. Required for 'update_project_item'. Two shapes are accepted: (1) by ID — {"id": 123456, "value": "..."}; (2) by name — {"name": "Status", "value": "In Progress"}. For single-select fields, option-name resolution requires the by-name shape; on the by-ID shape, pass the option ID. Set value to null to clear the field. (object, optional)
- `updated_field`: The field/value to apply, using {"id": 123, "value": ...} or {"name": "Status", "value": ...}; null clears the field. Required for 'update_project_item' and 'update_project_items', where one top-level field/value applies to every item in a batch. For 'update_project_item' SINGLE_SELECT fields, the name form accepts option names; the ID form expects an option ID. (object, optional)

</details>

Expand Down
99 changes: 97 additions & 2 deletions pkg/github/__toolsnaps__/projects_write.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"readOnlyHint": false,
"title": "Manage GitHub Projects"
},
"description": "Create and manage GitHub Projects: create projects, add/update/delete items, create status updates, and add iteration fields.",
"description": "Create and manage GitHub Projects: create projects, add/update/delete items, bulk-update many items at once, create status updates, and add iteration fields.",
"inputSchema": {
"properties": {
"body": {
Expand Down Expand Up @@ -40,6 +40,64 @@
],
"type": "string"
},
"items": {
"description": "The items to update with the top-level 'updated_field'. Required for 'update_project_items'; prefer it over calling 'update_project_item' in a loop. Each entry must match exactly one reference variant: 'node_id', numeric 'item_id', or 'item_owner' + 'item_repo' + 'issue_number'. Limit: 50 items per call.",
"items": {
"oneOf": [
{
"additionalProperties": false,
"properties": {
"node_id": {
"description": "The project item's GraphQL node ID, as returned by 'list_project_items' or 'add_project_item'.",
"type": "string"
}
},
"required": [
"node_id"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"item_id": {
"description": "The numeric project item ID.",
"type": "integer"
}
},
"required": [
"item_id"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"issue_number": {
"description": "Issue number used to resolve the project item.",
"type": "integer"
},
"item_owner": {
"description": "Owner of the repository containing the issue.",
"type": "string"
},
"item_repo": {
"description": "Repository containing the issue.",
"type": "string"
}
},
"required": [
"item_owner",
"item_repo",
"issue_number"
],
"type": "object"
}
],
"type": "object"
},
"type": "array"
},
"iteration_duration": {
"description": "Duration in days for iterations of the field (e.g. 7 for weekly, 14 for bi-weekly). Required for 'create_iteration_field' method.",
"type": "number"
Expand Down Expand Up @@ -76,6 +134,7 @@
"enum": [
"add_project_item",
"update_project_item",
"update_project_items",
"delete_project_item",
"create_project_status_update",
"create_project",
Expand Down Expand Up @@ -127,7 +186,43 @@
"type": "string"
},
"updated_field": {
"description": "Object describing the field to update and its new value. Required for 'update_project_item'. Two shapes are accepted: (1) by ID — {\"id\": 123456, \"value\": \"...\"}; (2) by name — {\"name\": \"Status\", \"value\": \"In Progress\"}. For single-select fields, option-name resolution requires the by-name shape; on the by-ID shape, pass the option ID. Set value to null to clear the field.",
"description": "The field/value to apply, using {\"id\": 123, \"value\": ...} or {\"name\": \"Status\", \"value\": ...}; null clears the field. Required for 'update_project_item' and 'update_project_items', where one top-level field/value applies to every item in a batch. For 'update_project_item' SINGLE_SELECT fields, the name form accepts option names; the ID form expects an option ID.",
"oneOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"description": "The numeric project field ID.",
"type": "integer"
},
"value": {
"description": "The value to apply. Any JSON value is accepted; use null to clear the field."
}
},
"required": [
"id",
"value"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"name": {
"description": "The project field name. Matching is case-insensitive.",
"type": "string"
},
"value": {
"description": "The value to apply. Any JSON value is accepted; use null to clear the field."
}
},
"required": [
"name",
"value"
],
"type": "object"
}
],
"type": "object"
}
},
Expand Down
92 changes: 88 additions & 4 deletions pkg/github/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const (
ProjectStatusUpdateCreateFailedError = "failed to create project status update"
ProjectResolveIDFailedError = "failed to resolve project ID"
MaxProjectsPerPage = 50
maxProjectItemsPerBatch = 50
)

// Method constants for consolidated project tools
Expand All @@ -44,6 +45,7 @@ const (
projectsMethodGetProjectItem = "get_project_item"
projectsMethodAddProjectItem = "add_project_item"
projectsMethodUpdateProjectItem = "update_project_item"
projectsMethodUpdateProjectItems = "update_project_items"
projectsMethodDeleteProjectItem = "delete_project_item"
projectsMethodListProjectStatusUpdates = "list_project_status_updates"
projectsMethodGetProjectStatusUpdate = "get_project_status_update"
Expand Down Expand Up @@ -490,13 +492,90 @@ Use this tool to get details about individual projects, project fields, and proj
return tool
}

func updateProjectItemsItemSchema() *jsonschema.Schema {
variant := func(required []string, properties map[string]*jsonschema.Schema) *jsonschema.Schema {
return &jsonschema.Schema{
Type: "object",
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
Properties: properties,
Required: required,
}
}

return &jsonschema.Schema{
Type: "object",
OneOf: []*jsonschema.Schema{
variant([]string{"node_id"}, map[string]*jsonschema.Schema{
"node_id": {
Type: "string",
Description: "The project item's GraphQL node ID, as returned by 'list_project_items' or 'add_project_item'.",
},
}),
variant([]string{"item_id"}, map[string]*jsonschema.Schema{
"item_id": {
Type: "integer",
Description: "The numeric project item ID.",
},
}),
variant([]string{"item_owner", "item_repo", "issue_number"}, map[string]*jsonschema.Schema{
"item_owner": {
Type: "string",
Description: "Owner of the repository containing the issue.",
},
"item_repo": {
Type: "string",
Description: "Repository containing the issue.",
},
"issue_number": {
Type: "integer",
Description: "Issue number used to resolve the project item.",
},
}),
},
}
}

func projectUpdatedFieldSchema() *jsonschema.Schema {
value := &jsonschema.Schema{
Description: "The value to apply. Any JSON value is accepted; use null to clear the field.",
}
variant := func(required []string, properties map[string]*jsonschema.Schema) *jsonschema.Schema {
properties["value"] = value
return &jsonschema.Schema{
Type: "object",
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
Properties: properties,
Required: required,
}
}

return &jsonschema.Schema{
Type: "object",
Description: "The field/value to apply, using {\"id\": 123, \"value\": ...} or {\"name\": \"Status\", \"value\": ...}; null clears the field. Required for 'update_project_item' and 'update_project_items', where one top-level field/value applies to every item in a batch. For 'update_project_item' SINGLE_SELECT fields, the name form accepts option names; the ID form expects an option ID.",
OneOf: []*jsonschema.Schema{
variant([]string{"id", "value"}, map[string]*jsonschema.Schema{
"id": {
Type: "integer",
Description: "The numeric project field ID.",
},
}),
variant([]string{"name", "value"}, map[string]*jsonschema.Schema{
"name": {
Type: "string",
Description: "The project field name. Matching is case-insensitive.",
},
}),
},
}
}

// ProjectsWrite returns the tool and handler for modifying GitHub Projects resources.
func ProjectsWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
tool := NewTool(
ToolsetMetadataProjects,
mcp.Tool{
Name: "projects_write",
Description: t("TOOL_PROJECTS_WRITE_DESCRIPTION", "Create and manage GitHub Projects: create projects, add/update/delete items, create status updates, and add iteration fields."),
Description: t("TOOL_PROJECTS_WRITE_DESCRIPTION", "Create and manage GitHub Projects: create projects, add/update/delete items, bulk-update many items at once, create status updates, and add iteration fields."),
Annotations: &mcp.ToolAnnotations{
Title: t("TOOL_PROJECTS_WRITE_USER_TITLE", "Manage GitHub Projects"),
ReadOnlyHint: false,
Expand All @@ -511,6 +590,7 @@ func ProjectsWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
Enum: []any{
projectsMethodAddProjectItem,
projectsMethodUpdateProjectItem,
projectsMethodUpdateProjectItems,
projectsMethodDeleteProjectItem,
projectsMethodCreateProjectStatusUpdate,
projectsMethodCreateProject,
Expand Down Expand Up @@ -559,9 +639,11 @@ func ProjectsWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
Type: "number",
Description: "The pull request number (use when item_type is 'pull_request' for 'add_project_item' method). Provide either issue_number or pull_request_number.",
},
"updated_field": {
Type: "object",
Description: "Object describing the field to update and its new value. Required for 'update_project_item'. Two shapes are accepted: (1) by ID — {\"id\": 123456, \"value\": \"...\"}; (2) by name — {\"name\": \"Status\", \"value\": \"In Progress\"}. For single-select fields, option-name resolution requires the by-name shape; on the by-ID shape, pass the option ID. Set value to null to clear the field.",
"updated_field": projectUpdatedFieldSchema(),
"items": {
Type: "array",
Description: "The items to update with the top-level 'updated_field'. Required for 'update_project_items'; prefer it over calling 'update_project_item' in a loop. Each entry must match exactly one reference variant: 'node_id', numeric 'item_id', or 'item_owner' + 'item_repo' + 'issue_number'. Limit: " + strconv.Itoa(maxProjectItemsPerBatch) + " items per call.",
Items: updateProjectItemsItemSchema(),
},
"body": {
Type: "string",
Expand Down Expand Up @@ -722,6 +804,8 @@ func ProjectsWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
return utils.NewToolResultError("updated_field must be an object"), nil, nil
}
return updateProjectItem(ctx, client, gqlClient, owner, ownerType, projectNumber, itemID, fieldValue)
case projectsMethodUpdateProjectItems:
return updateProjectItemsBatch(ctx, client, gqlClient, owner, ownerType, projectNumber, args)
case projectsMethodDeleteProjectItem:
itemID, err := RequiredBigInt(args, "item_id")
if err != nil {
Expand Down
Loading
Loading