Skip to content

Commit ba29245

Browse files
committed
fix: construct actual Docs API request objects instead of passing nil
All 15 docs tools were passing nil to BatchUpdate, causing every write operation to fail with "Must specify at least one request" against the real Google Docs API. The mock service accepted nil silently, masking the bug in tests. Each tool now constructs the proper *docs.Request objects: - Content: InsertText, ReplaceAllText, DeleteContentRange - Formatting: UpdateTextStyle, UpdateParagraphStyle, bullets - Structure: InsertTable, InsertPageBreak, InsertInlineImage, links - Sections: CreateHeader/Footer with optional content insertion - BatchUpdate: parses JSON into typed []*docs.Request Also fixes ReplaceText to return actual occurrence count from the API response, and BatchUpdate to return real reply count.
1 parent eb9a40a commit ba29245

File tree

3 files changed

+273
-26
lines changed

3 files changed

+273
-26
lines changed

internal/docs/docs_content_testable.go

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/aliwatters/gsuite-mcp/internal/common"
1010
"github.com/mark3labs/mcp-go/mcp"
11+
"google.golang.org/api/docs/v1"
1112
)
1213

1314
// docsEditURLFormat is the URL template for Google Docs edit links.
@@ -171,9 +172,13 @@ func TestableDocsAppendText(ctx context.Context, request mcp.CallToolRequest, de
171172
insertIndex = 1
172173
}
173174

174-
// For testable version, we don't need to actually construct requests
175-
// since we're using the mock service which tracks BatchUpdate calls
176-
_, err = srv.BatchUpdate(ctx, docID, nil)
175+
requests := []*docs.Request{{
176+
InsertText: &docs.InsertTextRequest{
177+
Text: text,
178+
Location: &docs.Location{Index: insertIndex},
179+
},
180+
}}
181+
_, err = srv.BatchUpdate(ctx, docID, requests)
177182
if err != nil {
178183
return mcp.NewToolResultError(fmt.Sprintf("Docs API error: %v", err)), nil
179184
}
@@ -216,7 +221,13 @@ func TestableDocsInsertText(ctx context.Context, request mcp.CallToolRequest, de
216221

217222
docID = common.ExtractGoogleResourceID(docID)
218223

219-
_, err := srv.BatchUpdate(ctx, docID, nil)
224+
requests := []*docs.Request{{
225+
InsertText: &docs.InsertTextRequest{
226+
Text: text,
227+
Location: &docs.Location{Index: index},
228+
},
229+
}}
230+
_, err := srv.BatchUpdate(ctx, docID, requests)
220231
if err != nil {
221232
return mcp.NewToolResultError(fmt.Sprintf("Docs API error: %v", err)), nil
222233
}
@@ -256,15 +267,29 @@ func TestableDocsReplaceText(ctx context.Context, request mcp.CallToolRequest, d
256267

257268
docID = common.ExtractGoogleResourceID(docID)
258269

259-
_, err := srv.BatchUpdate(ctx, docID, nil)
270+
replaceReq := &docs.ReplaceAllTextRequest{
271+
ContainsText: &docs.SubstringMatchCriteria{
272+
Text: findText,
273+
MatchCase: matchCase,
274+
},
275+
ReplaceText: replaceText,
276+
ForceSendFields: []string{"ReplaceText"},
277+
}
278+
requests := []*docs.Request{{ReplaceAllText: replaceReq}}
279+
resp, err := srv.BatchUpdate(ctx, docID, requests)
260280
if err != nil {
261281
return mcp.NewToolResultError(fmt.Sprintf("Docs API error: %v", err)), nil
262282
}
263283

284+
replacementsCount := int64(0)
285+
if resp != nil && resp.Replies != nil && len(resp.Replies) > 0 && resp.Replies[0].ReplaceAllText != nil {
286+
replacementsCount = resp.Replies[0].ReplaceAllText.OccurrencesChanged
287+
}
288+
264289
result := map[string]any{
265290
"success": true,
266291
"document_id": docID,
267-
"replacements_count": 1, // Mock assumes 1 replacement
292+
"replacements_count": replacementsCount,
268293
"match_case": matchCase,
269294
"message": fmt.Sprintf("Replaced occurrences of '%s' with '%s'", findText, replaceText),
270295
"url": fmt.Sprintf(docsEditURLFormat, docID),
@@ -292,7 +317,15 @@ func TestableDocsDeleteText(ctx context.Context, request mcp.CallToolRequest, de
292317

293318
docID = common.ExtractGoogleResourceID(docID)
294319

295-
_, err := srv.BatchUpdate(ctx, docID, nil)
320+
requests := []*docs.Request{{
321+
DeleteContentRange: &docs.DeleteContentRangeRequest{
322+
Range: &docs.Range{
323+
StartIndex: startIndex,
324+
EndIndex: endIndex,
325+
},
326+
},
327+
}}
328+
_, err := srv.BatchUpdate(ctx, docID, requests)
296329
if err != nil {
297330
return mcp.NewToolResultError(fmt.Sprintf("Docs API error: %v", err)), nil
298331
}
@@ -326,27 +359,32 @@ func TestableDocsBatchUpdate(ctx context.Context, request mcp.CallToolRequest, d
326359

327360
docID = common.ExtractGoogleResourceID(docID)
328361

329-
// Parse the JSON to validate it
330-
var requests []any
331-
if err := json.Unmarshal([]byte(requestsJSON), &requests); err != nil {
362+
// Parse the JSON into Docs API request objects
363+
var docsRequests []*docs.Request
364+
if err := json.Unmarshal([]byte(requestsJSON), &docsRequests); err != nil {
332365
return mcp.NewToolResultError(fmt.Sprintf("Failed to parse requests JSON: %v", err)), nil
333366
}
334367

335-
if len(requests) == 0 {
368+
if len(docsRequests) == 0 {
336369
return mcp.NewToolResultError("requests array cannot be empty"), nil
337370
}
338371

339-
_, err := srv.BatchUpdate(ctx, docID, nil)
372+
resp, err := srv.BatchUpdate(ctx, docID, docsRequests)
340373
if err != nil {
341374
return mcp.NewToolResultError(fmt.Sprintf("Docs API error: %v", err)), nil
342375
}
343376

377+
repliesCount := 0
378+
if resp != nil && resp.Replies != nil {
379+
repliesCount = len(resp.Replies)
380+
}
381+
344382
result := map[string]any{
345383
"success": true,
346384
"document_id": docID,
347-
"requests_count": len(requests),
348-
"replies_count": len(requests),
349-
"message": fmt.Sprintf("Successfully executed %d batch update request(s)", len(requests)),
385+
"requests_count": len(docsRequests),
386+
"replies_count": repliesCount,
387+
"message": fmt.Sprintf("Successfully executed %d batch update request(s)", len(docsRequests)),
350388
"url": fmt.Sprintf(docsEditURLFormat, docID),
351389
}
352390

internal/docs/docs_formatting_testable.go

Lines changed: 144 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/aliwatters/gsuite-mcp/internal/common"
99
"github.com/mark3labs/mcp-go/mcp"
10+
"google.golang.org/api/docs/v1"
1011
)
1112

1213
// textFormatFields defines the parameter-to-field mapping for text formatting.
@@ -144,7 +145,18 @@ func TestableDocsFormatText(ctx context.Context, request mcp.CallToolRequest, de
144145
return mcp.NewToolResultError("at least one formatting option must be specified"), nil
145146
}
146147

147-
_, err := srv.BatchUpdate(ctx, docID, nil)
148+
textStyle := buildTextStyle(request.Params.Arguments)
149+
requests := []*docs.Request{{
150+
UpdateTextStyle: &docs.UpdateTextStyleRequest{
151+
Range: &docs.Range{
152+
StartIndex: startIndex,
153+
EndIndex: endIndex,
154+
},
155+
TextStyle: textStyle,
156+
Fields: strings.Join(fields, ","),
157+
},
158+
}}
159+
_, err := srv.BatchUpdate(ctx, docID, requests)
148160
if err != nil {
149161
return mcp.NewToolResultError(fmt.Sprintf("Docs API error: %v", err)), nil
150162
}
@@ -179,7 +191,17 @@ func TestableDocsClearFormatting(ctx context.Context, request mcp.CallToolReques
179191

180192
docID = common.ExtractGoogleResourceID(docID)
181193

182-
_, err := srv.BatchUpdate(ctx, docID, nil)
194+
requests := []*docs.Request{{
195+
UpdateTextStyle: &docs.UpdateTextStyleRequest{
196+
Range: &docs.Range{
197+
StartIndex: startIndex,
198+
EndIndex: endIndex,
199+
},
200+
TextStyle: &docs.TextStyle{},
201+
Fields: "*",
202+
},
203+
}}
204+
_, err := srv.BatchUpdate(ctx, docID, requests)
183205
if err != nil {
184206
return mcp.NewToolResultError(fmt.Sprintf("Docs API error: %v", err)), nil
185207
}
@@ -221,7 +243,18 @@ func TestableDocsSetParagraphStyle(ctx context.Context, request mcp.CallToolRequ
221243
return mcp.NewToolResultError("at least one paragraph style option must be specified"), nil
222244
}
223245

224-
_, err := srv.BatchUpdate(ctx, docID, nil)
246+
paraStyle := buildParagraphStyle(request.Params.Arguments)
247+
requests := []*docs.Request{{
248+
UpdateParagraphStyle: &docs.UpdateParagraphStyleRequest{
249+
Range: &docs.Range{
250+
StartIndex: startIndex,
251+
EndIndex: endIndex,
252+
},
253+
ParagraphStyle: paraStyle,
254+
Fields: strings.Join(fields, ","),
255+
},
256+
}}
257+
_, err := srv.BatchUpdate(ctx, docID, requests)
225258
if err != nil {
226259
return mcp.NewToolResultError(fmt.Sprintf("Docs API error: %v", err)), nil
227260
}
@@ -281,7 +314,16 @@ func TestableDocsCreateList(ctx context.Context, request mcp.CallToolRequest, de
281314

282315
docID = common.ExtractGoogleResourceID(docID)
283316

284-
_, err := srv.BatchUpdate(ctx, docID, nil)
317+
requests := []*docs.Request{{
318+
CreateParagraphBullets: &docs.CreateParagraphBulletsRequest{
319+
Range: &docs.Range{
320+
StartIndex: startIndex,
321+
EndIndex: endIndex,
322+
},
323+
BulletPreset: bulletPreset,
324+
},
325+
}}
326+
_, err := srv.BatchUpdate(ctx, docID, requests)
285327
if err != nil {
286328
return mcp.NewToolResultError(fmt.Sprintf("Docs API error: %v", err)), nil
287329
}
@@ -322,7 +364,15 @@ func TestableDocsRemoveList(ctx context.Context, request mcp.CallToolRequest, de
322364

323365
docID = common.ExtractGoogleResourceID(docID)
324366

325-
_, err := srv.BatchUpdate(ctx, docID, nil)
367+
requests := []*docs.Request{{
368+
DeleteParagraphBullets: &docs.DeleteParagraphBulletsRequest{
369+
Range: &docs.Range{
370+
StartIndex: startIndex,
371+
EndIndex: endIndex,
372+
},
373+
},
374+
}}
375+
_, err := srv.BatchUpdate(ctx, docID, requests)
326376
if err != nil {
327377
return mcp.NewToolResultError(fmt.Sprintf("Docs API error: %v", err)), nil
328378
}
@@ -336,3 +386,92 @@ func TestableDocsRemoveList(ctx context.Context, request mcp.CallToolRequest, de
336386

337387
return common.MarshalToolResult(result)
338388
}
389+
390+
// buildTextStyle constructs a docs.TextStyle from the provided arguments.
391+
func buildTextStyle(args map[string]any) *docs.TextStyle {
392+
style := &docs.TextStyle{}
393+
if v, ok := args["bold"].(bool); ok {
394+
style.Bold = v
395+
if !v {
396+
style.ForceSendFields = append(style.ForceSendFields, "Bold")
397+
}
398+
}
399+
if v, ok := args["italic"].(bool); ok {
400+
style.Italic = v
401+
if !v {
402+
style.ForceSendFields = append(style.ForceSendFields, "Italic")
403+
}
404+
}
405+
if v, ok := args["underline"].(bool); ok {
406+
style.Underline = v
407+
if !v {
408+
style.ForceSendFields = append(style.ForceSendFields, "Underline")
409+
}
410+
}
411+
if v, ok := args["strikethrough"].(bool); ok {
412+
style.Strikethrough = v
413+
if !v {
414+
style.ForceSendFields = append(style.ForceSendFields, "Strikethrough")
415+
}
416+
}
417+
if v, ok := args["small_caps"].(bool); ok {
418+
style.SmallCaps = v
419+
if !v {
420+
style.ForceSendFields = append(style.ForceSendFields, "SmallCaps")
421+
}
422+
}
423+
if v, ok := args["font_family"].(string); ok && v != "" {
424+
style.WeightedFontFamily = &docs.WeightedFontFamily{FontFamily: v}
425+
}
426+
if v, ok := args["font_size"].(float64); ok && v > 0 {
427+
style.FontSize = &docs.Dimension{Magnitude: v, Unit: "PT"}
428+
}
429+
if v, ok := args["foreground_color"].(string); ok && v != "" {
430+
if r, g, b, err := parseColor(v); err == nil {
431+
style.ForegroundColor = &docs.OptionalColor{
432+
Color: &docs.Color{RgbColor: &docs.RgbColor{Red: r, Green: g, Blue: b}},
433+
}
434+
}
435+
}
436+
if v, ok := args["background_color"].(string); ok && v != "" {
437+
if r, g, b, err := parseColor(v); err == nil {
438+
style.BackgroundColor = &docs.OptionalColor{
439+
Color: &docs.Color{RgbColor: &docs.RgbColor{Red: r, Green: g, Blue: b}},
440+
}
441+
}
442+
}
443+
if v, ok := args["baseline_offset"].(string); ok && v != "" {
444+
style.BaselineOffset = v
445+
}
446+
return style
447+
}
448+
449+
// buildParagraphStyle constructs a docs.ParagraphStyle from the provided arguments.
450+
func buildParagraphStyle(args map[string]any) *docs.ParagraphStyle {
451+
style := &docs.ParagraphStyle{}
452+
if v, ok := args["alignment"].(string); ok && v != "" {
453+
style.Alignment = v
454+
}
455+
if v, ok := args["named_style_type"].(string); ok && v != "" {
456+
style.NamedStyleType = v
457+
}
458+
if v, ok := args["line_spacing"].(float64); ok && v > 0 {
459+
style.LineSpacing = v
460+
}
461+
if v, ok := args["indent_start"].(float64); ok {
462+
style.IndentStart = &docs.Dimension{Magnitude: v, Unit: "PT"}
463+
}
464+
if v, ok := args["indent_end"].(float64); ok {
465+
style.IndentEnd = &docs.Dimension{Magnitude: v, Unit: "PT"}
466+
}
467+
if v, ok := args["indent_first_line"].(float64); ok {
468+
style.IndentFirstLine = &docs.Dimension{Magnitude: v, Unit: "PT"}
469+
}
470+
if v, ok := args["space_above"].(float64); ok {
471+
style.SpaceAbove = &docs.Dimension{Magnitude: v, Unit: "PT"}
472+
}
473+
if v, ok := args["space_below"].(float64); ok {
474+
style.SpaceBelow = &docs.Dimension{Magnitude: v, Unit: "PT"}
475+
}
476+
return style
477+
}

0 commit comments

Comments
 (0)