google-services-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +225 -0
- package/dist/auth/config.d.ts +43 -0
- package/dist/auth/config.d.ts.map +1 -0
- package/dist/auth/config.js +108 -0
- package/dist/auth/config.js.map +1 -0
- package/dist/auth/manager.d.ts +50 -0
- package/dist/auth/manager.d.ts.map +1 -0
- package/dist/auth/manager.js +140 -0
- package/dist/auth/manager.js.map +1 -0
- package/dist/auth/oauth.d.ts +49 -0
- package/dist/auth/oauth.d.ts.map +1 -0
- package/dist/auth/oauth.js +211 -0
- package/dist/auth/oauth.js.map +1 -0
- package/dist/auth/store.d.ts +18 -0
- package/dist/auth/store.d.ts.map +1 -0
- package/dist/auth/store.js +45 -0
- package/dist/auth/store.js.map +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +112 -0
- package/dist/cli.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +21 -0
- package/dist/main.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +11 -0
- package/dist/server.js.map +1 -0
- package/dist/services/calendar.d.ts +77 -0
- package/dist/services/calendar.d.ts.map +1 -0
- package/dist/services/calendar.js +141 -0
- package/dist/services/calendar.js.map +1 -0
- package/dist/services/contacts-tasks.d.ts +61 -0
- package/dist/services/contacts-tasks.d.ts.map +1 -0
- package/dist/services/contacts-tasks.js +103 -0
- package/dist/services/contacts-tasks.js.map +1 -0
- package/dist/services/docs.d.ts +30 -0
- package/dist/services/docs.d.ts.map +1 -0
- package/dist/services/docs.js +60 -0
- package/dist/services/docs.js.map +1 -0
- package/dist/services/drive.d.ts +51 -0
- package/dist/services/drive.d.ts.map +1 -0
- package/dist/services/drive.js +80 -0
- package/dist/services/drive.js.map +1 -0
- package/dist/services/forms.d.ts +28 -0
- package/dist/services/forms.d.ts.map +1 -0
- package/dist/services/forms.js +54 -0
- package/dist/services/forms.js.map +1 -0
- package/dist/services/gmail.d.ts +64 -0
- package/dist/services/gmail.d.ts.map +1 -0
- package/dist/services/gmail.js +144 -0
- package/dist/services/gmail.js.map +1 -0
- package/dist/services/sheets.d.ts +44 -0
- package/dist/services/sheets.d.ts.map +1 -0
- package/dist/services/sheets.js +53 -0
- package/dist/services/sheets.js.map +1 -0
- package/dist/services/slides.d.ts +28 -0
- package/dist/services/slides.d.ts.map +1 -0
- package/dist/services/slides.js +54 -0
- package/dist/services/slides.js.map +1 -0
- package/dist/services/youtube.d.ts +40 -0
- package/dist/services/youtube.d.ts.map +1 -0
- package/dist/services/youtube.js +91 -0
- package/dist/services/youtube.js.map +1 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +659 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/util/result.d.ts +29 -0
- package/dist/util/result.d.ts.map +1 -0
- package/dist/util/result.js +21 -0
- package/dist/util/result.js.map +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nabheet S. Sandhu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# google-services-mcp
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server that gives AI agents direct access to your
|
|
4
|
+
Google services — **Gmail, Calendar, Google Meet, Drive, Contacts, Tasks,
|
|
5
|
+
Sheets, Docs, Slides, YouTube, and Forms** — with multi-account OAuth support.
|
|
6
|
+
|
|
7
|
+
Add one account, wire the server into any MCP-compatible AI client, and your
|
|
8
|
+
agent can read and send email, manage your calendar, find files in Drive,
|
|
9
|
+
create documents and spreadsheets, and more — using your real Google data.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Gmail** — send, list, read, modify, and reply to messages
|
|
14
|
+
- **Calendar** — list calendars, create/read/update/delete events, create Google Meet links
|
|
15
|
+
- **Drive** — list, read, upload, update, delete, and share files
|
|
16
|
+
- **Contacts** — list, search, and create contacts
|
|
17
|
+
- **Tasks** — list task lists, create/complete/delete tasks
|
|
18
|
+
- **Sheets** — create spreadsheets, read/write/append cell ranges, batch update
|
|
19
|
+
- **Docs** — create documents, read text, insert/replace text, batch update
|
|
20
|
+
- **Slides** — create presentations, add/delete slides, find-and-replace text
|
|
21
|
+
- **YouTube** — search videos, manage uploads/playlists/subscriptions
|
|
22
|
+
- **Forms** — create forms, add questions, read responses
|
|
23
|
+
- **Multi-account** — connect several Google accounts, set a default, or pass an `account` argument to any tool
|
|
24
|
+
- **OAuth 2.0** — one-time browser authorization; tokens are stored locally and refreshed automatically
|
|
25
|
+
|
|
26
|
+
> **60 tools** across 11 services, all prefixed `google_`. Full reference:
|
|
27
|
+
> [docs/TOOLS.md](docs/TOOLS.md).
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- Node.js **18+**
|
|
32
|
+
- A Google Cloud project with the required APIs enabled and an OAuth 2.0
|
|
33
|
+
**Desktop app** client (one-time setup, ~10 minutes — see below)
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install -g google-services-mcp
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
> **Note:** until the first public release lands on npm, install from source
|
|
42
|
+
> instead — see [Local development](#local-development).
|
|
43
|
+
|
|
44
|
+
## Quick start
|
|
45
|
+
|
|
46
|
+
1. **Add a Google account** — opens your browser for one-time authorization:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
google-services-mcp add personal
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
2. **Register the server** with your MCP client (see
|
|
53
|
+
[MCP client configuration](#mcp-client-configuration) for Claude Desktop,
|
|
54
|
+
opencode, and generic configs).
|
|
55
|
+
|
|
56
|
+
3. **Start using it** — ask your agent to "check my email" or "add an event to
|
|
57
|
+
my calendar". Or add more accounts any time: `google-services-mcp add work`.
|
|
58
|
+
|
|
59
|
+
## MCP client configuration
|
|
60
|
+
|
|
61
|
+
The server speaks MCP over stdio. Point your client at the `google-services-mcp`
|
|
62
|
+
executable and pass your OAuth credentials via environment variables.
|
|
63
|
+
|
|
64
|
+
### opencode
|
|
65
|
+
|
|
66
|
+
In `opencode.json` (or your global config):
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"mcp": {
|
|
71
|
+
"google-services-mcp": {
|
|
72
|
+
"type": "local",
|
|
73
|
+
"command": ["google-services-mcp"],
|
|
74
|
+
"enabled": true,
|
|
75
|
+
"environment": {
|
|
76
|
+
"GOOGLE_MCP_CLIENT_ID": "your-client-id",
|
|
77
|
+
"GOOGLE_MCP_CLIENT_SECRET": "your-client-secret"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Claude Desktop / Cursor / other MCP clients
|
|
85
|
+
|
|
86
|
+
In the client's MCP settings file (e.g. `claude_desktop_config.json`):
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"mcpServers": {
|
|
91
|
+
"google-services-mcp": {
|
|
92
|
+
"command": "google-services-mcp",
|
|
93
|
+
"env": {
|
|
94
|
+
"GOOGLE_MCP_CLIENT_ID": "your-client-id",
|
|
95
|
+
"GOOGLE_MCP_CLIENT_SECRET": "your-client-secret"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Google Cloud setup (one-time)
|
|
103
|
+
|
|
104
|
+
Each installation uses its own OAuth client — credentials are per-user and
|
|
105
|
+
never shared.
|
|
106
|
+
|
|
107
|
+
### 1. Create a project
|
|
108
|
+
|
|
109
|
+
Open the [Google Cloud Console](https://console.cloud.google.com/), choose
|
|
110
|
+
**Select a project → New Project**, and name it (e.g. `google-services-mcp`).
|
|
111
|
+
|
|
112
|
+
### 2. Configure the OAuth consent screen
|
|
113
|
+
|
|
114
|
+
**APIs & Services → OAuth consent screen**:
|
|
115
|
+
|
|
116
|
+
1. User type: **External** (required for regular Google accounts).
|
|
117
|
+
2. App name: anything you like (e.g. `Google Service MCP`); support email = yours.
|
|
118
|
+
3. Under **Audience → Test users**, click **Add users** and enter **every Google
|
|
119
|
+
account that will use this server**. While the app is in Testing mode,
|
|
120
|
+
accounts not on this list get `access blocked` on the consent screen.
|
|
121
|
+
|
|
122
|
+
### 3. Enable the required APIs
|
|
123
|
+
|
|
124
|
+
**APIs & Services → Library**, search and **Enable** each:
|
|
125
|
+
|
|
126
|
+
`Gmail API` · `Google Calendar API` · `Google Drive API` · `People API` ·
|
|
127
|
+
`Google Tasks API` · `Google Sheets API` · `Google Docs API` ·
|
|
128
|
+
`Google Slides API` · `YouTube Data API v3` · `Google Forms API`
|
|
129
|
+
|
|
130
|
+
### 4. Create OAuth client credentials
|
|
131
|
+
|
|
132
|
+
**APIs & Services → Credentials → Create Credentials → OAuth client ID**:
|
|
133
|
+
|
|
134
|
+
1. Application type: **Desktop app**.
|
|
135
|
+
2. Authorized redirect URIs: `http://localhost:8787`.
|
|
136
|
+
3. Copy the **Client ID** and **Client Secret**.
|
|
137
|
+
|
|
138
|
+
### 5. Provide the credentials
|
|
139
|
+
|
|
140
|
+
Set these environment variables (or put them in your MCP client config as
|
|
141
|
+
above):
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
GOOGLE_MCP_CLIENT_ID=<your-oauth-client-id>
|
|
145
|
+
GOOGLE_MCP_CLIENT_SECRET=<your-oauth-client-secret>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Optional:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
GOOGLE_MCP_DIR=~/.google-services-mcp # where config and tokens live (default ~/.google-services-mcp)
|
|
152
|
+
GOOGLE_MCP_REDIRECT_PORT=8787 # local loopback port for OAuth (default 8787)
|
|
153
|
+
# Full redirect URI override. Takes precedence over REDIRECT_PORT and must
|
|
154
|
+
# exactly match a URI registered for this OAuth client in the Cloud Console.
|
|
155
|
+
# GOOGLE_MCP_REDIRECT_URI=http://127.0.0.1:8787/oauth2callback
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### 6. Connect your first account
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
google-services-mcp add personal
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
A browser opens to the Google consent screen. After you approve, the account is
|
|
165
|
+
stored in `~/.google-services-mcp/accounts/` and ready to use. Repeat with a
|
|
166
|
+
different name (e.g. `work`) to connect more accounts.
|
|
167
|
+
|
|
168
|
+
## CLI reference
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
google-services-mcp # run the MCP server over stdio
|
|
172
|
+
google-services-mcp add <name> # add a Google account (opens browser)
|
|
173
|
+
google-services-mcp list # list connected accounts
|
|
174
|
+
google-services-mcp remove <name> # remove an account
|
|
175
|
+
google-services-mcp set-default <name> # set the default account
|
|
176
|
+
google-services-mcp status # show config and token health
|
|
177
|
+
google-services-mcp --help # show help
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## For AI agents
|
|
181
|
+
|
|
182
|
+
- All tools are prefixed `google_`, e.g. `google_gmail_list`,
|
|
183
|
+
`google_calendar_list_events`, `google_drive_list_files`.
|
|
184
|
+
- The `account` argument selects which connected account a tool uses;
|
|
185
|
+
omit it to use the default account.
|
|
186
|
+
- Agents can connect new accounts themselves via the **`account_add`** tool —
|
|
187
|
+
it opens the browser consent flow without needing the CLI.
|
|
188
|
+
- See [docs/TOOLS.md](docs/TOOLS.md) for the full tool reference.
|
|
189
|
+
|
|
190
|
+
## Security
|
|
191
|
+
|
|
192
|
+
- OAuth tokens are stored in `~/.google-services-mcp/accounts/<account-name>.json`
|
|
193
|
+
with restrictive file permissions. Keep them private — never commit them.
|
|
194
|
+
- The server never logs tokens or credentials.
|
|
195
|
+
- Scopes requested: Gmail modify, Calendar, Drive, Contacts, Tasks, Sheets,
|
|
196
|
+
Docs, Slides, YouTube, Forms, plus `userinfo.email` for account display.
|
|
197
|
+
- If a client secret is ever exposed, rotate it in the Google Cloud Console
|
|
198
|
+
(Credentials → your OAuth client → rotate secret).
|
|
199
|
+
|
|
200
|
+
## Troubleshooting
|
|
201
|
+
|
|
202
|
+
| Symptom | Cause / fix |
|
|
203
|
+
|---|---|
|
|
204
|
+
| `access blocked` on consent | Account not in **Test users** (setup step 2.3) — add it and retry |
|
|
205
|
+
| `redirect_uri_mismatch` | Redirect URI must be exactly `http://localhost:8787` — no trailing slash — and match what your OAuth client allows |
|
|
206
|
+
| Tokens stop working after ~7 days | App is in **Testing** mode — re-run `add`, or **Publish app** on the consent screen |
|
|
207
|
+
| `Google hasn't verified this app` | Normal for unverified apps — click **Advanced → Continue** for personal use |
|
|
208
|
+
|
|
209
|
+
## Local development
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
git clone https://github.com/nabheet/google-services-mcp.git
|
|
213
|
+
cd google-services-mcp
|
|
214
|
+
npm install
|
|
215
|
+
npm run build # compile to dist/
|
|
216
|
+
npm test # vitest, TDD suites
|
|
217
|
+
npm run typecheck # tsc --noEmit
|
|
218
|
+
npm run dev # run from source with tsx
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
See [AGENTS.md](AGENTS.md) for architecture and conventions.
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
MIT
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for the OAuth client and server defaults.
|
|
3
|
+
*
|
|
4
|
+
* Credentials come from (in order of precedence):
|
|
5
|
+
* 1. Environment variables GOOGLE_MCP_CLIENT_ID / GOOGLE_MCP_CLIENT_SECRET
|
|
6
|
+
* 2. The config file at <dir>/config.json
|
|
7
|
+
*
|
|
8
|
+
* The data directory defaults to ~/.google-services-mcp and can be overridden with
|
|
9
|
+
* GOOGLE_MCP_DIR.
|
|
10
|
+
*/
|
|
11
|
+
export declare const DEFAULT_REDIRECT_PORT = 8787;
|
|
12
|
+
/** Scopes requested when adding an account. Covers all services this server exposes. */
|
|
13
|
+
export declare const DEFAULT_SCOPES: string[];
|
|
14
|
+
export interface Config {
|
|
15
|
+
clientId: string;
|
|
16
|
+
clientSecret: string;
|
|
17
|
+
/** Loopback port used during the OAuth redirect. Default 8787. Ignored when redirectUri is set. */
|
|
18
|
+
redirectPort?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Full redirect URI registered with Google, e.g. "http://127.0.0.1:8787/oauth2callback".
|
|
21
|
+
* Takes precedence over redirectPort and must exactly match what is registered for
|
|
22
|
+
* this OAuth client in the Google Cloud Console.
|
|
23
|
+
*/
|
|
24
|
+
redirectUri?: string;
|
|
25
|
+
/** Account used when tools are called without an explicit `account`. */
|
|
26
|
+
defaultAccount?: string;
|
|
27
|
+
/** Override the scope list requested at authorization time. */
|
|
28
|
+
scopes?: string[];
|
|
29
|
+
}
|
|
30
|
+
export declare function getDataDir(): string;
|
|
31
|
+
export declare function getConfigPath(): string;
|
|
32
|
+
export declare function getAccountsDir(): string;
|
|
33
|
+
export declare function getAccountPath(name: string): string;
|
|
34
|
+
export declare function sanitizeName(name: string): string;
|
|
35
|
+
export declare function isValidName(name: string): boolean;
|
|
36
|
+
export declare function ensureDirs(): Promise<void>;
|
|
37
|
+
export declare function loadConfig(): Promise<Config>;
|
|
38
|
+
export declare function saveConfig(config: Config): Promise<void>;
|
|
39
|
+
export declare function saveClientCredentials(clientId: string, clientSecret: string): Promise<void>;
|
|
40
|
+
export declare function hasCredentials(config: Config): boolean;
|
|
41
|
+
/** Location of this package's root (for README links etc.). */
|
|
42
|
+
export declare function getPackageRoot(): string;
|
|
43
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/auth/config.ts"],"names":[],"mappings":"AAKA;;;;;;;;;GASG;AAEH,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAE1C,wFAAwF;AACxF,eAAO,MAAM,cAAc,EAAE,MAAM,EAuBlC,CAAC;AAEF,MAAM,WAAW,MAAM;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,mGAAmG;IACnG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMjD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAEhD;AAED,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAwBlD;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ9D;AAED,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKjG;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED,+DAA+D;AAC/D,wBAAgB,cAAc,IAAI,MAAM,CAEvC"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for the OAuth client and server defaults.
|
|
7
|
+
*
|
|
8
|
+
* Credentials come from (in order of precedence):
|
|
9
|
+
* 1. Environment variables GOOGLE_MCP_CLIENT_ID / GOOGLE_MCP_CLIENT_SECRET
|
|
10
|
+
* 2. The config file at <dir>/config.json
|
|
11
|
+
*
|
|
12
|
+
* The data directory defaults to ~/.google-services-mcp and can be overridden with
|
|
13
|
+
* GOOGLE_MCP_DIR.
|
|
14
|
+
*/
|
|
15
|
+
export const DEFAULT_REDIRECT_PORT = 8787;
|
|
16
|
+
/** Scopes requested when adding an account. Covers all services this server exposes. */
|
|
17
|
+
export const DEFAULT_SCOPES = [
|
|
18
|
+
// Gmail: read, send, and modify (labels/trash) — not permanent delete
|
|
19
|
+
"https://www.googleapis.com/auth/gmail.modify",
|
|
20
|
+
// Calendar + Google Meet (conferenceData on events)
|
|
21
|
+
"https://www.googleapis.com/auth/calendar",
|
|
22
|
+
// Drive: full access to the user's Drive
|
|
23
|
+
"https://www.googleapis.com/auth/drive",
|
|
24
|
+
// Contacts (read/write)
|
|
25
|
+
"https://www.googleapis.com/auth/contacts",
|
|
26
|
+
// Tasks
|
|
27
|
+
"https://www.googleapis.com/auth/tasks",
|
|
28
|
+
// Sheets: read/write spreadsheets
|
|
29
|
+
"https://www.googleapis.com/auth/spreadsheets",
|
|
30
|
+
// Docs: read/write documents
|
|
31
|
+
"https://www.googleapis.com/auth/documents",
|
|
32
|
+
// Slides: read/write presentations
|
|
33
|
+
"https://www.googleapis.com/auth/presentations",
|
|
34
|
+
// YouTube: read/write the user's channel data and playlists
|
|
35
|
+
"https://www.googleapis.com/auth/youtube",
|
|
36
|
+
// Forms: read/write forms and responses
|
|
37
|
+
"https://www.googleapis.com/auth/forms",
|
|
38
|
+
// Identity: lets fetchUserInfo return the signed-in email for account display
|
|
39
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
40
|
+
];
|
|
41
|
+
export function getDataDir() {
|
|
42
|
+
return process.env.GOOGLE_MCP_DIR || path.join(os.homedir(), ".google-services-mcp");
|
|
43
|
+
}
|
|
44
|
+
export function getConfigPath() {
|
|
45
|
+
return path.join(getDataDir(), "config.json");
|
|
46
|
+
}
|
|
47
|
+
export function getAccountsDir() {
|
|
48
|
+
return path.join(getDataDir(), "accounts");
|
|
49
|
+
}
|
|
50
|
+
export function getAccountPath(name) {
|
|
51
|
+
return path.join(getAccountsDir(), `${sanitizeName(name)}.json`);
|
|
52
|
+
}
|
|
53
|
+
export function sanitizeName(name) {
|
|
54
|
+
const cleaned = name.replace(/[^a-zA-Z0-9_-]/g, "_").toLowerCase();
|
|
55
|
+
if (!/[a-zA-Z0-9]/.test(cleaned)) {
|
|
56
|
+
throw new Error("Account name must contain at least one alphanumeric character.");
|
|
57
|
+
}
|
|
58
|
+
return cleaned;
|
|
59
|
+
}
|
|
60
|
+
export function isValidName(name) {
|
|
61
|
+
return /^[a-zA-Z0-9_-]+$/.test(name);
|
|
62
|
+
}
|
|
63
|
+
export async function ensureDirs() {
|
|
64
|
+
await fs.mkdir(getAccountsDir(), { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
export async function loadConfig() {
|
|
67
|
+
await ensureDirs();
|
|
68
|
+
let file = {};
|
|
69
|
+
try {
|
|
70
|
+
const raw = await fs.readFile(getConfigPath(), "utf8");
|
|
71
|
+
file = JSON.parse(raw);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// No config file yet — env vars may still provide credentials.
|
|
75
|
+
}
|
|
76
|
+
const envId = process.env.GOOGLE_MCP_CLIENT_ID;
|
|
77
|
+
const envSecret = process.env.GOOGLE_MCP_CLIENT_SECRET;
|
|
78
|
+
const envPort = process.env.GOOGLE_MCP_REDIRECT_PORT;
|
|
79
|
+
const envUri = process.env.GOOGLE_MCP_REDIRECT_URI;
|
|
80
|
+
const config = {
|
|
81
|
+
clientId: envId || file.clientId || "",
|
|
82
|
+
clientSecret: envSecret || file.clientSecret || "",
|
|
83
|
+
redirectPort: envPort ? Number(envPort) : file.redirectPort || DEFAULT_REDIRECT_PORT,
|
|
84
|
+
redirectUri: envUri || file.redirectUri || undefined,
|
|
85
|
+
defaultAccount: file.defaultAccount,
|
|
86
|
+
scopes: file.scopes || DEFAULT_SCOPES,
|
|
87
|
+
};
|
|
88
|
+
return config;
|
|
89
|
+
}
|
|
90
|
+
export async function saveConfig(config) {
|
|
91
|
+
await ensureDirs();
|
|
92
|
+
const { clientId, clientSecret, redirectPort, redirectUri, defaultAccount, scopes } = config;
|
|
93
|
+
await fs.writeFile(getConfigPath(), JSON.stringify({ clientId, clientSecret, redirectPort, redirectUri, defaultAccount, scopes }, null, 2), "utf8");
|
|
94
|
+
}
|
|
95
|
+
export async function saveClientCredentials(clientId, clientSecret) {
|
|
96
|
+
const config = await loadConfig();
|
|
97
|
+
config.clientId = clientId || config.clientId;
|
|
98
|
+
config.clientSecret = clientSecret || config.clientSecret;
|
|
99
|
+
await saveConfig(config);
|
|
100
|
+
}
|
|
101
|
+
export function hasCredentials(config) {
|
|
102
|
+
return Boolean(config.clientId && config.clientSecret);
|
|
103
|
+
}
|
|
104
|
+
/** Location of this package's root (for README links etc.). */
|
|
105
|
+
export function getPackageRoot() {
|
|
106
|
+
return path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/auth/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Auth } from "googleapis";
|
|
2
|
+
import { type Config } from "./config.js";
|
|
3
|
+
import { type StoredAccount } from "./store.js";
|
|
4
|
+
export interface AddAccountOptions {
|
|
5
|
+
/** Set to false to skip opening the browser (headless). */
|
|
6
|
+
openBrowser?: boolean;
|
|
7
|
+
/** Milliseconds to wait for the OAuth callback before failing. Default 5 minutes. */
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface AccountStatus {
|
|
11
|
+
name: string;
|
|
12
|
+
email?: string;
|
|
13
|
+
expiresAt?: number;
|
|
14
|
+
tokenHealthy: boolean;
|
|
15
|
+
scopes: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface ServerStatus {
|
|
18
|
+
credentialsConfigured: boolean;
|
|
19
|
+
dataDir: string;
|
|
20
|
+
defaultAccount?: string;
|
|
21
|
+
accounts: AccountStatus[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Coordinates credentials, per-account token storage, the interactive OAuth
|
|
25
|
+
* flow, and refresh. All service modules resolve auth through here.
|
|
26
|
+
*/
|
|
27
|
+
declare class AuthManager {
|
|
28
|
+
getConfig(): Promise<Config>;
|
|
29
|
+
listAccounts(): Promise<StoredAccount[]>;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve an account to use for a tool call.
|
|
32
|
+
* Priority: explicit name -> configured default -> sole account.
|
|
33
|
+
*/
|
|
34
|
+
resolveAccount(name?: string): Promise<StoredAccount>;
|
|
35
|
+
/** Get an authenticated googleapis OAuth2 client, refreshing first if needed. */
|
|
36
|
+
getClient(name?: string): Promise<Auth.OAuth2Client>;
|
|
37
|
+
/**
|
|
38
|
+
* Run the interactive OAuth flow to add a new account: opens the consent
|
|
39
|
+
* screen, waits for the loopback callback, exchanges the code, and persists
|
|
40
|
+
* the account alongside the signed-in email.
|
|
41
|
+
*/
|
|
42
|
+
addAccount(name: string, options?: AddAccountOptions): Promise<StoredAccount>;
|
|
43
|
+
removeAccount(name: string): Promise<boolean>;
|
|
44
|
+
setDefaultAccount(name: string): Promise<void>;
|
|
45
|
+
saveCredentials(clientId: string, clientSecret: string): Promise<void>;
|
|
46
|
+
getStatus(): Promise<ServerStatus>;
|
|
47
|
+
}
|
|
48
|
+
export declare const authManager: AuthManager;
|
|
49
|
+
export {};
|
|
50
|
+
//# sourceMappingURL=manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/auth/manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,IAAI,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAML,KAAK,MAAM,EACZ,MAAM,aAAa,CAAC;AACrB,OAAO,EAA4D,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAY1G,MAAM,WAAW,iBAAiB;IAChC,2DAA2D;IAC3D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,qFAAqF;IACrF,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B;AAED;;;GAGG;AACH,cAAM,WAAW;IACT,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC;IAI5B,YAAY,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;IAI9C;;;OAGG;IACG,cAAc,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IA0B3D,iFAAiF;IAC3E,SAAS,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC;IAwB1D;;;;OAIG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,aAAa,CAAC;IA6CjF,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAI7C,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU9C,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAItE,SAAS,IAAI,OAAO,CAAC,YAAY,CAAC;CAgBzC;AAED,eAAO,MAAM,WAAW,aAAoB,CAAC"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { google } from "googleapis";
|
|
2
|
+
import { loadConfig, saveConfig, saveClientCredentials, hasCredentials, getDataDir, } from "./config.js";
|
|
3
|
+
import { loadAccount, loadAllAccounts, saveAccount, deleteAccount } from "./store.js";
|
|
4
|
+
import { buildAuthUrl, waitForOAuthCallback, exchangeCode, refreshAccessToken, fetchUserInfo, openBrowser, generateState, DEFAULT_AUTH_TIMEOUT_MS, } from "./oauth.js";
|
|
5
|
+
/**
|
|
6
|
+
* Coordinates credentials, per-account token storage, the interactive OAuth
|
|
7
|
+
* flow, and refresh. All service modules resolve auth through here.
|
|
8
|
+
*/
|
|
9
|
+
class AuthManager {
|
|
10
|
+
async getConfig() {
|
|
11
|
+
return loadConfig();
|
|
12
|
+
}
|
|
13
|
+
async listAccounts() {
|
|
14
|
+
return loadAllAccounts();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Resolve an account to use for a tool call.
|
|
18
|
+
* Priority: explicit name -> configured default -> sole account.
|
|
19
|
+
*/
|
|
20
|
+
async resolveAccount(name) {
|
|
21
|
+
const accounts = await loadAllAccounts();
|
|
22
|
+
if (name) {
|
|
23
|
+
const found = accounts.find((a) => a.name === name);
|
|
24
|
+
if (!found) {
|
|
25
|
+
const available = accounts.length ? accounts.map((a) => a.name).join(", ") : "none";
|
|
26
|
+
throw new Error(`Account "${name}" not found. Available accounts: ${available}`);
|
|
27
|
+
}
|
|
28
|
+
return found;
|
|
29
|
+
}
|
|
30
|
+
if (accounts.length === 0) {
|
|
31
|
+
throw new Error("No Google accounts configured. Add one with the account_add tool or run: google-services-mcp add <name>");
|
|
32
|
+
}
|
|
33
|
+
const config = await this.getConfig();
|
|
34
|
+
if (config.defaultAccount) {
|
|
35
|
+
const found = accounts.find((a) => a.name === config.defaultAccount);
|
|
36
|
+
if (found)
|
|
37
|
+
return found;
|
|
38
|
+
}
|
|
39
|
+
if (accounts.length === 1)
|
|
40
|
+
return accounts[0];
|
|
41
|
+
throw new Error(`Multiple accounts configured but no default set. Pass account="..." or run: google-services-mcp set-default <name>`);
|
|
42
|
+
}
|
|
43
|
+
/** Get an authenticated googleapis OAuth2 client, refreshing first if needed. */
|
|
44
|
+
async getClient(name) {
|
|
45
|
+
const config = await this.getConfig();
|
|
46
|
+
let account = await this.resolveAccount(name);
|
|
47
|
+
if (account.expiryDate && Date.now() > account.expiryDate - 60_000) {
|
|
48
|
+
const tokens = await refreshAccessToken(config, account.refreshToken);
|
|
49
|
+
account = {
|
|
50
|
+
...account,
|
|
51
|
+
accessToken: tokens.accessToken,
|
|
52
|
+
refreshToken: tokens.refreshToken ?? account.refreshToken,
|
|
53
|
+
expiryDate: tokens.expiryDate,
|
|
54
|
+
};
|
|
55
|
+
await saveAccount(account);
|
|
56
|
+
}
|
|
57
|
+
const client = new google.auth.OAuth2(config.clientId, config.clientSecret);
|
|
58
|
+
client.setCredentials({
|
|
59
|
+
access_token: account.accessToken,
|
|
60
|
+
refresh_token: account.refreshToken,
|
|
61
|
+
expiry_date: account.expiryDate,
|
|
62
|
+
});
|
|
63
|
+
return client;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Run the interactive OAuth flow to add a new account: opens the consent
|
|
67
|
+
* screen, waits for the loopback callback, exchanges the code, and persists
|
|
68
|
+
* the account alongside the signed-in email.
|
|
69
|
+
*/
|
|
70
|
+
async addAccount(name, options = {}) {
|
|
71
|
+
const config = await this.getConfig();
|
|
72
|
+
if (!hasCredentials(config)) {
|
|
73
|
+
throw new Error("Google OAuth credentials are not configured. Set GOOGLE_MCP_CLIENT_ID and " +
|
|
74
|
+
"GOOGLE_MCP_CLIENT_SECRET (or add them to config.json) first.");
|
|
75
|
+
}
|
|
76
|
+
const state = generateState();
|
|
77
|
+
const authUrl = buildAuthUrl(config, state);
|
|
78
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_AUTH_TIMEOUT_MS;
|
|
79
|
+
// The callback owns its timeout so an aborted flow always closes the
|
|
80
|
+
// loopback listener (no stale port left behind).
|
|
81
|
+
const callbackPromise = waitForOAuthCallback(config, state, { timeoutMs });
|
|
82
|
+
if (options.openBrowser !== false) {
|
|
83
|
+
try {
|
|
84
|
+
await openBrowser(authUrl);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Browser unavailable (headless server) — the error message carries the URL.
|
|
88
|
+
throw new Error(`Could not open a browser. Open this URL manually to authorize "${name}": ${authUrl}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const callback = await callbackPromise;
|
|
92
|
+
const tokens = await exchangeCode(config, callback.code);
|
|
93
|
+
const user = await fetchUserInfo(tokens.accessToken);
|
|
94
|
+
const account = {
|
|
95
|
+
name,
|
|
96
|
+
email: user.email,
|
|
97
|
+
accessToken: tokens.accessToken,
|
|
98
|
+
refreshToken: tokens.refreshToken ?? "",
|
|
99
|
+
expiryDate: tokens.expiryDate,
|
|
100
|
+
scopes: config.scopes ?? [],
|
|
101
|
+
createdAt: new Date().toISOString(),
|
|
102
|
+
};
|
|
103
|
+
await saveAccount(account);
|
|
104
|
+
return account;
|
|
105
|
+
}
|
|
106
|
+
async removeAccount(name) {
|
|
107
|
+
return deleteAccount(name);
|
|
108
|
+
}
|
|
109
|
+
async setDefaultAccount(name) {
|
|
110
|
+
const config = await this.getConfig();
|
|
111
|
+
if (name) {
|
|
112
|
+
const exists = await loadAccount(name);
|
|
113
|
+
if (!exists)
|
|
114
|
+
throw new Error(`Cannot set default: account "${name}" does not exist.`);
|
|
115
|
+
}
|
|
116
|
+
config.defaultAccount = name || undefined;
|
|
117
|
+
await saveConfig(config);
|
|
118
|
+
}
|
|
119
|
+
async saveCredentials(clientId, clientSecret) {
|
|
120
|
+
await saveClientCredentials(clientId, clientSecret);
|
|
121
|
+
}
|
|
122
|
+
async getStatus() {
|
|
123
|
+
const config = await this.getConfig();
|
|
124
|
+
const accounts = await loadAllAccounts();
|
|
125
|
+
return {
|
|
126
|
+
credentialsConfigured: hasCredentials(config),
|
|
127
|
+
dataDir: getDataDir(),
|
|
128
|
+
defaultAccount: config.defaultAccount,
|
|
129
|
+
accounts: accounts.map((a) => ({
|
|
130
|
+
name: a.name,
|
|
131
|
+
email: a.email,
|
|
132
|
+
expiresAt: a.expiryDate,
|
|
133
|
+
tokenHealthy: !a.expiryDate || Date.now() < a.expiryDate,
|
|
134
|
+
scopes: a.scopes,
|
|
135
|
+
})),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export const authManager = new AuthManager();
|
|
140
|
+
//# sourceMappingURL=manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/auth/manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type Config } from "./config.js";
|
|
2
|
+
export interface TokenSet {
|
|
3
|
+
accessToken: string;
|
|
4
|
+
refreshToken?: string;
|
|
5
|
+
/** Epoch milliseconds at which the access token expires. */
|
|
6
|
+
expiryDate?: number;
|
|
7
|
+
}
|
|
8
|
+
/** How long the loopback callback server waits for Google's redirect. */
|
|
9
|
+
export declare const DEFAULT_AUTH_TIMEOUT_MS: number;
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the redirect URI this server registers with Google. A full
|
|
12
|
+
* `redirectUri` override wins; otherwise the URI is derived from the loopback
|
|
13
|
+
* port. The value must exactly match a redirect URI registered for the OAuth
|
|
14
|
+
* client in the Google Cloud Console.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveRedirectUri(config: Config): string;
|
|
17
|
+
/** Build the Google consent-screen URL. */
|
|
18
|
+
export declare function buildAuthUrl(config: Config, state: string): string;
|
|
19
|
+
/** Open a URL in the default browser on the current platform. */
|
|
20
|
+
export declare function openBrowser(url: string): Promise<void>;
|
|
21
|
+
export interface AuthCallback {
|
|
22
|
+
code: string;
|
|
23
|
+
state: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Start a loopback HTTP server on the configured redirect URI and wait for
|
|
27
|
+
* Google to redirect back with an authorization code. Resolves with the code
|
|
28
|
+
* and state; rejects on CSRF state mismatch, a missing code, a bind failure,
|
|
29
|
+
* or when `timeoutMs` elapses.
|
|
30
|
+
*
|
|
31
|
+
* The listener is always closed once the promise settles, so an aborted or
|
|
32
|
+
* timed-out flow never leaves a stale port occupied (which would otherwise
|
|
33
|
+
* cause EADDRINUSE on the next attempt).
|
|
34
|
+
*/
|
|
35
|
+
export declare function waitForOAuthCallback(config: Config, expectedState: string, options?: {
|
|
36
|
+
timeoutMs?: number;
|
|
37
|
+
}): Promise<AuthCallback>;
|
|
38
|
+
/** Exchange an authorization code for tokens. */
|
|
39
|
+
export declare function exchangeCode(config: Config, code: string): Promise<TokenSet>;
|
|
40
|
+
/** Refresh an expired access token. Keeps the existing refresh token if the provider does not rotate it. */
|
|
41
|
+
export declare function refreshAccessToken(config: Config, refreshToken: string): Promise<TokenSet>;
|
|
42
|
+
/** Best-effort fetch of the signed-in user's profile (email/name). Never throws. */
|
|
43
|
+
export declare function fetchUserInfo(accessToken: string): Promise<{
|
|
44
|
+
email?: string;
|
|
45
|
+
name?: string;
|
|
46
|
+
}>;
|
|
47
|
+
/** Generate a random state value for CSRF protection. */
|
|
48
|
+
export declare function generateState(): string;
|
|
49
|
+
//# sourceMappingURL=oauth.d.ts.map
|