mcp-google-extras 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +80 -0
- package/dist/auth.js +225 -0
- package/dist/cachedToolsList.js +38 -0
- package/dist/clients.js +92 -0
- package/dist/googleDocsApiHelpers.js +883 -0
- package/dist/googleSheetsApiHelpers.js +808 -0
- package/dist/index.js +54 -0
- package/dist/logger.js +58 -0
- package/dist/markdown-transformer/docsToMarkdown.js +259 -0
- package/dist/markdown-transformer/index.js +126 -0
- package/dist/markdown-transformer/markdownToDocs.js +834 -0
- package/dist/tools/docs/addTab.js +92 -0
- package/dist/tools/docs/appendToGoogleDoc.js +81 -0
- package/dist/tools/docs/comments/addComment.js +83 -0
- package/dist/tools/docs/comments/deleteComment.js +30 -0
- package/dist/tools/docs/comments/getComment.js +45 -0
- package/dist/tools/docs/comments/index.js +14 -0
- package/dist/tools/docs/comments/listComments.js +43 -0
- package/dist/tools/docs/comments/replyToComment.js +35 -0
- package/dist/tools/docs/comments/resolveComment.js +55 -0
- package/dist/tools/docs/deleteRange.js +72 -0
- package/dist/tools/docs/findAndReplace.js +54 -0
- package/dist/tools/docs/formatting/applyParagraphStyle.js +83 -0
- package/dist/tools/docs/formatting/applyTextStyle.js +49 -0
- package/dist/tools/docs/formatting/index.js +6 -0
- package/dist/tools/docs/index.js +38 -0
- package/dist/tools/docs/insertImage.js +122 -0
- package/dist/tools/docs/insertPageBreak.js +58 -0
- package/dist/tools/docs/insertTable.js +53 -0
- package/dist/tools/docs/insertTableWithData.js +135 -0
- package/dist/tools/docs/insertText.js +61 -0
- package/dist/tools/docs/listDocumentTabs.js +60 -0
- package/dist/tools/docs/modifyText.js +158 -0
- package/dist/tools/docs/readGoogleDoc.js +165 -0
- package/dist/tools/docs/renameTab.js +61 -0
- package/dist/tools/drive/copyFile.js +63 -0
- package/dist/tools/drive/createDocument.js +89 -0
- package/dist/tools/drive/createFolder.js +48 -0
- package/dist/tools/drive/createFromTemplate.js +82 -0
- package/dist/tools/drive/deleteFile.js +72 -0
- package/dist/tools/drive/getDocumentInfo.js +48 -0
- package/dist/tools/drive/getFolderInfo.js +48 -0
- package/dist/tools/drive/index.js +26 -0
- package/dist/tools/drive/listFolderContents.js +82 -0
- package/dist/tools/drive/listGoogleDocs.js +67 -0
- package/dist/tools/drive/moveFile.js +54 -0
- package/dist/tools/drive/renameFile.js +39 -0
- package/dist/tools/drive/searchGoogleDocs.js +73 -0
- package/dist/tools/extras/index.js +7 -0
- package/dist/tools/extras/readFile.js +82 -0
- package/dist/tools/extras/searchFileContents.js +81 -0
- package/dist/tools/index.js +15 -0
- package/dist/tools/sheets/addConditionalFormatting.js +143 -0
- package/dist/tools/sheets/addSpreadsheetSheet.js +34 -0
- package/dist/tools/sheets/appendSpreadsheetRows.js +43 -0
- package/dist/tools/sheets/appendTableRows.js +50 -0
- package/dist/tools/sheets/autoResizeColumns.js +67 -0
- package/dist/tools/sheets/batchWrite.js +59 -0
- package/dist/tools/sheets/clearSpreadsheetRange.js +31 -0
- package/dist/tools/sheets/copyFormatting.js +59 -0
- package/dist/tools/sheets/createSpreadsheet.js +71 -0
- package/dist/tools/sheets/createTable.js +120 -0
- package/dist/tools/sheets/deleteChart.js +41 -0
- package/dist/tools/sheets/deleteSheet.js +43 -0
- package/dist/tools/sheets/deleteTable.js +56 -0
- package/dist/tools/sheets/duplicateSheet.js +53 -0
- package/dist/tools/sheets/formatCells.js +106 -0
- package/dist/tools/sheets/freezeRowsAndColumns.js +58 -0
- package/dist/tools/sheets/getSpreadsheetInfo.js +44 -0
- package/dist/tools/sheets/getTable.js +48 -0
- package/dist/tools/sheets/groupRows.js +62 -0
- package/dist/tools/sheets/index.js +66 -0
- package/dist/tools/sheets/insertChart.js +225 -0
- package/dist/tools/sheets/listGoogleSheets.js +62 -0
- package/dist/tools/sheets/listTables.js +55 -0
- package/dist/tools/sheets/readCellFormat.js +143 -0
- package/dist/tools/sheets/readSpreadsheet.js +36 -0
- package/dist/tools/sheets/renameSheet.js +48 -0
- package/dist/tools/sheets/setColumnWidths.js +43 -0
- package/dist/tools/sheets/setDropdownValidation.js +51 -0
- package/dist/tools/sheets/ungroupAllRows.js +66 -0
- package/dist/tools/sheets/updateTableRange.js +51 -0
- package/dist/tools/sheets/writeSpreadsheet.js +43 -0
- package/dist/tools/utils/appendMarkdownToGoogleDoc.js +93 -0
- package/dist/tools/utils/index.js +6 -0
- package/dist/tools/utils/replaceDocumentWithMarkdown.js +154 -0
- package/dist/types.js +186 -0
- package/package.json +47 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDocsClient } from '../../clients.js';
|
|
4
|
+
import { DocumentIdParameter } from '../../types.js';
|
|
5
|
+
import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
|
|
6
|
+
export function register(server) {
|
|
7
|
+
server.addTool({
|
|
8
|
+
name: 'addTab',
|
|
9
|
+
description: "Adds a new tab to a Google Docs document. Optionally set the tab title, position, parent tab (for nesting), and icon emoji. Returns the new tab's ID and properties.",
|
|
10
|
+
parameters: DocumentIdParameter.extend({
|
|
11
|
+
title: z
|
|
12
|
+
.string()
|
|
13
|
+
.optional()
|
|
14
|
+
.describe('The title for the new tab. If not specified, Google Docs assigns a default name.'),
|
|
15
|
+
parentTabId: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe('The ID of an existing tab to nest this new tab under as a child. Use listDocumentTabs to get tab IDs. If not specified, the tab is created at the root level.'),
|
|
19
|
+
index: z
|
|
20
|
+
.number()
|
|
21
|
+
.int()
|
|
22
|
+
.min(0)
|
|
23
|
+
.optional()
|
|
24
|
+
.describe('The zero-based position among sibling tabs (within the same parent). If not specified, the tab is added at the end.'),
|
|
25
|
+
iconEmoji: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('An emoji to display as the tab icon (e.g., "📋").'),
|
|
29
|
+
}),
|
|
30
|
+
execute: async (args, { log }) => {
|
|
31
|
+
const docs = await getDocsClient();
|
|
32
|
+
log.info(`Adding new tab to doc ${args.documentId}`);
|
|
33
|
+
try {
|
|
34
|
+
// If parentTabId is provided, verify it exists
|
|
35
|
+
if (args.parentTabId) {
|
|
36
|
+
const docInfo = await docs.documents.get({
|
|
37
|
+
documentId: args.documentId,
|
|
38
|
+
includeTabsContent: true,
|
|
39
|
+
fields: 'tabs(tabProperties,documentTab)',
|
|
40
|
+
});
|
|
41
|
+
const parentTab = GDocsHelpers.findTabById(docInfo.data, args.parentTabId);
|
|
42
|
+
if (!parentTab) {
|
|
43
|
+
throw new UserError(`Parent tab with ID "${args.parentTabId}" not found in document.`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const tabProperties = {};
|
|
47
|
+
if (args.title !== undefined)
|
|
48
|
+
tabProperties.title = args.title;
|
|
49
|
+
if (args.parentTabId !== undefined)
|
|
50
|
+
tabProperties.parentTabId = args.parentTabId;
|
|
51
|
+
if (args.index !== undefined)
|
|
52
|
+
tabProperties.index = args.index;
|
|
53
|
+
if (args.iconEmoji !== undefined)
|
|
54
|
+
tabProperties.iconEmoji = args.iconEmoji;
|
|
55
|
+
const response = await docs.documents.batchUpdate({
|
|
56
|
+
documentId: args.documentId,
|
|
57
|
+
requestBody: {
|
|
58
|
+
requests: [
|
|
59
|
+
{
|
|
60
|
+
addDocumentTab: {
|
|
61
|
+
tabProperties,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
const newTabProps = response.data.replies?.[0]?.addDocumentTab?.tabProperties;
|
|
68
|
+
if (newTabProps) {
|
|
69
|
+
return JSON.stringify({
|
|
70
|
+
message: `Successfully added new tab "${newTabProps.title || '(untitled)'}"`,
|
|
71
|
+
tabId: newTabProps.tabId,
|
|
72
|
+
title: newTabProps.title,
|
|
73
|
+
index: newTabProps.index,
|
|
74
|
+
parentTabId: newTabProps.parentTabId,
|
|
75
|
+
nestingLevel: newTabProps.nestingLevel,
|
|
76
|
+
}, null, 2);
|
|
77
|
+
}
|
|
78
|
+
return 'Tab created successfully, but could not retrieve the new tab details.';
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
log.error(`Error adding tab to doc ${args.documentId}: ${error.message || error}`);
|
|
82
|
+
if (error instanceof UserError)
|
|
83
|
+
throw error;
|
|
84
|
+
if (error.code === 404)
|
|
85
|
+
throw new UserError(`Document not found (ID: ${args.documentId}).`);
|
|
86
|
+
if (error.code === 403)
|
|
87
|
+
throw new UserError(`Permission denied for document (ID: ${args.documentId}).`);
|
|
88
|
+
throw new UserError(`Failed to add tab: ${error.message || 'Unknown error'}`);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDocsClient } from '../../clients.js';
|
|
4
|
+
import { DocumentIdParameter, NotImplementedError } from '../../types.js';
|
|
5
|
+
import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
|
|
6
|
+
export function register(server) {
|
|
7
|
+
server.addTool({
|
|
8
|
+
name: 'appendText',
|
|
9
|
+
description: 'Appends plain text to the end of a document. For formatted content, use appendMarkdown instead.',
|
|
10
|
+
parameters: DocumentIdParameter.extend({
|
|
11
|
+
text: z.string().min(1).describe('The plain text to append to the end of the document.'),
|
|
12
|
+
addNewlineIfNeeded: z
|
|
13
|
+
.boolean()
|
|
14
|
+
.optional()
|
|
15
|
+
.default(true)
|
|
16
|
+
.describe("Automatically add a newline before the appended text if the doc doesn't end with one."),
|
|
17
|
+
tabId: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('The ID of the specific tab to append to. If not specified, appends to the first tab (or legacy document.body for documents without tabs).'),
|
|
21
|
+
}),
|
|
22
|
+
execute: async (args, { log }) => {
|
|
23
|
+
const docs = await getDocsClient();
|
|
24
|
+
log.info(`Appending to Google Doc: ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
|
|
25
|
+
try {
|
|
26
|
+
// Determine if we need tabs content
|
|
27
|
+
const needsTabsContent = !!args.tabId;
|
|
28
|
+
// Get the current end index
|
|
29
|
+
const docInfo = await docs.documents.get({
|
|
30
|
+
documentId: args.documentId,
|
|
31
|
+
includeTabsContent: needsTabsContent,
|
|
32
|
+
fields: needsTabsContent ? 'tabs' : 'body(content(endIndex)),documentStyle(pageSize)',
|
|
33
|
+
});
|
|
34
|
+
let endIndex = 1;
|
|
35
|
+
let bodyContent;
|
|
36
|
+
// If tabId is specified, find the specific tab
|
|
37
|
+
if (args.tabId) {
|
|
38
|
+
const targetTab = GDocsHelpers.findTabById(docInfo.data, args.tabId);
|
|
39
|
+
if (!targetTab) {
|
|
40
|
+
throw new UserError(`Tab with ID "${args.tabId}" not found in document.`);
|
|
41
|
+
}
|
|
42
|
+
if (!targetTab.documentTab) {
|
|
43
|
+
throw new UserError(`Tab "${args.tabId}" does not have content (may not be a document tab).`);
|
|
44
|
+
}
|
|
45
|
+
bodyContent = targetTab.documentTab.body?.content;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
bodyContent = docInfo.data.body?.content;
|
|
49
|
+
}
|
|
50
|
+
if (bodyContent) {
|
|
51
|
+
const lastElement = bodyContent[bodyContent.length - 1];
|
|
52
|
+
if (lastElement?.endIndex) {
|
|
53
|
+
endIndex = lastElement.endIndex - 1; // Insert *before* the final newline of the doc typically
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Simpler approach: Always assume insertion is needed unless explicitly told not to add newline
|
|
57
|
+
const textToInsert = (args.addNewlineIfNeeded && endIndex > 1 ? '\n' : '') + args.text;
|
|
58
|
+
if (!textToInsert)
|
|
59
|
+
return 'Nothing to append.';
|
|
60
|
+
const location = { index: endIndex };
|
|
61
|
+
if (args.tabId) {
|
|
62
|
+
location.tabId = args.tabId;
|
|
63
|
+
}
|
|
64
|
+
const request = {
|
|
65
|
+
insertText: { location, text: textToInsert },
|
|
66
|
+
};
|
|
67
|
+
await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [request]);
|
|
68
|
+
log.info(`Successfully appended to doc: ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
|
|
69
|
+
return `Successfully appended text to ${args.tabId ? `tab ${args.tabId} in ` : ''}document ${args.documentId}.`;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
log.error(`Error appending to doc ${args.documentId}: ${error.message || error}`);
|
|
73
|
+
if (error instanceof UserError)
|
|
74
|
+
throw error;
|
|
75
|
+
if (error instanceof NotImplementedError)
|
|
76
|
+
throw error;
|
|
77
|
+
throw new UserError(`Failed to append to doc: ${error.message || 'Unknown error'}`);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { google } from 'googleapis';
|
|
4
|
+
import { getDocsClient, getAuthClient } from '../../../clients.js';
|
|
5
|
+
import { DocumentIdParameter } from '../../../types.js';
|
|
6
|
+
export function register(server) {
|
|
7
|
+
server.addTool({
|
|
8
|
+
name: 'addComment',
|
|
9
|
+
description: 'Adds a comment to the document at the specified text range. Use listComments to retrieve the comment ID after creation. Note: programmatically created comments appear in the comments panel but may not show as anchored highlights in the document UI.',
|
|
10
|
+
parameters: DocumentIdParameter.extend({
|
|
11
|
+
startIndex: z
|
|
12
|
+
.number()
|
|
13
|
+
.int()
|
|
14
|
+
.min(1)
|
|
15
|
+
.describe('The starting index of the text range (inclusive, starts from 1).'),
|
|
16
|
+
endIndex: z.number().int().min(1).describe('The ending index of the text range (exclusive).'),
|
|
17
|
+
content: z.string().min(1).describe('The text content of the comment.'),
|
|
18
|
+
}).refine((data) => data.endIndex > data.startIndex, {
|
|
19
|
+
message: 'endIndex must be greater than startIndex',
|
|
20
|
+
path: ['endIndex'],
|
|
21
|
+
}),
|
|
22
|
+
execute: async (args, { log }) => {
|
|
23
|
+
log.info(`Adding comment to range ${args.startIndex}-${args.endIndex} in doc ${args.documentId}`);
|
|
24
|
+
try {
|
|
25
|
+
// First, get the text content that will be quoted
|
|
26
|
+
const docsClient = await getDocsClient();
|
|
27
|
+
const doc = await docsClient.documents.get({ documentId: args.documentId });
|
|
28
|
+
// Extract the quoted text from the document
|
|
29
|
+
let quotedText = '';
|
|
30
|
+
const content = doc.data.body?.content || [];
|
|
31
|
+
for (const element of content) {
|
|
32
|
+
if (element.paragraph) {
|
|
33
|
+
const elements = element.paragraph.elements || [];
|
|
34
|
+
for (const textElement of elements) {
|
|
35
|
+
if (textElement.textRun) {
|
|
36
|
+
const elementStart = textElement.startIndex || 0;
|
|
37
|
+
const elementEnd = textElement.endIndex || 0;
|
|
38
|
+
// Check if this element overlaps with our range
|
|
39
|
+
if (elementEnd > args.startIndex && elementStart < args.endIndex) {
|
|
40
|
+
const text = textElement.textRun.content || '';
|
|
41
|
+
const startOffset = Math.max(0, args.startIndex - elementStart);
|
|
42
|
+
const endOffset = Math.min(text.length, args.endIndex - elementStart);
|
|
43
|
+
quotedText += text.substring(startOffset, endOffset);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Use Drive API v3 for comments
|
|
50
|
+
const authClient = await getAuthClient();
|
|
51
|
+
const drive = google.drive({ version: 'v3', auth: authClient });
|
|
52
|
+
const response = await drive.comments.create({
|
|
53
|
+
fileId: args.documentId,
|
|
54
|
+
fields: 'id,content,quotedFileContent,author,createdTime,resolved',
|
|
55
|
+
requestBody: {
|
|
56
|
+
content: args.content,
|
|
57
|
+
quotedFileContent: {
|
|
58
|
+
value: quotedText,
|
|
59
|
+
mimeType: 'text/html',
|
|
60
|
+
},
|
|
61
|
+
anchor: JSON.stringify({
|
|
62
|
+
r: args.documentId,
|
|
63
|
+
a: [
|
|
64
|
+
{
|
|
65
|
+
txt: {
|
|
66
|
+
o: args.startIndex - 1, // Drive API uses 0-based indexing
|
|
67
|
+
l: args.endIndex - args.startIndex,
|
|
68
|
+
ml: args.endIndex - args.startIndex,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
}),
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
return `Comment added successfully. Comment ID: ${response.data.id}`;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
log.error(`Error adding comment: ${error.message || error}`);
|
|
79
|
+
throw new UserError(`Failed to add comment: ${error.message || 'Unknown error'}`);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { google } from 'googleapis';
|
|
4
|
+
import { getAuthClient } from '../../../clients.js';
|
|
5
|
+
import { DocumentIdParameter } from '../../../types.js';
|
|
6
|
+
export function register(server) {
|
|
7
|
+
server.addTool({
|
|
8
|
+
name: 'deleteComment',
|
|
9
|
+
description: 'Permanently deletes a comment and all its replies from the document.',
|
|
10
|
+
parameters: DocumentIdParameter.extend({
|
|
11
|
+
commentId: z.string().describe('The ID of the comment to delete'),
|
|
12
|
+
}),
|
|
13
|
+
execute: async (args, { log }) => {
|
|
14
|
+
log.info(`Deleting comment ${args.commentId} from doc ${args.documentId}`);
|
|
15
|
+
try {
|
|
16
|
+
const authClient = await getAuthClient();
|
|
17
|
+
const drive = google.drive({ version: 'v3', auth: authClient });
|
|
18
|
+
await drive.comments.delete({
|
|
19
|
+
fileId: args.documentId,
|
|
20
|
+
commentId: args.commentId,
|
|
21
|
+
});
|
|
22
|
+
return `Comment ${args.commentId} has been deleted.`;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
log.error(`Error deleting comment: ${error.message || error}`);
|
|
26
|
+
throw new UserError(`Failed to delete comment: ${error.message || 'Unknown error'}`);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { google } from 'googleapis';
|
|
4
|
+
import { getAuthClient } from '../../../clients.js';
|
|
5
|
+
import { DocumentIdParameter } from '../../../types.js';
|
|
6
|
+
export function register(server) {
|
|
7
|
+
server.addTool({
|
|
8
|
+
name: 'getComment',
|
|
9
|
+
description: 'Gets a specific comment and its full reply thread. Use listComments first to find the comment ID.',
|
|
10
|
+
parameters: DocumentIdParameter.extend({
|
|
11
|
+
commentId: z.string().describe('The ID of the comment to retrieve'),
|
|
12
|
+
}),
|
|
13
|
+
execute: async (args, { log }) => {
|
|
14
|
+
log.info(`Getting comment ${args.commentId} from document ${args.documentId}`);
|
|
15
|
+
try {
|
|
16
|
+
const authClient = await getAuthClient();
|
|
17
|
+
const drive = google.drive({ version: 'v3', auth: authClient });
|
|
18
|
+
const response = await drive.comments.get({
|
|
19
|
+
fileId: args.documentId,
|
|
20
|
+
commentId: args.commentId,
|
|
21
|
+
fields: 'id,content,quotedFileContent,author,createdTime,resolved,replies(id,content,author,createdTime)',
|
|
22
|
+
});
|
|
23
|
+
const comment = response.data;
|
|
24
|
+
return JSON.stringify({
|
|
25
|
+
id: comment.id,
|
|
26
|
+
author: comment.author?.displayName || null,
|
|
27
|
+
content: comment.content,
|
|
28
|
+
quotedText: comment.quotedFileContent?.value || null,
|
|
29
|
+
resolved: comment.resolved || false,
|
|
30
|
+
createdTime: comment.createdTime,
|
|
31
|
+
replies: (comment.replies || []).map((r) => ({
|
|
32
|
+
id: r.id,
|
|
33
|
+
author: r.author?.displayName || null,
|
|
34
|
+
content: r.content,
|
|
35
|
+
createdTime: r.createdTime,
|
|
36
|
+
})),
|
|
37
|
+
}, null, 2);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
log.error(`Error getting comment: ${error.message || error}`);
|
|
41
|
+
throw new UserError(`Failed to get comment: ${error.message || 'Unknown error'}`);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { register as listComments } from './listComments.js';
|
|
2
|
+
import { register as getComment } from './getComment.js';
|
|
3
|
+
import { register as addComment } from './addComment.js';
|
|
4
|
+
import { register as replyToComment } from './replyToComment.js';
|
|
5
|
+
import { register as resolveComment } from './resolveComment.js';
|
|
6
|
+
import { register as deleteComment } from './deleteComment.js';
|
|
7
|
+
export function registerCommentTools(server) {
|
|
8
|
+
listComments(server);
|
|
9
|
+
getComment(server);
|
|
10
|
+
addComment(server);
|
|
11
|
+
replyToComment(server);
|
|
12
|
+
resolveComment(server);
|
|
13
|
+
deleteComment(server);
|
|
14
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { google } from 'googleapis';
|
|
3
|
+
import { getDocsClient, getDriveClient, getAuthClient } from '../../../clients.js';
|
|
4
|
+
import { DocumentIdParameter } from '../../../types.js';
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.addTool({
|
|
7
|
+
name: 'listComments',
|
|
8
|
+
description: 'Lists all comments in a document with their IDs, authors, status, and quoted text. Returns data needed to call getComment, replyToComment, resolveComment, or deleteComment.',
|
|
9
|
+
parameters: DocumentIdParameter,
|
|
10
|
+
execute: async (args, { log }) => {
|
|
11
|
+
log.info(`Listing comments for document ${args.documentId}`);
|
|
12
|
+
const docsClient = await getDocsClient();
|
|
13
|
+
const driveClient = await getDriveClient();
|
|
14
|
+
try {
|
|
15
|
+
// First get the document to have context
|
|
16
|
+
const doc = await docsClient.documents.get({ documentId: args.documentId });
|
|
17
|
+
// Use Drive API v3 with proper fields to get quoted content
|
|
18
|
+
const authClient = await getAuthClient();
|
|
19
|
+
const drive = google.drive({ version: 'v3', auth: authClient });
|
|
20
|
+
const response = await drive.comments.list({
|
|
21
|
+
fileId: args.documentId,
|
|
22
|
+
fields: 'comments(id,content,quotedFileContent,author,createdTime,resolved)',
|
|
23
|
+
pageSize: 100,
|
|
24
|
+
});
|
|
25
|
+
const comments = (response.data.comments || []).map((comment) => ({
|
|
26
|
+
id: comment.id,
|
|
27
|
+
author: comment.author?.displayName || null,
|
|
28
|
+
content: comment.content,
|
|
29
|
+
quotedText: comment.quotedFileContent?.value || null,
|
|
30
|
+
resolved: comment.resolved || false,
|
|
31
|
+
createdTime: comment.createdTime,
|
|
32
|
+
modifiedTime: comment.modifiedTime,
|
|
33
|
+
replyCount: comment.replies?.length || 0,
|
|
34
|
+
}));
|
|
35
|
+
return JSON.stringify({ comments }, null, 2);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
log.error(`Error listing comments: ${error.message || error}`);
|
|
39
|
+
throw new UserError(`Failed to list comments: ${error.message || 'Unknown error'}`);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { google } from 'googleapis';
|
|
4
|
+
import { getAuthClient } from '../../../clients.js';
|
|
5
|
+
import { DocumentIdParameter } from '../../../types.js';
|
|
6
|
+
export function register(server) {
|
|
7
|
+
server.addTool({
|
|
8
|
+
name: 'replyToComment',
|
|
9
|
+
description: 'Adds a reply to an existing comment thread. Use listComments or getComment to find the comment ID.',
|
|
10
|
+
parameters: DocumentIdParameter.extend({
|
|
11
|
+
commentId: z.string().describe('The ID of the comment to reply to'),
|
|
12
|
+
content: z.string().min(1).describe('The text content of the reply.'),
|
|
13
|
+
}),
|
|
14
|
+
execute: async (args, { log }) => {
|
|
15
|
+
log.info(`Adding reply to comment ${args.commentId} in doc ${args.documentId}`);
|
|
16
|
+
try {
|
|
17
|
+
const authClient = await getAuthClient();
|
|
18
|
+
const drive = google.drive({ version: 'v3', auth: authClient });
|
|
19
|
+
const response = await drive.replies.create({
|
|
20
|
+
fileId: args.documentId,
|
|
21
|
+
commentId: args.commentId,
|
|
22
|
+
fields: 'id,content,author,createdTime',
|
|
23
|
+
requestBody: {
|
|
24
|
+
content: args.content,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
return `Reply added successfully. Reply ID: ${response.data.id}`;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
log.error(`Error adding reply: ${error.message || error}`);
|
|
31
|
+
throw new UserError(`Failed to add reply: ${error.message || 'Unknown error'}`);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { google } from 'googleapis';
|
|
4
|
+
import { getAuthClient } from '../../../clients.js';
|
|
5
|
+
import { DocumentIdParameter } from '../../../types.js';
|
|
6
|
+
export function register(server) {
|
|
7
|
+
server.addTool({
|
|
8
|
+
name: 'resolveComment',
|
|
9
|
+
description: 'Marks a comment as resolved. Note: resolved status may not persist in the Google Docs UI due to a Drive API limitation.',
|
|
10
|
+
parameters: DocumentIdParameter.extend({
|
|
11
|
+
commentId: z.string().describe('The ID of the comment to resolve'),
|
|
12
|
+
}),
|
|
13
|
+
execute: async (args, { log }) => {
|
|
14
|
+
log.info(`Resolving comment ${args.commentId} in doc ${args.documentId}`);
|
|
15
|
+
try {
|
|
16
|
+
const authClient = await getAuthClient();
|
|
17
|
+
const drive = google.drive({ version: 'v3', auth: authClient });
|
|
18
|
+
// First, get the current comment content (required by the API)
|
|
19
|
+
const currentComment = await drive.comments.get({
|
|
20
|
+
fileId: args.documentId,
|
|
21
|
+
commentId: args.commentId,
|
|
22
|
+
fields: 'content',
|
|
23
|
+
});
|
|
24
|
+
// Update with both content and resolved status
|
|
25
|
+
await drive.comments.update({
|
|
26
|
+
fileId: args.documentId,
|
|
27
|
+
commentId: args.commentId,
|
|
28
|
+
fields: 'id,resolved',
|
|
29
|
+
requestBody: {
|
|
30
|
+
content: currentComment.data.content,
|
|
31
|
+
resolved: true,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
// Verify the resolved status was set
|
|
35
|
+
const verifyComment = await drive.comments.get({
|
|
36
|
+
fileId: args.documentId,
|
|
37
|
+
commentId: args.commentId,
|
|
38
|
+
fields: 'resolved',
|
|
39
|
+
});
|
|
40
|
+
if (verifyComment.data.resolved) {
|
|
41
|
+
return `Comment ${args.commentId} has been marked as resolved.`;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
return `Attempted to resolve comment ${args.commentId}, but the resolved status may not persist in the Google Docs UI due to API limitations. The comment can be resolved manually in the Google Docs interface.`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
log.error(`Error resolving comment: ${error.message || error}`);
|
|
49
|
+
const errorDetails = error.response?.data?.error?.message || error.message || 'Unknown error';
|
|
50
|
+
const errorCode = error.response?.data?.error?.code;
|
|
51
|
+
throw new UserError(`Failed to resolve comment: ${errorDetails}${errorCode ? ` (Code: ${errorCode})` : ''}`);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDocsClient } from '../../clients.js';
|
|
4
|
+
import { DocumentIdParameter } from '../../types.js';
|
|
5
|
+
import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
|
|
6
|
+
export function register(server) {
|
|
7
|
+
server.addTool({
|
|
8
|
+
name: 'deleteRange',
|
|
9
|
+
description: "Deletes content within a character range [startIndex, endIndex) from a document. Use readDocument with format='json' to determine index positions.",
|
|
10
|
+
parameters: DocumentIdParameter.extend({
|
|
11
|
+
startIndex: z
|
|
12
|
+
.number()
|
|
13
|
+
.int()
|
|
14
|
+
.min(1)
|
|
15
|
+
.describe('1-based character index within the document body. The start of the range to delete (inclusive).'),
|
|
16
|
+
endIndex: z
|
|
17
|
+
.number()
|
|
18
|
+
.int()
|
|
19
|
+
.min(1)
|
|
20
|
+
.describe('1-based character index within the document body. The end of the range to delete (exclusive).'),
|
|
21
|
+
tabId: z
|
|
22
|
+
.string()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe('The ID of the specific tab to delete from. If not specified, deletes from the first tab (or legacy document.body for documents without tabs).'),
|
|
25
|
+
}).refine((data) => data.endIndex > data.startIndex, {
|
|
26
|
+
message: 'endIndex must be greater than startIndex',
|
|
27
|
+
path: ['endIndex'],
|
|
28
|
+
}),
|
|
29
|
+
execute: async (args, { log }) => {
|
|
30
|
+
const docs = await getDocsClient();
|
|
31
|
+
log.info(`Deleting range ${args.startIndex}-${args.endIndex} in doc ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
|
|
32
|
+
if (args.endIndex <= args.startIndex) {
|
|
33
|
+
throw new UserError('End index must be greater than start index for deletion.');
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
// If tabId is specified, verify the tab exists
|
|
37
|
+
if (args.tabId) {
|
|
38
|
+
const docInfo = await docs.documents.get({
|
|
39
|
+
documentId: args.documentId,
|
|
40
|
+
includeTabsContent: true,
|
|
41
|
+
fields: 'tabs(tabProperties,documentTab)',
|
|
42
|
+
});
|
|
43
|
+
const targetTab = GDocsHelpers.findTabById(docInfo.data, args.tabId);
|
|
44
|
+
if (!targetTab) {
|
|
45
|
+
throw new UserError(`Tab with ID "${args.tabId}" not found in document.`);
|
|
46
|
+
}
|
|
47
|
+
if (!targetTab.documentTab) {
|
|
48
|
+
throw new UserError(`Tab "${args.tabId}" does not have content (may not be a document tab).`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const range = {
|
|
52
|
+
startIndex: args.startIndex,
|
|
53
|
+
endIndex: args.endIndex,
|
|
54
|
+
};
|
|
55
|
+
if (args.tabId) {
|
|
56
|
+
range.tabId = args.tabId;
|
|
57
|
+
}
|
|
58
|
+
const request = {
|
|
59
|
+
deleteContentRange: { range },
|
|
60
|
+
};
|
|
61
|
+
await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [request]);
|
|
62
|
+
return `Successfully deleted content in range ${args.startIndex}-${args.endIndex}${args.tabId ? ` in tab ${args.tabId}` : ''}.`;
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
log.error(`Error deleting range in doc ${args.documentId}: ${error.message || error}`);
|
|
66
|
+
if (error instanceof UserError)
|
|
67
|
+
throw error;
|
|
68
|
+
throw new UserError(`Failed to delete range: ${error.message || 'Unknown error'}`);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDocsClient } from '../../clients.js';
|
|
4
|
+
import { DocumentIdParameter } from '../../types.js';
|
|
5
|
+
import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
|
|
6
|
+
const FindAndReplaceParameters = DocumentIdParameter.extend({
|
|
7
|
+
findText: z.string().min(1).describe('The text to search for in the document.'),
|
|
8
|
+
replaceText: z
|
|
9
|
+
.string()
|
|
10
|
+
.describe('The replacement text. Use an empty string to delete all occurrences.'),
|
|
11
|
+
matchCase: z
|
|
12
|
+
.boolean()
|
|
13
|
+
.optional()
|
|
14
|
+
.describe('Whether the search should be case-sensitive. Defaults to false.'),
|
|
15
|
+
tabId: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe('Scope replacement to a specific tab. If omitted, replaces across all tabs.'),
|
|
19
|
+
});
|
|
20
|
+
export function register(server) {
|
|
21
|
+
server.addTool({
|
|
22
|
+
name: 'findAndReplace',
|
|
23
|
+
description: 'Replaces all occurrences of a text string throughout the document (or a specific tab). ' +
|
|
24
|
+
'Returns the number of replacements made. Use an empty replaceText to delete all matches.',
|
|
25
|
+
parameters: FindAndReplaceParameters,
|
|
26
|
+
execute: async (args, { log }) => {
|
|
27
|
+
const docs = await getDocsClient();
|
|
28
|
+
log.info(`findAndReplace in doc ${args.documentId}: "${args.findText}" → "${args.replaceText}"` +
|
|
29
|
+
`${args.matchCase ? ' (case-sensitive)' : ''}` +
|
|
30
|
+
`${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
|
|
31
|
+
try {
|
|
32
|
+
const request = {
|
|
33
|
+
replaceAllText: {
|
|
34
|
+
containsText: {
|
|
35
|
+
text: args.findText,
|
|
36
|
+
matchCase: args.matchCase ?? false,
|
|
37
|
+
},
|
|
38
|
+
replaceText: args.replaceText,
|
|
39
|
+
...(args.tabId && { tabsCriteria: { tabIds: [args.tabId] } }),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
const response = await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [request]);
|
|
43
|
+
const changed = response.replies?.[0]?.replaceAllText?.occurrencesChanged ?? 0;
|
|
44
|
+
return `Replaced ${changed} occurrence(s) of "${args.findText}" with "${args.replaceText}".`;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
log.error(`Error in findAndReplace for doc ${args.documentId}: ${error.message || error}`);
|
|
48
|
+
if (error instanceof UserError)
|
|
49
|
+
throw error;
|
|
50
|
+
throw new UserError(`Failed to find and replace: ${error.message || 'Unknown error'}`);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|