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
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1068,7 +1068,8 @@ The following sets of tools are available:
- **Required OAuth Scopes**: `read:project`
- **Accepted OAuth Scopes**: `project`, `read:project`
- `field_id`: The field's ID. Required for 'get_project_field' method. (number, optional)
- `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)
- `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. Only used for 'get_project_item' method. (string[], optional)
- `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. Only used for 'get_project_item' method. (string[], optional)
- `item_id`: The item's ID. Required for 'get_project_item' method. (number, optional)
- `method`: The method to execute (string, required)
- `owner`: The owner (user or organization login). The name is not case sensitive. (string, optional)
Expand All @@ -1081,7 +1082,8 @@ The following sets of tools are available:
- **Accepted OAuth Scopes**: `project`, `read:project`
- `after`: Forward pagination cursor from previous pageInfo.nextCursor. (string, optional)
- `before`: Backward pagination cursor from previous pageInfo.prevCursor (rare). (string, optional)
- `fields`: Field IDs to include 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)
- `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. Only used for 'list_project_items' method. (string[], optional)
- `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. Only used for 'list_project_items' method. (string[], optional)
- `method`: The action to perform (string, required)
- `owner`: The owner (user or organization login). The name is not case sensitive. (string, required)
- `owner_type`: Owner type (user or org). If not provided, will automatically try both. (string, optional)
Expand All @@ -1093,10 +1095,10 @@ The following sets of tools are available:
- **Required OAuth Scopes**: `project`
- `body`: The body of the status update (markdown). Used for 'create_project_status_update' method. (string, optional)
- `field_name`: The name of the iteration field (e.g. 'Sprint'). Required for 'create_iteration_field' method. (string, optional)
- `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)
- `item_id`: The project item ID. Required for 'update_project_item' and 'delete_project_item' methods. (number, optional)
- `item_owner`: The owner (user or organization) of the repository containing the issue or pull request. Required for 'add_project_item' method. (string, optional)
- `item_repo`: The name of the repository containing the issue or pull request. Required for 'add_project_item' method. (string, optional)
- `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)
- `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)
- `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)
- `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)
Expand All @@ -1109,7 +1111,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 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)
- `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)

</details>

Expand Down
46 changes: 46 additions & 0 deletions pkg/errors/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package errors

import (
"context"
"encoding/json"
stderrors "errors"
"fmt"
"net/http"
Expand Down Expand Up @@ -218,3 +219,48 @@ func NewGitHubAPIStatusErrorResponse(ctx context.Context, message string, resp *
err := fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body))
return NewGitHubAPIErrorResponse(ctx, message, resp, err)
}

// StructuredResolutionError is a machine-readable error returned by name-resolution
// helpers (e.g. resolving a project field or single-select option by name). Agents
// can parse the JSON body to self-correct without re-prompting.
//
// Kind values:
// - "field_not_found" — no project field matches the supplied name
// - "field_ambiguous" — more than one project field shares the supplied name
// - "option_not_found" — no option on the resolved single-select field matches
// - "option_ambiguous" — duplicate option names on the resolved field
// - "item_not_in_project" — the issue/PR exists but is not an item on the project
// - "wrong_field_type" — the named field is not the data type the caller expected
type StructuredResolutionError struct {
Kind string `json:"error"`
Name string `json:"name,omitempty"`
Field string `json:"field,omitempty"`
Candidates []any `json:"candidates,omitempty"`
Hint string `json:"hint,omitempty"`
}

// Error implements the error interface; the message is the JSON body so that the
// downstream tool result also carries the structured payload as plain text.
func (e *StructuredResolutionError) Error() string {
b, err := json.Marshal(e)
if err != nil {
return fmt.Sprintf(`{"error":%q,"name":%q}`, e.Kind, e.Name)
}
return string(b)
}

// NewStructuredResolutionError constructs a StructuredResolutionError.
func NewStructuredResolutionError(kind, name, hint string, candidates []any) *StructuredResolutionError {
return &StructuredResolutionError{
Kind: kind,
Name: name,
Hint: hint,
Candidates: candidates,
}
}

// NewStructuredResolutionErrorResponse returns an mcp.CallToolResult whose text body
// is the JSON-serialised StructuredResolutionError, suitable for agent self-correction.
func NewStructuredResolutionErrorResponse(err *StructuredResolutionError) *mcp.CallToolResult {
return utils.NewToolResultError(err.Error())
}
9 changes: 8 additions & 1 deletion pkg/github/__toolsnaps__/projects_get.snap
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,15 @@
"description": "The field's ID. Required for 'get_project_field' method.",
"type": "number"
},
"field_names": {
"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. Only used for 'get_project_item' method.",
"items": {
"type": "string"
},
"type": "array"
},
"fields": {
"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.",
"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. Only used for 'get_project_item' method.",
Comment on lines +13 to +21

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fields and field_names both resolve to field IDs, and the "use one or the other" rule only lives in the description text. This same pair shows up four times in the PR — the get_project_item and list_project_items schema blocks in projects.go, plus both .snap fixtures — so whatever we decide needs to apply the same way in all of them (README too).

Right now nothing stops a caller from sending both, and if they do, the handler quietly appends the resolved names onto fields (fields = append(fields, resolvedIDs...)) — which none of the four descriptions mention.

Two ways to go:

  1. Put the constraint in the schema. It sits next to properties, at the inputSchema level (not inside a property), roughly:

    "inputSchema": {
      "properties": {
        "fields":      { "type": "array", "items": { "type": "string" }, "description": "..." },
        "field_names": { "type": "array", "items": { "type": "string" }, "description": "..." }
      },
      "oneOf": [
        { "not": { "required": ["fields", "field_names"] } },
        { "required": ["fields"],      "not": { "required": ["field_names"] } },
        { "required": ["field_names"], "not": { "required": ["fields"] } }
      ],
      "required": ["method"],
      "type": "object"
    }

    Two catches, and they hit every copy: neither field is required, so you need that first "neither" branch just to keep the common case legal; and these props are shared across methods on each tool (get_project_field/get_project_item, and the list methods), so a top-level oneOf would wrongly constrain the others. Doing it right means gating per method (if method == get_project_item ...) in all four spots — a lot of schema for what it buys.

  2. Leave it in prose but enforce it once in the handler — reject when both are set with a structured error, exactly like updated_field already does for {id}+{name}. One check covers both read methods, and we just make the descriptions symmetric (field_names currently says nothing about the both-set case).

I'd go with 2 — the write path already draws this line in code, so matching it on the read path keeps things consistent, avoids repeating awkward schema in four places, and the fix lives in one spot instead of four. Open to 1 if we think the schema-level guarantee is worth the per-method branching.

"items": {
"type": "string"
},
Expand Down
9 changes: 8 additions & 1 deletion pkg/github/__toolsnaps__/projects_list.snap
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,15 @@
"description": "Backward pagination cursor from previous pageInfo.prevCursor (rare).",
"type": "string"
},
"field_names": {
"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. Only used for 'list_project_items' method.",
"items": {
"type": "string"
},
"type": "array"
},
"fields": {
"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.",
"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. Only used for 'list_project_items' method.",
"items": {
"type": "string"
},
Expand Down
10 changes: 5 additions & 5 deletions pkg/github/__toolsnaps__/projects_write.snap
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@
"type": "string"
},
"issue_number": {
"description": "The issue number (use when item_type is 'issue' for 'add_project_item' method). Provide either issue_number or pull_request_number.",
"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).",
"type": "number"
},
"item_id": {
"description": "The project item ID. Required for 'update_project_item' and 'delete_project_item' methods.",
"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.",
"type": "number"
},
"item_owner": {
"description": "The owner (user or organization) of the repository containing the issue or pull request. Required for 'add_project_item' method.",
"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.",
"type": "string"
},
"item_repo": {
"description": "The name of the repository containing the issue or pull request. Required for 'add_project_item' method.",
"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.",
"type": "string"
},
"item_type": {
Expand Down Expand Up @@ -125,7 +125,7 @@
"type": "string"
},
"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\"}. Required for 'update_project_item' method.",
"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.",
"type": "object"
}
},
Expand Down
Loading
Loading