Skip to content

Commit fbd0cd0

Browse files
committed
Remove update_project_item tool
1 parent 6793b9d commit fbd0cd0

File tree

5 files changed

+1
-367
lines changed

5 files changed

+1
-367
lines changed

README.md

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -707,13 +707,6 @@ The following sets of tools are available (all are on by default):
707707
- `per_page`: Number of results per page (max 100, default: 30) (number, optional)
708708
- `query`: Filter projects by a search query (matches title and description) (string, optional)
709709

710-
- **update_project_item** - Update project item
711-
- `fields`: A list of field updates to apply. (array, required)
712-
- `item_id`: The numeric ID of the project item to update (not the issue or pull request ID). (number, required)
713-
- `owner`: If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive. (string, required)
714-
- `owner_type`: Owner type (string, required)
715-
- `project_number`: The project's number. (number, required)
716-
717710
</details>
718711

719712
<details>

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ require (
3131
github.com/fsnotify/fsnotify v1.8.0 // indirect
3232
github.com/go-viper/mapstructure/v2 v2.4.0
3333
github.com/google/go-github/v71 v71.0.0 // indirect
34-
github.com/google/go-querystring v1.1.0 // indirect
34+
github.com/google/go-querystring v1.1.0
3535
github.com/google/uuid v1.6.0 // indirect
3636
github.com/gorilla/mux v1.8.0 // indirect
3737
github.com/inconshreveable/mousetrap v1.1.0 // indirect

pkg/github/projects.go

Lines changed: 0 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -621,129 +621,6 @@ func DeleteProjectItem(getClient GetClientFn, t translations.TranslationHelperFu
621621
}
622622
}
623623

624-
func UpdateProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
625-
return mcp.NewTool("update_project_item",
626-
mcp.WithDescription(t("TOOL_UPDATE_PROJECT_ITEM_DESCRIPTION", "Update a specific Project item for a user or org")),
627-
mcp.WithToolAnnotation(mcp.ToolAnnotation{Title: t("TOOL_UPDATE_PROJECT_ITEM_USER_TITLE", "Update project item"), ReadOnlyHint: ToBoolPtr(false)}),
628-
mcp.WithString("owner_type", mcp.Required(), mcp.Description("Owner type"), mcp.Enum("user", "org")),
629-
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.")),
630-
mcp.WithNumber("project_number", mcp.Required(), mcp.Description("The project's number.")),
631-
mcp.WithNumber("item_id", mcp.Required(), mcp.Description("The numeric ID of the project item to update (not the issue or pull request ID).")),
632-
mcp.WithArray("fields", mcp.Required(), mcp.Description("A list of field updates to apply.")),
633-
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
634-
owner, err := RequiredParam[string](req, "owner")
635-
if err != nil {
636-
return mcp.NewToolResultError(err.Error()), nil
637-
}
638-
ownerType, err := RequiredParam[string](req, "owner_type")
639-
if err != nil {
640-
return mcp.NewToolResultError(err.Error()), nil
641-
}
642-
projectNumber, err := RequiredInt(req, "project_number")
643-
if err != nil {
644-
return mcp.NewToolResultError(err.Error()), nil
645-
}
646-
itemID, err := RequiredInt(req, "item_id")
647-
if err != nil {
648-
return mcp.NewToolResultError(err.Error()), nil
649-
}
650-
client, err := getClient(ctx)
651-
if err != nil {
652-
return mcp.NewToolResultError(err.Error()), nil
653-
}
654-
fieldsParam, ok := req.GetArguments()["fields"]
655-
if !ok {
656-
return mcp.NewToolResultError("missing required parameter: fields"), nil
657-
}
658-
659-
rawFields, ok := fieldsParam.([]any)
660-
if !ok {
661-
return mcp.NewToolResultError("parameter fields must be an array of objects"), nil
662-
}
663-
if len(rawFields) == 0 {
664-
return mcp.NewToolResultError("fields must contain at least one field update"), nil
665-
}
666-
667-
var projectsURL string
668-
if ownerType == "org" {
669-
projectsURL = fmt.Sprintf("orgs/%s/projectsV2/%d/items/%d", owner, projectNumber, itemID)
670-
} else {
671-
projectsURL = fmt.Sprintf("users/%s/projectsV2/%d/items/%d", owner, projectNumber, itemID)
672-
}
673-
674-
updateFields := make([]*newProjectV2Field, 0, len(rawFields))
675-
for idx, rawField := range rawFields {
676-
fieldMap, ok := rawField.(map[string]any)
677-
if !ok {
678-
return mcp.NewToolResultError(fmt.Sprintf("fields[%d] must be an object", idx)), nil
679-
}
680-
681-
rawID, ok := fieldMap["id"]
682-
if !ok {
683-
return mcp.NewToolResultError(fmt.Sprintf("fields[%d] is missing 'id'", idx)), nil
684-
}
685-
686-
var fieldID int64
687-
switch v := rawID.(type) {
688-
case float64:
689-
fieldID = int64(v)
690-
case int64:
691-
fieldID = v
692-
case json.Number:
693-
n, convErr := v.Int64()
694-
if convErr != nil {
695-
return mcp.NewToolResultError(fmt.Sprintf("fields[%d].id must be a numeric value", idx)), nil
696-
}
697-
fieldID = n
698-
default:
699-
return mcp.NewToolResultError(fmt.Sprintf("fields[%d].id must be a numeric value", idx)), nil
700-
}
701-
702-
value, ok := fieldMap["value"]
703-
if !ok {
704-
return mcp.NewToolResultError(fmt.Sprintf("fields[%d] is missing 'value'", idx)), nil
705-
}
706-
707-
updateFields = append(updateFields, &newProjectV2Field{
708-
ID: github.Ptr(fieldID),
709-
Value: value,
710-
})
711-
}
712-
713-
updateProjectItemOptions := &updateProjectItemOptions{Fields: updateFields}
714-
715-
httpRequest, err := client.NewRequest("PATCH", projectsURL, updateProjectItemOptions)
716-
if err != nil {
717-
return nil, fmt.Errorf("failed to create request: %w", err)
718-
}
719-
720-
updatedItem := projectV2Item{}
721-
resp, err := client.Do(ctx, httpRequest, &updatedItem)
722-
if err != nil {
723-
return ghErrors.NewGitHubAPIErrorResponse(ctx,
724-
"failed to update a project item",
725-
resp,
726-
err,
727-
), nil
728-
}
729-
defer func() { _ = resp.Body.Close() }()
730-
731-
if resp.StatusCode != http.StatusOK {
732-
body, err := io.ReadAll(resp.Body)
733-
if err != nil {
734-
return nil, fmt.Errorf("failed to read response body: %w", err)
735-
}
736-
return mcp.NewToolResultError(fmt.Sprintf("failed to update a project item: %s", string(body))), nil
737-
}
738-
r, err := json.Marshal(convertToMinimalProjectItem(&updatedItem))
739-
if err != nil {
740-
return nil, fmt.Errorf("failed to marshal response: %w", err)
741-
}
742-
743-
return mcp.NewToolResultText(string(r)), nil
744-
}
745-
}
746-
747624
type updateProjectItemOptions struct {
748625
Fields []*newProjectV2Field `json:"fields,omitempty"`
749626
}

pkg/github/projects_test.go

Lines changed: 0 additions & 235 deletions
Original file line numberDiff line numberDiff line change
@@ -1311,238 +1311,3 @@ func Test_DeleteProjectItem(t *testing.T) {
13111311
})
13121312
}
13131313
}
1314-
1315-
func Test_UpdateProjectItem(t *testing.T) {
1316-
mockClient := gh.NewClient(nil)
1317-
tool, _ := UpdateProjectItem(stubGetClientFn(mockClient), translations.NullTranslationHelper)
1318-
require.NoError(t, toolsnaps.Test(tool.Name, tool))
1319-
1320-
assert.Equal(t, "update_project_item", tool.Name)
1321-
assert.NotEmpty(t, tool.Description)
1322-
assert.Contains(t, tool.InputSchema.Properties, "owner_type")
1323-
assert.Contains(t, tool.InputSchema.Properties, "owner")
1324-
assert.Contains(t, tool.InputSchema.Properties, "project_number")
1325-
assert.Contains(t, tool.InputSchema.Properties, "item_id")
1326-
assert.Contains(t, tool.InputSchema.Properties, "fields")
1327-
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner_type", "owner", "project_number", "item_id", "fields"})
1328-
1329-
orgUpdated := map[string]any{
1330-
"id": 801,
1331-
"content_type": "Issue",
1332-
"creator": map[string]any{"login": "octocat"},
1333-
}
1334-
userUpdated := map[string]any{
1335-
"id": 901,
1336-
"content_type": "PullRequest",
1337-
"creator": map[string]any{"login": "hubot"},
1338-
}
1339-
1340-
tests := []struct {
1341-
name string
1342-
mockedClient *http.Client
1343-
requestArgs map[string]any
1344-
expectError bool
1345-
expectedErrMsg string
1346-
expectedID int
1347-
expectedCreatorLogin string
1348-
}{
1349-
{
1350-
name: "success organization update",
1351-
mockedClient: mock.NewMockedHTTPClient(
1352-
mock.WithRequestMatchHandler(
1353-
mock.EndpointPattern{Pattern: "/orgs/{org}/projectsV2/{project}/items/{item_id}", Method: http.MethodPatch},
1354-
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1355-
body, err := io.ReadAll(r.Body)
1356-
assert.NoError(t, err)
1357-
var payload struct {
1358-
Fields []struct {
1359-
ID int `json:"id"`
1360-
Value interface{} `json:"value"`
1361-
} `json:"fields"`
1362-
}
1363-
assert.NoError(t, json.Unmarshal(body, &payload))
1364-
assert.Len(t, payload.Fields, 1)
1365-
if len(payload.Fields) == 1 {
1366-
assert.Equal(t, 123, payload.Fields[0].ID)
1367-
assert.Equal(t, "In Progress", payload.Fields[0].Value)
1368-
}
1369-
w.WriteHeader(http.StatusOK)
1370-
_, _ = w.Write(mock.MustMarshal(orgUpdated))
1371-
}),
1372-
),
1373-
),
1374-
requestArgs: map[string]any{
1375-
"owner": "octo-org",
1376-
"owner_type": "org",
1377-
"project_number": float64(111),
1378-
"item_id": float64(2222),
1379-
"fields": []any{
1380-
map[string]any{"id": float64(123), "value": "In Progress"},
1381-
},
1382-
},
1383-
expectedID: 801,
1384-
expectedCreatorLogin: "octocat",
1385-
},
1386-
{
1387-
name: "success user update",
1388-
mockedClient: mock.NewMockedHTTPClient(
1389-
mock.WithRequestMatchHandler(
1390-
mock.EndpointPattern{Pattern: "/users/{user}/projectsV2/{project}/items/{item_id}", Method: http.MethodPatch},
1391-
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1392-
body, err := io.ReadAll(r.Body)
1393-
assert.NoError(t, err)
1394-
var payload map[string]any
1395-
assert.NoError(t, json.Unmarshal(body, &payload))
1396-
fields, ok := payload["fields"].([]any)
1397-
assert.True(t, ok)
1398-
assert.Len(t, fields, 1)
1399-
w.WriteHeader(http.StatusOK)
1400-
_, _ = w.Write(mock.MustMarshal(userUpdated))
1401-
}),
1402-
),
1403-
),
1404-
requestArgs: map[string]any{
1405-
"owner": "octocat",
1406-
"owner_type": "user",
1407-
"project_number": float64(222),
1408-
"item_id": float64(3333),
1409-
"fields": []any{
1410-
map[string]any{"id": float64(456), "value": 42},
1411-
},
1412-
},
1413-
expectedID: 901,
1414-
expectedCreatorLogin: "hubot",
1415-
},
1416-
{
1417-
name: "api error",
1418-
mockedClient: mock.NewMockedHTTPClient(
1419-
mock.WithRequestMatchHandler(
1420-
mock.EndpointPattern{Pattern: "/orgs/{org}/projectsV2/{project}/items/{item_id}", Method: http.MethodPatch},
1421-
mockResponse(t, http.StatusInternalServerError, map[string]string{"message": "boom"}),
1422-
),
1423-
),
1424-
requestArgs: map[string]any{
1425-
"owner": "octo-org",
1426-
"owner_type": "org",
1427-
"project_number": float64(333),
1428-
"item_id": float64(4444),
1429-
"fields": []any{
1430-
map[string]any{"id": float64(789), "value": "Done"},
1431-
},
1432-
},
1433-
expectError: true,
1434-
expectedErrMsg: "failed to update a project item",
1435-
},
1436-
{
1437-
name: "missing owner",
1438-
mockedClient: mock.NewMockedHTTPClient(),
1439-
requestArgs: map[string]any{
1440-
"owner_type": "org",
1441-
"project_number": float64(1),
1442-
"item_id": float64(1),
1443-
"fields": []any{map[string]any{"id": float64(1), "value": "X"}},
1444-
},
1445-
expectError: true,
1446-
},
1447-
{
1448-
name: "missing owner_type",
1449-
mockedClient: mock.NewMockedHTTPClient(),
1450-
requestArgs: map[string]any{
1451-
"owner": "octo-org",
1452-
"project_number": float64(1),
1453-
"item_id": float64(1),
1454-
"fields": []any{map[string]any{"id": float64(1), "value": "X"}},
1455-
},
1456-
expectError: true,
1457-
},
1458-
{
1459-
name: "missing project_number",
1460-
mockedClient: mock.NewMockedHTTPClient(),
1461-
requestArgs: map[string]any{
1462-
"owner": "octo-org",
1463-
"owner_type": "org",
1464-
"item_id": float64(1),
1465-
"fields": []any{map[string]any{"id": float64(1), "value": "X"}},
1466-
},
1467-
expectError: true,
1468-
},
1469-
{
1470-
name: "missing item_id",
1471-
mockedClient: mock.NewMockedHTTPClient(),
1472-
requestArgs: map[string]any{
1473-
"owner": "octo-org",
1474-
"owner_type": "org",
1475-
"project_number": float64(1),
1476-
"fields": []any{map[string]any{"id": float64(1), "value": "X"}},
1477-
},
1478-
expectError: true,
1479-
},
1480-
{
1481-
name: "missing fields",
1482-
mockedClient: mock.NewMockedHTTPClient(),
1483-
requestArgs: map[string]any{
1484-
"owner": "octo-org",
1485-
"owner_type": "org",
1486-
"project_number": float64(1),
1487-
"item_id": float64(1),
1488-
},
1489-
expectError: true,
1490-
expectedErrMsg: "missing required parameter: fields",
1491-
},
1492-
{
1493-
name: "empty fields",
1494-
mockedClient: mock.NewMockedHTTPClient(),
1495-
requestArgs: map[string]any{
1496-
"owner": "octo-org",
1497-
"owner_type": "org",
1498-
"project_number": float64(1),
1499-
"item_id": float64(1),
1500-
"fields": []any{},
1501-
},
1502-
expectError: true,
1503-
expectedErrMsg: "fields must contain at least one field update",
1504-
},
1505-
}
1506-
1507-
for _, tc := range tests {
1508-
t.Run(tc.name, func(t *testing.T) {
1509-
client := gh.NewClient(tc.mockedClient)
1510-
_, handler := UpdateProjectItem(stubGetClientFn(client), translations.NullTranslationHelper)
1511-
request := createMCPRequest(tc.requestArgs)
1512-
result, err := handler(context.Background(), request)
1513-
1514-
require.NoError(t, err)
1515-
if tc.expectError {
1516-
require.True(t, result.IsError)
1517-
text := getTextResult(t, result).Text
1518-
if tc.expectedErrMsg != "" {
1519-
assert.Contains(t, text, tc.expectedErrMsg)
1520-
}
1521-
switch tc.name {
1522-
case "missing owner":
1523-
assert.Contains(t, text, "missing required parameter: owner")
1524-
case "missing owner_type":
1525-
assert.Contains(t, text, "missing required parameter: owner_type")
1526-
case "missing project_number":
1527-
assert.Contains(t, text, "missing required parameter: project_number")
1528-
case "missing item_id":
1529-
assert.Contains(t, text, "missing required parameter: item_id")
1530-
}
1531-
return
1532-
}
1533-
1534-
require.False(t, result.IsError)
1535-
textContent := getTextResult(t, result)
1536-
var item map[string]any
1537-
require.NoError(t, json.Unmarshal([]byte(textContent.Text), &item))
1538-
if tc.expectedID != 0 {
1539-
assert.Equal(t, float64(tc.expectedID), item["id"])
1540-
}
1541-
if tc.expectedCreatorLogin != "" {
1542-
creator, ok := item["creator"].(map[string]any)
1543-
require.True(t, ok)
1544-
assert.Equal(t, tc.expectedCreatorLogin, creator["login"])
1545-
}
1546-
})
1547-
}
1548-
}

pkg/github/tools.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,6 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
202202
AddWriteTools(
203203
toolsets.NewServerTool(AddProjectItem(getClient, t)),
204204
toolsets.NewServerTool(DeleteProjectItem(getClient, t)),
205-
toolsets.NewServerTool(UpdateProjectItem(getClient, t)),
206205
)
207206

208207
// Add toolsets to the group

0 commit comments

Comments
 (0)