@starfysh/gdrive-mcp 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -6
- package/dist/auth.js +65 -41
- package/dist/server.js +767 -30
- package/dist/types.js +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
# Ultimate Google
|
|
1
|
+
# Ultimate Google Workspace MCP Server
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
Connect Claude Desktop (or other MCP clients) to
|
|
5
|
+
Connect Claude Desktop (or other MCP clients) to Google Docs, Sheets, Drive, Slides, Forms, and Apps Script!
|
|
6
6
|
|
|
7
|
-
> 🔥 **Check out [15 powerful tasks](SAMPLE_TASKS.md) you can accomplish with this
|
|
8
|
-
>
|
|
9
|
-
>
|
|
7
|
+
> 🔥 **Check out [15 powerful tasks](SAMPLE_TASKS.md) you can accomplish with this server!**
|
|
8
|
+
> 🎨 **NEW:** Google Slides support for presentations
|
|
9
|
+
> 📋 **NEW:** Google Forms read-only access
|
|
10
|
+
> 📜 **NEW:** Google Apps Script project management and execution
|
|
10
11
|
|
|
11
12
|
This comprehensive server uses the Model Context Protocol (MCP) and the `fastmcp` library to provide tools for reading, writing, formatting, structuring Google Documents and Spreadsheets, and managing your entire Google Drive. It acts as a powerful bridge, allowing AI assistants like Claude to interact with your documents, spreadsheets, and files programmatically with advanced capabilities.
|
|
12
13
|
|
|
@@ -74,6 +75,25 @@ This comprehensive server uses the Model Context Protocol (MCP) and the `fastmcp
|
|
|
74
75
|
- **Remove Access:** Revoke permissions with `removePermission` - remove specific users or groups from files
|
|
75
76
|
- **Version History:** View file revisions with `listRevisions` - track changes for compliance and auditing
|
|
76
77
|
|
|
78
|
+
### 🆕 Google Slides Support
|
|
79
|
+
|
|
80
|
+
- **Read Presentations:** Get presentation metadata with `getPresentation` and list all slides with `listSlides`
|
|
81
|
+
- **Create Presentations:** Create new presentations with `createPresentation`
|
|
82
|
+
- **Manage Slides:** Get slide details with `getSlide`, add slides with `createSlide`, remove with `deleteSlide`
|
|
83
|
+
|
|
84
|
+
### 🆕 Google Forms Support (Read-Only)
|
|
85
|
+
|
|
86
|
+
- **Read Forms:** Get form structure and questions with `getForm` and `listFormQuestions`
|
|
87
|
+
- **Read Responses:** Get all form responses with `listFormResponses` or specific ones with `getFormResponse`
|
|
88
|
+
- ⚠️ **Note:** The Forms API only supports reading - form creation/editing must be done via the web interface
|
|
89
|
+
|
|
90
|
+
### 🆕 Google Apps Script Support
|
|
91
|
+
|
|
92
|
+
- **Manage Projects:** Get project details with `getScriptProject`, create with `createScriptProject`
|
|
93
|
+
- **Manage Code:** Read scripts with `getScriptContent`, update with `updateScriptContent`
|
|
94
|
+
- **Manage Versions:** List script versions with `listScriptVersions`, list deployments with `listScriptDeployments`
|
|
95
|
+
- **Execute Scripts:** Run functions with `runScript` (requires API executable deployment)
|
|
96
|
+
|
|
77
97
|
### Integration
|
|
78
98
|
|
|
79
99
|
- **Google Authentication:** Secure OAuth 2.0 authentication with full Drive, Docs, and Sheets access
|
|
@@ -488,6 +508,44 @@ While this MCP server provides comprehensive Google Docs, Sheets, and Drive func
|
|
|
488
508
|
|
|
489
509
|
**Limited Support for Converted Documents**: Some Google Docs that were converted from other formats (especially Microsoft Word documents) may not support all Docs API operations. You may encounter errors like "This operation is not supported for this document" when trying to read or modify these files.
|
|
490
510
|
|
|
511
|
+
### Google Forms API Limitations
|
|
512
|
+
|
|
513
|
+
**Read-Only Access**: The Google Forms API only supports **reading** form structure and responses. You cannot create, update, or delete forms or questions programmatically. This is a fundamental limitation of the [Google Forms API](https://developers.google.com/workspace/forms/api/reference/rest).
|
|
514
|
+
|
|
515
|
+
- Forms can only be created and edited via the Google Forms web interface
|
|
516
|
+
- The API can read form structure (questions, settings) and responses
|
|
517
|
+
- See: [Google Forms API Documentation](https://developers.google.com/workspace/forms/api)
|
|
518
|
+
|
|
519
|
+
### Google Slides API Limitations
|
|
520
|
+
|
|
521
|
+
**No Batch Updates**: Unlike the Google Docs API, the Slides API does not support batch updates in the same way. Each modification requires individual API calls. See: [Google Slides API Documentation](https://developers.google.com/workspace/slides/api)
|
|
522
|
+
|
|
523
|
+
### Google Apps Script API Limitations
|
|
524
|
+
|
|
525
|
+
**No Project Listing**: The Apps Script API does not support listing all script projects. You must know the script ID to interact with a project. Script IDs can be found in the script editor URL: `script.google.com/d/{SCRIPT_ID}/edit`
|
|
526
|
+
|
|
527
|
+
**Script Execution Requirements**: Running Apps Script functions via the API requires specific setup:
|
|
528
|
+
|
|
529
|
+
1. The script must be deployed as an **API Executable**
|
|
530
|
+
2. The calling application and script must share a **common Google Cloud project**
|
|
531
|
+
3. The OAuth token must include **all scopes** used by the script
|
|
532
|
+
4. Only scripts with at least one required scope can be executed
|
|
533
|
+
|
|
534
|
+
See: [Execute Functions with the Apps Script API](https://developers.google.com/apps-script/api/how-tos/execute)
|
|
535
|
+
|
|
536
|
+
## API Documentation References
|
|
537
|
+
|
|
538
|
+
| API | Documentation | OAuth Scopes Reference |
|
|
539
|
+
|-----|---------------|----------------------|
|
|
540
|
+
| Google Docs | [developers.google.com/docs/api](https://developers.google.com/docs/api) | `auth/documents` |
|
|
541
|
+
| Google Sheets | [developers.google.com/sheets/api](https://developers.google.com/sheets/api) | `auth/spreadsheets` |
|
|
542
|
+
| Google Drive | [developers.google.com/drive/api](https://developers.google.com/drive/api) | `auth/drive` |
|
|
543
|
+
| Google Slides | [developers.google.com/slides/api](https://developers.google.com/workspace/slides/api) | `auth/presentations` |
|
|
544
|
+
| Google Forms | [developers.google.com/forms/api](https://developers.google.com/workspace/forms/api) | `auth/forms.body.readonly`, `auth/forms.responses.readonly` |
|
|
545
|
+
| Apps Script | [developers.google.com/apps-script/api](https://developers.google.com/apps-script/api) | `auth/script.projects` |
|
|
546
|
+
|
|
547
|
+
For the complete list of OAuth 2.0 scopes, see: [OAuth 2.0 Scopes for Google APIs](https://developers.google.com/identity/protocols/oauth2/scopes)
|
|
548
|
+
|
|
491
549
|
## Troubleshooting
|
|
492
550
|
|
|
493
551
|
- **Claude shows "Failed" or "Could not attach":**
|
|
@@ -500,7 +558,7 @@ While this MCP server provides comprehensive Google Docs, Sheets, and Drive func
|
|
|
500
558
|
- Ensure you enabled the correct APIs (Docs, Sheets, Drive).
|
|
501
559
|
- Make sure you added your email as a Test User on the OAuth Consent Screen.
|
|
502
560
|
- Verify the `credentials.json` file is correctly placed in the project root.
|
|
503
|
-
- **If you're upgrading from an older version:** You may need to delete your existing `token.json` file and re-authenticate to grant
|
|
561
|
+
- **If you're upgrading from an older version:** You may need to delete your existing `token.json` file and re-authenticate to grant new scopes (Sheets, Slides, Forms, Apps Script).
|
|
504
562
|
- **Tab-related Errors:**
|
|
505
563
|
- If you get "Tab with ID not found", use `listDocumentTabs` to see all available tab IDs
|
|
506
564
|
- Ensure you're using the correct tab ID format (typically a short alphanumeric string)
|
package/dist/auth.js
CHANGED
|
@@ -30,9 +30,17 @@ const TOKEN_PATH = resolvePath('GDRIVE_MCP_TOKEN_PATH', 'token.json');
|
|
|
30
30
|
const CREDENTIALS_PATH = resolvePath('GDRIVE_MCP_CREDENTIALS_PATH', 'credentials.json');
|
|
31
31
|
// --- End of path calculation ---
|
|
32
32
|
const SCOPES = [
|
|
33
|
+
// Core APIs
|
|
33
34
|
'https://www.googleapis.com/auth/documents',
|
|
34
35
|
'https://www.googleapis.com/auth/drive', // Full Drive access for listing, searching, and document discovery
|
|
35
|
-
'https://www.googleapis.com/auth/spreadsheets' // Google Sheets API access
|
|
36
|
+
'https://www.googleapis.com/auth/spreadsheets', // Google Sheets API access
|
|
37
|
+
// Slides API
|
|
38
|
+
'https://www.googleapis.com/auth/presentations',
|
|
39
|
+
// Forms API (read-only - API doesn't support writes)
|
|
40
|
+
'https://www.googleapis.com/auth/forms.body.readonly',
|
|
41
|
+
'https://www.googleapis.com/auth/forms.responses.readonly',
|
|
42
|
+
// Apps Script API
|
|
43
|
+
'https://www.googleapis.com/auth/script.projects',
|
|
36
44
|
];
|
|
37
45
|
// --- NEW FUNCTION: Handles Service Account Authentication ---
|
|
38
46
|
// This entire function is new. It is called only when the
|
|
@@ -51,21 +59,13 @@ async function authorizeWithServiceAccount() {
|
|
|
51
59
|
subject: impersonateUser, // Enables domain-wide delegation when set
|
|
52
60
|
});
|
|
53
61
|
await auth.authorize();
|
|
54
|
-
if (impersonateUser) {
|
|
55
|
-
console.error(`Service Account authentication successful, impersonating: ${impersonateUser}`);
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
console.error('Service Account authentication successful!');
|
|
59
|
-
}
|
|
60
62
|
return auth;
|
|
61
63
|
}
|
|
62
64
|
catch (error) {
|
|
63
65
|
if (error.code === 'ENOENT') {
|
|
64
|
-
|
|
65
|
-
throw new Error(`Service account key file not found. Please check the path in SERVICE_ACCOUNT_PATH.`);
|
|
66
|
+
throw new Error(`Service account key file not found at: ${serviceAccountPath}`);
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
-
throw new Error('Failed to authorize using the service account. Ensure the key file is valid and the path is correct.');
|
|
68
|
+
throw new Error(`Service account auth failed: ${error.message}`);
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
// --- END OF NEW FUNCTION---
|
|
@@ -83,17 +83,43 @@ async function loadSavedCredentialsIfExist() {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
async function loadClientSecrets() {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
86
|
+
try {
|
|
87
|
+
const content = await fs.readFile(CREDENTIALS_PATH);
|
|
88
|
+
const keys = JSON.parse(content.toString());
|
|
89
|
+
const key = keys.installed || keys.web;
|
|
90
|
+
if (!key)
|
|
91
|
+
throw new Error("Could not find client secrets in credentials.json.");
|
|
92
|
+
return {
|
|
93
|
+
client_id: key.client_id,
|
|
94
|
+
client_secret: key.client_secret,
|
|
95
|
+
redirect_uris: key.redirect_uris || ['http://localhost:3000/'],
|
|
96
|
+
client_type: keys.web ? 'web' : 'installed'
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (error.code === 'ENOENT') {
|
|
101
|
+
console.error(`
|
|
102
|
+
╔════════════════════════════════════════════════════════════════════╗
|
|
103
|
+
║ SETUP REQUIRED ║
|
|
104
|
+
╠════════════════════════════════════════════════════════════════════╣
|
|
105
|
+
║ credentials.json not found! ║
|
|
106
|
+
║ ║
|
|
107
|
+
║ To use this MCP server, you need Google OAuth credentials: ║
|
|
108
|
+
║ ║
|
|
109
|
+
║ 1. Go to: https://console.cloud.google.com/apis/credentials ║
|
|
110
|
+
║ 2. Create OAuth 2.0 Client ID (Desktop app) ║
|
|
111
|
+
║ 3. Download JSON and save as: credentials.json ║
|
|
112
|
+
║ 4. Place in: ${process.cwd()}
|
|
113
|
+
║ OR: ~/.config/gdrive-mcp/credentials.json ║
|
|
114
|
+
║ OR: Set GDRIVE_MCP_CREDENTIALS_PATH env var ║
|
|
115
|
+
║ ║
|
|
116
|
+
║ Full guide: https://github.com/starfysh-tech/gdrive-mcp#setup ║
|
|
117
|
+
╚════════════════════════════════════════════════════════════════════╝
|
|
118
|
+
`);
|
|
119
|
+
throw new Error('Setup required: credentials.json not found. See instructions above.');
|
|
120
|
+
}
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
97
123
|
}
|
|
98
124
|
async function saveCredentials(client) {
|
|
99
125
|
const { client_secret, client_id } = await loadClientSecrets();
|
|
@@ -104,39 +130,42 @@ async function saveCredentials(client) {
|
|
|
104
130
|
refresh_token: client.credentials.refresh_token,
|
|
105
131
|
});
|
|
106
132
|
await fs.writeFile(TOKEN_PATH, payload);
|
|
107
|
-
console.error('Token stored to', TOKEN_PATH);
|
|
108
133
|
}
|
|
109
134
|
async function authenticate() {
|
|
110
135
|
const { client_secret, client_id, redirect_uris, client_type } = await loadClientSecrets();
|
|
111
|
-
// For web clients, use the configured redirect URI; for desktop clients, use 'urn:ietf:wg:oauth:2.0:oob'
|
|
112
136
|
const redirectUri = client_type === 'web' ? redirect_uris[0] : 'urn:ietf:wg:oauth:2.0:oob';
|
|
113
|
-
console.error(`DEBUG: Using redirect URI: ${redirectUri}`);
|
|
114
|
-
console.error(`DEBUG: Client type: ${client_type}`);
|
|
115
137
|
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirectUri);
|
|
116
138
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
117
139
|
const authorizeUrl = oAuth2Client.generateAuthUrl({
|
|
118
140
|
access_type: 'offline',
|
|
119
141
|
scope: SCOPES.join(' '),
|
|
120
142
|
});
|
|
121
|
-
console.error(
|
|
122
|
-
|
|
123
|
-
|
|
143
|
+
console.error(`
|
|
144
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
145
|
+
│ Google Authorization Required │
|
|
146
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
147
|
+
│ 1. Open this URL in your browser: │
|
|
148
|
+
│ │
|
|
149
|
+
│ ${authorizeUrl}
|
|
150
|
+
│ │
|
|
151
|
+
│ 2. Sign in and grant access │
|
|
152
|
+
│ 3. Copy the authorization code │
|
|
153
|
+
│ 4. Paste it below │
|
|
154
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
155
|
+
`);
|
|
156
|
+
const code = await rl.question('Paste code here: ');
|
|
124
157
|
rl.close();
|
|
125
158
|
try {
|
|
126
159
|
const { tokens } = await oAuth2Client.getToken(code);
|
|
127
160
|
oAuth2Client.setCredentials(tokens);
|
|
128
|
-
if (tokens.refresh_token) {
|
|
161
|
+
if (tokens.refresh_token) {
|
|
129
162
|
await saveCredentials(oAuth2Client);
|
|
130
163
|
}
|
|
131
|
-
|
|
132
|
-
console.error("Did not receive refresh token. Token might expire.");
|
|
133
|
-
}
|
|
134
|
-
console.error('Authentication successful!');
|
|
164
|
+
console.error('✓ Authenticated successfully!\n');
|
|
135
165
|
return oAuth2Client;
|
|
136
166
|
}
|
|
137
167
|
catch (err) {
|
|
138
|
-
|
|
139
|
-
throw new Error('Authentication failed');
|
|
168
|
+
throw new Error('Authentication failed - check that you copied the full code');
|
|
140
169
|
}
|
|
141
170
|
}
|
|
142
171
|
// --- MODIFIED: The Main Exported Function ---
|
|
@@ -145,19 +174,14 @@ async function authenticate() {
|
|
|
145
174
|
export async function authorize() {
|
|
146
175
|
// Check if the Service Account environment variable is set.
|
|
147
176
|
if (process.env.SERVICE_ACCOUNT_PATH) {
|
|
148
|
-
console.error('Service account path detected. Attempting service account authentication...');
|
|
149
177
|
return authorizeWithServiceAccount();
|
|
150
178
|
}
|
|
151
179
|
else {
|
|
152
180
|
// If not, execute the original OAuth 2.0 flow exactly as it was.
|
|
153
|
-
console.error('No service account path detected. Falling back to standard OAuth 2.0 flow...');
|
|
154
181
|
let client = await loadSavedCredentialsIfExist();
|
|
155
182
|
if (client) {
|
|
156
|
-
// Optional: Add token refresh logic here if needed, though library often handles it.
|
|
157
|
-
console.error('Using saved credentials.');
|
|
158
183
|
return client;
|
|
159
184
|
}
|
|
160
|
-
console.error('Starting authentication flow...');
|
|
161
185
|
client = await authenticate();
|
|
162
186
|
return client;
|
|
163
187
|
}
|
package/dist/server.js
CHANGED
|
@@ -1,42 +1,52 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// src/server.ts
|
|
3
|
+
// Filter out noisy library warnings BEFORE importing FastMCP
|
|
4
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
5
|
+
process.stderr.write = (chunk, ...args) => {
|
|
6
|
+
if (typeof chunk === 'string' && chunk.includes('[FastMCP warning]'))
|
|
7
|
+
return true;
|
|
8
|
+
return originalStderrWrite(chunk, ...args);
|
|
9
|
+
};
|
|
3
10
|
import { FastMCP, UserError } from 'fastmcp';
|
|
4
11
|
import { z } from 'zod';
|
|
5
12
|
import { google } from 'googleapis';
|
|
6
13
|
import { authorize } from './auth.js';
|
|
7
14
|
// Import types and helpers
|
|
8
|
-
import { DocumentIdParameter, OptionalRangeParameters, TextStyleParameters, ParagraphStyleParameters, ApplyTextStyleToolParameters, ApplyParagraphStyleToolParameters, NotImplementedError } from './types.js';
|
|
15
|
+
import { DocumentIdParameter, OptionalRangeParameters, TextStyleParameters, ParagraphStyleParameters, ApplyTextStyleToolParameters, ApplyParagraphStyleToolParameters, NotImplementedError, PresentationIdParameter, FormIdParameter, ScriptIdParameter } from './types.js';
|
|
9
16
|
import * as GDocsHelpers from './googleDocsApiHelpers.js';
|
|
10
17
|
import * as SheetsHelpers from './googleSheetsApiHelpers.js';
|
|
11
18
|
let authClient = null;
|
|
12
19
|
let googleDocs = null;
|
|
13
20
|
let googleDrive = null;
|
|
14
21
|
let googleSheets = null;
|
|
22
|
+
let googleSlides = null;
|
|
23
|
+
let googleForms = null;
|
|
24
|
+
let googleScript = null;
|
|
15
25
|
// --- Initialization ---
|
|
16
26
|
async function initializeGoogleClient() {
|
|
17
|
-
if (googleDocs && googleDrive && googleSheets)
|
|
18
|
-
return { authClient, googleDocs, googleDrive, googleSheets };
|
|
19
|
-
|
|
27
|
+
if (googleDocs && googleDrive && googleSheets && googleSlides && googleForms && googleScript) {
|
|
28
|
+
return { authClient, googleDocs, googleDrive, googleSheets, googleSlides, googleForms, googleScript };
|
|
29
|
+
}
|
|
30
|
+
if (!authClient) {
|
|
20
31
|
try {
|
|
21
|
-
console.error("Attempting to authorize Google API client...");
|
|
22
32
|
const client = await authorize();
|
|
23
|
-
authClient = client;
|
|
33
|
+
authClient = client;
|
|
24
34
|
googleDocs = google.docs({ version: 'v1', auth: authClient });
|
|
25
35
|
googleDrive = google.drive({ version: 'v3', auth: authClient });
|
|
26
36
|
googleSheets = google.sheets({ version: 'v4', auth: authClient });
|
|
27
|
-
|
|
37
|
+
googleSlides = google.slides({ version: 'v1', auth: authClient });
|
|
38
|
+
googleForms = google.forms({ version: 'v1', auth: authClient });
|
|
39
|
+
googleScript = google.script({ version: 'v1', auth: authClient });
|
|
28
40
|
}
|
|
29
41
|
catch (error) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// Decide if server should exit or just fail tools
|
|
36
|
-
throw new Error("Google client initialization failed. Cannot start server tools.");
|
|
42
|
+
// Setup errors already show friendly instructions
|
|
43
|
+
if (!error.message?.includes('Setup required')) {
|
|
44
|
+
console.error("Error:", error.message || error);
|
|
45
|
+
}
|
|
46
|
+
process.exit(1);
|
|
37
47
|
}
|
|
38
48
|
}
|
|
39
|
-
// Ensure
|
|
49
|
+
// Ensure all clients are set if authClient is valid
|
|
40
50
|
if (authClient && !googleDocs) {
|
|
41
51
|
googleDocs = google.docs({ version: 'v1', auth: authClient });
|
|
42
52
|
}
|
|
@@ -46,10 +56,19 @@ async function initializeGoogleClient() {
|
|
|
46
56
|
if (authClient && !googleSheets) {
|
|
47
57
|
googleSheets = google.sheets({ version: 'v4', auth: authClient });
|
|
48
58
|
}
|
|
49
|
-
if (
|
|
50
|
-
|
|
59
|
+
if (authClient && !googleSlides) {
|
|
60
|
+
googleSlides = google.slides({ version: 'v1', auth: authClient });
|
|
61
|
+
}
|
|
62
|
+
if (authClient && !googleForms) {
|
|
63
|
+
googleForms = google.forms({ version: 'v1', auth: authClient });
|
|
64
|
+
}
|
|
65
|
+
if (authClient && !googleScript) {
|
|
66
|
+
googleScript = google.script({ version: 'v1', auth: authClient });
|
|
67
|
+
}
|
|
68
|
+
if (!googleDocs || !googleDrive || !googleSheets || !googleSlides || !googleForms || !googleScript) {
|
|
69
|
+
throw new Error("Google API clients could not be initialized.");
|
|
51
70
|
}
|
|
52
|
-
return { authClient, googleDocs, googleDrive, googleSheets };
|
|
71
|
+
return { authClient, googleDocs, googleDrive, googleSheets, googleSlides, googleForms, googleScript };
|
|
53
72
|
}
|
|
54
73
|
// Set up process-level unhandled error/rejection handlers to prevent crashes
|
|
55
74
|
process.on('uncaughtException', (error) => {
|
|
@@ -89,6 +108,30 @@ async function getSheetsClient() {
|
|
|
89
108
|
}
|
|
90
109
|
return sheets;
|
|
91
110
|
}
|
|
111
|
+
// --- Helper to get Slides client within tools ---
|
|
112
|
+
async function getSlidesClient() {
|
|
113
|
+
const { googleSlides: slides } = await initializeGoogleClient();
|
|
114
|
+
if (!slides) {
|
|
115
|
+
throw new UserError("Google Slides client is not initialized. Authentication might have failed during startup or lost connection.");
|
|
116
|
+
}
|
|
117
|
+
return slides;
|
|
118
|
+
}
|
|
119
|
+
// --- Helper to get Forms client within tools ---
|
|
120
|
+
async function getFormsClient() {
|
|
121
|
+
const { googleForms: forms } = await initializeGoogleClient();
|
|
122
|
+
if (!forms) {
|
|
123
|
+
throw new UserError("Google Forms client is not initialized. Authentication might have failed during startup or lost connection.");
|
|
124
|
+
}
|
|
125
|
+
return forms;
|
|
126
|
+
}
|
|
127
|
+
// --- Helper to get Script client within tools ---
|
|
128
|
+
async function getScriptClient() {
|
|
129
|
+
const { googleScript: script } = await initializeGoogleClient();
|
|
130
|
+
if (!script) {
|
|
131
|
+
throw new UserError("Google Apps Script client is not initialized. Authentication might have failed during startup or lost connection.");
|
|
132
|
+
}
|
|
133
|
+
return script;
|
|
134
|
+
}
|
|
92
135
|
// === HELPER FUNCTIONS ===
|
|
93
136
|
/**
|
|
94
137
|
* Converts Google Docs JSON structure to Markdown format
|
|
@@ -2428,23 +2471,717 @@ server.addTool({
|
|
|
2428
2471
|
}
|
|
2429
2472
|
}
|
|
2430
2473
|
});
|
|
2474
|
+
// =============================================
|
|
2475
|
+
// === GOOGLE SLIDES TOOLS ===
|
|
2476
|
+
// =============================================
|
|
2477
|
+
server.addTool({
|
|
2478
|
+
name: 'getPresentation',
|
|
2479
|
+
description: 'Gets metadata and structure of a Google Slides presentation.',
|
|
2480
|
+
parameters: PresentationIdParameter,
|
|
2481
|
+
execute: async (args, { log }) => {
|
|
2482
|
+
const slides = await getSlidesClient();
|
|
2483
|
+
log.info(`Getting presentation: ${args.presentationId}`);
|
|
2484
|
+
try {
|
|
2485
|
+
const response = await slides.presentations.get({
|
|
2486
|
+
presentationId: args.presentationId,
|
|
2487
|
+
});
|
|
2488
|
+
const presentation = response.data;
|
|
2489
|
+
const slideCount = presentation.slides?.length || 0;
|
|
2490
|
+
let result = `**${presentation.title || 'Untitled Presentation'}**\n`;
|
|
2491
|
+
result += `ID: ${presentation.presentationId}\n`;
|
|
2492
|
+
result += `Slides: ${slideCount}\n\n`;
|
|
2493
|
+
if (presentation.slides && presentation.slides.length > 0) {
|
|
2494
|
+
result += `## Slides\n`;
|
|
2495
|
+
presentation.slides.forEach((slide, index) => {
|
|
2496
|
+
result += `${index + 1}. Slide ID: ${slide.objectId}\n`;
|
|
2497
|
+
});
|
|
2498
|
+
}
|
|
2499
|
+
return result;
|
|
2500
|
+
}
|
|
2501
|
+
catch (error) {
|
|
2502
|
+
log.error(`Error getting presentation: ${error.message}`);
|
|
2503
|
+
if (error.code === 404)
|
|
2504
|
+
throw new UserError(`Presentation not found (ID: ${args.presentationId}).`);
|
|
2505
|
+
if (error.code === 403)
|
|
2506
|
+
throw new UserError(`Permission denied for presentation (ID: ${args.presentationId}).`);
|
|
2507
|
+
throw new UserError(`Failed to get presentation: ${error.message || 'Unknown error'}`);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
});
|
|
2511
|
+
server.addTool({
|
|
2512
|
+
name: 'listSlides',
|
|
2513
|
+
description: 'Lists all slides in a Google Slides presentation with their IDs and basic info.',
|
|
2514
|
+
parameters: PresentationIdParameter,
|
|
2515
|
+
execute: async (args, { log }) => {
|
|
2516
|
+
const slides = await getSlidesClient();
|
|
2517
|
+
log.info(`Listing slides for presentation: ${args.presentationId}`);
|
|
2518
|
+
try {
|
|
2519
|
+
const response = await slides.presentations.get({
|
|
2520
|
+
presentationId: args.presentationId,
|
|
2521
|
+
});
|
|
2522
|
+
const presentation = response.data;
|
|
2523
|
+
const slideList = presentation.slides || [];
|
|
2524
|
+
if (slideList.length === 0) {
|
|
2525
|
+
return 'This presentation has no slides.';
|
|
2526
|
+
}
|
|
2527
|
+
let result = `Found ${slideList.length} slide(s) in "${presentation.title || 'Untitled'}":\n\n`;
|
|
2528
|
+
slideList.forEach((slide, index) => {
|
|
2529
|
+
result += `**Slide ${index + 1}**\n`;
|
|
2530
|
+
result += ` ID: ${slide.objectId}\n`;
|
|
2531
|
+
const elementCount = slide.pageElements?.length || 0;
|
|
2532
|
+
result += ` Elements: ${elementCount}\n\n`;
|
|
2533
|
+
});
|
|
2534
|
+
return result;
|
|
2535
|
+
}
|
|
2536
|
+
catch (error) {
|
|
2537
|
+
log.error(`Error listing slides: ${error.message}`);
|
|
2538
|
+
if (error.code === 404)
|
|
2539
|
+
throw new UserError(`Presentation not found (ID: ${args.presentationId}).`);
|
|
2540
|
+
if (error.code === 403)
|
|
2541
|
+
throw new UserError(`Permission denied for presentation (ID: ${args.presentationId}).`);
|
|
2542
|
+
throw new UserError(`Failed to list slides: ${error.message || 'Unknown error'}`);
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
});
|
|
2546
|
+
server.addTool({
|
|
2547
|
+
name: 'createPresentation',
|
|
2548
|
+
description: 'Creates a new Google Slides presentation.',
|
|
2549
|
+
parameters: z.object({
|
|
2550
|
+
title: z.string().describe('The title for the new presentation.'),
|
|
2551
|
+
}),
|
|
2552
|
+
execute: async (args, { log }) => {
|
|
2553
|
+
const slides = await getSlidesClient();
|
|
2554
|
+
log.info(`Creating presentation: ${args.title}`);
|
|
2555
|
+
try {
|
|
2556
|
+
const response = await slides.presentations.create({
|
|
2557
|
+
requestBody: {
|
|
2558
|
+
title: args.title,
|
|
2559
|
+
},
|
|
2560
|
+
});
|
|
2561
|
+
const presentation = response.data;
|
|
2562
|
+
return `Created presentation: "${presentation.title}"\nID: ${presentation.presentationId}\nLink: https://docs.google.com/presentation/d/${presentation.presentationId}/edit`;
|
|
2563
|
+
}
|
|
2564
|
+
catch (error) {
|
|
2565
|
+
log.error(`Error creating presentation: ${error.message}`);
|
|
2566
|
+
throw new UserError(`Failed to create presentation: ${error.message || 'Unknown error'}`);
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
});
|
|
2570
|
+
server.addTool({
|
|
2571
|
+
name: 'getSlide',
|
|
2572
|
+
description: 'Gets detailed content of a specific slide in a presentation.',
|
|
2573
|
+
parameters: PresentationIdParameter.extend({
|
|
2574
|
+
slideId: z.string().describe('The object ID of the slide to retrieve.'),
|
|
2575
|
+
}),
|
|
2576
|
+
execute: async (args, { log }) => {
|
|
2577
|
+
const slides = await getSlidesClient();
|
|
2578
|
+
log.info(`Getting slide ${args.slideId} from presentation: ${args.presentationId}`);
|
|
2579
|
+
try {
|
|
2580
|
+
const response = await slides.presentations.pages.get({
|
|
2581
|
+
presentationId: args.presentationId,
|
|
2582
|
+
pageObjectId: args.slideId,
|
|
2583
|
+
});
|
|
2584
|
+
const slide = response.data;
|
|
2585
|
+
let result = `**Slide: ${slide.objectId}**\n\n`;
|
|
2586
|
+
if (slide.pageElements && slide.pageElements.length > 0) {
|
|
2587
|
+
result += `## Elements (${slide.pageElements.length})\n`;
|
|
2588
|
+
slide.pageElements.forEach((element, index) => {
|
|
2589
|
+
result += `${index + 1}. ID: ${element.objectId}`;
|
|
2590
|
+
if (element.shape) {
|
|
2591
|
+
result += ` (Shape: ${element.shape.shapeType || 'unknown'})`;
|
|
2592
|
+
if (element.shape.text?.textElements) {
|
|
2593
|
+
const textContent = element.shape.text.textElements
|
|
2594
|
+
.filter((te) => te.textRun?.content)
|
|
2595
|
+
.map((te) => te.textRun?.content)
|
|
2596
|
+
.join('');
|
|
2597
|
+
if (textContent.trim()) {
|
|
2598
|
+
result += `\n Text: "${textContent.trim().substring(0, 100)}${textContent.length > 100 ? '...' : ''}"`;
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
if (element.image)
|
|
2603
|
+
result += ` (Image)`;
|
|
2604
|
+
if (element.table)
|
|
2605
|
+
result += ` (Table: ${element.table.rows}x${element.table.columns})`;
|
|
2606
|
+
result += `\n`;
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
else {
|
|
2610
|
+
result += 'This slide has no elements.';
|
|
2611
|
+
}
|
|
2612
|
+
return result;
|
|
2613
|
+
}
|
|
2614
|
+
catch (error) {
|
|
2615
|
+
log.error(`Error getting slide: ${error.message}`);
|
|
2616
|
+
if (error.code === 404)
|
|
2617
|
+
throw new UserError(`Slide or presentation not found.`);
|
|
2618
|
+
if (error.code === 403)
|
|
2619
|
+
throw new UserError(`Permission denied.`);
|
|
2620
|
+
throw new UserError(`Failed to get slide: ${error.message || 'Unknown error'}`);
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
});
|
|
2624
|
+
server.addTool({
|
|
2625
|
+
name: 'createSlide',
|
|
2626
|
+
description: 'Adds a new slide to a Google Slides presentation.',
|
|
2627
|
+
parameters: PresentationIdParameter.extend({
|
|
2628
|
+
insertionIndex: z.number().int().min(0).optional().describe('Position to insert the slide (0 = beginning). Defaults to end.'),
|
|
2629
|
+
layoutType: z.enum(['BLANK', 'TITLE', 'TITLE_AND_BODY', 'TITLE_AND_TWO_COLUMNS', 'TITLE_ONLY', 'CAPTION_ONLY', 'BIG_NUMBER']).optional().default('BLANK').describe('The layout type for the new slide.'),
|
|
2630
|
+
}),
|
|
2631
|
+
execute: async (args, { log }) => {
|
|
2632
|
+
const slides = await getSlidesClient();
|
|
2633
|
+
log.info(`Creating slide in presentation: ${args.presentationId}`);
|
|
2634
|
+
try {
|
|
2635
|
+
const requests = [{
|
|
2636
|
+
createSlide: {
|
|
2637
|
+
insertionIndex: args.insertionIndex,
|
|
2638
|
+
slideLayoutReference: {
|
|
2639
|
+
predefinedLayout: args.layoutType,
|
|
2640
|
+
},
|
|
2641
|
+
},
|
|
2642
|
+
}];
|
|
2643
|
+
const response = await slides.presentations.batchUpdate({
|
|
2644
|
+
presentationId: args.presentationId,
|
|
2645
|
+
requestBody: { requests },
|
|
2646
|
+
});
|
|
2647
|
+
const createSlideResponse = response.data.replies?.[0]?.createSlide;
|
|
2648
|
+
if (createSlideResponse?.objectId) {
|
|
2649
|
+
return `Created new slide with ID: ${createSlideResponse.objectId}`;
|
|
2650
|
+
}
|
|
2651
|
+
return 'Slide created successfully.';
|
|
2652
|
+
}
|
|
2653
|
+
catch (error) {
|
|
2654
|
+
log.error(`Error creating slide: ${error.message}`);
|
|
2655
|
+
if (error.code === 404)
|
|
2656
|
+
throw new UserError(`Presentation not found (ID: ${args.presentationId}).`);
|
|
2657
|
+
if (error.code === 403)
|
|
2658
|
+
throw new UserError(`Permission denied for presentation (ID: ${args.presentationId}).`);
|
|
2659
|
+
throw new UserError(`Failed to create slide: ${error.message || 'Unknown error'}`);
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
});
|
|
2663
|
+
server.addTool({
|
|
2664
|
+
name: 'deleteSlide',
|
|
2665
|
+
description: 'Deletes a slide from a Google Slides presentation.',
|
|
2666
|
+
parameters: PresentationIdParameter.extend({
|
|
2667
|
+
slideId: z.string().describe('The object ID of the slide to delete.'),
|
|
2668
|
+
}),
|
|
2669
|
+
execute: async (args, { log }) => {
|
|
2670
|
+
const slides = await getSlidesClient();
|
|
2671
|
+
log.info(`Deleting slide ${args.slideId} from presentation: ${args.presentationId}`);
|
|
2672
|
+
try {
|
|
2673
|
+
const requests = [{
|
|
2674
|
+
deleteObject: {
|
|
2675
|
+
objectId: args.slideId,
|
|
2676
|
+
},
|
|
2677
|
+
}];
|
|
2678
|
+
await slides.presentations.batchUpdate({
|
|
2679
|
+
presentationId: args.presentationId,
|
|
2680
|
+
requestBody: { requests },
|
|
2681
|
+
});
|
|
2682
|
+
return `Deleted slide: ${args.slideId}`;
|
|
2683
|
+
}
|
|
2684
|
+
catch (error) {
|
|
2685
|
+
log.error(`Error deleting slide: ${error.message}`);
|
|
2686
|
+
if (error.code === 404)
|
|
2687
|
+
throw new UserError(`Slide or presentation not found.`);
|
|
2688
|
+
if (error.code === 403)
|
|
2689
|
+
throw new UserError(`Permission denied.`);
|
|
2690
|
+
throw new UserError(`Failed to delete slide: ${error.message || 'Unknown error'}`);
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
});
|
|
2694
|
+
// =============================================
|
|
2695
|
+
// === GOOGLE FORMS TOOLS (READ-ONLY) ===
|
|
2696
|
+
// =============================================
|
|
2697
|
+
server.addTool({
|
|
2698
|
+
name: 'getForm',
|
|
2699
|
+
description: 'Gets the structure and metadata of a Google Form. Note: Google Forms API is read-only.',
|
|
2700
|
+
parameters: FormIdParameter,
|
|
2701
|
+
execute: async (args, { log }) => {
|
|
2702
|
+
const forms = await getFormsClient();
|
|
2703
|
+
log.info(`Getting form: ${args.formId}`);
|
|
2704
|
+
try {
|
|
2705
|
+
const response = await forms.forms.get({
|
|
2706
|
+
formId: args.formId,
|
|
2707
|
+
});
|
|
2708
|
+
const form = response.data;
|
|
2709
|
+
let result = `**${form.info?.title || 'Untitled Form'}**\n`;
|
|
2710
|
+
if (form.info?.description) {
|
|
2711
|
+
result += `Description: ${form.info.description}\n`;
|
|
2712
|
+
}
|
|
2713
|
+
result += `Form ID: ${form.formId}\n`;
|
|
2714
|
+
result += `Response URL: ${form.responderUri}\n`;
|
|
2715
|
+
result += `Edit URL: https://docs.google.com/forms/d/${form.formId}/edit\n\n`;
|
|
2716
|
+
const items = form.items || [];
|
|
2717
|
+
result += `## Questions (${items.length})\n`;
|
|
2718
|
+
items.forEach((item, index) => {
|
|
2719
|
+
result += `\n${index + 1}. **${item.title || 'Untitled'}**`;
|
|
2720
|
+
if (item.questionItem) {
|
|
2721
|
+
const question = item.questionItem.question;
|
|
2722
|
+
if (question?.choiceQuestion) {
|
|
2723
|
+
result += ` (${question.choiceQuestion.type || 'Choice'})`;
|
|
2724
|
+
const options = question.choiceQuestion.options || [];
|
|
2725
|
+
options.forEach((opt) => {
|
|
2726
|
+
result += `\n - ${opt.value}`;
|
|
2727
|
+
});
|
|
2728
|
+
}
|
|
2729
|
+
else if (question?.textQuestion) {
|
|
2730
|
+
result += question.textQuestion.paragraph ? ' (Long answer)' : ' (Short answer)';
|
|
2731
|
+
}
|
|
2732
|
+
else if (question?.scaleQuestion) {
|
|
2733
|
+
result += ` (Scale: ${question.scaleQuestion.low}-${question.scaleQuestion.high})`;
|
|
2734
|
+
}
|
|
2735
|
+
if (question?.required)
|
|
2736
|
+
result += ' *Required*';
|
|
2737
|
+
}
|
|
2738
|
+
result += '\n';
|
|
2739
|
+
});
|
|
2740
|
+
return result;
|
|
2741
|
+
}
|
|
2742
|
+
catch (error) {
|
|
2743
|
+
log.error(`Error getting form: ${error.message}`);
|
|
2744
|
+
if (error.code === 404)
|
|
2745
|
+
throw new UserError(`Form not found (ID: ${args.formId}).`);
|
|
2746
|
+
if (error.code === 403)
|
|
2747
|
+
throw new UserError(`Permission denied for form (ID: ${args.formId}).`);
|
|
2748
|
+
throw new UserError(`Failed to get form: ${error.message || 'Unknown error'}`);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
});
|
|
2752
|
+
server.addTool({
|
|
2753
|
+
name: 'listFormQuestions',
|
|
2754
|
+
description: 'Lists all questions in a Google Form with their IDs and types.',
|
|
2755
|
+
parameters: FormIdParameter,
|
|
2756
|
+
execute: async (args, { log }) => {
|
|
2757
|
+
const forms = await getFormsClient();
|
|
2758
|
+
log.info(`Listing questions for form: ${args.formId}`);
|
|
2759
|
+
try {
|
|
2760
|
+
const response = await forms.forms.get({
|
|
2761
|
+
formId: args.formId,
|
|
2762
|
+
});
|
|
2763
|
+
const form = response.data;
|
|
2764
|
+
const items = form.items || [];
|
|
2765
|
+
if (items.length === 0) {
|
|
2766
|
+
return 'This form has no questions.';
|
|
2767
|
+
}
|
|
2768
|
+
let result = `Found ${items.length} item(s) in "${form.info?.title || 'Untitled'}":\n\n`;
|
|
2769
|
+
items.forEach((item, index) => {
|
|
2770
|
+
result += `**${index + 1}. ${item.title || 'Untitled'}**\n`;
|
|
2771
|
+
result += ` Item ID: ${item.itemId}\n`;
|
|
2772
|
+
if (item.questionItem) {
|
|
2773
|
+
const q = item.questionItem.question;
|
|
2774
|
+
if (q?.questionId)
|
|
2775
|
+
result += ` Question ID: ${q.questionId}\n`;
|
|
2776
|
+
if (q?.choiceQuestion)
|
|
2777
|
+
result += ` Type: ${q.choiceQuestion.type || 'Choice'}\n`;
|
|
2778
|
+
else if (q?.textQuestion)
|
|
2779
|
+
result += ` Type: ${q.textQuestion.paragraph ? 'Paragraph' : 'Text'}\n`;
|
|
2780
|
+
else if (q?.scaleQuestion)
|
|
2781
|
+
result += ` Type: Scale\n`;
|
|
2782
|
+
else if (q?.dateQuestion)
|
|
2783
|
+
result += ` Type: Date\n`;
|
|
2784
|
+
else if (q?.timeQuestion)
|
|
2785
|
+
result += ` Type: Time\n`;
|
|
2786
|
+
else if (q?.fileUploadQuestion)
|
|
2787
|
+
result += ` Type: File Upload\n`;
|
|
2788
|
+
if (q?.required)
|
|
2789
|
+
result += ` Required: Yes\n`;
|
|
2790
|
+
}
|
|
2791
|
+
else if (item.pageBreakItem) {
|
|
2792
|
+
result += ` Type: Page Break\n`;
|
|
2793
|
+
}
|
|
2794
|
+
else if (item.textItem) {
|
|
2795
|
+
result += ` Type: Text/Description\n`;
|
|
2796
|
+
}
|
|
2797
|
+
result += '\n';
|
|
2798
|
+
});
|
|
2799
|
+
return result;
|
|
2800
|
+
}
|
|
2801
|
+
catch (error) {
|
|
2802
|
+
log.error(`Error listing form questions: ${error.message}`);
|
|
2803
|
+
if (error.code === 404)
|
|
2804
|
+
throw new UserError(`Form not found (ID: ${args.formId}).`);
|
|
2805
|
+
if (error.code === 403)
|
|
2806
|
+
throw new UserError(`Permission denied for form (ID: ${args.formId}).`);
|
|
2807
|
+
throw new UserError(`Failed to list form questions: ${error.message || 'Unknown error'}`);
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
});
|
|
2811
|
+
server.addTool({
|
|
2812
|
+
name: 'listFormResponses',
|
|
2813
|
+
description: 'Lists all responses submitted to a Google Form.',
|
|
2814
|
+
parameters: FormIdParameter.extend({
|
|
2815
|
+
maxResults: z.number().int().min(1).max(500).optional().default(50).describe('Maximum number of responses to return.'),
|
|
2816
|
+
}),
|
|
2817
|
+
execute: async (args, { log }) => {
|
|
2818
|
+
const forms = await getFormsClient();
|
|
2819
|
+
log.info(`Listing responses for form: ${args.formId}`);
|
|
2820
|
+
try {
|
|
2821
|
+
const response = await forms.forms.responses.list({
|
|
2822
|
+
formId: args.formId,
|
|
2823
|
+
pageSize: args.maxResults,
|
|
2824
|
+
});
|
|
2825
|
+
const responses = response.data.responses || [];
|
|
2826
|
+
if (responses.length === 0) {
|
|
2827
|
+
return 'This form has no responses yet.';
|
|
2828
|
+
}
|
|
2829
|
+
let result = `Found ${responses.length} response(s):\n\n`;
|
|
2830
|
+
responses.forEach((resp, index) => {
|
|
2831
|
+
result += `**Response ${index + 1}**\n`;
|
|
2832
|
+
result += ` Response ID: ${resp.responseId}\n`;
|
|
2833
|
+
result += ` Submitted: ${resp.lastSubmittedTime}\n`;
|
|
2834
|
+
if (resp.respondentEmail) {
|
|
2835
|
+
result += ` Respondent: ${resp.respondentEmail}\n`;
|
|
2836
|
+
}
|
|
2837
|
+
result += '\n';
|
|
2838
|
+
});
|
|
2839
|
+
if (response.data.nextPageToken) {
|
|
2840
|
+
result += `\n_More responses available. Showing first ${responses.length}._`;
|
|
2841
|
+
}
|
|
2842
|
+
return result;
|
|
2843
|
+
}
|
|
2844
|
+
catch (error) {
|
|
2845
|
+
log.error(`Error listing form responses: ${error.message}`);
|
|
2846
|
+
if (error.code === 404)
|
|
2847
|
+
throw new UserError(`Form not found (ID: ${args.formId}).`);
|
|
2848
|
+
if (error.code === 403)
|
|
2849
|
+
throw new UserError(`Permission denied. Make sure you have access to view responses.`);
|
|
2850
|
+
throw new UserError(`Failed to list responses: ${error.message || 'Unknown error'}`);
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
});
|
|
2854
|
+
server.addTool({
|
|
2855
|
+
name: 'getFormResponse',
|
|
2856
|
+
description: 'Gets a specific response submitted to a Google Form with all answers.',
|
|
2857
|
+
parameters: FormIdParameter.extend({
|
|
2858
|
+
responseId: z.string().describe('The ID of the specific response to retrieve.'),
|
|
2859
|
+
}),
|
|
2860
|
+
execute: async (args, { log }) => {
|
|
2861
|
+
const forms = await getFormsClient();
|
|
2862
|
+
log.info(`Getting response ${args.responseId} for form: ${args.formId}`);
|
|
2863
|
+
try {
|
|
2864
|
+
// First get form structure to map question IDs to titles
|
|
2865
|
+
const formResponse = await forms.forms.get({
|
|
2866
|
+
formId: args.formId,
|
|
2867
|
+
});
|
|
2868
|
+
const form = formResponse.data;
|
|
2869
|
+
const questionMap = new Map();
|
|
2870
|
+
(form.items || []).forEach(item => {
|
|
2871
|
+
if (item.questionItem?.question?.questionId && item.title) {
|
|
2872
|
+
questionMap.set(item.questionItem.question.questionId, item.title);
|
|
2873
|
+
}
|
|
2874
|
+
});
|
|
2875
|
+
// Get the specific response
|
|
2876
|
+
const response = await forms.forms.responses.get({
|
|
2877
|
+
formId: args.formId,
|
|
2878
|
+
responseId: args.responseId,
|
|
2879
|
+
});
|
|
2880
|
+
const resp = response.data;
|
|
2881
|
+
let result = `**Response Details**\n`;
|
|
2882
|
+
result += `Response ID: ${resp.responseId}\n`;
|
|
2883
|
+
result += `Submitted: ${resp.lastSubmittedTime}\n`;
|
|
2884
|
+
if (resp.respondentEmail) {
|
|
2885
|
+
result += `Respondent: ${resp.respondentEmail}\n`;
|
|
2886
|
+
}
|
|
2887
|
+
result += `\n## Answers\n`;
|
|
2888
|
+
const answers = resp.answers || {};
|
|
2889
|
+
for (const [questionId, answer] of Object.entries(answers)) {
|
|
2890
|
+
const questionTitle = questionMap.get(questionId) || questionId;
|
|
2891
|
+
result += `\n**${questionTitle}**\n`;
|
|
2892
|
+
const textAnswers = answer.textAnswers?.answers || [];
|
|
2893
|
+
textAnswers.forEach((a) => {
|
|
2894
|
+
result += ` ${a.value}\n`;
|
|
2895
|
+
});
|
|
2896
|
+
}
|
|
2897
|
+
return result;
|
|
2898
|
+
}
|
|
2899
|
+
catch (error) {
|
|
2900
|
+
log.error(`Error getting form response: ${error.message}`);
|
|
2901
|
+
if (error.code === 404)
|
|
2902
|
+
throw new UserError(`Form or response not found.`);
|
|
2903
|
+
if (error.code === 403)
|
|
2904
|
+
throw new UserError(`Permission denied.`);
|
|
2905
|
+
throw new UserError(`Failed to get response: ${error.message || 'Unknown error'}`);
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
});
|
|
2909
|
+
// =============================================
|
|
2910
|
+
// === GOOGLE APPS SCRIPT TOOLS ===
|
|
2911
|
+
// =============================================
|
|
2912
|
+
server.addTool({
|
|
2913
|
+
name: 'listScriptDeployments',
|
|
2914
|
+
description: 'Lists deployments for a Google Apps Script project.',
|
|
2915
|
+
parameters: ScriptIdParameter.extend({
|
|
2916
|
+
maxResults: z.number().int().min(1).max(50).optional().default(20).describe('Maximum number of deployments to return.'),
|
|
2917
|
+
}),
|
|
2918
|
+
execute: async (args, { log }) => {
|
|
2919
|
+
const script = await getScriptClient();
|
|
2920
|
+
log.info(`Listing deployments for script: ${args.scriptId}`);
|
|
2921
|
+
try {
|
|
2922
|
+
const response = await script.projects.deployments.list({
|
|
2923
|
+
scriptId: args.scriptId,
|
|
2924
|
+
pageSize: args.maxResults,
|
|
2925
|
+
});
|
|
2926
|
+
const deployments = response.data.deployments || [];
|
|
2927
|
+
if (deployments.length === 0) {
|
|
2928
|
+
return `No deployments found for script ${args.scriptId}.`;
|
|
2929
|
+
}
|
|
2930
|
+
let result = `Found ${deployments.length} deployment(s):\n\n`;
|
|
2931
|
+
deployments.forEach((deployment, index) => {
|
|
2932
|
+
result += `**${index + 1}. ${deployment.deploymentId}**\n`;
|
|
2933
|
+
if (deployment.deploymentConfig) {
|
|
2934
|
+
result += ` Description: ${deployment.deploymentConfig.description || 'None'}\n`;
|
|
2935
|
+
result += ` Version: ${deployment.deploymentConfig.versionNumber || 'HEAD'}\n`;
|
|
2936
|
+
}
|
|
2937
|
+
if (deployment.updateTime) {
|
|
2938
|
+
result += ` Updated: ${deployment.updateTime}\n`;
|
|
2939
|
+
}
|
|
2940
|
+
result += '\n';
|
|
2941
|
+
});
|
|
2942
|
+
return result;
|
|
2943
|
+
}
|
|
2944
|
+
catch (error) {
|
|
2945
|
+
log.error(`Error listing deployments: ${error.message}`);
|
|
2946
|
+
if (error.code === 404)
|
|
2947
|
+
throw new UserError(`Script project not found (ID: ${args.scriptId}).`);
|
|
2948
|
+
if (error.code === 403)
|
|
2949
|
+
throw new UserError(`Permission denied. Make sure the Apps Script API is enabled.`);
|
|
2950
|
+
throw new UserError(`Failed to list deployments: ${error.message || 'Unknown error'}`);
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
});
|
|
2954
|
+
server.addTool({
|
|
2955
|
+
name: 'getScriptProject',
|
|
2956
|
+
description: 'Gets metadata about a Google Apps Script project.',
|
|
2957
|
+
parameters: ScriptIdParameter,
|
|
2958
|
+
execute: async (args, { log }) => {
|
|
2959
|
+
const script = await getScriptClient();
|
|
2960
|
+
log.info(`Getting script project: ${args.scriptId}`);
|
|
2961
|
+
try {
|
|
2962
|
+
const response = await script.projects.get({
|
|
2963
|
+
scriptId: args.scriptId,
|
|
2964
|
+
});
|
|
2965
|
+
const project = response.data;
|
|
2966
|
+
let result = `**${project.title || 'Untitled Project'}**\n`;
|
|
2967
|
+
result += `Script ID: ${project.scriptId}\n`;
|
|
2968
|
+
result += `Create Time: ${project.createTime}\n`;
|
|
2969
|
+
result += `Update Time: ${project.updateTime}\n`;
|
|
2970
|
+
if (project.parentId) {
|
|
2971
|
+
result += `Parent ID: ${project.parentId}\n`;
|
|
2972
|
+
}
|
|
2973
|
+
result += `\nEdit: https://script.google.com/d/${project.scriptId}/edit`;
|
|
2974
|
+
return result;
|
|
2975
|
+
}
|
|
2976
|
+
catch (error) {
|
|
2977
|
+
log.error(`Error getting script project: ${error.message}`);
|
|
2978
|
+
if (error.code === 404)
|
|
2979
|
+
throw new UserError(`Script project not found (ID: ${args.scriptId}).`);
|
|
2980
|
+
if (error.code === 403)
|
|
2981
|
+
throw new UserError(`Permission denied for script project.`);
|
|
2982
|
+
throw new UserError(`Failed to get project: ${error.message || 'Unknown error'}`);
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
});
|
|
2986
|
+
server.addTool({
|
|
2987
|
+
name: 'createScriptProject',
|
|
2988
|
+
description: 'Creates a new Google Apps Script project.',
|
|
2989
|
+
parameters: z.object({
|
|
2990
|
+
title: z.string().describe('The title for the new script project.'),
|
|
2991
|
+
parentId: z.string().optional().describe('Optional: ID of a Drive folder, Docs, Sheets, or Forms file to bind the script to.'),
|
|
2992
|
+
}),
|
|
2993
|
+
execute: async (args, { log }) => {
|
|
2994
|
+
const script = await getScriptClient();
|
|
2995
|
+
log.info(`Creating script project: ${args.title}`);
|
|
2996
|
+
try {
|
|
2997
|
+
const response = await script.projects.create({
|
|
2998
|
+
requestBody: {
|
|
2999
|
+
title: args.title,
|
|
3000
|
+
parentId: args.parentId,
|
|
3001
|
+
},
|
|
3002
|
+
});
|
|
3003
|
+
const project = response.data;
|
|
3004
|
+
return `Created script project: "${project.title}"\nScript ID: ${project.scriptId}\nEdit: https://script.google.com/d/${project.scriptId}/edit`;
|
|
3005
|
+
}
|
|
3006
|
+
catch (error) {
|
|
3007
|
+
log.error(`Error creating script project: ${error.message}`);
|
|
3008
|
+
throw new UserError(`Failed to create project: ${error.message || 'Unknown error'}`);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
});
|
|
3012
|
+
server.addTool({
|
|
3013
|
+
name: 'getScriptContent',
|
|
3014
|
+
description: 'Gets the source files of a Google Apps Script project.',
|
|
3015
|
+
parameters: ScriptIdParameter,
|
|
3016
|
+
execute: async (args, { log }) => {
|
|
3017
|
+
const script = await getScriptClient();
|
|
3018
|
+
log.info(`Getting content for script: ${args.scriptId}`);
|
|
3019
|
+
try {
|
|
3020
|
+
const response = await script.projects.getContent({
|
|
3021
|
+
scriptId: args.scriptId,
|
|
3022
|
+
});
|
|
3023
|
+
const content = response.data;
|
|
3024
|
+
const files = content.files || [];
|
|
3025
|
+
if (files.length === 0) {
|
|
3026
|
+
return 'This project has no files.';
|
|
3027
|
+
}
|
|
3028
|
+
let result = `Found ${files.length} file(s) in project:\n\n`;
|
|
3029
|
+
files.forEach((file, index) => {
|
|
3030
|
+
result += `**${index + 1}. ${file.name}** (${file.type})\n`;
|
|
3031
|
+
result += '```\n';
|
|
3032
|
+
result += (file.source || '(empty)').substring(0, 1000);
|
|
3033
|
+
if ((file.source || '').length > 1000) {
|
|
3034
|
+
result += '\n... (truncated)';
|
|
3035
|
+
}
|
|
3036
|
+
result += '\n```\n\n';
|
|
3037
|
+
});
|
|
3038
|
+
return result;
|
|
3039
|
+
}
|
|
3040
|
+
catch (error) {
|
|
3041
|
+
log.error(`Error getting script content: ${error.message}`);
|
|
3042
|
+
if (error.code === 404)
|
|
3043
|
+
throw new UserError(`Script project not found (ID: ${args.scriptId}).`);
|
|
3044
|
+
if (error.code === 403)
|
|
3045
|
+
throw new UserError(`Permission denied for script project.`);
|
|
3046
|
+
throw new UserError(`Failed to get content: ${error.message || 'Unknown error'}`);
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
});
|
|
3050
|
+
server.addTool({
|
|
3051
|
+
name: 'updateScriptContent',
|
|
3052
|
+
description: 'Updates the source files of a Google Apps Script project.',
|
|
3053
|
+
parameters: ScriptIdParameter.extend({
|
|
3054
|
+
files: z.array(z.object({
|
|
3055
|
+
name: z.string().describe('File name (e.g., "Code", "Utils").'),
|
|
3056
|
+
type: z.enum(['SERVER_JS', 'HTML', 'JSON']).describe('File type: SERVER_JS for .gs files, HTML for .html files, JSON for appsscript.json.'),
|
|
3057
|
+
source: z.string().describe('The source code content.'),
|
|
3058
|
+
})).describe('Array of files to update. This replaces all existing files.'),
|
|
3059
|
+
}),
|
|
3060
|
+
execute: async (args, { log }) => {
|
|
3061
|
+
const script = await getScriptClient();
|
|
3062
|
+
log.info(`Updating content for script: ${args.scriptId}`);
|
|
3063
|
+
try {
|
|
3064
|
+
const response = await script.projects.updateContent({
|
|
3065
|
+
scriptId: args.scriptId,
|
|
3066
|
+
requestBody: {
|
|
3067
|
+
files: args.files.map(f => ({
|
|
3068
|
+
name: f.name,
|
|
3069
|
+
type: f.type,
|
|
3070
|
+
source: f.source,
|
|
3071
|
+
})),
|
|
3072
|
+
},
|
|
3073
|
+
});
|
|
3074
|
+
const updatedFiles = response.data.files || [];
|
|
3075
|
+
return `Updated ${updatedFiles.length} file(s) in project.`;
|
|
3076
|
+
}
|
|
3077
|
+
catch (error) {
|
|
3078
|
+
log.error(`Error updating script content: ${error.message}`);
|
|
3079
|
+
if (error.code === 404)
|
|
3080
|
+
throw new UserError(`Script project not found (ID: ${args.scriptId}).`);
|
|
3081
|
+
if (error.code === 403)
|
|
3082
|
+
throw new UserError(`Permission denied for script project.`);
|
|
3083
|
+
throw new UserError(`Failed to update content: ${error.message || 'Unknown error'}`);
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
});
|
|
3087
|
+
server.addTool({
|
|
3088
|
+
name: 'listScriptVersions',
|
|
3089
|
+
description: 'Lists all versions of a Google Apps Script project.',
|
|
3090
|
+
parameters: ScriptIdParameter,
|
|
3091
|
+
execute: async (args, { log }) => {
|
|
3092
|
+
const script = await getScriptClient();
|
|
3093
|
+
log.info(`Listing versions for script: ${args.scriptId}`);
|
|
3094
|
+
try {
|
|
3095
|
+
const response = await script.projects.versions.list({
|
|
3096
|
+
scriptId: args.scriptId,
|
|
3097
|
+
pageSize: 50,
|
|
3098
|
+
});
|
|
3099
|
+
const versions = response.data.versions || [];
|
|
3100
|
+
if (versions.length === 0) {
|
|
3101
|
+
return 'This project has no saved versions.';
|
|
3102
|
+
}
|
|
3103
|
+
let result = `Found ${versions.length} version(s):\n\n`;
|
|
3104
|
+
versions.forEach((version, index) => {
|
|
3105
|
+
result += `**Version ${version.versionNumber}**\n`;
|
|
3106
|
+
if (version.description) {
|
|
3107
|
+
result += ` Description: ${version.description}\n`;
|
|
3108
|
+
}
|
|
3109
|
+
result += ` Created: ${version.createTime}\n\n`;
|
|
3110
|
+
});
|
|
3111
|
+
return result;
|
|
3112
|
+
}
|
|
3113
|
+
catch (error) {
|
|
3114
|
+
log.error(`Error listing script versions: ${error.message}`);
|
|
3115
|
+
if (error.code === 404)
|
|
3116
|
+
throw new UserError(`Script project not found (ID: ${args.scriptId}).`);
|
|
3117
|
+
if (error.code === 403)
|
|
3118
|
+
throw new UserError(`Permission denied for script project.`);
|
|
3119
|
+
throw new UserError(`Failed to list versions: ${error.message || 'Unknown error'}`);
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
});
|
|
3123
|
+
server.addTool({
|
|
3124
|
+
name: 'runScript',
|
|
3125
|
+
description: 'Executes a function in a deployed Google Apps Script project. IMPORTANT: The script must be deployed as an API Executable and share a Cloud project with this application.',
|
|
3126
|
+
parameters: ScriptIdParameter.extend({
|
|
3127
|
+
functionName: z.string().describe('The name of the function to execute.'),
|
|
3128
|
+
parameters: z.array(z.any()).optional().describe('Optional array of parameters to pass to the function.'),
|
|
3129
|
+
}),
|
|
3130
|
+
execute: async (args, { log }) => {
|
|
3131
|
+
const script = await getScriptClient();
|
|
3132
|
+
log.info(`Running function "${args.functionName}" in script: ${args.scriptId}`);
|
|
3133
|
+
try {
|
|
3134
|
+
const response = await script.scripts.run({
|
|
3135
|
+
scriptId: args.scriptId,
|
|
3136
|
+
requestBody: {
|
|
3137
|
+
function: args.functionName,
|
|
3138
|
+
parameters: args.parameters || [],
|
|
3139
|
+
},
|
|
3140
|
+
});
|
|
3141
|
+
const result = response.data;
|
|
3142
|
+
if (result.error) {
|
|
3143
|
+
const errorDetails = result.error.details || [];
|
|
3144
|
+
let errorMessage = `Script execution error: ${result.error.message || 'Unknown error'}`;
|
|
3145
|
+
errorDetails.forEach((detail) => {
|
|
3146
|
+
if (detail.scriptStackTraceElements) {
|
|
3147
|
+
errorMessage += '\n\nStack trace:';
|
|
3148
|
+
detail.scriptStackTraceElements.forEach((element) => {
|
|
3149
|
+
errorMessage += `\n at ${element.function} (line ${element.lineNumber})`;
|
|
3150
|
+
});
|
|
3151
|
+
}
|
|
3152
|
+
});
|
|
3153
|
+
throw new UserError(errorMessage);
|
|
3154
|
+
}
|
|
3155
|
+
if (result.response) {
|
|
3156
|
+
const returnValue = result.response.result;
|
|
3157
|
+
if (returnValue === undefined || returnValue === null) {
|
|
3158
|
+
return 'Function executed successfully (no return value).';
|
|
3159
|
+
}
|
|
3160
|
+
return `Function returned:\n${JSON.stringify(returnValue, null, 2)}`;
|
|
3161
|
+
}
|
|
3162
|
+
return 'Function executed successfully.';
|
|
3163
|
+
}
|
|
3164
|
+
catch (error) {
|
|
3165
|
+
log.error(`Error running script: ${error.message}`);
|
|
3166
|
+
if (error instanceof UserError)
|
|
3167
|
+
throw error;
|
|
3168
|
+
if (error.code === 404)
|
|
3169
|
+
throw new UserError(`Script not found or not deployed as API Executable.`);
|
|
3170
|
+
if (error.code === 403)
|
|
3171
|
+
throw new UserError(`Permission denied. Ensure the script is deployed and shares a Cloud project with this application.`);
|
|
3172
|
+
throw new UserError(`Failed to run script: ${error.message || 'Unknown error'}`);
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
});
|
|
2431
3176
|
// --- Server Startup ---
|
|
2432
3177
|
async function startServer() {
|
|
2433
3178
|
try {
|
|
2434
|
-
await initializeGoogleClient();
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
const configToUse = {
|
|
2438
|
-
transportType: "stdio",
|
|
2439
|
-
};
|
|
2440
|
-
// Start the server with proper error handling
|
|
2441
|
-
server.start(configToUse);
|
|
2442
|
-
console.error(`MCP Server running using ${configToUse.transportType}. Awaiting client connection...`);
|
|
2443
|
-
// Log that error handling has been enabled
|
|
2444
|
-
console.error('Process-level error handling configured to prevent crashes from timeout errors.');
|
|
3179
|
+
await initializeGoogleClient();
|
|
3180
|
+
server.start({ transportType: "stdio" });
|
|
3181
|
+
console.error('✓ Google Workspace MCP server ready\n');
|
|
2445
3182
|
}
|
|
2446
3183
|
catch (startError) {
|
|
2447
|
-
console.error("
|
|
3184
|
+
console.error("Server failed to start:", startError.message || startError);
|
|
2448
3185
|
process.exit(1);
|
|
2449
3186
|
}
|
|
2450
3187
|
}
|
package/dist/types.js
CHANGED
|
@@ -96,6 +96,21 @@ export const ApplyParagraphStyleToolParameters = DocumentIdParameter.extend({
|
|
|
96
96
|
]).describe("Specify the target paragraph either by start/end indices, by finding text within it, or by providing an index within it."),
|
|
97
97
|
style: ParagraphStyleParameters.refine(styleArgs => Object.values(styleArgs).some(v => v !== undefined), { message: "At least one paragraph style option must be provided." }).describe("The paragraph styling to apply.")
|
|
98
98
|
});
|
|
99
|
+
// --- Google Slides Parameter Schemas ---
|
|
100
|
+
export const PresentationIdParameter = z.object({
|
|
101
|
+
presentationId: z.string().describe('The ID of the Google Slides presentation (from the URL).'),
|
|
102
|
+
});
|
|
103
|
+
export const SlideIdParameter = z.object({
|
|
104
|
+
slideId: z.string().optional().describe('The object ID of a specific slide/page. Use listSlides to find IDs.'),
|
|
105
|
+
});
|
|
106
|
+
// --- Google Forms Parameter Schemas ---
|
|
107
|
+
export const FormIdParameter = z.object({
|
|
108
|
+
formId: z.string().describe('The ID of the Google Form (from the URL).'),
|
|
109
|
+
});
|
|
110
|
+
// --- Google Apps Script Parameter Schemas ---
|
|
111
|
+
export const ScriptIdParameter = z.object({
|
|
112
|
+
scriptId: z.string().describe('The Apps Script project ID.'),
|
|
113
|
+
});
|
|
99
114
|
// --- Error Class ---
|
|
100
115
|
// Use FastMCP's UserError for client-facing issues
|
|
101
116
|
// Define a custom error for internal issues if needed
|