mcp-google-extras 1.0.2 → 1.0.5
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 +99 -13
- package/dist/auth.js +7 -4
- package/dist/tools/drive/index.js +4 -0
- package/dist/tools/drive/listSharedDrives.js +52 -0
- package/dist/tools/drive/listSharedWithMe.js +97 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,19 +11,51 @@ All 44 tools from `@a-bonus/google-docs-mcp`, plus:
|
|
|
11
11
|
- **readFile** — Read the full text content of a `.docx` or `.pdf` file from Google Drive by file ID
|
|
12
12
|
- **searchFileContents** — Search Google Drive and extract matching text snippets from inside `.docx` and `.pdf` files
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## Getting Started
|
|
15
15
|
|
|
16
|
-
### 1
|
|
16
|
+
### Step 1: Create Google OAuth Credentials
|
|
17
17
|
|
|
18
18
|
1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
|
|
19
19
|
2. Create a project (or use an existing one)
|
|
20
|
-
3. Enable the Google Docs API
|
|
21
|
-
4. Create
|
|
22
|
-
5.
|
|
20
|
+
3. Enable the **Google Docs API**, **Google Sheets API**, and **Google Drive API**
|
|
21
|
+
4. Go to **Credentials** → **Create Credentials** → **OAuth Client ID**
|
|
22
|
+
5. Select **Desktop application** as the application type
|
|
23
|
+
6. Download the credentials or note your **Client ID** and **Client Secret**
|
|
23
24
|
|
|
24
|
-
### 2
|
|
25
|
+
### Step 2: Provide Your Credentials
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
Choose **one** of the following methods (whichever you prefer):
|
|
28
|
+
|
|
29
|
+
#### Option A: Use `credentials.json`
|
|
30
|
+
|
|
31
|
+
Download the JSON file from Google Cloud Console and place it in either location:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
~/.config/google-docs-mcp/credentials.json (recommended — shared across projects)
|
|
35
|
+
./credentials.json (local to your project)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
That's it — no env vars needed. The server will find it automatically.
|
|
39
|
+
|
|
40
|
+
#### Option B: Create a `.env` file
|
|
41
|
+
|
|
42
|
+
Create a `.env` file in either location:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
~/.config/google-docs-mcp/.env (recommended — shared across projects)
|
|
46
|
+
./.env (local to your project)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
With the following contents:
|
|
50
|
+
|
|
51
|
+
```env
|
|
52
|
+
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
|
|
53
|
+
GOOGLE_CLIENT_SECRET=your-client-secret
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
#### Option C: Set env vars in your MCP config
|
|
57
|
+
|
|
58
|
+
Add the credentials directly to your MCP configuration:
|
|
27
59
|
|
|
28
60
|
```json
|
|
29
61
|
{
|
|
@@ -40,16 +72,68 @@ Add this to your MCP configuration (e.g., `.mcp.json`):
|
|
|
40
72
|
}
|
|
41
73
|
```
|
|
42
74
|
|
|
43
|
-
|
|
75
|
+
> **Credential lookup order:** env vars → `~/.config/google-docs-mcp/.env` → project root `.env` → `~/.config/google-docs-mcp/credentials.json` → project root `credentials.json`
|
|
76
|
+
|
|
77
|
+
### Step 3: Add to Your MCP Client
|
|
78
|
+
|
|
79
|
+
#### Claude Code (recommended)
|
|
80
|
+
|
|
81
|
+
If you used Option A or B above:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
claude mcp add google-docs -- npx -y mcp-google-extras
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Or with env vars (Option C):
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
claude mcp add google-docs \
|
|
91
|
+
-e GOOGLE_CLIENT_ID=your-client-id \
|
|
92
|
+
-e GOOGLE_CLIENT_SECRET=your-client-secret \
|
|
93
|
+
-- npx -y mcp-google-extras
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### Other MCP clients
|
|
97
|
+
|
|
98
|
+
Add this to your MCP configuration (e.g., `.mcp.json`, `claude_desktop_config.json`):
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"mcpServers": {
|
|
103
|
+
"google-docs": {
|
|
104
|
+
"command": "npx",
|
|
105
|
+
"args": ["-y", "mcp-google-extras"]
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
If using Option C, add an `"env"` block with your `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`.
|
|
112
|
+
|
|
113
|
+
### Step 4: Authenticate
|
|
44
114
|
|
|
45
|
-
On first
|
|
115
|
+
On your first tool call, the server will automatically open your browser for Google OAuth consent. Sign in and grant access — the token is saved to `~/.config/google-docs-mcp/token.json` for future use.
|
|
46
116
|
|
|
47
|
-
You can also run the auth flow manually:
|
|
117
|
+
You can also run the auth flow manually anytime:
|
|
48
118
|
|
|
49
119
|
```bash
|
|
50
120
|
npx mcp-google-extras auth
|
|
51
121
|
```
|
|
52
122
|
|
|
123
|
+
### Multi-Account Support
|
|
124
|
+
|
|
125
|
+
Set the `GOOGLE_MCP_PROFILE` env var to use separate tokens per profile:
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"env": {
|
|
130
|
+
"GOOGLE_MCP_PROFILE": "work"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
This stores tokens in `~/.config/google-docs-mcp/work/` instead of the default directory.
|
|
136
|
+
|
|
53
137
|
## Tools
|
|
54
138
|
|
|
55
139
|
### Google Docs
|
|
@@ -68,13 +152,15 @@ npx mcp-google-extras auth
|
|
|
68
152
|
|
|
69
153
|
| Variable | Required | Description |
|
|
70
154
|
|---|---|---|
|
|
71
|
-
| `GOOGLE_CLIENT_ID` |
|
|
72
|
-
| `GOOGLE_CLIENT_SECRET` |
|
|
73
|
-
| `GOOGLE_MCP_PROFILE` | No | Profile name for multi-account support |
|
|
155
|
+
| `GOOGLE_CLIENT_ID` | No* | OAuth 2.0 Client ID |
|
|
156
|
+
| `GOOGLE_CLIENT_SECRET` | No* | OAuth 2.0 Client Secret |
|
|
157
|
+
| `GOOGLE_MCP_PROFILE` | No | Profile name for multi-account support (see above) |
|
|
74
158
|
| `LOG_LEVEL` | No | `debug`, `info`, `warn`, `error`, or `silent` |
|
|
75
159
|
| `SERVICE_ACCOUNT_PATH` | No | Path to service account JSON key (alternative to OAuth) |
|
|
76
160
|
| `GOOGLE_IMPERSONATE_USER` | No | Email to impersonate with service account |
|
|
77
161
|
|
|
162
|
+
\* Not required as env vars if you provide credentials via `.env` file or `credentials.json` (see [Step 2](#step-2-provide-your-credentials)).
|
|
163
|
+
|
|
78
164
|
## License
|
|
79
165
|
|
|
80
166
|
ISC (based on [@a-bonus/google-docs-mcp](https://www.npmjs.com/package/@a-bonus/google-docs-mcp))
|
package/dist/auth.js
CHANGED
|
@@ -87,17 +87,20 @@ async function loadClientSecrets() {
|
|
|
87
87
|
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
|
|
88
88
|
return { client_id: process.env.GOOGLE_CLIENT_ID, client_secret: process.env.GOOGLE_CLIENT_SECRET };
|
|
89
89
|
}
|
|
90
|
-
// 2–
|
|
90
|
+
// 2–4. Try loading .env files (config dir, cwd, then package root)
|
|
91
91
|
const configDir = getConfigDir();
|
|
92
|
+
const cwd = process.cwd();
|
|
92
93
|
await loadEnvFile(path.join(configDir, '.env'));
|
|
94
|
+
await loadEnvFile(path.join(cwd, '.env'));
|
|
93
95
|
await loadEnvFile(path.join(projectRootDir, '.env'));
|
|
94
96
|
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
|
|
95
97
|
logger.info('Loaded client credentials from .env file.');
|
|
96
98
|
return { client_id: process.env.GOOGLE_CLIENT_ID, client_secret: process.env.GOOGLE_CLIENT_SECRET };
|
|
97
99
|
}
|
|
98
|
-
//
|
|
100
|
+
// 5–7. Try credentials.json (config dir, cwd, then package root)
|
|
99
101
|
const credentialsPaths = [
|
|
100
102
|
path.join(configDir, 'credentials.json'),
|
|
103
|
+
path.join(cwd, 'credentials.json'),
|
|
101
104
|
CREDENTIALS_PATH,
|
|
102
105
|
];
|
|
103
106
|
for (const credPath of credentialsPaths) {
|
|
@@ -117,8 +120,8 @@ async function loadClientSecrets() {
|
|
|
117
120
|
throw new Error(
|
|
118
121
|
'No OAuth credentials found. Provide them in any of these ways:\n' +
|
|
119
122
|
` 1. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET env vars in your MCP config\n` +
|
|
120
|
-
` 2. Create
|
|
121
|
-
` 3. Place your credentials.json (from Google Cloud Console) in ${configDirDisplay}
|
|
123
|
+
` 2. Create a .env file with GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in ${configDirDisplay}/ or your project directory\n` +
|
|
124
|
+
` 3. Place your credentials.json (from Google Cloud Console) in ${configDirDisplay}/ or your project directory`
|
|
122
125
|
);
|
|
123
126
|
}
|
|
124
127
|
// ---------------------------------------------------------------------------
|
|
@@ -10,6 +10,8 @@ import { register as renameFile } from './renameFile.js';
|
|
|
10
10
|
import { register as deleteFile } from './deleteFile.js';
|
|
11
11
|
import { register as createDocument } from './createDocument.js';
|
|
12
12
|
import { register as createFromTemplate } from './createFromTemplate.js';
|
|
13
|
+
import { register as listSharedDrives } from './listSharedDrives.js';
|
|
14
|
+
import { register as listSharedWithMe } from './listSharedWithMe.js';
|
|
13
15
|
export function registerDriveTools(server) {
|
|
14
16
|
listGoogleDocs(server);
|
|
15
17
|
searchGoogleDocs(server);
|
|
@@ -23,4 +25,6 @@ export function registerDriveTools(server) {
|
|
|
23
25
|
deleteFile(server);
|
|
24
26
|
createDocument(server);
|
|
25
27
|
createFromTemplate(server);
|
|
28
|
+
listSharedDrives(server);
|
|
29
|
+
listSharedWithMe(server);
|
|
26
30
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDriveClient } from '../../clients.js';
|
|
4
|
+
export function register(server) {
|
|
5
|
+
server.addTool({
|
|
6
|
+
name: 'listSharedDrives',
|
|
7
|
+
description: 'Lists all Shared Drives (Team Drives) the user has access to. Use the returned drive ID with listFolderContents to browse its contents.',
|
|
8
|
+
parameters: z.object({
|
|
9
|
+
maxResults: z
|
|
10
|
+
.number()
|
|
11
|
+
.int()
|
|
12
|
+
.min(1)
|
|
13
|
+
.max(100)
|
|
14
|
+
.optional()
|
|
15
|
+
.default(50)
|
|
16
|
+
.describe('Maximum number of shared drives to return.'),
|
|
17
|
+
pageToken: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('Token for fetching the next page of results.'),
|
|
21
|
+
}),
|
|
22
|
+
execute: async (args, { log }) => {
|
|
23
|
+
const drive = await getDriveClient();
|
|
24
|
+
log.info('Listing shared drives');
|
|
25
|
+
try {
|
|
26
|
+
const response = await drive.drives.list({
|
|
27
|
+
pageSize: args.maxResults,
|
|
28
|
+
pageToken: args.pageToken || undefined,
|
|
29
|
+
fields: 'nextPageToken,drives(id,name,createdTime,hidden)',
|
|
30
|
+
});
|
|
31
|
+
const drives = (response.data.drives || []).map((d) => ({
|
|
32
|
+
id: d.id,
|
|
33
|
+
name: d.name,
|
|
34
|
+
createdTime: d.createdTime,
|
|
35
|
+
hidden: d.hidden || false,
|
|
36
|
+
}));
|
|
37
|
+
const result = {
|
|
38
|
+
drives,
|
|
39
|
+
nextPageToken: response.data.nextPageToken || null,
|
|
40
|
+
totalCount: drives.length,
|
|
41
|
+
};
|
|
42
|
+
return JSON.stringify(result, null, 2);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
log.error(`Error listing shared drives: ${error.message || error}`);
|
|
46
|
+
if (error.code === 403)
|
|
47
|
+
throw new UserError('Permission denied. Make sure the Drive API scope includes shared drives.');
|
|
48
|
+
throw new UserError(`Failed to list shared drives: ${error.message || 'Unknown error'}`);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDriveClient } from '../../clients.js';
|
|
4
|
+
export function register(server) {
|
|
5
|
+
server.addTool({
|
|
6
|
+
name: 'listSharedWithMe',
|
|
7
|
+
description: 'Lists files and folders that have been shared with the user ("Shared with me"). Returns items shared by others that are not in the user\'s own Drive. Use folder IDs with listFolderContents to navigate into shared folders.',
|
|
8
|
+
parameters: z.object({
|
|
9
|
+
mimeType: z
|
|
10
|
+
.string()
|
|
11
|
+
.optional()
|
|
12
|
+
.describe('Filter by MIME type. Use "application/vnd.google-apps.folder" for folders only, "application/vnd.google-apps.document" for Docs, "application/vnd.google-apps.spreadsheet" for Sheets, etc.'),
|
|
13
|
+
query: z
|
|
14
|
+
.string()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('Search query to filter shared items by name (uses "contains" matching).'),
|
|
17
|
+
maxResults: z
|
|
18
|
+
.number()
|
|
19
|
+
.int()
|
|
20
|
+
.min(1)
|
|
21
|
+
.max(100)
|
|
22
|
+
.optional()
|
|
23
|
+
.default(50)
|
|
24
|
+
.describe('Maximum number of items to return.'),
|
|
25
|
+
pageToken: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('Token for fetching the next page of results.'),
|
|
29
|
+
orderBy: z
|
|
30
|
+
.enum(['name', 'modifiedTime', 'sharedWithMeTime'])
|
|
31
|
+
.optional()
|
|
32
|
+
.default('sharedWithMeTime')
|
|
33
|
+
.describe('How to sort results. Defaults to most recently shared first.'),
|
|
34
|
+
}),
|
|
35
|
+
execute: async (args, { log }) => {
|
|
36
|
+
const drive = await getDriveClient();
|
|
37
|
+
log.info('Listing shared with me');
|
|
38
|
+
try {
|
|
39
|
+
let queryParts = ['sharedWithMe=true', 'trashed=false'];
|
|
40
|
+
if (args.mimeType) {
|
|
41
|
+
queryParts.push(`mimeType='${args.mimeType}'`);
|
|
42
|
+
}
|
|
43
|
+
if (args.query) {
|
|
44
|
+
queryParts.push(`name contains '${args.query.replace(/'/g, "\\'")}'`);
|
|
45
|
+
}
|
|
46
|
+
const orderBy = args.orderBy === 'sharedWithMeTime'
|
|
47
|
+
? 'sharedWithMeTime desc'
|
|
48
|
+
: args.orderBy === 'modifiedTime'
|
|
49
|
+
? 'modifiedTime desc'
|
|
50
|
+
: 'name';
|
|
51
|
+
const response = await drive.files.list({
|
|
52
|
+
q: queryParts.join(' and '),
|
|
53
|
+
pageSize: args.maxResults,
|
|
54
|
+
pageToken: args.pageToken || undefined,
|
|
55
|
+
orderBy,
|
|
56
|
+
fields: 'nextPageToken,files(id,name,mimeType,modifiedTime,sharedWithMeTime,sharingUser(displayName,emailAddress),webViewLink,owners(displayName))',
|
|
57
|
+
supportsAllDrives: true,
|
|
58
|
+
includeItemsFromAllDrives: true,
|
|
59
|
+
});
|
|
60
|
+
const items = response.data.files || [];
|
|
61
|
+
const folders = items
|
|
62
|
+
.filter((f) => f.mimeType === 'application/vnd.google-apps.folder')
|
|
63
|
+
.map((f) => ({
|
|
64
|
+
id: f.id,
|
|
65
|
+
name: f.name,
|
|
66
|
+
sharedWithMeTime: f.sharedWithMeTime,
|
|
67
|
+
sharedBy: f.sharingUser?.displayName || f.owners?.[0]?.displayName || null,
|
|
68
|
+
sharedByEmail: f.sharingUser?.emailAddress || null,
|
|
69
|
+
}));
|
|
70
|
+
const files = items
|
|
71
|
+
.filter((f) => f.mimeType !== 'application/vnd.google-apps.folder')
|
|
72
|
+
.map((f) => ({
|
|
73
|
+
id: f.id,
|
|
74
|
+
name: f.name,
|
|
75
|
+
mimeType: f.mimeType,
|
|
76
|
+
modifiedTime: f.modifiedTime,
|
|
77
|
+
sharedWithMeTime: f.sharedWithMeTime,
|
|
78
|
+
sharedBy: f.sharingUser?.displayName || f.owners?.[0]?.displayName || null,
|
|
79
|
+
sharedByEmail: f.sharingUser?.emailAddress || null,
|
|
80
|
+
}));
|
|
81
|
+
const result = {
|
|
82
|
+
folders,
|
|
83
|
+
files,
|
|
84
|
+
nextPageToken: response.data.nextPageToken || null,
|
|
85
|
+
totalCount: items.length,
|
|
86
|
+
};
|
|
87
|
+
return JSON.stringify(result, null, 2);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
log.error(`Error listing shared items: ${error.message || error}`);
|
|
91
|
+
if (error.code === 403)
|
|
92
|
+
throw new UserError('Permission denied. Make sure you have the correct Drive API scopes.');
|
|
93
|
+
throw new UserError(`Failed to list shared items: ${error.message || 'Unknown error'}`);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|