diff --git a/docs/tools.md b/docs/tools.md index ed699ee..a98ab02 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -166,6 +166,8 @@ multipart clients. PATCH `/projects/{project_id}/issues/{issue_id}/issue-attachments/{asset_id}/`. Required: `project_id`, `issue_id`, `asset_id`. +Returns `{ "completed": true, "asset_id": "..." }`. When Plane returns a +non-empty response body, it is available under `result`. ### `plane_attachment_upload` diff --git a/internal/mcpserver/server_test.go b/internal/mcpserver/server_test.go index 4eb5e59..ee15358 100644 --- a/internal/mcpserver/server_test.go +++ b/internal/mcpserver/server_test.go @@ -492,6 +492,42 @@ func TestAttachmentInitUploadRawReturnsFullPayload(t *testing.T) { require.Equal(t, "secret-policy", fields["policy"]) } +func TestAttachmentCompleteUploadReturnsStructuredSuccessForNoContent(t *testing.T) { + srv := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPatch, r.Method) + require.Equal(t, "/api/v1/workspaces/ws/projects/P/issues/I/issue-attachments/asset-1/", r.URL.Path) + w.WriteHeader(http.StatusNoContent) + }) + + payload := callToolPayload(t, srv, "plane_attachment_complete_upload", map[string]any{ + "project_id": "P", + "issue_id": "I", + "asset_id": "asset-1", + }) + require.Equal(t, true, payload["completed"]) + require.Equal(t, "asset-1", payload["asset_id"]) + require.NotContains(t, payload, "result") +} + +func TestAttachmentCompleteUploadPreservesPlaneResult(t *testing.T) { + srv := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPatch, r.Method) + require.Equal(t, "/api/v1/workspaces/ws/projects/P/issues/I/issue-attachments/asset-1/", r.URL.Path) + _, _ = w.Write([]byte(`{"id":"asset-1","status":"uploaded"}`)) + }) + + payload := callToolPayload(t, srv, "plane_attachment_complete_upload", map[string]any{ + "project_id": "P", + "issue_id": "I", + "asset_id": "asset-1", + }) + require.Equal(t, true, payload["completed"]) + require.Equal(t, "asset-1", payload["asset_id"]) + result, ok := payload["result"].(map[string]any) + require.True(t, ok) + require.Equal(t, "uploaded", result["status"]) +} + func TestAttachmentMimeTypeStripsParameters(t *testing.T) { require.Equal(t, "text/plain", attachmentMimeType("notes.txt")) require.Equal(t, "application/octet-stream", attachmentMimeType("archive.unknownext")) diff --git a/internal/mcpserver/tools_attachments.go b/internal/mcpserver/tools_attachments.go index b812e70..990cf1e 100644 --- a/internal/mcpserver/tools_attachments.go +++ b/internal/mcpserver/tools_attachments.go @@ -114,7 +114,7 @@ func (s *Server) handleAttachmentCompleteUpload(ctx context.Context, req mcp.Cal if err != nil { return toolError(err), nil } - return asTextResult(out) + return asTextResult(completeAttachmentUploadResult(assetID, out)) } func (s *Server) handleAttachmentUpload(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -293,6 +293,17 @@ func compactAttachmentInitUpload(init map[string]any) map[string]any { return out } +func completeAttachmentUploadResult(assetID string, planeResult map[string]any) map[string]any { + out := map[string]any{ + "completed": true, + "asset_id": assetID, + } + if len(planeResult) > 0 { + out["result"] = planeResult + } + return out +} + func initUploadAssetID(init map[string]any) string { for _, key := range []string{"id", "asset_id"} { if value, ok := init[key].(string); ok && value != "" {