@piotr-agier/google-drive-mcp 2.1.0 → 2.2.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 +16 -2
- package/dist/index.js +100 -41
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -292,6 +292,16 @@ Notes:
|
|
|
292
292
|
`drive`, `drive.file`, `drive.readonly`, `documents`, `spreadsheets`, `presentations`, `calendar`, `calendar.events`.
|
|
293
293
|
- Changing scopes usually requires re-authentication.
|
|
294
294
|
|
|
295
|
+
### Auth Server Port Configuration
|
|
296
|
+
|
|
297
|
+
During OAuth authentication, a local HTTP server is started to receive the callback. By default it tries ports 3000–3004. If those conflict with other services (e.g., a dev server), you can change the starting port:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
export GOOGLE_DRIVE_MCP_AUTH_PORT=3100
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
The server will try 5 consecutive ports starting from the configured value (e.g., 3100–3104).
|
|
304
|
+
|
|
295
305
|
### Token Storage
|
|
296
306
|
|
|
297
307
|
Authentication tokens are stored securely following the XDG Base Directory specification:
|
|
@@ -1104,7 +1114,7 @@ OAuth credentials not found. Please provide credentials using one of these metho
|
|
|
1104
1114
|
#### "Authentication failed" or Browser doesn't open
|
|
1105
1115
|
**Possible causes:**
|
|
1106
1116
|
1. **Wrong credential type**: Must be "Desktop app", not "Web application"
|
|
1107
|
-
2. **Port blocked**: Ports 3000-3004 must be available
|
|
1117
|
+
2. **Port blocked**: Ports 3000-3004 must be available (or custom range if `GOOGLE_DRIVE_MCP_AUTH_PORT` is set)
|
|
1108
1118
|
3. **Test user not added**: Add your email in OAuth consent screen
|
|
1109
1119
|
|
|
1110
1120
|
**Solution:**
|
|
@@ -1112,9 +1122,12 @@ OAuth credentials not found. Please provide credentials using one of these metho
|
|
|
1112
1122
|
# Check if ports are in use
|
|
1113
1123
|
lsof -i :3000-3004
|
|
1114
1124
|
|
|
1115
|
-
# Kill processes if needed
|
|
1125
|
+
# Option 1: Kill processes if needed
|
|
1116
1126
|
kill -9 <PID>
|
|
1117
1127
|
|
|
1128
|
+
# Option 2: Use a different port range
|
|
1129
|
+
export GOOGLE_DRIVE_MCP_AUTH_PORT=3100
|
|
1130
|
+
|
|
1118
1131
|
# Re-run authentication
|
|
1119
1132
|
npx @piotr-agier/google-drive-mcp auth
|
|
1120
1133
|
```
|
|
@@ -1309,6 +1322,7 @@ npm run typecheck # Type checking without compilation
|
|
|
1309
1322
|
| Variable | Description | Default | Example |
|
|
1310
1323
|
|----------|-------------|---------|---------|
|
|
1311
1324
|
| `GOOGLE_DRIVE_MCP_TOKEN_PATH` | Override token storage location | `~/.config/google-drive-mcp/tokens.json` | `/custom/path/tokens.json` |
|
|
1325
|
+
| `GOOGLE_DRIVE_MCP_AUTH_PORT` | Starting port for OAuth callback server (uses 5 consecutive ports) | `3000` | `3100` |
|
|
1312
1326
|
| `DEBUG` | Enable debug logging | (disabled) | `google-drive-mcp:*` |
|
|
1313
1327
|
|
|
1314
1328
|
**External Authentication** (alternative to local OAuth flow):
|
package/dist/index.js
CHANGED
|
@@ -427,7 +427,14 @@ var AuthServer = class {
|
|
|
427
427
|
this.baseOAuth2Client = oauth2Client;
|
|
428
428
|
this.tokenManager = new TokenManager(oauth2Client);
|
|
429
429
|
this.app = express();
|
|
430
|
-
|
|
430
|
+
const raw = process.env.GOOGLE_DRIVE_MCP_AUTH_PORT;
|
|
431
|
+
const portStart = raw ? Number(raw) : 3e3;
|
|
432
|
+
if (!Number.isInteger(portStart) || portStart < 1 || portStart > 65531) {
|
|
433
|
+
throw new Error(
|
|
434
|
+
`Invalid GOOGLE_DRIVE_MCP_AUTH_PORT: "${raw}". Must be an integer between 1 and 65531.`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
this.portRange = { start: portStart, end: portStart + 4 };
|
|
431
438
|
this.setupRoutes();
|
|
432
439
|
}
|
|
433
440
|
setupRoutes() {
|
|
@@ -1085,7 +1092,8 @@ var AddPermissionSchema = z.object({
|
|
|
1085
1092
|
emailAddress: z.string().email("Valid email is required"),
|
|
1086
1093
|
role: z.enum(["owner", "organizer", "fileOrganizer", "writer", "commenter", "reader"]).default("reader"),
|
|
1087
1094
|
type: z.enum(["user", "group", "domain", "anyone"]).default("user"),
|
|
1088
|
-
sendNotificationEmail: z.boolean().optional().default(false)
|
|
1095
|
+
sendNotificationEmail: z.boolean().optional().default(false),
|
|
1096
|
+
emailMessage: z.string().optional()
|
|
1089
1097
|
});
|
|
1090
1098
|
var UpdatePermissionSchema = z.object({
|
|
1091
1099
|
fileId: z.string().min(1, "File ID is required"),
|
|
@@ -1105,7 +1113,8 @@ var ShareFileSchema = z.object({
|
|
|
1105
1113
|
fileId: z.string().min(1, "File ID is required"),
|
|
1106
1114
|
emailAddress: z.string().email("Valid email is required"),
|
|
1107
1115
|
role: z.enum(["writer", "commenter", "reader"]).default("reader"),
|
|
1108
|
-
sendNotificationEmail: z.boolean().optional().default(true)
|
|
1116
|
+
sendNotificationEmail: z.boolean().optional().default(true),
|
|
1117
|
+
emailMessage: z.string().optional()
|
|
1109
1118
|
});
|
|
1110
1119
|
var ConvertPdfToGoogleDocSchema = z.object({
|
|
1111
1120
|
fileId: z.string().min(1, "File ID is required"),
|
|
@@ -1349,7 +1358,8 @@ var toolDefinitions = [
|
|
|
1349
1358
|
emailAddress: { type: "string", description: "Target user/group email" },
|
|
1350
1359
|
role: { type: "string", enum: ["owner", "organizer", "fileOrganizer", "writer", "commenter", "reader"], description: "Permission role" },
|
|
1351
1360
|
type: { type: "string", enum: ["user", "group", "domain", "anyone"], description: "Principal type" },
|
|
1352
|
-
sendNotificationEmail: { type: "boolean", description: "Send notification email" }
|
|
1361
|
+
sendNotificationEmail: { type: "boolean", description: "Send notification email" },
|
|
1362
|
+
emailMessage: { type: "string", description: "Custom message to include in the notification email. Ignored unless sendNotificationEmail is true." }
|
|
1353
1363
|
},
|
|
1354
1364
|
required: ["fileId", "emailAddress"]
|
|
1355
1365
|
}
|
|
@@ -1389,7 +1399,8 @@ var toolDefinitions = [
|
|
|
1389
1399
|
fileId: { type: "string", description: "Google Drive file ID" },
|
|
1390
1400
|
emailAddress: { type: "string", description: "User email" },
|
|
1391
1401
|
role: { type: "string", enum: ["writer", "commenter", "reader"], description: "Access role" },
|
|
1392
|
-
sendNotificationEmail: { type: "boolean", description: "Send notification email" }
|
|
1402
|
+
sendNotificationEmail: { type: "boolean", description: "Send notification email" },
|
|
1403
|
+
emailMessage: { type: "string", description: "Custom message to include in the notification email. Ignored unless sendNotificationEmail is true." }
|
|
1393
1404
|
},
|
|
1394
1405
|
required: ["fileId", "emailAddress"]
|
|
1395
1406
|
}
|
|
@@ -2139,6 +2150,7 @@ ${lines.join("\n")}` }], isError: false };
|
|
|
2139
2150
|
emailAddress: data.emailAddress
|
|
2140
2151
|
},
|
|
2141
2152
|
sendNotificationEmail: data.sendNotificationEmail,
|
|
2153
|
+
...data.emailMessage && { emailMessage: data.emailMessage },
|
|
2142
2154
|
fields: "id,type,role,emailAddress",
|
|
2143
2155
|
supportsAllDrives: true
|
|
2144
2156
|
});
|
|
@@ -2225,6 +2237,7 @@ ${lines.join("\n")}` }], isError: false };
|
|
|
2225
2237
|
emailAddress: data.emailAddress
|
|
2226
2238
|
},
|
|
2227
2239
|
sendNotificationEmail: data.sendNotificationEmail,
|
|
2240
|
+
...data.emailMessage && { emailMessage: data.emailMessage },
|
|
2228
2241
|
fields: "id,type,role,emailAddress",
|
|
2229
2242
|
supportsAllDrives: true
|
|
2230
2243
|
});
|
|
@@ -3143,7 +3156,8 @@ var CreateGoogleDocSchema = z2.object({
|
|
|
3143
3156
|
});
|
|
3144
3157
|
var UpdateGoogleDocSchema = z2.object({
|
|
3145
3158
|
documentId: z2.string().min(1, "Document ID is required"),
|
|
3146
|
-
content: z2.string()
|
|
3159
|
+
content: z2.string(),
|
|
3160
|
+
tabId: z2.string().optional()
|
|
3147
3161
|
});
|
|
3148
3162
|
var GetGoogleDocContentSchema = z2.object({
|
|
3149
3163
|
documentId: z2.string().min(1, "Document ID is required"),
|
|
@@ -3152,12 +3166,14 @@ var GetGoogleDocContentSchema = z2.object({
|
|
|
3152
3166
|
var InsertTextSchema = z2.object({
|
|
3153
3167
|
documentId: z2.string().min(1, "Document ID is required"),
|
|
3154
3168
|
text: z2.string().min(1, "Text to insert is required"),
|
|
3155
|
-
index: z2.number().int().min(1, "Index must be at least 1 (1-based)")
|
|
3169
|
+
index: z2.number().int().min(1, "Index must be at least 1 (1-based)"),
|
|
3170
|
+
tabId: z2.string().optional()
|
|
3156
3171
|
});
|
|
3157
3172
|
var DeleteRangeSchema = z2.object({
|
|
3158
3173
|
documentId: z2.string().min(1, "Document ID is required"),
|
|
3159
3174
|
startIndex: z2.number().int().min(1, "Start index must be at least 1"),
|
|
3160
|
-
endIndex: z2.number().int().min(1, "End index must be at least 1")
|
|
3175
|
+
endIndex: z2.number().int().min(1, "End index must be at least 1"),
|
|
3176
|
+
tabId: z2.string().optional()
|
|
3161
3177
|
}).refine((data) => data.endIndex > data.startIndex, {
|
|
3162
3178
|
message: "End index must be greater than start index",
|
|
3163
3179
|
path: ["endIndex"]
|
|
@@ -3298,7 +3314,8 @@ var FindAndReplaceInDocSchema = z2.object({
|
|
|
3298
3314
|
findText: z2.string().min(1, "findText is required"),
|
|
3299
3315
|
replaceText: z2.string(),
|
|
3300
3316
|
matchCase: z2.boolean().optional().default(false),
|
|
3301
|
-
dryRun: z2.boolean().optional().default(false)
|
|
3317
|
+
dryRun: z2.boolean().optional().default(false),
|
|
3318
|
+
tabId: z2.string().optional()
|
|
3302
3319
|
});
|
|
3303
3320
|
var AddDocumentTabSchema = z2.object({
|
|
3304
3321
|
documentId: z2.string().min(1, "Document ID is required"),
|
|
@@ -3342,38 +3359,41 @@ var toolDefinitions2 = [
|
|
|
3342
3359
|
},
|
|
3343
3360
|
{
|
|
3344
3361
|
name: "updateGoogleDoc",
|
|
3345
|
-
description: "Update an existing Google Doc",
|
|
3362
|
+
description: "Update an existing Google Doc (replaces all content). For multi-tab docs, specify tabId to replace a single tab's content atomically; leaves other tabs untouched.",
|
|
3346
3363
|
inputSchema: {
|
|
3347
3364
|
type: "object",
|
|
3348
3365
|
properties: {
|
|
3349
3366
|
documentId: { type: "string", description: "Doc ID" },
|
|
3350
|
-
content: { type: "string", description: "New content" }
|
|
3367
|
+
content: { type: "string", description: "New content" },
|
|
3368
|
+
tabId: { type: "string", description: "Optional. Tab ID to replace (from listDocumentTabs). If set, delete+insert run in a single atomic batchUpdate scoped to that tab." }
|
|
3351
3369
|
},
|
|
3352
3370
|
required: ["documentId", "content"]
|
|
3353
3371
|
}
|
|
3354
3372
|
},
|
|
3355
3373
|
{
|
|
3356
3374
|
name: "insertText",
|
|
3357
|
-
description: "Insert text at a specific index in a Google Doc (surgical edit, doesn't replace entire doc)",
|
|
3375
|
+
description: "Insert text at a specific index in a Google Doc (surgical edit, doesn't replace entire doc). For multi-tab docs, specify tabId to target a specific tab.",
|
|
3358
3376
|
inputSchema: {
|
|
3359
3377
|
type: "object",
|
|
3360
3378
|
properties: {
|
|
3361
3379
|
documentId: { type: "string", description: "The document ID" },
|
|
3362
3380
|
text: { type: "string", description: "Text to insert" },
|
|
3363
|
-
index: { type: "number", description: "Position to insert at (1-based)" }
|
|
3381
|
+
index: { type: "number", description: "Position to insert at (1-based)" },
|
|
3382
|
+
tabId: { type: "string", description: "Optional. Tab ID to insert into (from listDocumentTabs). If omitted, inserts into the first/default tab." }
|
|
3364
3383
|
},
|
|
3365
3384
|
required: ["documentId", "text", "index"]
|
|
3366
3385
|
}
|
|
3367
3386
|
},
|
|
3368
3387
|
{
|
|
3369
3388
|
name: "deleteRange",
|
|
3370
|
-
description: "Delete content between start and end indices in a Google Doc",
|
|
3389
|
+
description: "Delete content between start and end indices in a Google Doc. For multi-tab docs, specify tabId to target a specific tab.",
|
|
3371
3390
|
inputSchema: {
|
|
3372
3391
|
type: "object",
|
|
3373
3392
|
properties: {
|
|
3374
3393
|
documentId: { type: "string", description: "The document ID" },
|
|
3375
3394
|
startIndex: { type: "number", description: "Start index (1-based, inclusive)" },
|
|
3376
|
-
endIndex: { type: "number", description: "End index (exclusive)" }
|
|
3395
|
+
endIndex: { type: "number", description: "End index (exclusive)" },
|
|
3396
|
+
tabId: { type: "string", description: "Optional. Tab ID to delete from (from listDocumentTabs). If omitted, deletes from the first/default tab." }
|
|
3377
3397
|
},
|
|
3378
3398
|
required: ["documentId", "startIndex", "endIndex"]
|
|
3379
3399
|
}
|
|
@@ -3516,7 +3536,7 @@ var toolDefinitions2 = [
|
|
|
3516
3536
|
},
|
|
3517
3537
|
{
|
|
3518
3538
|
name: "findAndReplaceInDoc",
|
|
3519
|
-
description: "Find and replace text across a Google Document. Dry-run mode counts matches from paragraph text only (may differ from actual replacements which cover tables, headers, footers, etc.)",
|
|
3539
|
+
description: "Find and replace text across a Google Document. Dry-run mode counts matches from paragraph text only (may differ from actual replacements which cover tables, headers, footers, etc.). For multi-tab docs, specify tabId to scope replacements to a single tab.",
|
|
3520
3540
|
inputSchema: {
|
|
3521
3541
|
type: "object",
|
|
3522
3542
|
properties: {
|
|
@@ -3524,7 +3544,8 @@ var toolDefinitions2 = [
|
|
|
3524
3544
|
findText: { type: "string", description: "Text to find" },
|
|
3525
3545
|
replaceText: { type: "string", description: "Replacement text" },
|
|
3526
3546
|
matchCase: { type: "boolean", description: "Case-sensitive match (default: false)" },
|
|
3527
|
-
dryRun: { type: "boolean", description: "Only count approximate matches from paragraph text, do not modify document (default: false)" }
|
|
3547
|
+
dryRun: { type: "boolean", description: "Only count approximate matches from paragraph text, do not modify document (default: false). Ignores tabId \u2014 always scans the full document body." },
|
|
3548
|
+
tabId: { type: "string", description: "Optional. Tab ID to scope replacements to (from listDocumentTabs). If omitted, replaces across all tabs." }
|
|
3528
3549
|
},
|
|
3529
3550
|
required: ["documentId", "findText", "replaceText"]
|
|
3530
3551
|
}
|
|
@@ -3838,6 +3859,43 @@ Link: ${doc.webViewLink}` }],
|
|
|
3838
3859
|
}
|
|
3839
3860
|
const a = validation.data;
|
|
3840
3861
|
const docs = ctx.google.docs({ version: "v1", auth: ctx.authClient });
|
|
3862
|
+
if (a.tabId) {
|
|
3863
|
+
const document2 = await docs.documents.get({ documentId: a.documentId, includeTabsContent: true });
|
|
3864
|
+
const tabs = document2.data.tabs;
|
|
3865
|
+
const tab = tabs ? findTabById(tabs, a.tabId) : null;
|
|
3866
|
+
if (!tab) {
|
|
3867
|
+
return errorResponse(`Tab with ID "${a.tabId}" not found. Use listDocumentTabs to see available tabs.`);
|
|
3868
|
+
}
|
|
3869
|
+
const bodyContent = tab.documentTab?.body?.content;
|
|
3870
|
+
const lastEndIndex = bodyContent?.[bodyContent.length - 1]?.endIndex ?? 1;
|
|
3871
|
+
const deleteEndIndex2 = Math.max(1, lastEndIndex - 1);
|
|
3872
|
+
const requests = [];
|
|
3873
|
+
if (deleteEndIndex2 > 1) {
|
|
3874
|
+
requests.push({
|
|
3875
|
+
deleteContentRange: {
|
|
3876
|
+
range: { startIndex: 1, endIndex: deleteEndIndex2, tabId: a.tabId }
|
|
3877
|
+
}
|
|
3878
|
+
});
|
|
3879
|
+
}
|
|
3880
|
+
requests.push({
|
|
3881
|
+
insertText: { location: { index: 1, tabId: a.tabId }, text: a.content }
|
|
3882
|
+
});
|
|
3883
|
+
requests.push({
|
|
3884
|
+
updateParagraphStyle: {
|
|
3885
|
+
range: { startIndex: 1, endIndex: a.content.length + 1, tabId: a.tabId },
|
|
3886
|
+
paragraphStyle: { namedStyleType: "NORMAL_TEXT" },
|
|
3887
|
+
fields: "namedStyleType"
|
|
3888
|
+
}
|
|
3889
|
+
});
|
|
3890
|
+
await docs.documents.batchUpdate({
|
|
3891
|
+
documentId: a.documentId,
|
|
3892
|
+
requestBody: { requests }
|
|
3893
|
+
});
|
|
3894
|
+
return {
|
|
3895
|
+
content: [{ type: "text", text: `Updated Google Doc: ${document2.data.title} (tab: ${a.tabId})` }],
|
|
3896
|
+
isError: false
|
|
3897
|
+
};
|
|
3898
|
+
}
|
|
3841
3899
|
const document = await docs.documents.get({ documentId: a.documentId });
|
|
3842
3900
|
const endIndex = document.data.body?.content?.[document.data.body.content.length - 1]?.endIndex || 1;
|
|
3843
3901
|
const deleteEndIndex = Math.max(1, endIndex - 1);
|
|
@@ -4118,20 +4176,22 @@ Total length: ${totalLength} characters`
|
|
|
4118
4176
|
return errorResponse(validation.error.errors[0].message);
|
|
4119
4177
|
}
|
|
4120
4178
|
const a = validation.data;
|
|
4179
|
+
const location = { index: a.index };
|
|
4180
|
+
if (a.tabId) location.tabId = a.tabId;
|
|
4121
4181
|
const docs = ctx.google.docs({ version: "v1", auth: ctx.authClient });
|
|
4122
4182
|
await docs.documents.batchUpdate({
|
|
4123
4183
|
documentId: a.documentId,
|
|
4124
4184
|
requestBody: {
|
|
4125
4185
|
requests: [{
|
|
4126
4186
|
insertText: {
|
|
4127
|
-
location
|
|
4187
|
+
location,
|
|
4128
4188
|
text: a.text
|
|
4129
4189
|
}
|
|
4130
4190
|
}]
|
|
4131
4191
|
}
|
|
4132
4192
|
});
|
|
4133
4193
|
return {
|
|
4134
|
-
content: [{ type: "text", text: `Successfully inserted ${a.text.length} characters at index ${a.index}` }],
|
|
4194
|
+
content: [{ type: "text", text: `Successfully inserted ${a.text.length} characters at index ${a.index}${a.tabId ? ` in tab ${a.tabId}` : ""}` }],
|
|
4135
4195
|
isError: false
|
|
4136
4196
|
};
|
|
4137
4197
|
}
|
|
@@ -4144,22 +4204,22 @@ Total length: ${totalLength} characters`
|
|
|
4144
4204
|
if (a.endIndex <= a.startIndex) {
|
|
4145
4205
|
return errorResponse("endIndex must be greater than startIndex");
|
|
4146
4206
|
}
|
|
4207
|
+
const range = {
|
|
4208
|
+
startIndex: a.startIndex,
|
|
4209
|
+
endIndex: a.endIndex
|
|
4210
|
+
};
|
|
4211
|
+
if (a.tabId) range.tabId = a.tabId;
|
|
4147
4212
|
const docs = ctx.google.docs({ version: "v1", auth: ctx.authClient });
|
|
4148
4213
|
await docs.documents.batchUpdate({
|
|
4149
4214
|
documentId: a.documentId,
|
|
4150
4215
|
requestBody: {
|
|
4151
4216
|
requests: [{
|
|
4152
|
-
deleteContentRange: {
|
|
4153
|
-
range: {
|
|
4154
|
-
startIndex: a.startIndex,
|
|
4155
|
-
endIndex: a.endIndex
|
|
4156
|
-
}
|
|
4157
|
-
}
|
|
4217
|
+
deleteContentRange: { range }
|
|
4158
4218
|
}]
|
|
4159
4219
|
}
|
|
4160
4220
|
});
|
|
4161
4221
|
return {
|
|
4162
|
-
content: [{ type: "text", text: `Successfully deleted content from index ${a.startIndex} to ${a.endIndex}` }],
|
|
4222
|
+
content: [{ type: "text", text: `Successfully deleted content from index ${a.startIndex} to ${a.endIndex}${a.tabId ? ` in tab ${a.tabId}` : ""}` }],
|
|
4163
4223
|
isError: false
|
|
4164
4224
|
};
|
|
4165
4225
|
}
|
|
@@ -4541,25 +4601,20 @@ ${text}`;
|
|
|
4541
4601
|
isError: false
|
|
4542
4602
|
};
|
|
4543
4603
|
}
|
|
4604
|
+
const replaceAllText = {
|
|
4605
|
+
containsText: { text: a.findText, matchCase: a.matchCase },
|
|
4606
|
+
replaceText: a.replaceText
|
|
4607
|
+
};
|
|
4608
|
+
if (a.tabId) replaceAllText.tabsCriteria = { tabIds: [a.tabId] };
|
|
4544
4609
|
const response = await docs.documents.batchUpdate({
|
|
4545
4610
|
documentId: a.documentId,
|
|
4546
4611
|
requestBody: {
|
|
4547
|
-
requests: [
|
|
4548
|
-
{
|
|
4549
|
-
replaceAllText: {
|
|
4550
|
-
containsText: {
|
|
4551
|
-
text: a.findText,
|
|
4552
|
-
matchCase: a.matchCase
|
|
4553
|
-
},
|
|
4554
|
-
replaceText: a.replaceText
|
|
4555
|
-
}
|
|
4556
|
-
}
|
|
4557
|
-
]
|
|
4612
|
+
requests: [{ replaceAllText }]
|
|
4558
4613
|
}
|
|
4559
4614
|
});
|
|
4560
4615
|
const occurrences = response.data.replies?.[0]?.replaceAllText?.occurrencesChanged ?? 0;
|
|
4561
4616
|
return {
|
|
4562
|
-
content: [{ type: "text", text: `Replaced ${occurrences} occurrence(s) of "${a.findText}".` }],
|
|
4617
|
+
content: [{ type: "text", text: `Replaced ${occurrences} occurrence(s) of "${a.findText}"${a.tabId ? ` in tab ${a.tabId}` : ""}.` }],
|
|
4563
4618
|
isError: false
|
|
4564
4619
|
};
|
|
4565
4620
|
}
|
|
@@ -5131,8 +5186,10 @@ Image URL: ${imageUrl}` }],
|
|
|
5131
5186
|
const docs = ctx.google.docs({ version: "v1", auth: ctx.authClient });
|
|
5132
5187
|
await docs.documents.batchUpdate({
|
|
5133
5188
|
documentId: a.documentId,
|
|
5134
|
-
// updateDocumentTabProperties is not yet in the googleapis TypeScript types — cast required
|
|
5135
|
-
|
|
5189
|
+
// updateDocumentTabProperties is not yet in the googleapis TypeScript types — cast required.
|
|
5190
|
+
// Per Google Docs API spec: tabId lives INSIDE tabProperties (it's the tab identifier),
|
|
5191
|
+
// and `fields` is a FieldMask for which properties to update (excludes tabId).
|
|
5192
|
+
requestBody: { requests: [{ updateDocumentTabProperties: { tabProperties: { tabId: a.tabId, title: a.title }, fields: "title" } }] }
|
|
5136
5193
|
});
|
|
5137
5194
|
return { content: [{ type: "text", text: `Renamed tab ${a.tabId} to "${a.title}".` }], isError: false };
|
|
5138
5195
|
}
|
|
@@ -8812,6 +8869,7 @@ Examples:
|
|
|
8812
8869
|
Environment Variables:
|
|
8813
8870
|
GOOGLE_DRIVE_OAUTH_CREDENTIALS Path to OAuth credentials file
|
|
8814
8871
|
GOOGLE_DRIVE_MCP_TOKEN_PATH Path to store authentication tokens
|
|
8872
|
+
GOOGLE_DRIVE_MCP_AUTH_PORT Starting port for OAuth callback server (default: 3000, uses 5 consecutive ports)
|
|
8815
8873
|
|
|
8816
8874
|
Transport Configuration:
|
|
8817
8875
|
MCP_TRANSPORT Transport mode: stdio or http (default: stdio)
|
|
@@ -8837,8 +8895,9 @@ async function runAuthServer() {
|
|
|
8837
8895
|
const authServerInstance = new AuthServer(oauth2Client);
|
|
8838
8896
|
const success = await authServerInstance.start(true);
|
|
8839
8897
|
if (!success && !authServerInstance.authCompletedSuccessfully) {
|
|
8898
|
+
const { start, end } = authServerInstance.portRange;
|
|
8840
8899
|
console.error(
|
|
8841
|
-
|
|
8900
|
+
`Authentication failed. Could not start server or validate existing tokens. Check port availability (${start}-${end}) and try again.`
|
|
8842
8901
|
);
|
|
8843
8902
|
process.exit(1);
|
|
8844
8903
|
} else if (authServerInstance.authCompletedSuccessfully) {
|