mcp-google-docs 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.
@@ -0,0 +1,61 @@
1
+ import { getClients } from '../auth.js';
2
+ import { MIME_TYPES } from '../types.js';
3
+ export async function listFiles(folderId, pageSize = 20) {
4
+ const { drive } = await getClients();
5
+ let query = 'trashed = false';
6
+ if (folderId) {
7
+ query += ` and '${folderId}' in parents`;
8
+ }
9
+ const response = await drive.files.list({
10
+ q: query,
11
+ pageSize,
12
+ fields: 'files(id, name, mimeType, modifiedTime, webViewLink)',
13
+ orderBy: 'modifiedTime desc',
14
+ });
15
+ return (response.data.files || []).map(file => ({
16
+ id: file.id,
17
+ name: file.name,
18
+ mimeType: file.mimeType,
19
+ modifiedTime: file.modifiedTime || undefined,
20
+ webViewLink: file.webViewLink || undefined,
21
+ }));
22
+ }
23
+ export async function searchFiles(query, fileType, pageSize = 20) {
24
+ const { drive } = await getClients();
25
+ let searchQuery = `name contains '${query}' and trashed = false`;
26
+ if (fileType) {
27
+ const mimeType = fileType === 'document'
28
+ ? MIME_TYPES.DOCUMENT
29
+ : fileType === 'spreadsheet'
30
+ ? MIME_TYPES.SPREADSHEET
31
+ : MIME_TYPES.FOLDER;
32
+ searchQuery += ` and mimeType = '${mimeType}'`;
33
+ }
34
+ const response = await drive.files.list({
35
+ q: searchQuery,
36
+ pageSize,
37
+ fields: 'files(id, name, mimeType, modifiedTime, webViewLink)',
38
+ orderBy: 'modifiedTime desc',
39
+ });
40
+ return (response.data.files || []).map(file => ({
41
+ id: file.id,
42
+ name: file.name,
43
+ mimeType: file.mimeType,
44
+ modifiedTime: file.modifiedTime || undefined,
45
+ webViewLink: file.webViewLink || undefined,
46
+ }));
47
+ }
48
+ export async function getFileInfo(fileId) {
49
+ const { drive } = await getClients();
50
+ const response = await drive.files.get({
51
+ fileId,
52
+ fields: 'id, name, mimeType, modifiedTime, webViewLink',
53
+ });
54
+ return {
55
+ id: response.data.id,
56
+ name: response.data.name,
57
+ mimeType: response.data.mimeType,
58
+ modifiedTime: response.data.modifiedTime || undefined,
59
+ webViewLink: response.data.webViewLink || undefined,
60
+ };
61
+ }
@@ -0,0 +1,15 @@
1
+ import { CellUpdate } from '../types.js';
2
+ export declare function readSheet(spreadsheetId: string, range: string): Promise<any[][]>;
3
+ export declare function getSheetInfo(spreadsheetId: string): Promise<{
4
+ title: string;
5
+ sheets: {
6
+ id: number;
7
+ title: string;
8
+ rowCount: number;
9
+ columnCount: number;
10
+ }[];
11
+ }>;
12
+ export declare function updateCell(spreadsheetId: string, range: string, value: string | number | boolean): Promise<void>;
13
+ export declare function updateRange(spreadsheetId: string, range: string, values: any[][]): Promise<void>;
14
+ export declare function batchUpdate(spreadsheetId: string, sheetName: string, updates: CellUpdate[]): Promise<number>;
15
+ export declare function appendRow(spreadsheetId: string, sheetName: string, values: any[]): Promise<void>;
@@ -0,0 +1,81 @@
1
+ import { getClients } from '../auth.js';
2
+ export async function readSheet(spreadsheetId, range) {
3
+ const { sheets } = await getClients();
4
+ const response = await sheets.spreadsheets.values.get({
5
+ spreadsheetId,
6
+ range,
7
+ });
8
+ return response.data.values || [];
9
+ }
10
+ export async function getSheetInfo(spreadsheetId) {
11
+ const { sheets } = await getClients();
12
+ const response = await sheets.spreadsheets.get({
13
+ spreadsheetId,
14
+ fields: 'properties.title,sheets.properties',
15
+ });
16
+ return {
17
+ title: response.data.properties?.title || '',
18
+ sheets: (response.data.sheets || []).map(sheet => ({
19
+ id: sheet.properties?.sheetId || 0,
20
+ title: sheet.properties?.title || '',
21
+ rowCount: sheet.properties?.gridProperties?.rowCount || 0,
22
+ columnCount: sheet.properties?.gridProperties?.columnCount || 0,
23
+ })),
24
+ };
25
+ }
26
+ export async function updateCell(spreadsheetId, range, value) {
27
+ const { sheets } = await getClients();
28
+ await sheets.spreadsheets.values.update({
29
+ spreadsheetId,
30
+ range,
31
+ valueInputOption: 'USER_ENTERED',
32
+ requestBody: {
33
+ values: [[value]],
34
+ },
35
+ });
36
+ }
37
+ export async function updateRange(spreadsheetId, range, values) {
38
+ const { sheets } = await getClients();
39
+ await sheets.spreadsheets.values.update({
40
+ spreadsheetId,
41
+ range,
42
+ valueInputOption: 'USER_ENTERED',
43
+ requestBody: { values },
44
+ });
45
+ }
46
+ export async function batchUpdate(spreadsheetId, sheetName, updates) {
47
+ const { sheets } = await getClients();
48
+ const data = updates.map(update => ({
49
+ range: `${sheetName}!${columnToLetter(update.col)}${update.row}`,
50
+ values: [[update.value]],
51
+ }));
52
+ const response = await sheets.spreadsheets.values.batchUpdate({
53
+ spreadsheetId,
54
+ requestBody: {
55
+ valueInputOption: 'USER_ENTERED',
56
+ data,
57
+ },
58
+ });
59
+ return response.data.totalUpdatedCells || 0;
60
+ }
61
+ export async function appendRow(spreadsheetId, sheetName, values) {
62
+ const { sheets } = await getClients();
63
+ await sheets.spreadsheets.values.append({
64
+ spreadsheetId,
65
+ range: `${sheetName}!A:A`,
66
+ valueInputOption: 'USER_ENTERED',
67
+ insertDataOption: 'INSERT_ROWS',
68
+ requestBody: {
69
+ values: [values],
70
+ },
71
+ });
72
+ }
73
+ function columnToLetter(col) {
74
+ let letter = '';
75
+ while (col > 0) {
76
+ const mod = (col - 1) % 26;
77
+ letter = String.fromCharCode(65 + mod) + letter;
78
+ col = Math.floor((col - 1) / 26);
79
+ }
80
+ return letter;
81
+ }
@@ -0,0 +1,33 @@
1
+ import { drive_v3, docs_v1, sheets_v4 } from 'googleapis';
2
+ export interface GoogleClients {
3
+ drive: drive_v3.Drive;
4
+ docs: docs_v1.Docs;
5
+ sheets: sheets_v4.Sheets;
6
+ }
7
+ export interface DriveFile {
8
+ id: string;
9
+ name: string;
10
+ mimeType: string;
11
+ modifiedTime?: string;
12
+ webViewLink?: string;
13
+ }
14
+ export interface SheetRange {
15
+ spreadsheetId: string;
16
+ range: string;
17
+ }
18
+ export interface CellUpdate {
19
+ row: number;
20
+ col: number;
21
+ value: string | number | boolean;
22
+ }
23
+ export interface BatchUpdateRequest {
24
+ spreadsheetId: string;
25
+ sheetName: string;
26
+ updates: CellUpdate[];
27
+ }
28
+ export declare const SCOPES: string[];
29
+ export declare const MIME_TYPES: {
30
+ readonly FOLDER: "application/vnd.google-apps.folder";
31
+ readonly DOCUMENT: "application/vnd.google-apps.document";
32
+ readonly SPREADSHEET: "application/vnd.google-apps.spreadsheet";
33
+ };
package/dist/types.js ADDED
@@ -0,0 +1,11 @@
1
+ export const SCOPES = [
2
+ 'https://www.googleapis.com/auth/drive.readonly',
3
+ 'https://www.googleapis.com/auth/drive.file',
4
+ 'https://www.googleapis.com/auth/documents',
5
+ 'https://www.googleapis.com/auth/spreadsheets',
6
+ ];
7
+ export const MIME_TYPES = {
8
+ FOLDER: 'application/vnd.google-apps.folder',
9
+ DOCUMENT: 'application/vnd.google-apps.document',
10
+ SPREADSHEET: 'application/vnd.google-apps.spreadsheet',
11
+ };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "mcp-google-docs",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Google Drive, Docs and Sheets",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "mcp-google-docs": "./dist/index.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js",
13
+ "dev": "tsc && node dist/index.js"
14
+ },
15
+ "keywords": [
16
+ "mcp",
17
+ "google-drive",
18
+ "google-docs",
19
+ "google-sheets",
20
+ "ai"
21
+ ],
22
+ "author": "",
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "@modelcontextprotocol/sdk": "^1.0.0",
26
+ "googleapis": "^140.0.0",
27
+ "open": "^10.0.0",
28
+ "zod": "^3.23.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^20.0.0",
32
+ "typescript": "^5.0.0"
33
+ },
34
+ "engines": {
35
+ "node": ">=18.0.0"
36
+ }
37
+ }
package/src/auth.ts ADDED
@@ -0,0 +1,145 @@
1
+ import { google } from 'googleapis';
2
+ import { OAuth2Client } from 'google-auth-library';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import * as http from 'http';
6
+ import { URL } from 'url';
7
+ import open from 'open';
8
+ import { SCOPES, GoogleClients } from './types.js';
9
+
10
+ const TOKEN_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.mcp-google-docs');
11
+ const TOKEN_PATH = path.join(TOKEN_DIR, 'token.json');
12
+
13
+ let oauth2Client: OAuth2Client | null = null;
14
+
15
+ function getCredentials(): { clientId: string; clientSecret: string } {
16
+ const clientId = process.env.GOOGLE_CLIENT_ID;
17
+ const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
18
+
19
+ if (!clientId || !clientSecret) {
20
+ throw new Error('Missing GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET environment variables');
21
+ }
22
+
23
+ return { clientId, clientSecret };
24
+ }
25
+
26
+ function createOAuth2Client(): OAuth2Client {
27
+ const { clientId, clientSecret } = getCredentials();
28
+ return new google.auth.OAuth2(clientId, clientSecret, 'http://localhost:3000/callback');
29
+ }
30
+
31
+ function loadToken(): any | null {
32
+ try {
33
+ if (fs.existsSync(TOKEN_PATH)) {
34
+ const content = fs.readFileSync(TOKEN_PATH, 'utf-8');
35
+ return JSON.parse(content);
36
+ }
37
+ } catch (error) {
38
+ console.error('Error loading token:', error);
39
+ }
40
+ return null;
41
+ }
42
+
43
+ function saveToken(token: any): void {
44
+ try {
45
+ if (!fs.existsSync(TOKEN_DIR)) {
46
+ fs.mkdirSync(TOKEN_DIR, { recursive: true });
47
+ }
48
+ fs.writeFileSync(TOKEN_PATH, JSON.stringify(token, null, 2));
49
+ } catch (error) {
50
+ console.error('Error saving token:', error);
51
+ }
52
+ }
53
+
54
+ export async function authenticate(): Promise<OAuth2Client> {
55
+ if (oauth2Client) {
56
+ return oauth2Client;
57
+ }
58
+
59
+ oauth2Client = createOAuth2Client();
60
+ const token = loadToken();
61
+
62
+ if (token) {
63
+ oauth2Client.setCredentials(token);
64
+
65
+ if (token.expiry_date && token.expiry_date < Date.now()) {
66
+ try {
67
+ const { credentials } = await oauth2Client.refreshAccessToken();
68
+ oauth2Client.setCredentials(credentials);
69
+ saveToken(credentials);
70
+ } catch (error) {
71
+ oauth2Client = null;
72
+ throw new Error('Token expired. Please run auth_login again.');
73
+ }
74
+ }
75
+ return oauth2Client;
76
+ }
77
+
78
+ throw new Error('Not authenticated. Please run auth_login first.');
79
+ }
80
+
81
+ export async function startAuthFlow(): Promise<string> {
82
+ const client = createOAuth2Client();
83
+
84
+ const authUrl = client.generateAuthUrl({
85
+ access_type: 'offline',
86
+ scope: SCOPES,
87
+ prompt: 'consent',
88
+ });
89
+
90
+ return new Promise((resolve, reject) => {
91
+ const server = http.createServer(async (req, res) => {
92
+ try {
93
+ const url = new URL(req.url!, `http://localhost:3000`);
94
+ if (url.pathname === '/callback') {
95
+ const code = url.searchParams.get('code');
96
+ if (code) {
97
+ const { tokens } = await client.getToken(code);
98
+ client.setCredentials(tokens);
99
+ saveToken(tokens);
100
+ oauth2Client = client;
101
+
102
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
103
+ res.end('<html><body><h1>授权成功!</h1><p>您可以关闭此窗口。</p></body></html>');
104
+
105
+ server.close();
106
+ resolve('Authentication successful! Token saved.');
107
+ } else {
108
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
109
+ res.end('<html><body><h1>授权失败</h1></body></html>');
110
+ server.close();
111
+ reject(new Error('No authorization code received'));
112
+ }
113
+ }
114
+ } catch (error) {
115
+ res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
116
+ res.end('<html><body><h1>错误</h1></body></html>');
117
+ server.close();
118
+ reject(error);
119
+ }
120
+ });
121
+
122
+ server.listen(3000, async () => {
123
+ console.error('Opening browser for authentication...');
124
+ await open(authUrl);
125
+ });
126
+
127
+ setTimeout(() => {
128
+ server.close();
129
+ reject(new Error('Authentication timeout (60s)'));
130
+ }, 60000);
131
+ });
132
+ }
133
+
134
+ export async function getClients(): Promise<GoogleClients> {
135
+ const auth = await authenticate();
136
+ return {
137
+ drive: google.drive({ version: 'v3', auth }),
138
+ docs: google.docs({ version: 'v1', auth }),
139
+ sheets: google.sheets({ version: 'v4', auth }),
140
+ };
141
+ }
142
+
143
+ export function isAuthenticated(): boolean {
144
+ return loadToken() !== null;
145
+ }
package/src/index.ts ADDED
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ } from '@modelcontextprotocol/sdk/types.js';
9
+ import { z } from 'zod';
10
+
11
+ import { startAuthFlow, isAuthenticated } from './auth.js';
12
+ import { listFiles, searchFiles, getFileInfo } from './tools/drive.js';
13
+ import { readDocument, insertText, replaceText, appendText } from './tools/docs.js';
14
+ import { readSheet, getSheetInfo, updateCell, updateRange, batchUpdate, appendRow } from './tools/sheets.js';
15
+
16
+ const server = new Server(
17
+ { name: 'mcp-google-docs', version: '1.0.0' },
18
+ { capabilities: { tools: {} } }
19
+ );
20
+
21
+ // Tool definitions
22
+ const TOOLS = [
23
+ {
24
+ name: 'auth_login',
25
+ description: 'Authenticate with Google. Opens browser for OAuth login. Run this first if not authenticated.',
26
+ inputSchema: { type: 'object', properties: {}, required: [] },
27
+ },
28
+ {
29
+ name: 'auth_status',
30
+ description: 'Check if authenticated with Google.',
31
+ inputSchema: { type: 'object', properties: {}, required: [] },
32
+ },
33
+ {
34
+ name: 'gdrive_list',
35
+ description: 'List files in Google Drive. Optionally filter by folder.',
36
+ inputSchema: {
37
+ type: 'object',
38
+ properties: {
39
+ folderId: { type: 'string', description: 'Folder ID to list files from (optional)' },
40
+ pageSize: { type: 'number', description: 'Number of files to return (default: 20)' },
41
+ },
42
+ required: [],
43
+ },
44
+ },
45
+ {
46
+ name: 'gdrive_search',
47
+ description: 'Search for files in Google Drive by name.',
48
+ inputSchema: {
49
+ type: 'object',
50
+ properties: {
51
+ query: { type: 'string', description: 'Search query (file name)' },
52
+ fileType: { type: 'string', enum: ['document', 'spreadsheet', 'folder'], description: 'Filter by file type' },
53
+ pageSize: { type: 'number', description: 'Number of results (default: 20)' },
54
+ },
55
+ required: ['query'],
56
+ },
57
+ },
58
+ {
59
+ name: 'gdrive_info',
60
+ description: 'Get information about a specific file.',
61
+ inputSchema: {
62
+ type: 'object',
63
+ properties: { fileId: { type: 'string', description: 'File ID' } },
64
+ required: ['fileId'],
65
+ },
66
+ },
67
+ {
68
+ name: 'gdocs_read',
69
+ description: 'Read content from a Google Doc.',
70
+ inputSchema: {
71
+ type: 'object',
72
+ properties: { documentId: { type: 'string', description: 'Document ID' } },
73
+ required: ['documentId'],
74
+ },
75
+ },
76
+ {
77
+ name: 'gdocs_insert',
78
+ description: 'Insert text into a Google Doc at a specific position.',
79
+ inputSchema: {
80
+ type: 'object',
81
+ properties: {
82
+ documentId: { type: 'string', description: 'Document ID' },
83
+ text: { type: 'string', description: 'Text to insert' },
84
+ index: { type: 'number', description: 'Position to insert (default: 1, start of doc)' },
85
+ },
86
+ required: ['documentId', 'text'],
87
+ },
88
+ },
89
+ {
90
+ name: 'gdocs_append',
91
+ description: 'Append text to the end of a Google Doc.',
92
+ inputSchema: {
93
+ type: 'object',
94
+ properties: {
95
+ documentId: { type: 'string', description: 'Document ID' },
96
+ text: { type: 'string', description: 'Text to append' },
97
+ },
98
+ required: ['documentId', 'text'],
99
+ },
100
+ },
101
+ {
102
+ name: 'gdocs_replace',
103
+ description: 'Find and replace text in a Google Doc.',
104
+ inputSchema: {
105
+ type: 'object',
106
+ properties: {
107
+ documentId: { type: 'string', description: 'Document ID' },
108
+ searchText: { type: 'string', description: 'Text to find' },
109
+ replaceText: { type: 'string', description: 'Text to replace with' },
110
+ matchCase: { type: 'boolean', description: 'Case sensitive (default: false)' },
111
+ },
112
+ required: ['documentId', 'searchText', 'replaceText'],
113
+ },
114
+ },
115
+ {
116
+ name: 'gsheets_info',
117
+ description: 'Get spreadsheet info including sheet names.',
118
+ inputSchema: {
119
+ type: 'object',
120
+ properties: { spreadsheetId: { type: 'string', description: 'Spreadsheet ID' } },
121
+ required: ['spreadsheetId'],
122
+ },
123
+ },
124
+ {
125
+ name: 'gsheets_read',
126
+ description: 'Read data from a Google Sheet range.',
127
+ inputSchema: {
128
+ type: 'object',
129
+ properties: {
130
+ spreadsheetId: { type: 'string', description: 'Spreadsheet ID' },
131
+ range: { type: 'string', description: 'Range in A1 notation (e.g., Sheet1!A1:D10)' },
132
+ },
133
+ required: ['spreadsheetId', 'range'],
134
+ },
135
+ },
136
+ {
137
+ name: 'gsheets_update',
138
+ description: 'Update a single cell or range in a Google Sheet.',
139
+ inputSchema: {
140
+ type: 'object',
141
+ properties: {
142
+ spreadsheetId: { type: 'string', description: 'Spreadsheet ID' },
143
+ range: { type: 'string', description: 'Range in A1 notation (e.g., Sheet1!A1)' },
144
+ value: { type: 'string', description: 'Value to set (for single cell)' },
145
+ values: { type: 'array', description: 'Array of arrays for range update' },
146
+ },
147
+ required: ['spreadsheetId', 'range'],
148
+ },
149
+ },
150
+ {
151
+ name: 'gsheets_batch_update',
152
+ description: 'Batch update multiple cells in a Google Sheet.',
153
+ inputSchema: {
154
+ type: 'object',
155
+ properties: {
156
+ spreadsheetId: { type: 'string', description: 'Spreadsheet ID' },
157
+ sheetName: { type: 'string', description: 'Sheet name' },
158
+ updates: {
159
+ type: 'array',
160
+ description: 'Array of {row, col, value} objects. Row/col are 1-indexed.',
161
+ items: {
162
+ type: 'object',
163
+ properties: {
164
+ row: { type: 'number' },
165
+ col: { type: 'number' },
166
+ value: { type: ['string', 'number', 'boolean'] },
167
+ },
168
+ },
169
+ },
170
+ },
171
+ required: ['spreadsheetId', 'sheetName', 'updates'],
172
+ },
173
+ },
174
+ {
175
+ name: 'gsheets_append_row',
176
+ description: 'Append a new row to a Google Sheet.',
177
+ inputSchema: {
178
+ type: 'object',
179
+ properties: {
180
+ spreadsheetId: { type: 'string', description: 'Spreadsheet ID' },
181
+ sheetName: { type: 'string', description: 'Sheet name' },
182
+ values: { type: 'array', description: 'Array of values for the new row' },
183
+ },
184
+ required: ['spreadsheetId', 'sheetName', 'values'],
185
+ },
186
+ },
187
+ ];
188
+
189
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
190
+
191
+ // Tool call handler
192
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
193
+ const { name, arguments: args } = request.params;
194
+
195
+ try {
196
+ switch (name) {
197
+ case 'auth_login': {
198
+ const result = await startAuthFlow();
199
+ return { content: [{ type: 'text', text: result }] };
200
+ }
201
+
202
+ case 'auth_status': {
203
+ const authenticated = isAuthenticated();
204
+ return {
205
+ content: [{ type: 'text', text: authenticated ? 'Authenticated' : 'Not authenticated. Run auth_login first.' }]
206
+ };
207
+ }
208
+
209
+ case 'gdrive_list': {
210
+ const files = await listFiles(args?.folderId as string, args?.pageSize as number);
211
+ return { content: [{ type: 'text', text: JSON.stringify(files, null, 2) }] };
212
+ }
213
+
214
+ case 'gdrive_search': {
215
+ const files = await searchFiles(
216
+ args!.query as string,
217
+ args?.fileType as 'document' | 'spreadsheet' | 'folder',
218
+ args?.pageSize as number
219
+ );
220
+ return { content: [{ type: 'text', text: JSON.stringify(files, null, 2) }] };
221
+ }
222
+
223
+ case 'gdrive_info': {
224
+ const info = await getFileInfo(args!.fileId as string);
225
+ return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] };
226
+ }
227
+
228
+ case 'gdocs_read': {
229
+ const content = await readDocument(args!.documentId as string);
230
+ return { content: [{ type: 'text', text: content }] };
231
+ }
232
+
233
+ case 'gdocs_insert': {
234
+ await insertText(args!.documentId as string, args!.text as string, args?.index as number);
235
+ return { content: [{ type: 'text', text: 'Text inserted successfully' }] };
236
+ }
237
+
238
+ case 'gdocs_append': {
239
+ await appendText(args!.documentId as string, args!.text as string);
240
+ return { content: [{ type: 'text', text: 'Text appended successfully' }] };
241
+ }
242
+
243
+ case 'gdocs_replace': {
244
+ const count = await replaceText(
245
+ args!.documentId as string,
246
+ args!.searchText as string,
247
+ args!.replaceText as string,
248
+ args?.matchCase as boolean
249
+ );
250
+ return { content: [{ type: 'text', text: `Replaced ${count} occurrence(s)` }] };
251
+ }
252
+
253
+ case 'gsheets_info': {
254
+ const info = await getSheetInfo(args!.spreadsheetId as string);
255
+ return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] };
256
+ }
257
+
258
+ case 'gsheets_read': {
259
+ const data = await readSheet(args!.spreadsheetId as string, args!.range as string);
260
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
261
+ }
262
+
263
+ case 'gsheets_update': {
264
+ if (args?.values) {
265
+ await updateRange(args!.spreadsheetId as string, args!.range as string, args.values as any[][]);
266
+ } else {
267
+ await updateCell(args!.spreadsheetId as string, args!.range as string, args!.value as string);
268
+ }
269
+ return { content: [{ type: 'text', text: 'Updated successfully' }] };
270
+ }
271
+
272
+ case 'gsheets_batch_update': {
273
+ const count = await batchUpdate(
274
+ args!.spreadsheetId as string,
275
+ args!.sheetName as string,
276
+ args!.updates as any[]
277
+ );
278
+ return { content: [{ type: 'text', text: `Updated ${count} cell(s)` }] };
279
+ }
280
+
281
+ case 'gsheets_append_row': {
282
+ await appendRow(args!.spreadsheetId as string, args!.sheetName as string, args!.values as any[]);
283
+ return { content: [{ type: 'text', text: 'Row appended successfully' }] };
284
+ }
285
+
286
+ default:
287
+ throw new Error(`Unknown tool: ${name}`);
288
+ }
289
+ } catch (error) {
290
+ const message = error instanceof Error ? error.message : String(error);
291
+ return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
292
+ }
293
+ });
294
+
295
+ // Start server
296
+ async function main() {
297
+ const transport = new StdioServerTransport();
298
+ await server.connect(transport);
299
+ console.error('MCP Google Docs server running on stdio');
300
+ }
301
+
302
+ main().catch(console.error);