@brainwavesio/google-docs-mcp 1.0.0 → 1.1.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/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
- }
@@ -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
- });
@@ -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!