gdocs-mcp 0.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/LICENSE +21 -0
- package/README.md +94 -0
- package/bin/cli.js +11 -0
- package/dist/auth/cli.d.ts +6 -0
- package/dist/auth/cli.js +121 -0
- package/dist/auth/google-auth.d.ts +9 -0
- package/dist/auth/google-auth.js +51 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +75 -0
- package/dist/tools/create-document.d.ts +10 -0
- package/dist/tools/create-document.js +37 -0
- package/dist/tools/get-charts.d.ts +9 -0
- package/dist/tools/get-charts.js +30 -0
- package/dist/tools/read-document.d.ts +10 -0
- package/dist/tools/read-document.js +46 -0
- package/dist/tools/replace-all-text.d.ts +11 -0
- package/dist/tools/replace-all-text.js +34 -0
- package/dist/tools/replace-image.d.ts +11 -0
- package/dist/tools/replace-image.js +30 -0
- package/dist/tools/search-documents.d.ts +14 -0
- package/dist/tools/search-documents.js +37 -0
- package/dist/tools/unmerge-table-cells.d.ts +14 -0
- package/dist/tools/unmerge-table-cells.js +40 -0
- package/dist/tools/update-document-markdown.d.ts +10 -0
- package/dist/tools/update-document-markdown.js +45 -0
- package/dist/tools/update-document-section-markdown.d.ts +14 -0
- package/dist/tools/update-document-section-markdown.js +43 -0
- package/dist/tools/update-document-style.d.ts +65 -0
- package/dist/tools/update-document-style.js +72 -0
- package/dist/tools/update-document.d.ts +10 -0
- package/dist/tools/update-document.js +31 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Apurwa Sarwajit
|
|
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,94 @@
|
|
|
1
|
+
# gdocs-mcp
|
|
2
|
+
|
|
3
|
+
Open-source MCP server for Google Docs and Sheets. Give Claude (or any MCP-compatible AI) the ability to read, create, edit, and search your Google Docs — with OAuth tokens that never leave your machine.
|
|
4
|
+
|
|
5
|
+
**Like Composio, but open-source and self-hosted.**
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
### 1. Set up Google Cloud credentials (one-time)
|
|
10
|
+
|
|
11
|
+
1. Go to [Google Cloud Console](https://console.cloud.google.com)
|
|
12
|
+
2. Create a project (or use an existing one)
|
|
13
|
+
3. Enable the **Google Docs API**, **Google Sheets API**, and **Google Drive API**
|
|
14
|
+
4. Go to **Google Auth Platform** > **Audience**, set to External, add yourself as a test user
|
|
15
|
+
5. Go to **Credentials** > **Create Credentials** > **OAuth client ID** > **Desktop app**
|
|
16
|
+
6. Download the JSON file
|
|
17
|
+
|
|
18
|
+
### 2. Authenticate
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx gdocs-mcp auth ~/Downloads/client_secret_*.json
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This opens your browser for Google sign-in, saves the token to `~/.gdocs-mcp/`, and prints the MCP config.
|
|
25
|
+
|
|
26
|
+
### 3. Add to Claude Desktop
|
|
27
|
+
|
|
28
|
+
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"mcpServers": {
|
|
33
|
+
"gdocs": {
|
|
34
|
+
"command": "npx",
|
|
35
|
+
"args": ["gdocs-mcp"]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Restart Claude Desktop. Ask Claude: *"List my recent Google Docs"* to verify.
|
|
42
|
+
|
|
43
|
+
### Add to Claude Code
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
claude mcp add gdocs -- npx gdocs-mcp
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Tools
|
|
50
|
+
|
|
51
|
+
### Google Docs
|
|
52
|
+
|
|
53
|
+
| Tool | Description |
|
|
54
|
+
|------|-------------|
|
|
55
|
+
| `read_document` | Read the full content of a Google Doc |
|
|
56
|
+
| `search_documents` | Search Google Drive for documents by name, content, or date |
|
|
57
|
+
| `create_document` | Create a new Google Doc with optional Markdown content |
|
|
58
|
+
| `replace_all_text` | Find and replace all occurrences of text in a document |
|
|
59
|
+
| `replace_image` | Replace an existing image with a new image from a URI |
|
|
60
|
+
| `update_document_markdown` | Replace the entire document body with Markdown |
|
|
61
|
+
| `update_document_section_markdown` | Insert or replace a section by index with Markdown |
|
|
62
|
+
| `update_document_style` | Update page size, margins, text direction |
|
|
63
|
+
| `update_document` | Raw batchUpdate with 35+ request types |
|
|
64
|
+
| `unmerge_table_cells` | Unmerge previously merged table cells |
|
|
65
|
+
|
|
66
|
+
### Google Sheets
|
|
67
|
+
|
|
68
|
+
| Tool | Description |
|
|
69
|
+
|------|-------------|
|
|
70
|
+
| `get_charts` | List all charts in a spreadsheet with IDs and specs |
|
|
71
|
+
|
|
72
|
+
## Security
|
|
73
|
+
|
|
74
|
+
- OAuth tokens are stored locally at `~/.gdocs-mcp/token.json` with `600` permissions
|
|
75
|
+
- Credentials are stored at `~/.gdocs-mcp/credentials.json` with `600` permissions
|
|
76
|
+
- No telemetry, no data collection, no third-party token storage
|
|
77
|
+
- Tokens refresh automatically; if refresh fails, run `npx gdocs-mcp auth` again
|
|
78
|
+
|
|
79
|
+
## Configuration
|
|
80
|
+
|
|
81
|
+
By default, credentials and tokens are stored in `~/.gdocs-mcp/`. Override with:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
GDOCS_CREDENTIALS=/path/to/credentials.json npx gdocs-mcp
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Requirements
|
|
88
|
+
|
|
89
|
+
- Node.js 18+
|
|
90
|
+
- A Google Cloud project with Docs, Sheets, and Drive APIs enabled
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const args = process.argv.slice(2);
|
|
4
|
+
|
|
5
|
+
if (args[0] === 'auth') {
|
|
6
|
+
const authArgs = args.slice(1);
|
|
7
|
+
process.argv = [process.argv[0], process.argv[1], ...authArgs];
|
|
8
|
+
await import('../dist/auth/cli.js');
|
|
9
|
+
} else {
|
|
10
|
+
await import('../dist/index.js');
|
|
11
|
+
}
|
package/dist/auth/cli.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* One-time OAuth flow. Run: npx gdocs-mcp auth
|
|
4
|
+
* Copies credentials to ~/.gdocs-mcp/ and saves OAuth token.
|
|
5
|
+
*/
|
|
6
|
+
import { google } from 'googleapis';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import http from 'http';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { CONFIG_DIR, TOKEN_PATH, CREDENTIALS_PATH_DEFAULT, getScopes } from './google-auth.js';
|
|
11
|
+
async function main() {
|
|
12
|
+
// Ensure config directory exists
|
|
13
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
14
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
15
|
+
}
|
|
16
|
+
// Find credentials.json
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
let inputCredPath = args[0];
|
|
19
|
+
if (!inputCredPath) {
|
|
20
|
+
// Check default location first
|
|
21
|
+
if (fs.existsSync(CREDENTIALS_PATH_DEFAULT)) {
|
|
22
|
+
console.log(`Using existing credentials at ${CREDENTIALS_PATH_DEFAULT}`);
|
|
23
|
+
inputCredPath = CREDENTIALS_PATH_DEFAULT;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
console.error('Usage: npx gdocs-mcp auth [path/to/credentials.json]');
|
|
27
|
+
console.error('');
|
|
28
|
+
console.error('Download credentials.json from Google Cloud Console:');
|
|
29
|
+
console.error(' 1. Go to console.cloud.google.com/apis/credentials');
|
|
30
|
+
console.error(' 2. Create OAuth 2.0 Client ID (Desktop app)');
|
|
31
|
+
console.error(' 3. Download JSON');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Resolve and validate credentials
|
|
36
|
+
const resolvedPath = path.resolve(inputCredPath.replace('~', process.env.HOME || ''));
|
|
37
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
38
|
+
console.error(`File not found: ${resolvedPath}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
const raw = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
|
|
42
|
+
const creds = raw.installed || raw.web;
|
|
43
|
+
if (!creds?.client_id || !creds?.client_secret) {
|
|
44
|
+
console.error('Invalid credentials file. Expected OAuth 2.0 client credentials with client_id and client_secret.');
|
|
45
|
+
console.error('Make sure you downloaded the OAuth client JSON, not an API key or service account.');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
// Copy credentials to config dir if not already there
|
|
49
|
+
if (resolvedPath !== CREDENTIALS_PATH_DEFAULT) {
|
|
50
|
+
fs.copyFileSync(resolvedPath, CREDENTIALS_PATH_DEFAULT);
|
|
51
|
+
fs.chmodSync(CREDENTIALS_PATH_DEFAULT, 0o600);
|
|
52
|
+
console.log(`Credentials copied to ${CREDENTIALS_PATH_DEFAULT}`);
|
|
53
|
+
}
|
|
54
|
+
// Set up OAuth client
|
|
55
|
+
const oAuth2Client = new google.auth.OAuth2(creds.client_id, creds.client_secret, 'http://localhost:3333');
|
|
56
|
+
const authUrl = oAuth2Client.generateAuthUrl({
|
|
57
|
+
access_type: 'offline',
|
|
58
|
+
scope: getScopes(),
|
|
59
|
+
prompt: 'consent',
|
|
60
|
+
});
|
|
61
|
+
console.log('\nOpening browser for Google authorization...');
|
|
62
|
+
// Start local server to catch the redirect
|
|
63
|
+
const server = http.createServer(async (req, res) => {
|
|
64
|
+
const reqUrl = new URL(req.url || '', 'http://localhost:3333');
|
|
65
|
+
const code = reqUrl.searchParams.get('code') || undefined;
|
|
66
|
+
if (!code) {
|
|
67
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
68
|
+
res.end('<h2>No authorization code received.</h2>');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
72
|
+
res.end(`
|
|
73
|
+
<html>
|
|
74
|
+
<body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0;">
|
|
75
|
+
<div style="text-align: center;">
|
|
76
|
+
<h2>Auth complete!</h2>
|
|
77
|
+
<p>You can close this tab and return to your terminal.</p>
|
|
78
|
+
</div>
|
|
79
|
+
</body>
|
|
80
|
+
</html>
|
|
81
|
+
`);
|
|
82
|
+
server.close();
|
|
83
|
+
try {
|
|
84
|
+
const { tokens } = await oAuth2Client.getToken(code);
|
|
85
|
+
fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2));
|
|
86
|
+
fs.chmodSync(TOKEN_PATH, 0o600);
|
|
87
|
+
console.log(`\nToken saved to ${TOKEN_PATH}`);
|
|
88
|
+
console.log('\nSetup complete! Add this to your MCP config:\n');
|
|
89
|
+
console.log(JSON.stringify({
|
|
90
|
+
mcpServers: {
|
|
91
|
+
gdocs: {
|
|
92
|
+
command: 'npx',
|
|
93
|
+
args: ['gdocs-mcp'],
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}, null, 2));
|
|
97
|
+
console.log('');
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
console.error('Failed to exchange authorization code:', err);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
server.listen(3333, async () => {
|
|
106
|
+
// Open browser
|
|
107
|
+
const { exec } = await import('child_process');
|
|
108
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
109
|
+
exec(`${cmd} "${authUrl}"`, (err) => {
|
|
110
|
+
if (err) {
|
|
111
|
+
console.log('\nCould not open browser automatically. Open this URL:\n');
|
|
112
|
+
console.log(authUrl);
|
|
113
|
+
console.log('');
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
main().catch((err) => {
|
|
119
|
+
console.error('Auth failed:', err.message);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { OAuth2Client } from 'google-auth-library';
|
|
2
|
+
declare const CONFIG_DIR: string;
|
|
3
|
+
declare const TOKEN_PATH: string;
|
|
4
|
+
declare const CREDENTIALS_PATH_DEFAULT: string;
|
|
5
|
+
export declare function getAuthClient(): OAuth2Client;
|
|
6
|
+
export declare function getConfigDir(): string;
|
|
7
|
+
export declare function getTokenPath(): string;
|
|
8
|
+
export declare function getScopes(): string[];
|
|
9
|
+
export { CONFIG_DIR, TOKEN_PATH, CREDENTIALS_PATH_DEFAULT };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
const CONFIG_DIR = path.join(process.env.HOME || '~', '.gdocs-mcp');
|
|
5
|
+
const TOKEN_PATH = path.join(CONFIG_DIR, 'token.json');
|
|
6
|
+
const CREDENTIALS_PATH_DEFAULT = path.join(CONFIG_DIR, 'credentials.json');
|
|
7
|
+
const SCOPES = [
|
|
8
|
+
'https://www.googleapis.com/auth/documents',
|
|
9
|
+
'https://www.googleapis.com/auth/spreadsheets.readonly',
|
|
10
|
+
'https://www.googleapis.com/auth/drive.readonly',
|
|
11
|
+
];
|
|
12
|
+
function getCredentialsPath() {
|
|
13
|
+
return process.env.GDOCS_CREDENTIALS || CREDENTIALS_PATH_DEFAULT;
|
|
14
|
+
}
|
|
15
|
+
function loadCredentials() {
|
|
16
|
+
const credPath = getCredentialsPath();
|
|
17
|
+
if (!fs.existsSync(credPath)) {
|
|
18
|
+
throw new Error(`Credentials not found at ${credPath}. Run: npx gdocs-mcp auth`);
|
|
19
|
+
}
|
|
20
|
+
const raw = JSON.parse(fs.readFileSync(credPath, 'utf8'));
|
|
21
|
+
const creds = raw.installed || raw.web;
|
|
22
|
+
if (!creds) {
|
|
23
|
+
throw new Error('Invalid credentials.json format. Expected "installed" or "web" key.');
|
|
24
|
+
}
|
|
25
|
+
return creds;
|
|
26
|
+
}
|
|
27
|
+
export function getAuthClient() {
|
|
28
|
+
const creds = loadCredentials();
|
|
29
|
+
const oAuth2Client = new google.auth.OAuth2(creds.client_id, creds.client_secret, 'http://localhost:3333');
|
|
30
|
+
if (!fs.existsSync(TOKEN_PATH)) {
|
|
31
|
+
throw new Error('Auth required. Run: npx gdocs-mcp auth');
|
|
32
|
+
}
|
|
33
|
+
const token = JSON.parse(fs.readFileSync(TOKEN_PATH, 'utf8'));
|
|
34
|
+
oAuth2Client.setCredentials(token);
|
|
35
|
+
oAuth2Client.on('tokens', (newTokens) => {
|
|
36
|
+
const existing = JSON.parse(fs.readFileSync(TOKEN_PATH, 'utf8'));
|
|
37
|
+
const merged = { ...existing, ...newTokens };
|
|
38
|
+
fs.writeFileSync(TOKEN_PATH, JSON.stringify(merged, null, 2));
|
|
39
|
+
});
|
|
40
|
+
return oAuth2Client;
|
|
41
|
+
}
|
|
42
|
+
export function getConfigDir() {
|
|
43
|
+
return CONFIG_DIR;
|
|
44
|
+
}
|
|
45
|
+
export function getTokenPath() {
|
|
46
|
+
return TOKEN_PATH;
|
|
47
|
+
}
|
|
48
|
+
export function getScopes() {
|
|
49
|
+
return SCOPES;
|
|
50
|
+
}
|
|
51
|
+
export { CONFIG_DIR, TOKEN_PATH, CREDENTIALS_PATH_DEFAULT };
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { ReadDocumentSchema, readDocument } from './tools/read-document.js';
|
|
5
|
+
import { SearchDocumentsSchema, searchDocuments } from './tools/search-documents.js';
|
|
6
|
+
import { CreateDocumentSchema, createDocument } from './tools/create-document.js';
|
|
7
|
+
import { ReplaceAllTextSchema, replaceAllText } from './tools/replace-all-text.js';
|
|
8
|
+
import { ReplaceImageSchema, replaceImage } from './tools/replace-image.js';
|
|
9
|
+
import { UpdateDocumentMarkdownSchema, updateDocumentMarkdown } from './tools/update-document-markdown.js';
|
|
10
|
+
import { UpdateDocumentSectionMarkdownSchema, updateDocumentSectionMarkdown } from './tools/update-document-section-markdown.js';
|
|
11
|
+
import { UpdateDocumentStyleSchema, updateDocumentStyle } from './tools/update-document-style.js';
|
|
12
|
+
import { UpdateDocumentSchema, updateDocument } from './tools/update-document.js';
|
|
13
|
+
import { UnmergeTableCellsSchema, unmergeTableCells } from './tools/unmerge-table-cells.js';
|
|
14
|
+
import { GetChartsSchema, getCharts } from './tools/get-charts.js';
|
|
15
|
+
const server = new McpServer({
|
|
16
|
+
name: 'gdocs-mcp',
|
|
17
|
+
version: '0.1.0',
|
|
18
|
+
});
|
|
19
|
+
function formatError(err) {
|
|
20
|
+
if (err instanceof Error) {
|
|
21
|
+
const message = err.message;
|
|
22
|
+
if (message.includes('not found') || message.includes('404')) {
|
|
23
|
+
return 'Document not found. Check the document ID and ensure it is shared with your account.';
|
|
24
|
+
}
|
|
25
|
+
if (message.includes('UNAUTHENTICATED') || message.includes('invalid_grant')) {
|
|
26
|
+
return 'Auth expired. Run: npx gdocs-mcp auth';
|
|
27
|
+
}
|
|
28
|
+
if (message.includes('RATE_LIMIT') || message.includes('429')) {
|
|
29
|
+
return 'Google API quota exceeded. Wait 60 seconds and retry.';
|
|
30
|
+
}
|
|
31
|
+
if (message.includes('PERMISSION_DENIED') || message.includes('403')) {
|
|
32
|
+
return 'Permission denied. Ensure the document is shared with your Google account.';
|
|
33
|
+
}
|
|
34
|
+
return message;
|
|
35
|
+
}
|
|
36
|
+
return String(err);
|
|
37
|
+
}
|
|
38
|
+
function registerTool(name, description, schema, handler) {
|
|
39
|
+
server.tool(name, description, schema.shape, async (args) => {
|
|
40
|
+
try {
|
|
41
|
+
const result = await handler(args);
|
|
42
|
+
return {
|
|
43
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
return {
|
|
48
|
+
content: [{ type: 'text', text: formatError(err) }],
|
|
49
|
+
isError: true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
// Google Docs tools
|
|
55
|
+
registerTool('read_document', 'Read the full content of a Google Doc', ReadDocumentSchema, readDocument);
|
|
56
|
+
registerTool('search_documents', 'Search Google Drive for documents by name, content, or date', SearchDocumentsSchema, searchDocuments);
|
|
57
|
+
registerTool('create_document', 'Create a new Google Doc with optional Markdown content', CreateDocumentSchema, createDocument);
|
|
58
|
+
registerTool('replace_all_text', 'Find and replace all occurrences of text in a document', ReplaceAllTextSchema, replaceAllText);
|
|
59
|
+
registerTool('replace_image', 'Replace an existing image in a document with a new image from a URI', ReplaceImageSchema, replaceImage);
|
|
60
|
+
registerTool('update_document_markdown', 'Replace the entire document body with Markdown content', UpdateDocumentMarkdownSchema, updateDocumentMarkdown);
|
|
61
|
+
registerTool('update_document_section_markdown', 'Insert or replace a section of a document with Markdown content', UpdateDocumentSectionMarkdownSchema, updateDocumentSectionMarkdown);
|
|
62
|
+
registerTool('update_document_style', 'Update document style: page size, margins, text direction', UpdateDocumentStyleSchema, updateDocumentStyle);
|
|
63
|
+
registerTool('update_document', 'Apply batch updates to a document (insertText, updateTextStyle, createParagraphBullets, insertTable, etc.)', UpdateDocumentSchema, updateDocument);
|
|
64
|
+
registerTool('unmerge_table_cells', 'Unmerge previously merged cells in a document table', UnmergeTableCellsSchema, unmergeTableCells);
|
|
65
|
+
// Google Sheets tools
|
|
66
|
+
registerTool('get_charts', 'List all charts in a Google Sheets spreadsheet with IDs and specs', GetChartsSchema, getCharts);
|
|
67
|
+
async function main() {
|
|
68
|
+
const transport = new StdioServerTransport();
|
|
69
|
+
await server.connect(transport);
|
|
70
|
+
console.error('gdocs-mcp v0.1.0 started');
|
|
71
|
+
}
|
|
72
|
+
main().catch((err) => {
|
|
73
|
+
console.error('Failed to start gdocs-mcp:', err);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const CreateDocumentSchema: z.ZodObject<{
|
|
3
|
+
title: z.ZodString;
|
|
4
|
+
markdown: z.ZodOptional<z.ZodString>;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
export declare function createDocument(args: z.infer<typeof CreateDocumentSchema>): Promise<{
|
|
7
|
+
documentId: string;
|
|
8
|
+
title: string;
|
|
9
|
+
url: string;
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getAuthClient } from '../auth/google-auth.js';
|
|
4
|
+
export const CreateDocumentSchema = z.object({
|
|
5
|
+
title: z.string().describe('Title for the new Google Doc'),
|
|
6
|
+
markdown: z.string().optional().describe('Optional Markdown content to populate the document with'),
|
|
7
|
+
});
|
|
8
|
+
export async function createDocument(args) {
|
|
9
|
+
const auth = getAuthClient();
|
|
10
|
+
const docs = google.docs({ version: 'v1', auth });
|
|
11
|
+
// Create empty doc
|
|
12
|
+
const createRes = await docs.documents.create({
|
|
13
|
+
requestBody: { title: args.title },
|
|
14
|
+
});
|
|
15
|
+
const documentId = createRes.data.documentId;
|
|
16
|
+
// If markdown provided, insert it as text content
|
|
17
|
+
if (args.markdown) {
|
|
18
|
+
await docs.documents.batchUpdate({
|
|
19
|
+
documentId,
|
|
20
|
+
requestBody: {
|
|
21
|
+
requests: [
|
|
22
|
+
{
|
|
23
|
+
insertText: {
|
|
24
|
+
location: { index: 1 },
|
|
25
|
+
text: args.markdown,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
documentId,
|
|
34
|
+
title: args.title,
|
|
35
|
+
url: `https://docs.google.com/document/d/${documentId}/edit`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const GetChartsSchema: z.ZodObject<{
|
|
3
|
+
spreadsheetId: z.ZodString;
|
|
4
|
+
}, z.core.$strip>;
|
|
5
|
+
export declare function getCharts(args: z.infer<typeof GetChartsSchema>): Promise<{
|
|
6
|
+
spreadsheetId: string;
|
|
7
|
+
charts: any[];
|
|
8
|
+
count: number;
|
|
9
|
+
}>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getAuthClient } from '../auth/google-auth.js';
|
|
4
|
+
export const GetChartsSchema = z.object({
|
|
5
|
+
spreadsheetId: z.string().describe('The Google Sheets spreadsheet ID (from the URL)'),
|
|
6
|
+
});
|
|
7
|
+
export async function getCharts(args) {
|
|
8
|
+
const auth = getAuthClient();
|
|
9
|
+
const sheets = google.sheets({ version: 'v4', auth });
|
|
10
|
+
const res = await sheets.spreadsheets.get({
|
|
11
|
+
spreadsheetId: args.spreadsheetId,
|
|
12
|
+
fields: 'sheets(charts(chartId,position,spec))',
|
|
13
|
+
});
|
|
14
|
+
const allCharts = [];
|
|
15
|
+
for (const sheet of res.data.sheets || []) {
|
|
16
|
+
for (const chart of sheet.charts || []) {
|
|
17
|
+
allCharts.push({
|
|
18
|
+
chartId: chart.chartId,
|
|
19
|
+
title: chart.spec?.title || 'Untitled',
|
|
20
|
+
chartType: chart.spec?.basicChart?.chartType || chart.spec?.pieChart ? 'PIE' : 'UNKNOWN',
|
|
21
|
+
position: chart.position,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
spreadsheetId: args.spreadsheetId,
|
|
27
|
+
charts: allCharts,
|
|
28
|
+
count: allCharts.length,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const ReadDocumentSchema: z.ZodObject<{
|
|
3
|
+
documentId: z.ZodString;
|
|
4
|
+
}, z.core.$strip>;
|
|
5
|
+
export declare function readDocument(args: z.infer<typeof ReadDocumentSchema>): Promise<{
|
|
6
|
+
documentId: string;
|
|
7
|
+
title: string;
|
|
8
|
+
content: string;
|
|
9
|
+
revisionId: string | null | undefined;
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getAuthClient } from '../auth/google-auth.js';
|
|
4
|
+
export const ReadDocumentSchema = z.object({
|
|
5
|
+
documentId: z.string().describe('The Google Doc document ID (from the URL)'),
|
|
6
|
+
});
|
|
7
|
+
export async function readDocument(args) {
|
|
8
|
+
const auth = getAuthClient();
|
|
9
|
+
const docs = google.docs({ version: 'v1', auth });
|
|
10
|
+
const res = await docs.documents.get({ documentId: args.documentId });
|
|
11
|
+
const doc = res.data;
|
|
12
|
+
const title = doc.title || 'Untitled';
|
|
13
|
+
const body = doc.body?.content || [];
|
|
14
|
+
const textContent = extractText(body);
|
|
15
|
+
return {
|
|
16
|
+
documentId: args.documentId,
|
|
17
|
+
title,
|
|
18
|
+
content: textContent,
|
|
19
|
+
revisionId: doc.revisionId,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function extractText(content) {
|
|
23
|
+
const parts = [];
|
|
24
|
+
for (const element of content) {
|
|
25
|
+
if (element.paragraph) {
|
|
26
|
+
const paragraphText = element.paragraph.elements
|
|
27
|
+
?.map((el) => el.textRun?.content || '')
|
|
28
|
+
.join('') || '';
|
|
29
|
+
parts.push(paragraphText);
|
|
30
|
+
}
|
|
31
|
+
else if (element.table) {
|
|
32
|
+
for (const row of element.table.tableRows || []) {
|
|
33
|
+
const cells = row.tableCells?.map((cell) => {
|
|
34
|
+
const cellContent = cell.content || [];
|
|
35
|
+
return extractText(cellContent).trim();
|
|
36
|
+
}) || [];
|
|
37
|
+
parts.push('| ' + cells.join(' | ') + ' |');
|
|
38
|
+
}
|
|
39
|
+
parts.push('');
|
|
40
|
+
}
|
|
41
|
+
else if (element.sectionBreak) {
|
|
42
|
+
parts.push('---');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return parts.join('');
|
|
46
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const ReplaceAllTextSchema: z.ZodObject<{
|
|
3
|
+
documentId: z.ZodString;
|
|
4
|
+
find: z.ZodString;
|
|
5
|
+
replaceWith: z.ZodString;
|
|
6
|
+
matchCase: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
export declare function replaceAllText(args: z.infer<typeof ReplaceAllTextSchema>): Promise<{
|
|
9
|
+
documentId: string;
|
|
10
|
+
occurrencesReplaced: number;
|
|
11
|
+
}>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getAuthClient } from '../auth/google-auth.js';
|
|
4
|
+
export const ReplaceAllTextSchema = z.object({
|
|
5
|
+
documentId: z.string().describe('The Google Doc document ID'),
|
|
6
|
+
find: z.string().describe('Text to find (all occurrences will be replaced)'),
|
|
7
|
+
replaceWith: z.string().describe('Replacement text'),
|
|
8
|
+
matchCase: z.boolean().optional().default(true).describe('Whether the search is case-sensitive (default: true)'),
|
|
9
|
+
});
|
|
10
|
+
export async function replaceAllText(args) {
|
|
11
|
+
const auth = getAuthClient();
|
|
12
|
+
const docs = google.docs({ version: 'v1', auth });
|
|
13
|
+
const res = await docs.documents.batchUpdate({
|
|
14
|
+
documentId: args.documentId,
|
|
15
|
+
requestBody: {
|
|
16
|
+
requests: [
|
|
17
|
+
{
|
|
18
|
+
replaceAllText: {
|
|
19
|
+
containsText: {
|
|
20
|
+
text: args.find,
|
|
21
|
+
matchCase: args.matchCase,
|
|
22
|
+
},
|
|
23
|
+
replaceText: args.replaceWith,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
const replaceResult = res.data.replies?.[0]?.replaceAllText;
|
|
30
|
+
return {
|
|
31
|
+
documentId: args.documentId,
|
|
32
|
+
occurrencesReplaced: replaceResult?.occurrencesChanged || 0,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const ReplaceImageSchema: z.ZodObject<{
|
|
3
|
+
documentId: z.ZodString;
|
|
4
|
+
imageObjectId: z.ZodString;
|
|
5
|
+
newImageUri: z.ZodString;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
export declare function replaceImage(args: z.infer<typeof ReplaceImageSchema>): Promise<{
|
|
8
|
+
documentId: string;
|
|
9
|
+
imageObjectId: string;
|
|
10
|
+
replaced: boolean;
|
|
11
|
+
}>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getAuthClient } from '../auth/google-auth.js';
|
|
4
|
+
export const ReplaceImageSchema = z.object({
|
|
5
|
+
documentId: z.string().describe('The Google Doc document ID'),
|
|
6
|
+
imageObjectId: z.string().describe('The object ID of the image to replace (from read_document)'),
|
|
7
|
+
newImageUri: z.string().url().describe('URI of the new image to insert'),
|
|
8
|
+
});
|
|
9
|
+
export async function replaceImage(args) {
|
|
10
|
+
const auth = getAuthClient();
|
|
11
|
+
const docs = google.docs({ version: 'v1', auth });
|
|
12
|
+
await docs.documents.batchUpdate({
|
|
13
|
+
documentId: args.documentId,
|
|
14
|
+
requestBody: {
|
|
15
|
+
requests: [
|
|
16
|
+
{
|
|
17
|
+
replaceImage: {
|
|
18
|
+
imageObjectId: args.imageObjectId,
|
|
19
|
+
uri: args.newImageUri,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
documentId: args.documentId,
|
|
27
|
+
imageObjectId: args.imageObjectId,
|
|
28
|
+
replaced: true,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const SearchDocumentsSchema: z.ZodObject<{
|
|
3
|
+
query: z.ZodString;
|
|
4
|
+
dateFrom: z.ZodOptional<z.ZodString>;
|
|
5
|
+
dateTo: z.ZodOptional<z.ZodString>;
|
|
6
|
+
maxResults: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
export declare function searchDocuments(args: z.infer<typeof SearchDocumentsSchema>): Promise<{
|
|
9
|
+
documentId: string | null | undefined;
|
|
10
|
+
title: string | null | undefined;
|
|
11
|
+
lastModified: string | null | undefined;
|
|
12
|
+
url: string | null | undefined;
|
|
13
|
+
owner: string;
|
|
14
|
+
}[]>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getAuthClient } from '../auth/google-auth.js';
|
|
4
|
+
export const SearchDocumentsSchema = z.object({
|
|
5
|
+
query: z.string().describe('Search query — matches document title and content'),
|
|
6
|
+
dateFrom: z.string().optional().describe('Filter: modified after this date (ISO 8601, e.g. 2026-01-01)'),
|
|
7
|
+
dateTo: z.string().optional().describe('Filter: modified before this date (ISO 8601)'),
|
|
8
|
+
maxResults: z.number().optional().default(10).describe('Maximum number of results to return (default: 10)'),
|
|
9
|
+
});
|
|
10
|
+
export async function searchDocuments(args) {
|
|
11
|
+
const auth = getAuthClient();
|
|
12
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
13
|
+
const queryParts = [
|
|
14
|
+
"mimeType='application/vnd.google-apps.document'",
|
|
15
|
+
`fullText contains '${args.query.replace(/'/g, "\\'")}'`,
|
|
16
|
+
];
|
|
17
|
+
if (args.dateFrom) {
|
|
18
|
+
queryParts.push(`modifiedTime >= '${args.dateFrom}T00:00:00'`);
|
|
19
|
+
}
|
|
20
|
+
if (args.dateTo) {
|
|
21
|
+
queryParts.push(`modifiedTime <= '${args.dateTo}T23:59:59'`);
|
|
22
|
+
}
|
|
23
|
+
const res = await drive.files.list({
|
|
24
|
+
q: queryParts.join(' and '),
|
|
25
|
+
pageSize: args.maxResults,
|
|
26
|
+
fields: 'files(id, name, modifiedTime, webViewLink, owners)',
|
|
27
|
+
orderBy: 'modifiedTime desc',
|
|
28
|
+
});
|
|
29
|
+
const files = res.data.files || [];
|
|
30
|
+
return files.map(f => ({
|
|
31
|
+
documentId: f.id,
|
|
32
|
+
title: f.name,
|
|
33
|
+
lastModified: f.modifiedTime,
|
|
34
|
+
url: f.webViewLink,
|
|
35
|
+
owner: f.owners?.[0]?.emailAddress || 'unknown',
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const UnmergeTableCellsSchema: z.ZodObject<{
|
|
3
|
+
documentId: z.ZodString;
|
|
4
|
+
tableStartIndex: z.ZodNumber;
|
|
5
|
+
rowIndex: z.ZodNumber;
|
|
6
|
+
columnIndex: z.ZodNumber;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
export declare function unmergeTableCells(args: z.infer<typeof UnmergeTableCellsSchema>): Promise<{
|
|
9
|
+
documentId: string;
|
|
10
|
+
unmerged: boolean;
|
|
11
|
+
tableStartIndex: number;
|
|
12
|
+
rowIndex: number;
|
|
13
|
+
columnIndex: number;
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getAuthClient } from '../auth/google-auth.js';
|
|
4
|
+
export const UnmergeTableCellsSchema = z.object({
|
|
5
|
+
documentId: z.string().describe('The Google Doc document ID'),
|
|
6
|
+
tableStartIndex: z.number().describe('The start index of the table in the document body'),
|
|
7
|
+
rowIndex: z.number().describe('The zero-based row index of the cell to unmerge'),
|
|
8
|
+
columnIndex: z.number().describe('The zero-based column index of the cell to unmerge'),
|
|
9
|
+
});
|
|
10
|
+
export async function unmergeTableCells(args) {
|
|
11
|
+
const auth = getAuthClient();
|
|
12
|
+
const docs = google.docs({ version: 'v1', auth });
|
|
13
|
+
await docs.documents.batchUpdate({
|
|
14
|
+
documentId: args.documentId,
|
|
15
|
+
requestBody: {
|
|
16
|
+
requests: [
|
|
17
|
+
{
|
|
18
|
+
unmergeTableCells: {
|
|
19
|
+
tableRange: {
|
|
20
|
+
tableCellLocation: {
|
|
21
|
+
tableStartLocation: { index: args.tableStartIndex },
|
|
22
|
+
rowIndex: args.rowIndex,
|
|
23
|
+
columnIndex: args.columnIndex,
|
|
24
|
+
},
|
|
25
|
+
rowSpan: 1,
|
|
26
|
+
columnSpan: 1,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
documentId: args.documentId,
|
|
35
|
+
unmerged: true,
|
|
36
|
+
tableStartIndex: args.tableStartIndex,
|
|
37
|
+
rowIndex: args.rowIndex,
|
|
38
|
+
columnIndex: args.columnIndex,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const UpdateDocumentMarkdownSchema: z.ZodObject<{
|
|
3
|
+
documentId: z.ZodString;
|
|
4
|
+
markdown: z.ZodString;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
export declare function updateDocumentMarkdown(args: z.infer<typeof UpdateDocumentMarkdownSchema>): Promise<{
|
|
7
|
+
documentId: string;
|
|
8
|
+
updated: boolean;
|
|
9
|
+
contentLength: number;
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getAuthClient } from '../auth/google-auth.js';
|
|
4
|
+
export const UpdateDocumentMarkdownSchema = z.object({
|
|
5
|
+
documentId: z.string().describe('The Google Doc document ID'),
|
|
6
|
+
markdown: z.string().describe('Markdown content to replace the entire document body with'),
|
|
7
|
+
});
|
|
8
|
+
export async function updateDocumentMarkdown(args) {
|
|
9
|
+
const auth = getAuthClient();
|
|
10
|
+
const docs = google.docs({ version: 'v1', auth });
|
|
11
|
+
// Get current document to find end index
|
|
12
|
+
const current = await docs.documents.get({ documentId: args.documentId });
|
|
13
|
+
const body = current.data.body;
|
|
14
|
+
const endIndex = body?.content
|
|
15
|
+
? body.content[body.content.length - 1]?.endIndex || 1
|
|
16
|
+
: 1;
|
|
17
|
+
const requests = [];
|
|
18
|
+
// Delete existing content (except the trailing newline at index 1)
|
|
19
|
+
if (endIndex > 2) {
|
|
20
|
+
requests.push({
|
|
21
|
+
deleteContentRange: {
|
|
22
|
+
range: {
|
|
23
|
+
startIndex: 1,
|
|
24
|
+
endIndex: endIndex - 1,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
// Insert new content
|
|
30
|
+
requests.push({
|
|
31
|
+
insertText: {
|
|
32
|
+
location: { index: 1 },
|
|
33
|
+
text: args.markdown,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
await docs.documents.batchUpdate({
|
|
37
|
+
documentId: args.documentId,
|
|
38
|
+
requestBody: { requests },
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
documentId: args.documentId,
|
|
42
|
+
updated: true,
|
|
43
|
+
contentLength: args.markdown.length,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const UpdateDocumentSectionMarkdownSchema: z.ZodObject<{
|
|
3
|
+
documentId: z.ZodString;
|
|
4
|
+
markdown: z.ZodString;
|
|
5
|
+
startIndex: z.ZodNumber;
|
|
6
|
+
endIndex: z.ZodOptional<z.ZodNumber>;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
export declare function updateDocumentSectionMarkdown(args: z.infer<typeof UpdateDocumentSectionMarkdownSchema>): Promise<{
|
|
9
|
+
documentId: string;
|
|
10
|
+
updated: boolean;
|
|
11
|
+
startIndex: number;
|
|
12
|
+
endIndex: number | undefined;
|
|
13
|
+
insertedLength: number;
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getAuthClient } from '../auth/google-auth.js';
|
|
4
|
+
export const UpdateDocumentSectionMarkdownSchema = z.object({
|
|
5
|
+
documentId: z.string().describe('The Google Doc document ID'),
|
|
6
|
+
markdown: z.string().describe('Markdown content to insert or replace within the section'),
|
|
7
|
+
startIndex: z.number().describe('Start index of the section to replace'),
|
|
8
|
+
endIndex: z.number().optional().describe('End index of the section. If omitted, content is inserted at startIndex without deleting.'),
|
|
9
|
+
});
|
|
10
|
+
export async function updateDocumentSectionMarkdown(args) {
|
|
11
|
+
const auth = getAuthClient();
|
|
12
|
+
const docs = google.docs({ version: 'v1', auth });
|
|
13
|
+
const requests = [];
|
|
14
|
+
// Delete the existing section if endIndex is provided
|
|
15
|
+
if (args.endIndex !== undefined && args.endIndex > args.startIndex) {
|
|
16
|
+
requests.push({
|
|
17
|
+
deleteContentRange: {
|
|
18
|
+
range: {
|
|
19
|
+
startIndex: args.startIndex,
|
|
20
|
+
endIndex: args.endIndex,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
// Insert new content at the start index
|
|
26
|
+
requests.push({
|
|
27
|
+
insertText: {
|
|
28
|
+
location: { index: args.startIndex },
|
|
29
|
+
text: args.markdown,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
await docs.documents.batchUpdate({
|
|
33
|
+
documentId: args.documentId,
|
|
34
|
+
requestBody: { requests },
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
documentId: args.documentId,
|
|
38
|
+
updated: true,
|
|
39
|
+
startIndex: args.startIndex,
|
|
40
|
+
endIndex: args.endIndex,
|
|
41
|
+
insertedLength: args.markdown.length,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const UpdateDocumentStyleSchema: z.ZodObject<{
|
|
3
|
+
documentId: z.ZodString;
|
|
4
|
+
pageSize: z.ZodOptional<z.ZodObject<{
|
|
5
|
+
width: z.ZodOptional<z.ZodObject<{
|
|
6
|
+
magnitude: z.ZodNumber;
|
|
7
|
+
unit: z.ZodDefault<z.ZodEnum<{
|
|
8
|
+
PT: "PT";
|
|
9
|
+
MM: "MM";
|
|
10
|
+
INCH: "INCH";
|
|
11
|
+
}>>;
|
|
12
|
+
}, z.core.$strip>>;
|
|
13
|
+
height: z.ZodOptional<z.ZodObject<{
|
|
14
|
+
magnitude: z.ZodNumber;
|
|
15
|
+
unit: z.ZodDefault<z.ZodEnum<{
|
|
16
|
+
PT: "PT";
|
|
17
|
+
MM: "MM";
|
|
18
|
+
INCH: "INCH";
|
|
19
|
+
}>>;
|
|
20
|
+
}, z.core.$strip>>;
|
|
21
|
+
}, z.core.$strip>>;
|
|
22
|
+
marginTop: z.ZodOptional<z.ZodObject<{
|
|
23
|
+
magnitude: z.ZodNumber;
|
|
24
|
+
unit: z.ZodDefault<z.ZodEnum<{
|
|
25
|
+
PT: "PT";
|
|
26
|
+
MM: "MM";
|
|
27
|
+
INCH: "INCH";
|
|
28
|
+
}>>;
|
|
29
|
+
}, z.core.$strip>>;
|
|
30
|
+
marginBottom: z.ZodOptional<z.ZodObject<{
|
|
31
|
+
magnitude: z.ZodNumber;
|
|
32
|
+
unit: z.ZodDefault<z.ZodEnum<{
|
|
33
|
+
PT: "PT";
|
|
34
|
+
MM: "MM";
|
|
35
|
+
INCH: "INCH";
|
|
36
|
+
}>>;
|
|
37
|
+
}, z.core.$strip>>;
|
|
38
|
+
marginLeft: z.ZodOptional<z.ZodObject<{
|
|
39
|
+
magnitude: z.ZodNumber;
|
|
40
|
+
unit: z.ZodDefault<z.ZodEnum<{
|
|
41
|
+
PT: "PT";
|
|
42
|
+
MM: "MM";
|
|
43
|
+
INCH: "INCH";
|
|
44
|
+
}>>;
|
|
45
|
+
}, z.core.$strip>>;
|
|
46
|
+
marginRight: z.ZodOptional<z.ZodObject<{
|
|
47
|
+
magnitude: z.ZodNumber;
|
|
48
|
+
unit: z.ZodDefault<z.ZodEnum<{
|
|
49
|
+
PT: "PT";
|
|
50
|
+
MM: "MM";
|
|
51
|
+
INCH: "INCH";
|
|
52
|
+
}>>;
|
|
53
|
+
}, z.core.$strip>>;
|
|
54
|
+
}, z.core.$strip>;
|
|
55
|
+
export declare function updateDocumentStyle(args: z.infer<typeof UpdateDocumentStyleSchema>): Promise<{
|
|
56
|
+
documentId: string;
|
|
57
|
+
updated: boolean;
|
|
58
|
+
reason: string;
|
|
59
|
+
fieldsUpdated?: undefined;
|
|
60
|
+
} | {
|
|
61
|
+
documentId: string;
|
|
62
|
+
updated: boolean;
|
|
63
|
+
fieldsUpdated: string[];
|
|
64
|
+
reason?: undefined;
|
|
65
|
+
}>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getAuthClient } from '../auth/google-auth.js';
|
|
4
|
+
const DimensionSchema = z.object({
|
|
5
|
+
magnitude: z.number(),
|
|
6
|
+
unit: z.enum(['PT', 'MM', 'INCH']).default('PT'),
|
|
7
|
+
});
|
|
8
|
+
export const UpdateDocumentStyleSchema = z.object({
|
|
9
|
+
documentId: z.string().describe('The Google Doc document ID'),
|
|
10
|
+
pageSize: z.object({
|
|
11
|
+
width: DimensionSchema.optional(),
|
|
12
|
+
height: DimensionSchema.optional(),
|
|
13
|
+
}).optional().describe('Page dimensions'),
|
|
14
|
+
marginTop: DimensionSchema.optional().describe('Top margin'),
|
|
15
|
+
marginBottom: DimensionSchema.optional().describe('Bottom margin'),
|
|
16
|
+
marginLeft: DimensionSchema.optional().describe('Left margin'),
|
|
17
|
+
marginRight: DimensionSchema.optional().describe('Right margin'),
|
|
18
|
+
});
|
|
19
|
+
export async function updateDocumentStyle(args) {
|
|
20
|
+
const auth = getAuthClient();
|
|
21
|
+
const docs = google.docs({ version: 'v1', auth });
|
|
22
|
+
const documentStyle = {};
|
|
23
|
+
const fields = [];
|
|
24
|
+
if (args.pageSize) {
|
|
25
|
+
documentStyle.pageSize = {};
|
|
26
|
+
if (args.pageSize.width) {
|
|
27
|
+
documentStyle.pageSize.width = args.pageSize.width;
|
|
28
|
+
fields.push('pageSize.width');
|
|
29
|
+
}
|
|
30
|
+
if (args.pageSize.height) {
|
|
31
|
+
documentStyle.pageSize.height = args.pageSize.height;
|
|
32
|
+
fields.push('pageSize.height');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (args.marginTop) {
|
|
36
|
+
documentStyle.marginTop = args.marginTop;
|
|
37
|
+
fields.push('marginTop');
|
|
38
|
+
}
|
|
39
|
+
if (args.marginBottom) {
|
|
40
|
+
documentStyle.marginBottom = args.marginBottom;
|
|
41
|
+
fields.push('marginBottom');
|
|
42
|
+
}
|
|
43
|
+
if (args.marginLeft) {
|
|
44
|
+
documentStyle.marginLeft = args.marginLeft;
|
|
45
|
+
fields.push('marginLeft');
|
|
46
|
+
}
|
|
47
|
+
if (args.marginRight) {
|
|
48
|
+
documentStyle.marginRight = args.marginRight;
|
|
49
|
+
fields.push('marginRight');
|
|
50
|
+
}
|
|
51
|
+
if (fields.length === 0) {
|
|
52
|
+
return { documentId: args.documentId, updated: false, reason: 'No style changes specified' };
|
|
53
|
+
}
|
|
54
|
+
await docs.documents.batchUpdate({
|
|
55
|
+
documentId: args.documentId,
|
|
56
|
+
requestBody: {
|
|
57
|
+
requests: [
|
|
58
|
+
{
|
|
59
|
+
updateDocumentStyle: {
|
|
60
|
+
documentStyle,
|
|
61
|
+
fields: fields.join(','),
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
documentId: args.documentId,
|
|
69
|
+
updated: true,
|
|
70
|
+
fieldsUpdated: fields,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const UpdateDocumentSchema: z.ZodObject<{
|
|
3
|
+
documentId: z.ZodString;
|
|
4
|
+
requests: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodAny>>;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
export declare function updateDocument(args: z.infer<typeof UpdateDocumentSchema>): Promise<{
|
|
7
|
+
documentId: string;
|
|
8
|
+
repliesCount: number;
|
|
9
|
+
writeControl: import("googleapis").docs_v1.Schema$WriteControl | undefined;
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getAuthClient } from '../auth/google-auth.js';
|
|
4
|
+
export const UpdateDocumentSchema = z.object({
|
|
5
|
+
documentId: z.string().describe('The Google Doc document ID'),
|
|
6
|
+
requests: z.array(z.record(z.string(), z.any())).min(1).describe('Array of Google Docs API batchUpdate requests. ' +
|
|
7
|
+
'Supported types include: insertText, deleteContentRange, replaceAllText, ' +
|
|
8
|
+
'updateTextStyle, createParagraphBullets, insertTable, insertInlineImage, ' +
|
|
9
|
+
'createHeader, createFooter, updateDocumentStyle, and more. ' +
|
|
10
|
+
'See: https://developers.google.com/docs/api/reference/rest/v1/documents/request'),
|
|
11
|
+
});
|
|
12
|
+
export async function updateDocument(args) {
|
|
13
|
+
const auth = getAuthClient();
|
|
14
|
+
const docs = google.docs({ version: 'v1', auth });
|
|
15
|
+
// Validate each request has exactly one key (the request type)
|
|
16
|
+
for (const [i, req] of args.requests.entries()) {
|
|
17
|
+
const keys = Object.keys(req);
|
|
18
|
+
if (keys.length !== 1) {
|
|
19
|
+
throw new Error(`Request at index ${i} must have exactly one key (the request type), got: ${keys.join(', ')}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const res = await docs.documents.batchUpdate({
|
|
23
|
+
documentId: args.documentId,
|
|
24
|
+
requestBody: { requests: args.requests },
|
|
25
|
+
});
|
|
26
|
+
return {
|
|
27
|
+
documentId: args.documentId,
|
|
28
|
+
repliesCount: res.data.replies?.length || 0,
|
|
29
|
+
writeControl: res.data.writeControl,
|
|
30
|
+
};
|
|
31
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gdocs-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Open-source MCP server for Google Docs and Sheets. Self-hosted, local OAuth, no third-party token storage.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"gdocs-mcp": "./bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"bin",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"google-docs",
|
|
22
|
+
"google-sheets",
|
|
23
|
+
"model-context-protocol",
|
|
24
|
+
"claude",
|
|
25
|
+
"ai-tools",
|
|
26
|
+
"google-workspace",
|
|
27
|
+
"document-automation"
|
|
28
|
+
],
|
|
29
|
+
"author": "Apurwa Sarwajit",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/apurwa-sudo/gdocs-mcp"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
40
|
+
"googleapis": "^171.4.0",
|
|
41
|
+
"zod": "^4.3.6"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^25.5.2",
|
|
45
|
+
"tsx": "^4.21.0",
|
|
46
|
+
"typescript": "^6.0.2"
|
|
47
|
+
}
|
|
48
|
+
}
|