Skip to content

Commit c36e4e4

Browse files
authored
MCP: name-based resolution for Projects fields (#2760)
* Let agents address Project fields, single-select options, and item field values by name through the GitHub MCP server * Adding a method to resolve params resolveItemIDFromIssueArgs * Adding the DatabaseID to be able to match the graphQL * Changing the parsing to strconv.ParseInt * Changing description. * update readme * Fixing copilot comments * Adding Case-insensitive matching and fields + field_names guard * running snaps and readme
1 parent 439ec71 commit c36e4e4

9 files changed

Lines changed: 1209 additions & 40 deletions

File tree

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,7 +1069,8 @@ The following sets of tools are available:
10691069
- **Required OAuth Scopes**: `read:project`
10701070
- **Accepted OAuth Scopes**: `project`, `read:project`
10711071
- `field_id`: The field's ID. Required for 'get_project_field' method. (number, optional)
1072-
- `fields`: Specific list of field IDs to include in the response when getting a project item (e.g. ["102589", "985201", "169875"]). If not provided, only the title field is included. Only used for 'get_project_item' method. (string[], optional)
1072+
- `field_names`: Specific list of field names to include in the response when getting a project item (e.g. ["Status", "Priority"]). Resolved server-side to field IDs — pass this instead of 'fields' when you only know the human-readable names. Mutually exclusive with 'fields' — provide one, not both. Only used for 'get_project_item' method. (string[], optional)
1073+
- `fields`: Specific list of field IDs to include in the response when getting a project item (e.g. ["102589", "985201", "169875"]). If neither 'fields' nor 'field_names' is provided, only the title field is included. Mutually exclusive with 'field_names' — provide one, not both. Only used for 'get_project_item' method. (string[], optional)
10731074
- `item_id`: The item's ID. Required for 'get_project_item' method. (number, optional)
10741075
- `method`: The method to execute (string, required)
10751076
- `owner`: The owner (user or organization login). The name is not case sensitive. (string, optional)
@@ -1082,7 +1083,8 @@ The following sets of tools are available:
10821083
- **Accepted OAuth Scopes**: `project`, `read:project`
10831084
- `after`: Forward pagination cursor from previous pageInfo.nextCursor. (string, optional)
10841085
- `before`: Backward pagination cursor from previous pageInfo.prevCursor (rare). (string, optional)
1085-
- `fields`: Field IDs to include when listing project items (e.g. ["102589", "985201"]). CRITICAL: Always provide to get field values. Without this, only titles returned. Only used for 'list_project_items' method. (string[], optional)
1086+
- `field_names`: Field names to include when listing project items (e.g. ["Status", "Priority"]). Resolved server-side to field IDs — pass this instead of 'fields' when you only know the human-readable names. Names that fail to resolve return a structured error. Mutually exclusive with 'fields' — provide one, not both. Only used for 'list_project_items' method. (string[], optional)
1087+
- `fields`: Field IDs to include when listing project items (e.g. ["102589", "985201"]). CRITICAL: Always provide to get field values. Without this (and without 'field_names'), only titles returned. Mutually exclusive with 'field_names' — provide one, not both. Only used for 'list_project_items' method. (string[], optional)
10861088
- `method`: The action to perform (string, required)
10871089
- `owner`: The owner (user or organization login). The name is not case sensitive. (string, required)
10881090
- `owner_type`: Owner type (user or org). If not provided, will automatically try both. (string, optional)
@@ -1094,10 +1096,10 @@ The following sets of tools are available:
10941096
- **Required OAuth Scopes**: `project`
10951097
- `body`: The body of the status update (markdown). Used for 'create_project_status_update' method. (string, optional)
10961098
- `field_name`: The name of the iteration field (e.g. 'Sprint'). Required for 'create_iteration_field' method. (string, optional)
1097-
- `issue_number`: The issue number (use when item_type is 'issue' for 'add_project_item' method). Provide either issue_number or pull_request_number. (number, optional)
1098-
- `item_id`: The project item ID. Required for 'update_project_item' and 'delete_project_item' methods. (number, optional)
1099-
- `item_owner`: The owner (user or organization) of the repository containing the issue or pull request. Required for 'add_project_item' method. (string, optional)
1100-
- `item_repo`: The name of the repository containing the issue or pull request. Required for 'add_project_item' method. (string, optional)
1099+
- `issue_number`: The issue number. Required for 'add_project_item' when item_type is 'issue'. Also accepted by 'update_project_item' to resolve the item by issue number (combine with item_owner and item_repo). (number, optional)
1100+
- `item_id`: The project item ID. Required for 'delete_project_item'. For 'update_project_item', provide either item_id, or (item_owner + item_repo + issue_number) to resolve the item by issue. (number, optional)
1101+
- `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)
1102+
- `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)
11011103
- `item_type`: The item's type, either issue or pull_request. Required for 'add_project_item' method. (string, optional)
11021104
- `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)
11031105
- `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)
@@ -1110,7 +1112,7 @@ The following sets of tools are available:
11101112
- `status`: The status of the project. Used for 'create_project_status_update' method. (string, optional)
11111113
- `target_date`: The target date of the status update in YYYY-MM-DD format. Used for 'create_project_status_update' method. (string, optional)
11121114
- `title`: The project title. Required for 'create_project' method. (string, optional)
1113-
- `updated_field`: 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 for 'update_project_item' method. (object, optional)
1115+
- `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)
11141116

11151117
</details>
11161118

pkg/errors/error.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package errors
22

33
import (
44
"context"
5+
"encoding/json"
56
stderrors "errors"
67
"fmt"
78
"net/http"
@@ -218,3 +219,48 @@ func NewGitHubAPIStatusErrorResponse(ctx context.Context, message string, resp *
218219
err := fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body))
219220
return NewGitHubAPIErrorResponse(ctx, message, resp, err)
220221
}
222+
223+
// StructuredResolutionError is a machine-readable error returned by name-resolution
224+
// helpers (e.g. resolving a project field or single-select option by name). Agents
225+
// can parse the JSON body to self-correct without re-prompting.
226+
//
227+
// Kind values:
228+
// - "field_not_found" — no project field matches the supplied name
229+
// - "field_ambiguous" — more than one project field shares the supplied name
230+
// - "option_not_found" — no option on the resolved single-select field matches
231+
// - "option_ambiguous" — duplicate option names on the resolved field
232+
// - "item_not_in_project" — the issue/PR exists but is not an item on the project
233+
// - "wrong_field_type" — the named field is not the data type the caller expected
234+
type StructuredResolutionError struct {
235+
Kind string `json:"error"`
236+
Name string `json:"name,omitempty"`
237+
Field string `json:"field,omitempty"`
238+
Candidates []any `json:"candidates,omitempty"`
239+
Hint string `json:"hint,omitempty"`
240+
}
241+
242+
// Error implements the error interface; the message is the JSON body so that the
243+
// downstream tool result also carries the structured payload as plain text.
244+
func (e *StructuredResolutionError) Error() string {
245+
b, err := json.Marshal(e)
246+
if err != nil {
247+
return fmt.Sprintf(`{"error":%q,"name":%q}`, e.Kind, e.Name)
248+
}
249+
return string(b)
250+
}
251+
252+
// NewStructuredResolutionError constructs a StructuredResolutionError.
253+
func NewStructuredResolutionError(kind, name, hint string, candidates []any) *StructuredResolutionError {
254+
return &StructuredResolutionError{
255+
Kind: kind,
256+
Name: name,
257+
Hint: hint,
258+
Candidates: candidates,
259+
}
260+
}
261+
262+
// NewStructuredResolutionErrorResponse returns an mcp.CallToolResult whose text body
263+
// is the JSON-serialised StructuredResolutionError, suitable for agent self-correction.
264+
func NewStructuredResolutionErrorResponse(err *StructuredResolutionError) *mcp.CallToolResult {
265+
return utils.NewToolResultError(err.Error())
266+
}

pkg/github/__toolsnaps__/projects_get.snap

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,15 @@
1111
"description": "The field's ID. Required for 'get_project_field' method.",
1212
"type": "number"
1313
},
14+
"field_names": {
15+
"description": "Specific list of field names to include in the response when getting a project item (e.g. [\"Status\", \"Priority\"]). Resolved server-side to field IDs — pass this instead of 'fields' when you only know the human-readable names. Mutually exclusive with 'fields' — provide one, not both. Only used for 'get_project_item' method.",
16+
"items": {
17+
"type": "string"
18+
},
19+
"type": "array"
20+
},
1421
"fields": {
15-
"description": "Specific list of field IDs to include in the response when getting a project item (e.g. [\"102589\", \"985201\", \"169875\"]). If not provided, only the title field is included. Only used for 'get_project_item' method.",
22+
"description": "Specific list of field IDs to include in the response when getting a project item (e.g. [\"102589\", \"985201\", \"169875\"]). If neither 'fields' nor 'field_names' is provided, only the title field is included. Mutually exclusive with 'field_names' — provide one, not both. Only used for 'get_project_item' method.",
1623
"items": {
1724
"type": "string"
1825
},

pkg/github/__toolsnaps__/projects_list.snap

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,15 @@
1515
"description": "Backward pagination cursor from previous pageInfo.prevCursor (rare).",
1616
"type": "string"
1717
},
18+
"field_names": {
19+
"description": "Field names to include when listing project items (e.g. [\"Status\", \"Priority\"]). Resolved server-side to field IDs — pass this instead of 'fields' when you only know the human-readable names. Names that fail to resolve return a structured error. Mutually exclusive with 'fields' — provide one, not both. Only used for 'list_project_items' method.",
20+
"items": {
21+
"type": "string"
22+
},
23+
"type": "array"
24+
},
1825
"fields": {
19-
"description": "Field IDs to include when listing project items (e.g. [\"102589\", \"985201\"]). CRITICAL: Always provide to get field values. Without this, only titles returned. Only used for 'list_project_items' method.",
26+
"description": "Field IDs to include when listing project items (e.g. [\"102589\", \"985201\"]). CRITICAL: Always provide to get field values. Without this (and without 'field_names'), only titles returned. Mutually exclusive with 'field_names' — provide one, not both. Only used for 'list_project_items' method.",
2027
"items": {
2128
"type": "string"
2229
},

pkg/github/__toolsnaps__/projects_write.snap

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,19 @@
1717
"type": "string"
1818
},
1919
"issue_number": {
20-
"description": "The issue number (use when item_type is 'issue' for 'add_project_item' method). Provide either issue_number or pull_request_number.",
20+
"description": "The issue number. Required for 'add_project_item' when item_type is 'issue'. Also accepted by 'update_project_item' to resolve the item by issue number (combine with item_owner and item_repo).",
2121
"type": "number"
2222
},
2323
"item_id": {
24-
"description": "The project item ID. Required for 'update_project_item' and 'delete_project_item' methods.",
24+
"description": "The project item ID. Required for 'delete_project_item'. For 'update_project_item', provide either item_id, or (item_owner + item_repo + issue_number) to resolve the item by issue.",
2525
"type": "number"
2626
},
2727
"item_owner": {
28-
"description": "The owner (user or organization) of the repository containing the issue or pull request. Required for 'add_project_item' method.",
28+
"description": "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.",
2929
"type": "string"
3030
},
3131
"item_repo": {
32-
"description": "The name of the repository containing the issue or pull request. Required for 'add_project_item' method.",
32+
"description": "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.",
3333
"type": "string"
3434
},
3535
"item_type": {
@@ -127,7 +127,7 @@
127127
"type": "string"
128128
},
129129
"updated_field": {
130-
"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 for 'update_project_item' method.",
130+
"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.",
131131
"type": "object"
132132
}
133133
},

0 commit comments

Comments
 (0)