@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.
- package/LICENSE +92 -18
- package/README.md +135 -36
- package/dist/auth.js +3 -2
- package/dist/index.js +6 -6
- package/dist/lib/document-id-resolver.js +76 -0
- package/dist/lib/response-formatter.js +82 -0
- package/dist/lib/validation.js +112 -0
- package/dist/tools/docs/basic/gdocs_create.js +37 -0
- package/dist/tools/docs/basic/gdocs_get_metadata.js +45 -0
- package/dist/tools/docs/basic/gdocs_list_documents.js +59 -0
- package/dist/tools/docs/basic/gdocs_read.js +62 -0
- package/dist/tools/docs/content/gdocs_append_text.js +57 -0
- package/dist/tools/docs/content/gdocs_apply_style.js +86 -0
- package/dist/tools/docs/content/gdocs_create_heading.js +89 -0
- package/dist/tools/docs/content/gdocs_create_list.js +86 -0
- package/dist/tools/docs/content/gdocs_delete_text.js +64 -0
- package/dist/tools/docs/content/gdocs_format_text.js +137 -0
- package/dist/tools/docs/content/gdocs_insert_text.js +62 -0
- package/dist/tools/docs/content/gdocs_replace_text.js +64 -0
- package/dist/tools/docs/content/gdocs_set_alignment.js +76 -0
- package/dist/tools/docs/content/gdocs_update_text.js +78 -0
- package/dist/tools/docs/elements/gdocs_batch_update.js +108 -0
- package/dist/tools/docs/elements/gdocs_create_table.js +73 -0
- package/dist/tools/docs/elements/gdocs_export.js +62 -0
- package/dist/tools/docs/elements/gdocs_insert_image.js +96 -0
- package/dist/tools/docs/elements/gdocs_insert_link.js +77 -0
- package/dist/tools/docs/elements/gdocs_insert_page_break.js +55 -0
- package/dist/tools/docs/elements/gdocs_insert_toc.js +71 -0
- package/dist/tools/docs/elements/gdocs_merge_documents.js +104 -0
- package/dist/tools/docs/elements/gdocs_suggest_mode.js +41 -0
- package/dist/tools/drive/drive_read_file.js +77 -0
- package/dist/tools/drive/drive_search.js +71 -0
- package/dist/tools/index.js +124 -5
- 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
|
+
}
|