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.
- package/LICENSE +21 -0
- package/README.md +161 -0
- package/dist/auth.d.ts +6 -0
- package/dist/auth.js +127 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +261 -0
- package/dist/tools/docs.d.ts +4 -0
- package/dist/tools/docs.js +96 -0
- package/dist/tools/drive.d.ts +4 -0
- package/dist/tools/drive.js +61 -0
- package/dist/tools/sheets.d.ts +15 -0
- package/dist/tools/sheets.js +81 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.js +11 -0
- package/package.json +37 -0
- package/src/auth.ts +145 -0
- package/src/index.ts +302 -0
- package/src/tools/docs.ts +121 -0
- package/src/tools/drive.ts +80 -0
- package/src/tools/sheets.ts +121 -0
- package/src/types.ts +45 -0
- package/tsconfig.json +17 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# MCP Google Docs
|
|
2
|
+
|
|
3
|
+
[English](#english) | [中文](#中文)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## English
|
|
8
|
+
|
|
9
|
+
Lightweight MCP Server for querying, reading, and modifying Google Drive documents and spreadsheets.
|
|
10
|
+
|
|
11
|
+
### Setup
|
|
12
|
+
|
|
13
|
+
#### 1. Create Google Cloud Project
|
|
14
|
+
|
|
15
|
+
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
|
16
|
+
2. Create a new project
|
|
17
|
+
3. Enable Google Drive API, Google Docs API, Google Sheets API
|
|
18
|
+
4. Create OAuth 2.0 credentials (Desktop app type)
|
|
19
|
+
5. Save your Client ID and Client Secret
|
|
20
|
+
|
|
21
|
+
#### 2. Configure MCP Client
|
|
22
|
+
|
|
23
|
+
Add to your Claude Desktop config:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"mcpServers": {
|
|
28
|
+
"google-docs": {
|
|
29
|
+
"command": "npx",
|
|
30
|
+
"args": ["-y", "mcp-google-docs"],
|
|
31
|
+
"env": {
|
|
32
|
+
"GOOGLE_CLIENT_ID": "your_client_id",
|
|
33
|
+
"GOOGLE_CLIENT_SECRET": "your_client_secret"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
#### 3. First Use
|
|
41
|
+
|
|
42
|
+
Call `auth_login` tool, browser will open Google authorization page.
|
|
43
|
+
|
|
44
|
+
### Tools
|
|
45
|
+
|
|
46
|
+
| Tool | Description |
|
|
47
|
+
|------|-------------|
|
|
48
|
+
| `auth_login` | OAuth login |
|
|
49
|
+
| `auth_status` | Check auth status |
|
|
50
|
+
| `gdrive_list` | List files |
|
|
51
|
+
| `gdrive_search` | Search files |
|
|
52
|
+
| `gdrive_info` | Get file info |
|
|
53
|
+
| `gdocs_read` | Read document |
|
|
54
|
+
| `gdocs_insert` | Insert text |
|
|
55
|
+
| `gdocs_append` | Append text |
|
|
56
|
+
| `gdocs_replace` | Find and replace |
|
|
57
|
+
| `gsheets_info` | Get spreadsheet info |
|
|
58
|
+
| `gsheets_read` | Read sheet |
|
|
59
|
+
| `gsheets_update` | Update cells |
|
|
60
|
+
| `gsheets_batch_update` | Batch update |
|
|
61
|
+
| `gsheets_append_row` | Append row |
|
|
62
|
+
|
|
63
|
+
### Prompt Template
|
|
64
|
+
|
|
65
|
+
```markdown
|
|
66
|
+
## Google Docs MCP Rules
|
|
67
|
+
|
|
68
|
+
### Workflow
|
|
69
|
+
1. Run auth_login first if not authenticated
|
|
70
|
+
2. Use gdrive_search or gdrive_list to find files
|
|
71
|
+
3. Use read/update tools to operate
|
|
72
|
+
|
|
73
|
+
### Required
|
|
74
|
+
- Always read before modifying
|
|
75
|
+
- Use batch_update for bulk operations
|
|
76
|
+
|
|
77
|
+
### Forbidden
|
|
78
|
+
- Never guess file IDs, always search first
|
|
79
|
+
- Never update more than 100 cells at once
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 中文
|
|
85
|
+
|
|
86
|
+
轻量级 MCP Server,支持查询、阅读、修改 Google Drive 中的文档和表格。
|
|
87
|
+
|
|
88
|
+
### 安装配置
|
|
89
|
+
|
|
90
|
+
#### 1. 创建 Google Cloud 项目
|
|
91
|
+
|
|
92
|
+
1. 访问 [Google Cloud Console](https://console.cloud.google.com/)
|
|
93
|
+
2. 创建新项目
|
|
94
|
+
3. 启用 Google Drive API、Google Docs API、Google Sheets API
|
|
95
|
+
4. 创建 OAuth 2.0 凭据(桌面应用类型)
|
|
96
|
+
5. 记录 Client ID 和 Client Secret
|
|
97
|
+
|
|
98
|
+
#### 2. 配置 MCP 客户端
|
|
99
|
+
|
|
100
|
+
在 Claude Desktop 配置文件中添加:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"mcpServers": {
|
|
105
|
+
"google-docs": {
|
|
106
|
+
"command": "npx",
|
|
107
|
+
"args": ["-y", "mcp-google-docs"],
|
|
108
|
+
"env": {
|
|
109
|
+
"GOOGLE_CLIENT_ID": "your_client_id",
|
|
110
|
+
"GOOGLE_CLIENT_SECRET": "your_client_secret"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### 3. 首次使用
|
|
118
|
+
|
|
119
|
+
调用 `auth_login` 工具,浏览器会弹出 Google 授权页面,授权后即可使用。
|
|
120
|
+
|
|
121
|
+
### 工具列表
|
|
122
|
+
|
|
123
|
+
| 工具 | 说明 |
|
|
124
|
+
|------|------|
|
|
125
|
+
| `auth_login` | OAuth 授权登录 |
|
|
126
|
+
| `auth_status` | 检查授权状态 |
|
|
127
|
+
| `gdrive_list` | 列出文件 |
|
|
128
|
+
| `gdrive_search` | 搜索文件 |
|
|
129
|
+
| `gdrive_info` | 获取文件信息 |
|
|
130
|
+
| `gdocs_read` | 读取文档 |
|
|
131
|
+
| `gdocs_insert` | 插入文本 |
|
|
132
|
+
| `gdocs_append` | 追加文本 |
|
|
133
|
+
| `gdocs_replace` | 查找替换 |
|
|
134
|
+
| `gsheets_info` | 获取表格信息 |
|
|
135
|
+
| `gsheets_read` | 读取表格 |
|
|
136
|
+
| `gsheets_update` | 更新单元格 |
|
|
137
|
+
| `gsheets_batch_update` | 批量更新 |
|
|
138
|
+
| `gsheets_append_row` | 追加行 |
|
|
139
|
+
|
|
140
|
+
### 使用提示词
|
|
141
|
+
|
|
142
|
+
```markdown
|
|
143
|
+
## Google Docs MCP 使用规则
|
|
144
|
+
|
|
145
|
+
### 流程
|
|
146
|
+
1. 首次使用先调用 auth_login 授权
|
|
147
|
+
2. 用 gdrive_search 或 gdrive_list 找到目标文件
|
|
148
|
+
3. 用对应的 read/update 工具操作
|
|
149
|
+
|
|
150
|
+
### 强制
|
|
151
|
+
- 修改前必须先读取确认内容
|
|
152
|
+
- 批量操作使用 batch_update
|
|
153
|
+
|
|
154
|
+
### 禁止
|
|
155
|
+
- 不要猜测文件ID,必须先搜索
|
|
156
|
+
- 不要一次修改超过100个单元格
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { OAuth2Client } from 'google-auth-library';
|
|
2
|
+
import { GoogleClients } from './types.js';
|
|
3
|
+
export declare function authenticate(): Promise<OAuth2Client>;
|
|
4
|
+
export declare function startAuthFlow(): Promise<string>;
|
|
5
|
+
export declare function getClients(): Promise<GoogleClients>;
|
|
6
|
+
export declare function isAuthenticated(): boolean;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as http from 'http';
|
|
5
|
+
import { URL } from 'url';
|
|
6
|
+
import open from 'open';
|
|
7
|
+
import { SCOPES } from './types.js';
|
|
8
|
+
const TOKEN_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.mcp-google-docs');
|
|
9
|
+
const TOKEN_PATH = path.join(TOKEN_DIR, 'token.json');
|
|
10
|
+
let oauth2Client = null;
|
|
11
|
+
function getCredentials() {
|
|
12
|
+
const clientId = process.env.GOOGLE_CLIENT_ID;
|
|
13
|
+
const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
|
|
14
|
+
if (!clientId || !clientSecret) {
|
|
15
|
+
throw new Error('Missing GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET environment variables');
|
|
16
|
+
}
|
|
17
|
+
return { clientId, clientSecret };
|
|
18
|
+
}
|
|
19
|
+
function createOAuth2Client() {
|
|
20
|
+
const { clientId, clientSecret } = getCredentials();
|
|
21
|
+
return new google.auth.OAuth2(clientId, clientSecret, 'http://localhost:3000/callback');
|
|
22
|
+
}
|
|
23
|
+
function loadToken() {
|
|
24
|
+
try {
|
|
25
|
+
if (fs.existsSync(TOKEN_PATH)) {
|
|
26
|
+
const content = fs.readFileSync(TOKEN_PATH, 'utf-8');
|
|
27
|
+
return JSON.parse(content);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.error('Error loading token:', error);
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function saveToken(token) {
|
|
36
|
+
try {
|
|
37
|
+
if (!fs.existsSync(TOKEN_DIR)) {
|
|
38
|
+
fs.mkdirSync(TOKEN_DIR, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
fs.writeFileSync(TOKEN_PATH, JSON.stringify(token, null, 2));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
console.error('Error saving token:', error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function authenticate() {
|
|
47
|
+
if (oauth2Client) {
|
|
48
|
+
return oauth2Client;
|
|
49
|
+
}
|
|
50
|
+
oauth2Client = createOAuth2Client();
|
|
51
|
+
const token = loadToken();
|
|
52
|
+
if (token) {
|
|
53
|
+
oauth2Client.setCredentials(token);
|
|
54
|
+
if (token.expiry_date && token.expiry_date < Date.now()) {
|
|
55
|
+
try {
|
|
56
|
+
const { credentials } = await oauth2Client.refreshAccessToken();
|
|
57
|
+
oauth2Client.setCredentials(credentials);
|
|
58
|
+
saveToken(credentials);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
oauth2Client = null;
|
|
62
|
+
throw new Error('Token expired. Please run auth_login again.');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return oauth2Client;
|
|
66
|
+
}
|
|
67
|
+
throw new Error('Not authenticated. Please run auth_login first.');
|
|
68
|
+
}
|
|
69
|
+
export async function startAuthFlow() {
|
|
70
|
+
const client = createOAuth2Client();
|
|
71
|
+
const authUrl = client.generateAuthUrl({
|
|
72
|
+
access_type: 'offline',
|
|
73
|
+
scope: SCOPES,
|
|
74
|
+
prompt: 'consent',
|
|
75
|
+
});
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const server = http.createServer(async (req, res) => {
|
|
78
|
+
try {
|
|
79
|
+
const url = new URL(req.url, `http://localhost:3000`);
|
|
80
|
+
if (url.pathname === '/callback') {
|
|
81
|
+
const code = url.searchParams.get('code');
|
|
82
|
+
if (code) {
|
|
83
|
+
const { tokens } = await client.getToken(code);
|
|
84
|
+
client.setCredentials(tokens);
|
|
85
|
+
saveToken(tokens);
|
|
86
|
+
oauth2Client = client;
|
|
87
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
88
|
+
res.end('<html><body><h1>授权成功!</h1><p>您可以关闭此窗口。</p></body></html>');
|
|
89
|
+
server.close();
|
|
90
|
+
resolve('Authentication successful! Token saved.');
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
94
|
+
res.end('<html><body><h1>授权失败</h1></body></html>');
|
|
95
|
+
server.close();
|
|
96
|
+
reject(new Error('No authorization code received'));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
102
|
+
res.end('<html><body><h1>错误</h1></body></html>');
|
|
103
|
+
server.close();
|
|
104
|
+
reject(error);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
server.listen(3000, async () => {
|
|
108
|
+
console.error('Opening browser for authentication...');
|
|
109
|
+
await open(authUrl);
|
|
110
|
+
});
|
|
111
|
+
setTimeout(() => {
|
|
112
|
+
server.close();
|
|
113
|
+
reject(new Error('Authentication timeout (60s)'));
|
|
114
|
+
}, 60000);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
export async function getClients() {
|
|
118
|
+
const auth = await authenticate();
|
|
119
|
+
return {
|
|
120
|
+
drive: google.drive({ version: 'v3', auth }),
|
|
121
|
+
docs: google.docs({ version: 'v1', auth }),
|
|
122
|
+
sheets: google.sheets({ version: 'v4', auth }),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export function isAuthenticated() {
|
|
126
|
+
return loadToken() !== null;
|
|
127
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { startAuthFlow, isAuthenticated } from './auth.js';
|
|
6
|
+
import { listFiles, searchFiles, getFileInfo } from './tools/drive.js';
|
|
7
|
+
import { readDocument, insertText, replaceText, appendText } from './tools/docs.js';
|
|
8
|
+
import { readSheet, getSheetInfo, updateCell, updateRange, batchUpdate, appendRow } from './tools/sheets.js';
|
|
9
|
+
const server = new Server({ name: 'mcp-google-docs', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
10
|
+
// Tool definitions
|
|
11
|
+
const TOOLS = [
|
|
12
|
+
{
|
|
13
|
+
name: 'auth_login',
|
|
14
|
+
description: 'Authenticate with Google. Opens browser for OAuth login. Run this first if not authenticated.',
|
|
15
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'auth_status',
|
|
19
|
+
description: 'Check if authenticated with Google.',
|
|
20
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'gdrive_list',
|
|
24
|
+
description: 'List files in Google Drive. Optionally filter by folder.',
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
folderId: { type: 'string', description: 'Folder ID to list files from (optional)' },
|
|
29
|
+
pageSize: { type: 'number', description: 'Number of files to return (default: 20)' },
|
|
30
|
+
},
|
|
31
|
+
required: [],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'gdrive_search',
|
|
36
|
+
description: 'Search for files in Google Drive by name.',
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
query: { type: 'string', description: 'Search query (file name)' },
|
|
41
|
+
fileType: { type: 'string', enum: ['document', 'spreadsheet', 'folder'], description: 'Filter by file type' },
|
|
42
|
+
pageSize: { type: 'number', description: 'Number of results (default: 20)' },
|
|
43
|
+
},
|
|
44
|
+
required: ['query'],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'gdrive_info',
|
|
49
|
+
description: 'Get information about a specific file.',
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: { fileId: { type: 'string', description: 'File ID' } },
|
|
53
|
+
required: ['fileId'],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'gdocs_read',
|
|
58
|
+
description: 'Read content from a Google Doc.',
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: { documentId: { type: 'string', description: 'Document ID' } },
|
|
62
|
+
required: ['documentId'],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'gdocs_insert',
|
|
67
|
+
description: 'Insert text into a Google Doc at a specific position.',
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
documentId: { type: 'string', description: 'Document ID' },
|
|
72
|
+
text: { type: 'string', description: 'Text to insert' },
|
|
73
|
+
index: { type: 'number', description: 'Position to insert (default: 1, start of doc)' },
|
|
74
|
+
},
|
|
75
|
+
required: ['documentId', 'text'],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'gdocs_append',
|
|
80
|
+
description: 'Append text to the end of a Google Doc.',
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: 'object',
|
|
83
|
+
properties: {
|
|
84
|
+
documentId: { type: 'string', description: 'Document ID' },
|
|
85
|
+
text: { type: 'string', description: 'Text to append' },
|
|
86
|
+
},
|
|
87
|
+
required: ['documentId', 'text'],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'gdocs_replace',
|
|
92
|
+
description: 'Find and replace text in a Google Doc.',
|
|
93
|
+
inputSchema: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
properties: {
|
|
96
|
+
documentId: { type: 'string', description: 'Document ID' },
|
|
97
|
+
searchText: { type: 'string', description: 'Text to find' },
|
|
98
|
+
replaceText: { type: 'string', description: 'Text to replace with' },
|
|
99
|
+
matchCase: { type: 'boolean', description: 'Case sensitive (default: false)' },
|
|
100
|
+
},
|
|
101
|
+
required: ['documentId', 'searchText', 'replaceText'],
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'gsheets_info',
|
|
106
|
+
description: 'Get spreadsheet info including sheet names.',
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: { spreadsheetId: { type: 'string', description: 'Spreadsheet ID' } },
|
|
110
|
+
required: ['spreadsheetId'],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'gsheets_read',
|
|
115
|
+
description: 'Read data from a Google Sheet range.',
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: 'object',
|
|
118
|
+
properties: {
|
|
119
|
+
spreadsheetId: { type: 'string', description: 'Spreadsheet ID' },
|
|
120
|
+
range: { type: 'string', description: 'Range in A1 notation (e.g., Sheet1!A1:D10)' },
|
|
121
|
+
},
|
|
122
|
+
required: ['spreadsheetId', 'range'],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: 'gsheets_update',
|
|
127
|
+
description: 'Update a single cell or range in a Google Sheet.',
|
|
128
|
+
inputSchema: {
|
|
129
|
+
type: 'object',
|
|
130
|
+
properties: {
|
|
131
|
+
spreadsheetId: { type: 'string', description: 'Spreadsheet ID' },
|
|
132
|
+
range: { type: 'string', description: 'Range in A1 notation (e.g., Sheet1!A1)' },
|
|
133
|
+
value: { type: 'string', description: 'Value to set (for single cell)' },
|
|
134
|
+
values: { type: 'array', description: 'Array of arrays for range update' },
|
|
135
|
+
},
|
|
136
|
+
required: ['spreadsheetId', 'range'],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: 'gsheets_batch_update',
|
|
141
|
+
description: 'Batch update multiple cells in a Google Sheet.',
|
|
142
|
+
inputSchema: {
|
|
143
|
+
type: 'object',
|
|
144
|
+
properties: {
|
|
145
|
+
spreadsheetId: { type: 'string', description: 'Spreadsheet ID' },
|
|
146
|
+
sheetName: { type: 'string', description: 'Sheet name' },
|
|
147
|
+
updates: {
|
|
148
|
+
type: 'array',
|
|
149
|
+
description: 'Array of {row, col, value} objects. Row/col are 1-indexed.',
|
|
150
|
+
items: {
|
|
151
|
+
type: 'object',
|
|
152
|
+
properties: {
|
|
153
|
+
row: { type: 'number' },
|
|
154
|
+
col: { type: 'number' },
|
|
155
|
+
value: { type: ['string', 'number', 'boolean'] },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
required: ['spreadsheetId', 'sheetName', 'updates'],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'gsheets_append_row',
|
|
165
|
+
description: 'Append a new row to a Google Sheet.',
|
|
166
|
+
inputSchema: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
properties: {
|
|
169
|
+
spreadsheetId: { type: 'string', description: 'Spreadsheet ID' },
|
|
170
|
+
sheetName: { type: 'string', description: 'Sheet name' },
|
|
171
|
+
values: { type: 'array', description: 'Array of values for the new row' },
|
|
172
|
+
},
|
|
173
|
+
required: ['spreadsheetId', 'sheetName', 'values'],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
178
|
+
// Tool call handler
|
|
179
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
180
|
+
const { name, arguments: args } = request.params;
|
|
181
|
+
try {
|
|
182
|
+
switch (name) {
|
|
183
|
+
case 'auth_login': {
|
|
184
|
+
const result = await startAuthFlow();
|
|
185
|
+
return { content: [{ type: 'text', text: result }] };
|
|
186
|
+
}
|
|
187
|
+
case 'auth_status': {
|
|
188
|
+
const authenticated = isAuthenticated();
|
|
189
|
+
return {
|
|
190
|
+
content: [{ type: 'text', text: authenticated ? 'Authenticated' : 'Not authenticated. Run auth_login first.' }]
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
case 'gdrive_list': {
|
|
194
|
+
const files = await listFiles(args?.folderId, args?.pageSize);
|
|
195
|
+
return { content: [{ type: 'text', text: JSON.stringify(files, null, 2) }] };
|
|
196
|
+
}
|
|
197
|
+
case 'gdrive_search': {
|
|
198
|
+
const files = await searchFiles(args.query, args?.fileType, args?.pageSize);
|
|
199
|
+
return { content: [{ type: 'text', text: JSON.stringify(files, null, 2) }] };
|
|
200
|
+
}
|
|
201
|
+
case 'gdrive_info': {
|
|
202
|
+
const info = await getFileInfo(args.fileId);
|
|
203
|
+
return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] };
|
|
204
|
+
}
|
|
205
|
+
case 'gdocs_read': {
|
|
206
|
+
const content = await readDocument(args.documentId);
|
|
207
|
+
return { content: [{ type: 'text', text: content }] };
|
|
208
|
+
}
|
|
209
|
+
case 'gdocs_insert': {
|
|
210
|
+
await insertText(args.documentId, args.text, args?.index);
|
|
211
|
+
return { content: [{ type: 'text', text: 'Text inserted successfully' }] };
|
|
212
|
+
}
|
|
213
|
+
case 'gdocs_append': {
|
|
214
|
+
await appendText(args.documentId, args.text);
|
|
215
|
+
return { content: [{ type: 'text', text: 'Text appended successfully' }] };
|
|
216
|
+
}
|
|
217
|
+
case 'gdocs_replace': {
|
|
218
|
+
const count = await replaceText(args.documentId, args.searchText, args.replaceText, args?.matchCase);
|
|
219
|
+
return { content: [{ type: 'text', text: `Replaced ${count} occurrence(s)` }] };
|
|
220
|
+
}
|
|
221
|
+
case 'gsheets_info': {
|
|
222
|
+
const info = await getSheetInfo(args.spreadsheetId);
|
|
223
|
+
return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] };
|
|
224
|
+
}
|
|
225
|
+
case 'gsheets_read': {
|
|
226
|
+
const data = await readSheet(args.spreadsheetId, args.range);
|
|
227
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
228
|
+
}
|
|
229
|
+
case 'gsheets_update': {
|
|
230
|
+
if (args?.values) {
|
|
231
|
+
await updateRange(args.spreadsheetId, args.range, args.values);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
await updateCell(args.spreadsheetId, args.range, args.value);
|
|
235
|
+
}
|
|
236
|
+
return { content: [{ type: 'text', text: 'Updated successfully' }] };
|
|
237
|
+
}
|
|
238
|
+
case 'gsheets_batch_update': {
|
|
239
|
+
const count = await batchUpdate(args.spreadsheetId, args.sheetName, args.updates);
|
|
240
|
+
return { content: [{ type: 'text', text: `Updated ${count} cell(s)` }] };
|
|
241
|
+
}
|
|
242
|
+
case 'gsheets_append_row': {
|
|
243
|
+
await appendRow(args.spreadsheetId, args.sheetName, args.values);
|
|
244
|
+
return { content: [{ type: 'text', text: 'Row appended successfully' }] };
|
|
245
|
+
}
|
|
246
|
+
default:
|
|
247
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
252
|
+
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
// Start server
|
|
256
|
+
async function main() {
|
|
257
|
+
const transport = new StdioServerTransport();
|
|
258
|
+
await server.connect(transport);
|
|
259
|
+
console.error('MCP Google Docs server running on stdio');
|
|
260
|
+
}
|
|
261
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function readDocument(documentId: string): Promise<string>;
|
|
2
|
+
export declare function insertText(documentId: string, text: string, index?: number): Promise<void>;
|
|
3
|
+
export declare function replaceText(documentId: string, searchText: string, replaceText: string, matchCase?: boolean): Promise<number>;
|
|
4
|
+
export declare function appendText(documentId: string, text: string): Promise<void>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { getClients } from '../auth.js';
|
|
2
|
+
export async function readDocument(documentId) {
|
|
3
|
+
const { docs } = await getClients();
|
|
4
|
+
const response = await docs.documents.get({ documentId });
|
|
5
|
+
const document = response.data;
|
|
6
|
+
let text = '';
|
|
7
|
+
if (document.body?.content) {
|
|
8
|
+
for (const element of document.body.content) {
|
|
9
|
+
if (element.paragraph?.elements) {
|
|
10
|
+
for (const elem of element.paragraph.elements) {
|
|
11
|
+
if (elem.textRun?.content) {
|
|
12
|
+
text += elem.textRun.content;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (element.table) {
|
|
17
|
+
text += '\n[TABLE]\n';
|
|
18
|
+
for (const row of element.table.tableRows || []) {
|
|
19
|
+
const cells = [];
|
|
20
|
+
for (const cell of row.tableCells || []) {
|
|
21
|
+
let cellText = '';
|
|
22
|
+
for (const content of cell.content || []) {
|
|
23
|
+
if (content.paragraph?.elements) {
|
|
24
|
+
for (const elem of content.paragraph.elements) {
|
|
25
|
+
if (elem.textRun?.content) {
|
|
26
|
+
cellText += elem.textRun.content.trim();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
cells.push(cellText);
|
|
32
|
+
}
|
|
33
|
+
text += cells.join(' | ') + '\n';
|
|
34
|
+
}
|
|
35
|
+
text += '[/TABLE]\n';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return text;
|
|
40
|
+
}
|
|
41
|
+
export async function insertText(documentId, text, index) {
|
|
42
|
+
const { docs } = await getClients();
|
|
43
|
+
const insertIndex = index ?? 1;
|
|
44
|
+
await docs.documents.batchUpdate({
|
|
45
|
+
documentId,
|
|
46
|
+
requestBody: {
|
|
47
|
+
requests: [
|
|
48
|
+
{
|
|
49
|
+
insertText: {
|
|
50
|
+
location: { index: insertIndex },
|
|
51
|
+
text,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export async function replaceText(documentId, searchText, replaceText, matchCase = false) {
|
|
59
|
+
const { docs } = await getClients();
|
|
60
|
+
const response = await docs.documents.batchUpdate({
|
|
61
|
+
documentId,
|
|
62
|
+
requestBody: {
|
|
63
|
+
requests: [
|
|
64
|
+
{
|
|
65
|
+
replaceAllText: {
|
|
66
|
+
containsText: {
|
|
67
|
+
text: searchText,
|
|
68
|
+
matchCase,
|
|
69
|
+
},
|
|
70
|
+
replaceText,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
const replies = response.data.replies || [];
|
|
77
|
+
return replies[0]?.replaceAllText?.occurrencesChanged || 0;
|
|
78
|
+
}
|
|
79
|
+
export async function appendText(documentId, text) {
|
|
80
|
+
const { docs } = await getClients();
|
|
81
|
+
const doc = await docs.documents.get({ documentId });
|
|
82
|
+
const endIndex = doc.data.body?.content?.slice(-1)[0]?.endIndex || 1;
|
|
83
|
+
await docs.documents.batchUpdate({
|
|
84
|
+
documentId,
|
|
85
|
+
requestBody: {
|
|
86
|
+
requests: [
|
|
87
|
+
{
|
|
88
|
+
insertText: {
|
|
89
|
+
location: { index: endIndex - 1 },
|
|
90
|
+
text,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { DriveFile } from '../types.js';
|
|
2
|
+
export declare function listFiles(folderId?: string, pageSize?: number): Promise<DriveFile[]>;
|
|
3
|
+
export declare function searchFiles(query: string, fileType?: 'document' | 'spreadsheet' | 'folder', pageSize?: number): Promise<DriveFile[]>;
|
|
4
|
+
export declare function getFileInfo(fileId: string): Promise<DriveFile>;
|