@alanse/mcp-server-google-workspace 0.2.0 → 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.
Files changed (34) hide show
  1. package/LICENSE +92 -18
  2. package/README.md +135 -36
  3. package/dist/auth.js +3 -2
  4. package/dist/index.js +6 -6
  5. package/dist/lib/document-id-resolver.js +76 -0
  6. package/dist/lib/response-formatter.js +82 -0
  7. package/dist/lib/validation.js +112 -0
  8. package/dist/tools/docs/basic/gdocs_create.js +37 -0
  9. package/dist/tools/docs/basic/gdocs_get_metadata.js +45 -0
  10. package/dist/tools/docs/basic/gdocs_list_documents.js +59 -0
  11. package/dist/tools/docs/basic/gdocs_read.js +62 -0
  12. package/dist/tools/docs/content/gdocs_append_text.js +57 -0
  13. package/dist/tools/docs/content/gdocs_apply_style.js +86 -0
  14. package/dist/tools/docs/content/gdocs_create_heading.js +89 -0
  15. package/dist/tools/docs/content/gdocs_create_list.js +86 -0
  16. package/dist/tools/docs/content/gdocs_delete_text.js +64 -0
  17. package/dist/tools/docs/content/gdocs_format_text.js +137 -0
  18. package/dist/tools/docs/content/gdocs_insert_text.js +62 -0
  19. package/dist/tools/docs/content/gdocs_replace_text.js +64 -0
  20. package/dist/tools/docs/content/gdocs_set_alignment.js +76 -0
  21. package/dist/tools/docs/content/gdocs_update_text.js +78 -0
  22. package/dist/tools/docs/elements/gdocs_batch_update.js +108 -0
  23. package/dist/tools/docs/elements/gdocs_create_table.js +73 -0
  24. package/dist/tools/docs/elements/gdocs_export.js +62 -0
  25. package/dist/tools/docs/elements/gdocs_insert_image.js +96 -0
  26. package/dist/tools/docs/elements/gdocs_insert_link.js +77 -0
  27. package/dist/tools/docs/elements/gdocs_insert_page_break.js +55 -0
  28. package/dist/tools/docs/elements/gdocs_insert_toc.js +71 -0
  29. package/dist/tools/docs/elements/gdocs_merge_documents.js +104 -0
  30. package/dist/tools/docs/elements/gdocs_suggest_mode.js +41 -0
  31. package/dist/tools/drive/drive_read_file.js +77 -0
  32. package/dist/tools/drive/drive_search.js +71 -0
  33. package/dist/tools/index.js +124 -5
  34. package/package.json +3 -3
@@ -0,0 +1,96 @@
1
+ import { google } from "googleapis";
2
+ import { ResponseFormatter } from "../../../lib/response-formatter.js";
3
+ import { DocumentIdResolver } from "../../../lib/document-id-resolver.js";
4
+ import { Validator } from "../../../lib/validation.js";
5
+ export const schema = {
6
+ name: "gdocs_insert_image",
7
+ description: "Insert an image from a URL into a Google Document at a specific position.",
8
+ inputSchema: {
9
+ type: "object",
10
+ properties: {
11
+ documentId: {
12
+ type: "string",
13
+ description: "Document ID or full Google Docs URL",
14
+ },
15
+ imageUrl: {
16
+ type: "string",
17
+ description: "URL of the image to insert (must be publicly accessible)",
18
+ },
19
+ index: {
20
+ type: "number",
21
+ description: "Position to insert image (1-based)",
22
+ },
23
+ width: {
24
+ type: "number",
25
+ description: "Image width in points (optional)",
26
+ },
27
+ height: {
28
+ type: "number",
29
+ description: "Image height in points (optional)",
30
+ },
31
+ },
32
+ required: ["documentId", "imageUrl", "index"],
33
+ },
34
+ };
35
+ export async function insertImage(args) {
36
+ try {
37
+ // Validate inputs
38
+ if (!Validator.validateIndex(args.index)) {
39
+ throw new Error("Index must be a positive integer");
40
+ }
41
+ if (!Validator.validateUrl(args.imageUrl)) {
42
+ throw new Error("Invalid image URL");
43
+ }
44
+ if (args.width && !Validator.validatePositiveNumber(args.width)) {
45
+ throw new Error("Width must be a positive number");
46
+ }
47
+ if (args.height && !Validator.validatePositiveNumber(args.height)) {
48
+ throw new Error("Height must be a positive number");
49
+ }
50
+ // Resolve document ID from URL if needed
51
+ const docRef = DocumentIdResolver.resolve(args.documentId);
52
+ const documentId = docRef.id;
53
+ const docs = google.docs("v1");
54
+ // Build image properties
55
+ const imageProperties = {
56
+ contentUri: args.imageUrl,
57
+ };
58
+ if (args.width || args.height) {
59
+ imageProperties.sourceUri = args.imageUrl;
60
+ }
61
+ // Build object size if dimensions specified
62
+ let objectSize;
63
+ if (args.width || args.height) {
64
+ objectSize = {
65
+ width: args.width ? { magnitude: args.width, unit: "PT" } : undefined,
66
+ height: args.height ? { magnitude: args.height, unit: "PT" } : undefined,
67
+ };
68
+ }
69
+ const response = await docs.documents.batchUpdate({
70
+ documentId,
71
+ requestBody: {
72
+ requests: [
73
+ {
74
+ insertInlineImage: {
75
+ uri: args.imageUrl,
76
+ location: {
77
+ index: args.index,
78
+ },
79
+ objectSize,
80
+ },
81
+ },
82
+ ],
83
+ },
84
+ });
85
+ return ResponseFormatter.success({
86
+ documentId,
87
+ imageUrl: args.imageUrl,
88
+ insertedAt: args.index,
89
+ width: args.width,
90
+ height: args.height,
91
+ }, "Image inserted successfully");
92
+ }
93
+ catch (error) {
94
+ return ResponseFormatter.error(error);
95
+ }
96
+ }
@@ -0,0 +1,77 @@
1
+ import { google } from "googleapis";
2
+ import { ResponseFormatter } from "../../../lib/response-formatter.js";
3
+ import { DocumentIdResolver } from "../../../lib/document-id-resolver.js";
4
+ import { Validator } from "../../../lib/validation.js";
5
+ export const schema = {
6
+ name: "gdocs_insert_link",
7
+ description: "Insert a hyperlink on a text range in a Google Document.",
8
+ inputSchema: {
9
+ type: "object",
10
+ properties: {
11
+ documentId: {
12
+ type: "string",
13
+ description: "Document ID or full Google Docs URL",
14
+ },
15
+ startIndex: {
16
+ type: "number",
17
+ description: "Start position of text to link (1-based, inclusive)",
18
+ },
19
+ endIndex: {
20
+ type: "number",
21
+ description: "End position of text to link (1-based, exclusive)",
22
+ },
23
+ url: {
24
+ type: "string",
25
+ description: "URL to link to (must start with http:// or https://)",
26
+ },
27
+ },
28
+ required: ["documentId", "startIndex", "endIndex", "url"],
29
+ },
30
+ };
31
+ export async function insertLink(args) {
32
+ try {
33
+ // Validate inputs
34
+ if (!Validator.validateRange(args.startIndex, args.endIndex)) {
35
+ throw new Error("Invalid range: startIndex must be less than endIndex and both must be positive");
36
+ }
37
+ if (!Validator.validateUrl(args.url)) {
38
+ throw new Error("Invalid URL: must start with http:// or https://");
39
+ }
40
+ // Resolve document ID from URL if needed
41
+ const docRef = DocumentIdResolver.resolve(args.documentId);
42
+ const documentId = docRef.id;
43
+ const docs = google.docs("v1");
44
+ const response = await docs.documents.batchUpdate({
45
+ documentId,
46
+ requestBody: {
47
+ requests: [
48
+ {
49
+ updateTextStyle: {
50
+ textStyle: {
51
+ link: {
52
+ url: args.url,
53
+ },
54
+ },
55
+ fields: "link",
56
+ range: {
57
+ startIndex: args.startIndex,
58
+ endIndex: args.endIndex,
59
+ },
60
+ },
61
+ },
62
+ ],
63
+ },
64
+ });
65
+ return ResponseFormatter.success({
66
+ documentId,
67
+ url: args.url,
68
+ range: {
69
+ startIndex: args.startIndex,
70
+ endIndex: args.endIndex,
71
+ },
72
+ }, "Hyperlink inserted successfully");
73
+ }
74
+ catch (error) {
75
+ return ResponseFormatter.error(error);
76
+ }
77
+ }
@@ -0,0 +1,55 @@
1
+ import { google } from "googleapis";
2
+ import { ResponseFormatter } from "../../../lib/response-formatter.js";
3
+ import { DocumentIdResolver } from "../../../lib/document-id-resolver.js";
4
+ import { Validator } from "../../../lib/validation.js";
5
+ export const schema = {
6
+ name: "gdocs_insert_page_break",
7
+ description: "Insert a page break at a specific position in a Google Document.",
8
+ inputSchema: {
9
+ type: "object",
10
+ properties: {
11
+ documentId: {
12
+ type: "string",
13
+ description: "Document ID or full Google Docs URL",
14
+ },
15
+ index: {
16
+ type: "number",
17
+ description: "Position to insert page break (1-based)",
18
+ },
19
+ },
20
+ required: ["documentId", "index"],
21
+ },
22
+ };
23
+ export async function insertPageBreak(args) {
24
+ try {
25
+ // Validate inputs
26
+ if (!Validator.validateIndex(args.index)) {
27
+ throw new Error("Index must be a positive integer");
28
+ }
29
+ // Resolve document ID from URL if needed
30
+ const docRef = DocumentIdResolver.resolve(args.documentId);
31
+ const documentId = docRef.id;
32
+ const docs = google.docs("v1");
33
+ const response = await docs.documents.batchUpdate({
34
+ documentId,
35
+ requestBody: {
36
+ requests: [
37
+ {
38
+ insertPageBreak: {
39
+ location: {
40
+ index: args.index,
41
+ },
42
+ },
43
+ },
44
+ ],
45
+ },
46
+ });
47
+ return ResponseFormatter.success({
48
+ documentId,
49
+ insertedAt: args.index,
50
+ }, "Page break inserted successfully");
51
+ }
52
+ catch (error) {
53
+ return ResponseFormatter.error(error);
54
+ }
55
+ }
@@ -0,0 +1,71 @@
1
+ import { google } from "googleapis";
2
+ import { ResponseFormatter } from "../../../lib/response-formatter.js";
3
+ import { DocumentIdResolver } from "../../../lib/document-id-resolver.js";
4
+ import { Validator } from "../../../lib/validation.js";
5
+ export const schema = {
6
+ name: "gdocs_insert_toc",
7
+ description: "Insert a 'Table of Contents' heading at a specific position. NOTE: The Google Docs API doesn't support auto-generated TOC insertion. To insert an actual auto-updating TOC, use Insert > Table of contents in the Google Docs UI.",
8
+ inputSchema: {
9
+ type: "object",
10
+ properties: {
11
+ documentId: {
12
+ type: "string",
13
+ description: "Document ID or full Google Docs URL",
14
+ },
15
+ index: {
16
+ type: "number",
17
+ description: "Position to insert TOC heading (1-based)",
18
+ },
19
+ },
20
+ required: ["documentId", "index"],
21
+ },
22
+ };
23
+ export async function insertToc(args) {
24
+ try {
25
+ // Validate inputs
26
+ if (!Validator.validateIndex(args.index)) {
27
+ throw new Error("Index must be a positive integer");
28
+ }
29
+ // Resolve document ID from URL if needed
30
+ const docRef = DocumentIdResolver.resolve(args.documentId);
31
+ const documentId = docRef.id;
32
+ const docs = google.docs("v1");
33
+ // NOTE: Google Docs API doesn't support programmatic TOC insertion
34
+ // This is a workaround that inserts a styled "Table of Contents" heading
35
+ const response = await docs.documents.batchUpdate({
36
+ documentId,
37
+ requestBody: {
38
+ requests: [
39
+ {
40
+ insertText: {
41
+ location: {
42
+ index: args.index,
43
+ },
44
+ text: "Table of Contents\n",
45
+ },
46
+ },
47
+ {
48
+ updateParagraphStyle: {
49
+ range: {
50
+ startIndex: args.index,
51
+ endIndex: args.index + 19, // "Table of Contents\n".length
52
+ },
53
+ paragraphStyle: {
54
+ namedStyleType: "HEADING_1",
55
+ },
56
+ fields: "namedStyleType",
57
+ },
58
+ },
59
+ ],
60
+ },
61
+ });
62
+ return ResponseFormatter.success({
63
+ documentId,
64
+ insertedAt: args.index,
65
+ note: "Inserted 'Table of Contents' heading. To insert an auto-updating TOC, use Insert > Table of contents in the Google Docs UI. The API doesn't support programmatic TOC generation.",
66
+ }, "Table of Contents heading inserted successfully");
67
+ }
68
+ catch (error) {
69
+ return ResponseFormatter.error(error);
70
+ }
71
+ }
@@ -0,0 +1,104 @@
1
+ import { google } from "googleapis";
2
+ import { ResponseFormatter } from "../../../lib/response-formatter.js";
3
+ import { DocumentIdResolver } from "../../../lib/document-id-resolver.js";
4
+ export const schema = {
5
+ name: "gdocs_merge_documents",
6
+ description: "Merge multiple Google Documents into one. Creates a new document or appends to an existing target document.",
7
+ inputSchema: {
8
+ type: "object",
9
+ properties: {
10
+ sourceDocumentIds: {
11
+ type: "array",
12
+ description: "Array of source document IDs or URLs to merge (in order)",
13
+ items: {
14
+ type: "string",
15
+ },
16
+ },
17
+ targetDocumentId: {
18
+ type: "string",
19
+ description: "Target document ID or URL to merge into. If not provided, creates a new document.",
20
+ },
21
+ title: {
22
+ type: "string",
23
+ description: "Title for the new merged document (only used if targetDocumentId is not provided)",
24
+ },
25
+ },
26
+ required: ["sourceDocumentIds"],
27
+ },
28
+ };
29
+ export async function mergeDocuments(args) {
30
+ try {
31
+ if (args.sourceDocumentIds.length === 0) {
32
+ throw new Error("At least one source document is required");
33
+ }
34
+ const docs = google.docs("v1");
35
+ // Resolve source document IDs
36
+ const sourceIds = args.sourceDocumentIds.map((id) => {
37
+ const ref = DocumentIdResolver.resolve(id);
38
+ return ref.id;
39
+ });
40
+ // Determine target document
41
+ let targetDocumentId;
42
+ if (args.targetDocumentId) {
43
+ const targetRef = DocumentIdResolver.resolve(args.targetDocumentId);
44
+ targetDocumentId = targetRef.id;
45
+ }
46
+ else {
47
+ // Create a new document
48
+ const createResponse = await docs.documents.create({
49
+ requestBody: {
50
+ title: args.title || "Merged Document",
51
+ },
52
+ });
53
+ targetDocumentId = createResponse.data.documentId;
54
+ }
55
+ // Read each source document and append to target
56
+ for (const sourceId of sourceIds) {
57
+ const sourceDoc = await docs.documents.get({ documentId: sourceId });
58
+ // Extract text content from source
59
+ let textContent = "";
60
+ if (sourceDoc.data.body?.content) {
61
+ for (const element of sourceDoc.data.body.content) {
62
+ if (element.paragraph?.elements) {
63
+ for (const textElement of element.paragraph.elements) {
64
+ if (textElement.textRun?.content) {
65
+ textContent += textElement.textRun.content;
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ // Get target document end index
72
+ const targetDoc = await docs.documents.get({ documentId: targetDocumentId });
73
+ const endIndex = targetDoc.data.body?.content?.[targetDoc.data.body.content.length - 1]?.endIndex || 1;
74
+ // Append content to target
75
+ if (textContent) {
76
+ await docs.documents.batchUpdate({
77
+ documentId: targetDocumentId,
78
+ requestBody: {
79
+ requests: [
80
+ {
81
+ insertText: {
82
+ text: "\n" + textContent,
83
+ location: {
84
+ index: endIndex - 1,
85
+ },
86
+ },
87
+ },
88
+ ],
89
+ },
90
+ });
91
+ }
92
+ }
93
+ const targetUrl = `https://docs.google.com/document/d/${targetDocumentId}/edit`;
94
+ return ResponseFormatter.success({
95
+ targetDocumentId,
96
+ targetDocumentUrl: targetUrl,
97
+ mergedDocuments: sourceIds.length,
98
+ sourceDocumentIds: sourceIds,
99
+ }, `Successfully merged ${sourceIds.length} document(s)`);
100
+ }
101
+ catch (error) {
102
+ return ResponseFormatter.error(error);
103
+ }
104
+ }
@@ -0,0 +1,41 @@
1
+ import { google } from "googleapis";
2
+ import { ResponseFormatter } from "../../../lib/response-formatter.js";
3
+ import { DocumentIdResolver } from "../../../lib/document-id-resolver.js";
4
+ export const schema = {
5
+ name: "gdocs_suggest_mode",
6
+ description: "NOTE: This tool cannot programmatically control suggestion mode. The Google Docs API doesn't support enabling/disabling suggestion mode. To use suggestion mode, open the document in Google Docs UI and use the 'Editing/Suggesting/Viewing' dropdown in the top-right corner.",
7
+ inputSchema: {
8
+ type: "object",
9
+ properties: {
10
+ documentId: {
11
+ type: "string",
12
+ description: "Document ID or full Google Docs URL",
13
+ },
14
+ enabled: {
15
+ type: "boolean",
16
+ description: "true to enable suggestion mode, false to disable",
17
+ },
18
+ },
19
+ required: ["documentId", "enabled"],
20
+ },
21
+ };
22
+ export async function suggestMode(args) {
23
+ try {
24
+ // Resolve document ID from URL if needed
25
+ const docRef = DocumentIdResolver.resolve(args.documentId);
26
+ const documentId = docRef.id;
27
+ const docs = google.docs("v1");
28
+ // Verify document exists
29
+ await docs.documents.get({ documentId });
30
+ // Return informative error about API limitation
31
+ return ResponseFormatter.error(new Error(`Suggestion mode cannot be controlled programmatically via the Google Docs API. ` +
32
+ `To ${args.enabled ? "enable" : "disable"} suggestion mode, please:\n\n` +
33
+ `1. Open the document at: https://docs.google.com/document/d/${documentId}/edit\n` +
34
+ `2. Click the 'Editing' dropdown in the top-right corner\n` +
35
+ `3. Select '${args.enabled ? "Suggesting" : "Editing"}'\n\n` +
36
+ `This is a limitation of the Google Docs API, not this tool.`));
37
+ }
38
+ catch (error) {
39
+ return ResponseFormatter.error(error);
40
+ }
41
+ }
@@ -0,0 +1,77 @@
1
+ import { google } from "googleapis";
2
+ export const schema = {
3
+ name: "drive_read_file",
4
+ description: "Read contents of a file from Google Drive",
5
+ inputSchema: {
6
+ type: "object",
7
+ properties: {
8
+ fileId: {
9
+ type: "string",
10
+ description: "ID of the file to read",
11
+ },
12
+ },
13
+ required: ["fileId"],
14
+ },
15
+ };
16
+ const drive = google.drive("v3");
17
+ export async function readFile(args) {
18
+ const result = await readGoogleDriveFile(args.fileId);
19
+ return {
20
+ content: [
21
+ {
22
+ type: "text",
23
+ text: `Contents of ${result.name}:\n\n${result.contents.text || result.contents.blob}`,
24
+ },
25
+ ],
26
+ isError: false,
27
+ };
28
+ }
29
+ async function readGoogleDriveFile(fileId) {
30
+ // First get file metadata to check mime type
31
+ const file = await drive.files.get({
32
+ fileId,
33
+ fields: "mimeType,name",
34
+ });
35
+ // For Google Docs/Sheets/etc we need to export
36
+ if (file.data.mimeType?.startsWith("application/vnd.google-apps")) {
37
+ let exportMimeType;
38
+ switch (file.data.mimeType) {
39
+ case "application/vnd.google-apps.document":
40
+ exportMimeType = "text/markdown";
41
+ break;
42
+ case "application/vnd.google-apps.spreadsheet":
43
+ exportMimeType = "text/csv";
44
+ break;
45
+ case "application/vnd.google-apps.presentation":
46
+ exportMimeType = "text/plain";
47
+ break;
48
+ case "application/vnd.google-apps.drawing":
49
+ exportMimeType = "image/png";
50
+ break;
51
+ default:
52
+ exportMimeType = "text/plain";
53
+ }
54
+ const res = await drive.files.export({ fileId, mimeType: exportMimeType }, { responseType: "text" });
55
+ return {
56
+ name: file.data.name || fileId,
57
+ contents: {
58
+ mimeType: exportMimeType,
59
+ text: res.data,
60
+ },
61
+ };
62
+ }
63
+ // For regular files download content
64
+ const res = await drive.files.get({ fileId, alt: "media" }, { responseType: "arraybuffer" });
65
+ const mimeType = file.data.mimeType || "application/octet-stream";
66
+ const isText = mimeType.startsWith("text/") || mimeType === "application/json";
67
+ const content = Buffer.from(res.data);
68
+ return {
69
+ name: file.data.name || fileId,
70
+ contents: {
71
+ mimeType,
72
+ ...(isText
73
+ ? { text: content.toString("utf-8") }
74
+ : { blob: content.toString("base64") }),
75
+ },
76
+ };
77
+ }
@@ -0,0 +1,71 @@
1
+ import { google } from "googleapis";
2
+ export const schema = {
3
+ name: "drive_search",
4
+ description: "Search for files in Google Drive",
5
+ inputSchema: {
6
+ type: "object",
7
+ properties: {
8
+ query: {
9
+ type: "string",
10
+ description: "Search query",
11
+ },
12
+ pageToken: {
13
+ type: "string",
14
+ description: "Token for the next page of results",
15
+ optional: true,
16
+ },
17
+ pageSize: {
18
+ type: "number",
19
+ description: "Number of results per page (max 100)",
20
+ optional: true,
21
+ },
22
+ },
23
+ required: ["query"],
24
+ },
25
+ };
26
+ export async function search(args) {
27
+ const drive = google.drive("v3");
28
+ const userQuery = args.query.trim();
29
+ let searchQuery = "";
30
+ // If query is empty, list all files
31
+ if (!userQuery) {
32
+ searchQuery = "trashed = false";
33
+ }
34
+ else {
35
+ // Escape special characters in the query
36
+ const escapedQuery = userQuery.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
37
+ // Build search query with multiple conditions
38
+ const conditions = [];
39
+ // Search in title
40
+ conditions.push(`name contains '${escapedQuery}'`);
41
+ // If specific file type is mentioned in query, add mimeType condition
42
+ if (userQuery.toLowerCase().includes("sheet")) {
43
+ conditions.push("mimeType = 'application/vnd.google-sheets.spreadsheet'");
44
+ }
45
+ searchQuery = `(${conditions.join(" or ")}) and trashed = false`;
46
+ }
47
+ const res = await drive.files.list({
48
+ q: searchQuery,
49
+ pageSize: args.pageSize || 10,
50
+ pageToken: args.pageToken,
51
+ orderBy: "modifiedTime desc",
52
+ fields: "nextPageToken, files(id, name, mimeType, modifiedTime, size)",
53
+ });
54
+ const fileList = res.data.files
55
+ ?.map((file) => `${file.id} ${file.name} (${file.mimeType})`)
56
+ .join("\n");
57
+ let response = `Found ${res.data.files?.length ?? 0} files:\n${fileList}`;
58
+ // Add pagination info if there are more results
59
+ if (res.data.nextPageToken) {
60
+ response += `\n\nMore results available. Use pageToken: ${res.data.nextPageToken}`;
61
+ }
62
+ return {
63
+ content: [
64
+ {
65
+ type: "text",
66
+ text: response,
67
+ },
68
+ ],
69
+ isError: false,
70
+ };
71
+ }