@brainwavesio/google-docs-mcp 1.0.0 → 1.0.1
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/dist/auth.js +205 -0
- package/dist/googleDocsApiHelpers.js +617 -0
- package/dist/googleSheetsApiHelpers.js +356 -0
- package/dist/server.js +2215 -0
- package/dist/types.js +107 -0
- package/package.json +1 -1
- package/.repomix/bundles.json +0 -3
- package/SAMPLE_TASKS.md +0 -230
- package/assets/google.docs.mcp.1.gif +0 -0
- package/claude.md +0 -46
- package/docs/index.html +0 -228
- package/google docs mcp.mp4 +0 -0
- package/pages/pages.md +0 -0
- package/repomix-output.txt.xml +0 -4447
- package/src/auth.ts +0 -228
- package/src/backup/auth.ts.bak +0 -101
- package/src/backup/server.ts.bak +0 -481
- package/src/googleDocsApiHelpers.ts +0 -710
- package/src/googleSheetsApiHelpers.ts +0 -427
- package/src/server.ts +0 -2494
- package/src/types.ts +0 -136
- package/tests/helpers.test.js +0 -164
- package/tests/types.test.js +0 -69
- package/tsconfig.json +0 -17
- package/vscode.md +0 -168
package/src/types.ts
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
// src/types.ts
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { docs_v1 } from 'googleapis';
|
|
4
|
-
|
|
5
|
-
// --- Helper function for hex color validation ---
|
|
6
|
-
export const hexColorRegex = /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/;
|
|
7
|
-
export const validateHexColor = (color: string) => hexColorRegex.test(color);
|
|
8
|
-
|
|
9
|
-
// --- Helper function for Hex to RGB conversion ---
|
|
10
|
-
export function hexToRgbColor(hex: string): docs_v1.Schema$RgbColor | null {
|
|
11
|
-
if (!hex) return null;
|
|
12
|
-
let hexClean = hex.startsWith('#') ? hex.slice(1) : hex;
|
|
13
|
-
|
|
14
|
-
if (hexClean.length === 3) {
|
|
15
|
-
hexClean = hexClean[0] + hexClean[0] + hexClean[1] + hexClean[1] + hexClean[2] + hexClean[2];
|
|
16
|
-
}
|
|
17
|
-
if (hexClean.length !== 6) return null;
|
|
18
|
-
const bigint = parseInt(hexClean, 16);
|
|
19
|
-
if (isNaN(bigint)) return null;
|
|
20
|
-
|
|
21
|
-
const r = ((bigint >> 16) & 255) / 255;
|
|
22
|
-
const g = ((bigint >> 8) & 255) / 255;
|
|
23
|
-
const b = (bigint & 255) / 255;
|
|
24
|
-
|
|
25
|
-
return { red: r, green: g, blue: b };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// --- Zod Schema Fragments for Reusability ---
|
|
29
|
-
|
|
30
|
-
export const DocumentIdParameter = z.object({
|
|
31
|
-
documentId: z.string().describe('The ID of the Google Document (from the URL).'),
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
export const RangeParameters = z.object({
|
|
35
|
-
startIndex: z.number().int().min(1).describe('The starting index of the text range (inclusive, starts from 1).'),
|
|
36
|
-
endIndex: z.number().int().min(1).describe('The ending index of the text range (exclusive).'),
|
|
37
|
-
}).refine(data => data.endIndex > data.startIndex, {
|
|
38
|
-
message: "endIndex must be greater than startIndex",
|
|
39
|
-
path: ["endIndex"],
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
export const OptionalRangeParameters = z.object({
|
|
43
|
-
startIndex: z.number().int().min(1).optional().describe('Optional: The starting index of the text range (inclusive, starts from 1). If omitted, might apply to a found element or whole paragraph.'),
|
|
44
|
-
endIndex: z.number().int().min(1).optional().describe('Optional: The ending index of the text range (exclusive). If omitted, might apply to a found element or whole paragraph.'),
|
|
45
|
-
}).refine(data => !data.startIndex || !data.endIndex || data.endIndex > data.startIndex, {
|
|
46
|
-
message: "If both startIndex and endIndex are provided, endIndex must be greater than startIndex",
|
|
47
|
-
path: ["endIndex"],
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
export const TextFindParameter = z.object({
|
|
51
|
-
textToFind: z.string().min(1).describe('The exact text string to locate.'),
|
|
52
|
-
matchInstance: z.number().int().min(1).optional().default(1).describe('Which instance of the text to target (1st, 2nd, etc.). Defaults to 1.'),
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// --- Style Parameter Schemas ---
|
|
56
|
-
|
|
57
|
-
export const TextStyleParameters = z.object({
|
|
58
|
-
bold: z.boolean().optional().describe('Apply bold formatting.'),
|
|
59
|
-
italic: z.boolean().optional().describe('Apply italic formatting.'),
|
|
60
|
-
underline: z.boolean().optional().describe('Apply underline formatting.'),
|
|
61
|
-
strikethrough: z.boolean().optional().describe('Apply strikethrough formatting.'),
|
|
62
|
-
fontSize: z.number().min(1).optional().describe('Set font size (in points, e.g., 12).'),
|
|
63
|
-
fontFamily: z.string().optional().describe('Set font family (e.g., "Arial", "Times New Roman").'),
|
|
64
|
-
foregroundColor: z.string()
|
|
65
|
-
.refine(validateHexColor, { message: "Invalid hex color format (e.g., #FF0000 or #F00)" })
|
|
66
|
-
.optional()
|
|
67
|
-
.describe('Set text color using hex format (e.g., "#FF0000").'),
|
|
68
|
-
backgroundColor: z.string()
|
|
69
|
-
.refine(validateHexColor, { message: "Invalid hex color format (e.g., #00FF00 or #0F0)" })
|
|
70
|
-
.optional()
|
|
71
|
-
.describe('Set text background color using hex format (e.g., "#FFFF00").'),
|
|
72
|
-
linkUrl: z.string().url().optional().describe('Make the text a hyperlink pointing to this URL.'),
|
|
73
|
-
// clearDirectFormatting: z.boolean().optional().describe('If true, attempts to clear all direct text formatting within the range before applying new styles.') // Harder to implement perfectly
|
|
74
|
-
}).describe("Parameters for character-level text formatting.");
|
|
75
|
-
|
|
76
|
-
// Subset of TextStyle used for passing to helpers
|
|
77
|
-
export type TextStyleArgs = z.infer<typeof TextStyleParameters>;
|
|
78
|
-
|
|
79
|
-
export const ParagraphStyleParameters = z.object({
|
|
80
|
-
alignment: z.enum(['START', 'END', 'CENTER', 'JUSTIFIED']).optional().describe('Paragraph alignment. START=left for LTR languages, END=right for LTR languages.'),
|
|
81
|
-
indentStart: z.number().min(0).optional().describe('Left indentation in points.'),
|
|
82
|
-
indentEnd: z.number().min(0).optional().describe('Right indentation in points.'),
|
|
83
|
-
spaceAbove: z.number().min(0).optional().describe('Space before the paragraph in points.'),
|
|
84
|
-
spaceBelow: z.number().min(0).optional().describe('Space after the paragraph in points.'),
|
|
85
|
-
namedStyleType: z.enum([
|
|
86
|
-
'NORMAL_TEXT', 'TITLE', 'SUBTITLE',
|
|
87
|
-
'HEADING_1', 'HEADING_2', 'HEADING_3', 'HEADING_4', 'HEADING_5', 'HEADING_6'
|
|
88
|
-
]).optional().describe('Apply a built-in named paragraph style (e.g., HEADING_1).'),
|
|
89
|
-
keepWithNext: z.boolean().optional().describe('Keep this paragraph together with the next one on the same page.'),
|
|
90
|
-
// Borders are more complex, might need separate objects/tools
|
|
91
|
-
// clearDirectFormatting: z.boolean().optional().describe('If true, attempts to clear all direct paragraph formatting within the range before applying new styles.') // Harder to implement perfectly
|
|
92
|
-
}).describe("Parameters for paragraph-level formatting.");
|
|
93
|
-
|
|
94
|
-
// Subset of ParagraphStyle used for passing to helpers
|
|
95
|
-
export type ParagraphStyleArgs = z.infer<typeof ParagraphStyleParameters>;
|
|
96
|
-
|
|
97
|
-
// --- Combination Schemas for Tools ---
|
|
98
|
-
|
|
99
|
-
export const ApplyTextStyleToolParameters = DocumentIdParameter.extend({
|
|
100
|
-
// Target EITHER by range OR by finding text
|
|
101
|
-
target: z.union([
|
|
102
|
-
RangeParameters,
|
|
103
|
-
TextFindParameter
|
|
104
|
-
]).describe("Specify the target range either by start/end indices or by finding specific text."),
|
|
105
|
-
style: TextStyleParameters.refine(
|
|
106
|
-
styleArgs => Object.values(styleArgs).some(v => v !== undefined),
|
|
107
|
-
{ message: "At least one text style option must be provided." }
|
|
108
|
-
).describe("The text styling to apply.")
|
|
109
|
-
});
|
|
110
|
-
export type ApplyTextStyleToolArgs = z.infer<typeof ApplyTextStyleToolParameters>;
|
|
111
|
-
|
|
112
|
-
export const ApplyParagraphStyleToolParameters = DocumentIdParameter.extend({
|
|
113
|
-
// Target EITHER by range OR by finding text (tool logic needs to find paragraph boundaries)
|
|
114
|
-
target: z.union([
|
|
115
|
-
RangeParameters, // User provides paragraph start/end (less likely)
|
|
116
|
-
TextFindParameter, // Find text within paragraph to apply style
|
|
117
|
-
z.object({ // Target by specific index within the paragraph
|
|
118
|
-
indexWithinParagraph: z.number().int().min(1).describe("An index located anywhere within the target paragraph.")
|
|
119
|
-
})
|
|
120
|
-
]).describe("Specify the target paragraph either by start/end indices, by finding text within it, or by providing an index within it."),
|
|
121
|
-
style: ParagraphStyleParameters.refine(
|
|
122
|
-
styleArgs => Object.values(styleArgs).some(v => v !== undefined),
|
|
123
|
-
{ message: "At least one paragraph style option must be provided." }
|
|
124
|
-
).describe("The paragraph styling to apply.")
|
|
125
|
-
});
|
|
126
|
-
export type ApplyParagraphStyleToolArgs = z.infer<typeof ApplyParagraphStyleToolParameters>;
|
|
127
|
-
|
|
128
|
-
// --- Error Class ---
|
|
129
|
-
// Use FastMCP's UserError for client-facing issues
|
|
130
|
-
// Define a custom error for internal issues if needed
|
|
131
|
-
export class NotImplementedError extends Error {
|
|
132
|
-
constructor(message = "This feature is not yet implemented.") {
|
|
133
|
-
super(message);
|
|
134
|
-
this.name = "NotImplementedError";
|
|
135
|
-
}
|
|
136
|
-
}
|
package/tests/helpers.test.js
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
// tests/helpers.test.js
|
|
2
|
-
import { findTextRange } from '../dist/googleDocsApiHelpers.js';
|
|
3
|
-
import assert from 'node:assert';
|
|
4
|
-
import { describe, it, mock } from 'node:test';
|
|
5
|
-
|
|
6
|
-
describe('Text Range Finding', () => {
|
|
7
|
-
// Test hypothesis 1: Text range finding works correctly
|
|
8
|
-
|
|
9
|
-
describe('findTextRange', () => {
|
|
10
|
-
it('should find text within a single text run correctly', async () => {
|
|
11
|
-
// Mock the docs.documents.get method to return a predefined structure
|
|
12
|
-
const mockDocs = {
|
|
13
|
-
documents: {
|
|
14
|
-
get: mock.fn(async () => ({
|
|
15
|
-
data: {
|
|
16
|
-
body: {
|
|
17
|
-
content: [
|
|
18
|
-
{
|
|
19
|
-
paragraph: {
|
|
20
|
-
elements: [
|
|
21
|
-
{
|
|
22
|
-
startIndex: 1,
|
|
23
|
-
endIndex: 25,
|
|
24
|
-
textRun: {
|
|
25
|
-
content: 'This is a test sentence.'
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
]
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
]
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}))
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
// Test finding "test" in the sample text
|
|
39
|
-
const result = await findTextRange(mockDocs, 'doc123', 'test', 1);
|
|
40
|
-
assert.deepStrictEqual(result, { startIndex: 11, endIndex: 15 });
|
|
41
|
-
|
|
42
|
-
// Verify the docs.documents.get was called with the right parameters
|
|
43
|
-
assert.strictEqual(mockDocs.documents.get.mock.calls.length, 1);
|
|
44
|
-
assert.deepStrictEqual(
|
|
45
|
-
mockDocs.documents.get.mock.calls[0].arguments[0],
|
|
46
|
-
{
|
|
47
|
-
documentId: 'doc123',
|
|
48
|
-
fields: 'body(content(paragraph(elements(startIndex,endIndex,textRun(content)))))'
|
|
49
|
-
}
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('should find the nth instance of text correctly', async () => {
|
|
54
|
-
// Mock with a document that has repeated text
|
|
55
|
-
const mockDocs = {
|
|
56
|
-
documents: {
|
|
57
|
-
get: mock.fn(async () => ({
|
|
58
|
-
data: {
|
|
59
|
-
body: {
|
|
60
|
-
content: [
|
|
61
|
-
{
|
|
62
|
-
paragraph: {
|
|
63
|
-
elements: [
|
|
64
|
-
{
|
|
65
|
-
startIndex: 1,
|
|
66
|
-
endIndex: 41,
|
|
67
|
-
textRun: {
|
|
68
|
-
content: 'Test test test. This is a test sentence.'
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
]
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
]
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}))
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
// Find the 3rd instance of "test"
|
|
82
|
-
const result = await findTextRange(mockDocs, 'doc123', 'test', 3);
|
|
83
|
-
assert.deepStrictEqual(result, { startIndex: 27, endIndex: 31 });
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('should return null if text is not found', async () => {
|
|
87
|
-
const mockDocs = {
|
|
88
|
-
documents: {
|
|
89
|
-
get: mock.fn(async () => ({
|
|
90
|
-
data: {
|
|
91
|
-
body: {
|
|
92
|
-
content: [
|
|
93
|
-
{
|
|
94
|
-
paragraph: {
|
|
95
|
-
elements: [
|
|
96
|
-
{
|
|
97
|
-
startIndex: 1,
|
|
98
|
-
endIndex: 25,
|
|
99
|
-
textRun: {
|
|
100
|
-
content: 'This is a sample sentence.'
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
]
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
]
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}))
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
// Try to find text that doesn't exist
|
|
114
|
-
const result = await findTextRange(mockDocs, 'doc123', 'test', 1);
|
|
115
|
-
assert.strictEqual(result, null);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('should handle text spanning multiple text runs', async () => {
|
|
119
|
-
const mockDocs = {
|
|
120
|
-
documents: {
|
|
121
|
-
get: mock.fn(async () => ({
|
|
122
|
-
data: {
|
|
123
|
-
body: {
|
|
124
|
-
content: [
|
|
125
|
-
{
|
|
126
|
-
paragraph: {
|
|
127
|
-
elements: [
|
|
128
|
-
{
|
|
129
|
-
startIndex: 1,
|
|
130
|
-
endIndex: 6,
|
|
131
|
-
textRun: {
|
|
132
|
-
content: 'This '
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
startIndex: 6,
|
|
137
|
-
endIndex: 11,
|
|
138
|
-
textRun: {
|
|
139
|
-
content: 'is a '
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
startIndex: 11,
|
|
144
|
-
endIndex: 20,
|
|
145
|
-
textRun: {
|
|
146
|
-
content: 'test case'
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
]
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
]
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}))
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
// Find text that spans runs: "a test"
|
|
160
|
-
const result = await findTextRange(mockDocs, 'doc123', 'a test', 1);
|
|
161
|
-
assert.deepStrictEqual(result, { startIndex: 9, endIndex: 15 });
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
});
|
package/tests/types.test.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
// tests/types.test.js
|
|
2
|
-
import { hexToRgbColor, validateHexColor } from '../dist/types.js';
|
|
3
|
-
import assert from 'node:assert';
|
|
4
|
-
import { describe, it } from 'node:test';
|
|
5
|
-
|
|
6
|
-
describe('Color Validation and Conversion', () => {
|
|
7
|
-
// Test hypothesis 3: Hex color validation and conversion
|
|
8
|
-
|
|
9
|
-
describe('validateHexColor', () => {
|
|
10
|
-
it('should validate correct hex colors with hash', () => {
|
|
11
|
-
assert.strictEqual(validateHexColor('#FF0000'), true); // 6 digits red
|
|
12
|
-
assert.strictEqual(validateHexColor('#F00'), true); // 3 digits red
|
|
13
|
-
assert.strictEqual(validateHexColor('#00FF00'), true); // 6 digits green
|
|
14
|
-
assert.strictEqual(validateHexColor('#0F0'), true); // 3 digits green
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('should validate correct hex colors without hash', () => {
|
|
18
|
-
assert.strictEqual(validateHexColor('FF0000'), true); // 6 digits red
|
|
19
|
-
assert.strictEqual(validateHexColor('F00'), true); // 3 digits red
|
|
20
|
-
assert.strictEqual(validateHexColor('00FF00'), true); // 6 digits green
|
|
21
|
-
assert.strictEqual(validateHexColor('0F0'), true); // 3 digits green
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('should reject invalid hex colors', () => {
|
|
25
|
-
assert.strictEqual(validateHexColor(''), false); // Empty
|
|
26
|
-
assert.strictEqual(validateHexColor('#XYZ'), false); // Invalid characters
|
|
27
|
-
assert.strictEqual(validateHexColor('#12345'), false); // Invalid length (5)
|
|
28
|
-
assert.strictEqual(validateHexColor('#1234567'), false);// Invalid length (7)
|
|
29
|
-
assert.strictEqual(validateHexColor('invalid'), false); // Not a hex color
|
|
30
|
-
assert.strictEqual(validateHexColor('#12'), false); // Too short
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
describe('hexToRgbColor', () => {
|
|
35
|
-
it('should convert 6-digit hex colors with hash correctly', () => {
|
|
36
|
-
const result = hexToRgbColor('#FF0000');
|
|
37
|
-
assert.deepStrictEqual(result, { red: 1, green: 0, blue: 0 }); // Red
|
|
38
|
-
|
|
39
|
-
const resultGreen = hexToRgbColor('#00FF00');
|
|
40
|
-
assert.deepStrictEqual(resultGreen, { red: 0, green: 1, blue: 0 }); // Green
|
|
41
|
-
|
|
42
|
-
const resultBlue = hexToRgbColor('#0000FF');
|
|
43
|
-
assert.deepStrictEqual(resultBlue, { red: 0, green: 0, blue: 1 }); // Blue
|
|
44
|
-
|
|
45
|
-
const resultPurple = hexToRgbColor('#800080');
|
|
46
|
-
assert.deepStrictEqual(resultPurple, { red: 0.5019607843137255, green: 0, blue: 0.5019607843137255 }); // Purple
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should convert 3-digit hex colors correctly', () => {
|
|
50
|
-
const result = hexToRgbColor('#F00');
|
|
51
|
-
assert.deepStrictEqual(result, { red: 1, green: 0, blue: 0 }); // Red from shorthand
|
|
52
|
-
|
|
53
|
-
const resultWhite = hexToRgbColor('#FFF');
|
|
54
|
-
assert.deepStrictEqual(resultWhite, { red: 1, green: 1, blue: 1 }); // White from shorthand
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('should convert hex colors without hash correctly', () => {
|
|
58
|
-
const result = hexToRgbColor('FF0000');
|
|
59
|
-
assert.deepStrictEqual(result, { red: 1, green: 0, blue: 0 }); // Red without hash
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('should return null for invalid hex colors', () => {
|
|
63
|
-
assert.strictEqual(hexToRgbColor(''), null); // Empty
|
|
64
|
-
assert.strictEqual(hexToRgbColor('#XYZ'), null); // Invalid characters
|
|
65
|
-
assert.strictEqual(hexToRgbColor('#12345'), null); // Invalid length
|
|
66
|
-
assert.strictEqual(hexToRgbColor('invalid'), null); // Not a hex color
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
// tsconfig.json
|
|
2
|
-
{
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"target": "ES2022",
|
|
5
|
-
"module": "NodeNext",
|
|
6
|
-
"moduleResolution": "NodeNext",
|
|
7
|
-
"outDir": "./dist",
|
|
8
|
-
"rootDir": "./src",
|
|
9
|
-
"strict": true,
|
|
10
|
-
"esModuleInterop": true,
|
|
11
|
-
"skipLibCheck": true,
|
|
12
|
-
"forceConsistentCasingInFileNames": true,
|
|
13
|
-
"resolveJsonModule": true
|
|
14
|
-
},
|
|
15
|
-
"include": ["src/**/*"],
|
|
16
|
-
"exclude": ["node_modules"]
|
|
17
|
-
}
|
package/vscode.md
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
# VS Code Integration Guide
|
|
2
|
-
|
|
3
|
-
This guide shows you how to integrate the Ultimate Google Docs & Drive MCP Server with VS Code using the MCP extension.
|
|
4
|
-
|
|
5
|
-
## Prerequisites
|
|
6
|
-
|
|
7
|
-
Before setting up VS Code integration, make sure you have:
|
|
8
|
-
|
|
9
|
-
1. **Completed the main setup** - Follow the [README.md](README.md) setup instructions first
|
|
10
|
-
2. **VS Code installed** - Download from [code.visualstudio.com](https://code.visualstudio.com/)
|
|
11
|
-
3. **Working MCP server** - Verify your server works with Claude Desktop first
|
|
12
|
-
|
|
13
|
-
## Installation
|
|
14
|
-
|
|
15
|
-
### Step 1: Install the MCP Extension
|
|
16
|
-
|
|
17
|
-
1. Open VS Code
|
|
18
|
-
2. Go to Extensions (Ctrl+Shift+X / Cmd+Shift+X)
|
|
19
|
-
3. Search for "MCP" or "Model Context Protocol"
|
|
20
|
-
4. Install the official MCP extension
|
|
21
|
-
|
|
22
|
-
### Step 2: Configure the MCP Server
|
|
23
|
-
|
|
24
|
-
1. Open VS Code Settings (Ctrl+, / Cmd+,)
|
|
25
|
-
2. Search for "MCP" in settings
|
|
26
|
-
3. Find "MCP: Servers" configuration
|
|
27
|
-
4. Add a new server configuration:
|
|
28
|
-
|
|
29
|
-
```json
|
|
30
|
-
{
|
|
31
|
-
"google-docs-drive": {
|
|
32
|
-
"command": "node",
|
|
33
|
-
"args": ["${workspaceFolder}/dist/server.js"],
|
|
34
|
-
"env": {
|
|
35
|
-
"NODE_ENV": "production"
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### Step 3: Verify Configuration
|
|
42
|
-
|
|
43
|
-
1. Open the Command Palette (Ctrl+Shift+P / Cmd+Shift+P)
|
|
44
|
-
2. Type "MCP: Restart Servers" and run it
|
|
45
|
-
3. Check the Output panel and select "MCP" from the dropdown
|
|
46
|
-
4. You should see your server connecting successfully
|
|
47
|
-
|
|
48
|
-
## Usage
|
|
49
|
-
|
|
50
|
-
Once configured, you can use the MCP server with AI assistants in VS Code:
|
|
51
|
-
|
|
52
|
-
### Document Operations
|
|
53
|
-
|
|
54
|
-
```
|
|
55
|
-
"List my recent Google Docs from the last 7 days"
|
|
56
|
-
"Read the content of document ID: 1ABC..."
|
|
57
|
-
"Create a new document called 'Project Notes' in my Work folder"
|
|
58
|
-
"Search for documents containing 'meeting notes'"
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### File Management
|
|
62
|
-
|
|
63
|
-
```
|
|
64
|
-
"Show me the contents of my root Drive folder"
|
|
65
|
-
"Create a folder called 'Project X' in folder ID: 1DEF..."
|
|
66
|
-
"Move document ID: 1GHI... to the Project X folder"
|
|
67
|
-
"Copy my template document and rename it to 'New Report'"
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### Document Editing
|
|
71
|
-
|
|
72
|
-
```
|
|
73
|
-
"Add a heading 'Summary' to the beginning of document ID: 1JKL..."
|
|
74
|
-
"Format all text containing 'important' as bold in my document"
|
|
75
|
-
"Insert a table with 3 columns and 5 rows at the end of the document"
|
|
76
|
-
"Apply paragraph formatting to make all headings centered"
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
## Troubleshooting
|
|
80
|
-
|
|
81
|
-
### Server Not Starting
|
|
82
|
-
|
|
83
|
-
1. **Check the path** - Ensure the absolute path in your configuration is correct
|
|
84
|
-
2. **Verify build** - Run `npm run build` in your project directory
|
|
85
|
-
3. **Check permissions** - Ensure `token.json` and `credentials.json` exist and are readable
|
|
86
|
-
|
|
87
|
-
### Authentication Issues
|
|
88
|
-
|
|
89
|
-
1. **Re-authorize** - Delete `token.json` and run the server manually once:
|
|
90
|
-
```bash
|
|
91
|
-
cd /path/to/your/google-docs-mcp
|
|
92
|
-
node dist/server.js
|
|
93
|
-
```
|
|
94
|
-
2. **Follow the authorization flow** again
|
|
95
|
-
3. **Restart VS Code** after successful authorization
|
|
96
|
-
|
|
97
|
-
### Tool Not Found Errors
|
|
98
|
-
|
|
99
|
-
1. **Restart MCP servers** using Command Palette
|
|
100
|
-
2. **Check server logs** in VS Code Output panel (MCP channel)
|
|
101
|
-
|
|
102
|
-
## Available Tools
|
|
103
|
-
|
|
104
|
-
The server provides these tools in VS Code:
|
|
105
|
-
|
|
106
|
-
### Document Discovery
|
|
107
|
-
- `listGoogleDocs` - List documents with filtering
|
|
108
|
-
- `searchGoogleDocs` - Search by name/content
|
|
109
|
-
- `getRecentGoogleDocs` - Get recently modified docs
|
|
110
|
-
- `getDocumentInfo` - Get detailed document metadata
|
|
111
|
-
|
|
112
|
-
### Document Editing
|
|
113
|
-
- `readGoogleDoc` - Read document content
|
|
114
|
-
- `appendToGoogleDoc` - Add text to end
|
|
115
|
-
- `insertText` - Insert at specific position
|
|
116
|
-
- `deleteRange` - Remove content
|
|
117
|
-
- `applyTextStyle` - Format text (bold, italic, colors)
|
|
118
|
-
- `applyParagraphStyle` - Format paragraphs (alignment, spacing)
|
|
119
|
-
- `formatMatchingText` - Find and format text
|
|
120
|
-
- `insertTable` - Create tables
|
|
121
|
-
- `insertPageBreak` - Add page breaks
|
|
122
|
-
|
|
123
|
-
### File Management
|
|
124
|
-
- `createFolder` - Create new folders
|
|
125
|
-
- `listFolderContents` - List folder contents
|
|
126
|
-
- `getFolderInfo` - Get folder metadata
|
|
127
|
-
- `moveFile` - Move files/folders
|
|
128
|
-
- `copyFile` - Copy files/folders
|
|
129
|
-
- `renameFile` - Rename files/folders
|
|
130
|
-
- `deleteFile` - Delete files/folders
|
|
131
|
-
- `createDocument` - Create new documents
|
|
132
|
-
- `createFromTemplate` - Create from templates
|
|
133
|
-
|
|
134
|
-
## Tips for Better Integration
|
|
135
|
-
|
|
136
|
-
1. **Use specific document IDs** - More reliable than document names
|
|
137
|
-
2. **Combine operations** - Create and format documents in single requests
|
|
138
|
-
3. **Check tool results** - Review what was actually done before proceeding
|
|
139
|
-
4. **Use templates** - Create template documents for consistent formatting
|
|
140
|
-
|
|
141
|
-
## Security Notes
|
|
142
|
-
|
|
143
|
-
- The server uses OAuth 2.0 for secure authentication
|
|
144
|
-
- Credentials are stored locally in `token.json` and `credentials.json`
|
|
145
|
-
- Never share these files or commit them to version control
|
|
146
|
-
- The server only has access to your Google Drive, not other Google services
|
|
147
|
-
|
|
148
|
-
## Example Workflows
|
|
149
|
-
|
|
150
|
-
### Create a Formatted Report
|
|
151
|
-
|
|
152
|
-
```
|
|
153
|
-
1. "Create a new document called 'Monthly Report' in my Reports folder"
|
|
154
|
-
2. "Add the title 'Monthly Performance Report' as a centered Heading 1"
|
|
155
|
-
3. "Insert a table with 4 columns and 6 rows for the data"
|
|
156
|
-
4. "Add section headings for Executive Summary, Key Metrics, and Action Items"
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### Organize Project Documents
|
|
160
|
-
|
|
161
|
-
```
|
|
162
|
-
1. "Create a folder called 'Q1 Project' in my Work folder"
|
|
163
|
-
2. "Search for all documents containing 'Q1' in the title"
|
|
164
|
-
3. "Move the found documents to the Q1 Project folder"
|
|
165
|
-
4. "Create a new document called 'Q1 Project Overview' in that folder"
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
This integration brings the full power of Google Docs and Drive management directly into your VS Code workflow!
|