@a1-x-tech/mcp-google-slides 0.1.0 → 1.0.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 +36 -43
- package/README.ru.md +35 -42
- package/dist/client.js +33 -7
- package/dist/index.js +8 -8
- package/dist/tools/auth.js +39 -0
- package/package.json +2 -1
- package/test/dist-smoke.test.js +37 -4
package/README.md
CHANGED
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
**English** | [Русский](./README.ru.md)
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@a1-x-tech/mcp-google-slides)
|
|
6
|
-
[](https://github.com/A1-x-Tech/mcp-google-slides/actions/workflows/ci.yml)
|
|
7
6
|
[](https://glama.ai/mcp/servers/A1-x-Tech/mcp-google-slides)
|
|
7
|
+
[](https://github.com/A1-x-Tech/mcp-google-slides/actions/workflows/ci.yml)
|
|
8
8
|
[](./LICENSE)
|
|
9
9
|
|
|
10
10
|
**A1 Google Slides MCP** lets an AI app build and edit Google Slides presentations in plain language. Draft a deck, add and rearrange slides, write and style text, place shapes, images and tables, keep speaker notes, review comments and export the result as PDF or PPTX.
|
|
11
11
|
|
|
12
12
|
It uses the Google Slides API with your Google account. Everything inside a deck is addressed by explicit object ids, and it makes the limits of the Slides API explicit instead of implying that every presentation task is possible.
|
|
13
13
|
|
|
14
|
-
- **
|
|
14
|
+
- **32 tools.** Inspect decks and pages, edit slides, text, shapes, images and tables, manage speaker notes and comments, render thumbnails and export files.
|
|
15
|
+
- **Connects from the conversation.** Say "connect Google Slides": the server walks you through the OAuth client, catches Google's redirect on `127.0.0.1` with PKCE and keeps the tokens itself — no config files, no restart.
|
|
15
16
|
- **Edits are atomic.** Changes ride the API's `batchUpdate`: one invalid request voids the whole batch, so a deck is never left half-edited.
|
|
16
17
|
- **Your Drive stays out of reach.** Comments, export and local-image upload use the Drive API strictly for the one presentation file — there are no tools to list, share, rename or delete Drive files.
|
|
17
18
|
- **Minimal Google scopes.** It needs `presentations`; Drive scopes are required only for local images, comments and export.
|
|
@@ -52,10 +53,10 @@ Start with a read-only question:
|
|
|
52
53
|
|
|
53
54
|
## Quick start
|
|
54
55
|
|
|
55
|
-
You need Node.js 20
|
|
56
|
+
You need Node.js 20+ and a Google account. Credentials are not required at install time — the server connects from the conversation.
|
|
56
57
|
|
|
57
|
-
1.
|
|
58
|
-
2.
|
|
58
|
+
1. Add the server to your AI app.
|
|
59
|
+
2. Say "connect Google Slides": the assistant walks you through [creating the OAuth client and approving access](#getting-access) without editing config files.
|
|
59
60
|
3. Ask the read-only question above.
|
|
60
61
|
|
|
61
62
|
<details open>
|
|
@@ -63,15 +64,12 @@ You need Node.js 20+, a Google account and OAuth credentials from a Google Cloud
|
|
|
63
64
|
|
|
64
65
|
<br>
|
|
65
66
|
|
|
66
|
-
**In the app:** open **Settings →
|
|
67
|
+
**In the app:** open **Settings → MCP servers**, select **Add server**, choose **STDIO**, enter the command `npx -y @a1-x-tech/mcp-google-slides@latest` and environment variables `GOOGLE_SLIDES_CLIENT_ID`, `GOOGLE_SLIDES_CLIENT_SECRET`, `GOOGLE_SLIDES_REFRESH_TOKEN`, then select **Save** and **Restart**.
|
|
67
68
|
|
|
68
69
|
**From the command line:**
|
|
69
70
|
|
|
70
71
|
```bash
|
|
71
72
|
codex mcp add google-slides \
|
|
72
|
-
--env GOOGLE_SLIDES_CLIENT_ID=your_client_id \
|
|
73
|
-
--env GOOGLE_SLIDES_CLIENT_SECRET=your_client_secret \
|
|
74
|
-
--env GOOGLE_SLIDES_REFRESH_TOKEN=your_refresh_token \
|
|
75
73
|
-- npx -y @a1-x-tech/mcp-google-slides@latest
|
|
76
74
|
```
|
|
77
75
|
|
|
@@ -90,9 +88,6 @@ codex mcp list
|
|
|
90
88
|
|
|
91
89
|
```bash
|
|
92
90
|
claude mcp add \
|
|
93
|
-
--env GOOGLE_SLIDES_CLIENT_ID=your_client_id \
|
|
94
|
-
--env GOOGLE_SLIDES_CLIENT_SECRET=your_client_secret \
|
|
95
|
-
--env GOOGLE_SLIDES_REFRESH_TOKEN=your_refresh_token \
|
|
96
91
|
--transport stdio --scope user google-slides \
|
|
97
92
|
-- npx -y @a1-x-tech/mcp-google-slides@latest
|
|
98
93
|
```
|
|
@@ -110,25 +105,22 @@ claude mcp list
|
|
|
110
105
|
|
|
111
106
|
<br>
|
|
112
107
|
|
|
113
|
-
|
|
108
|
+
The current official path is **Settings → Extensions**. For a custom desktop extension, open **Advanced settings → Extension Developer → Install Extension…**, select a `.mcpb` file and follow the prompts.
|
|
109
|
+
|
|
110
|
+
This repository currently publishes an npm stdio package and does not contain a `.mcpb` bundle. For Claude Desktop builds that still support local configuration, use the following JSON stdio configuration as a fallback:
|
|
114
111
|
|
|
115
112
|
```json
|
|
116
113
|
{
|
|
117
114
|
"mcpServers": {
|
|
118
115
|
"google-slides": {
|
|
119
116
|
"command": "npx",
|
|
120
|
-
"args": ["-y", "@a1-x-tech/mcp-google-slides@latest"]
|
|
121
|
-
"env": {
|
|
122
|
-
"GOOGLE_SLIDES_CLIENT_ID": "your_client_id",
|
|
123
|
-
"GOOGLE_SLIDES_CLIENT_SECRET": "your_client_secret",
|
|
124
|
-
"GOOGLE_SLIDES_REFRESH_TOKEN": "your_refresh_token"
|
|
125
|
-
}
|
|
117
|
+
"args": ["-y", "@a1-x-tech/mcp-google-slides@latest"]
|
|
126
118
|
}
|
|
127
119
|
}
|
|
128
120
|
}
|
|
129
121
|
```
|
|
130
122
|
|
|
131
|
-
|
|
123
|
+
In those builds, save it to `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS or `%APPDATA%\Claude\claude_desktop_config.json` on Windows.
|
|
132
124
|
|
|
133
125
|
[Claude Desktop MCP documentation](https://support.claude.com/en/articles/10949351-getting-started-with-local-mcp-servers-on-claude-desktop)
|
|
134
126
|
|
|
@@ -147,12 +139,7 @@ Add this to `~/.cursor/mcp.json` on macOS/Linux or `%USERPROFILE%\.cursor\mcp.js
|
|
|
147
139
|
"google-slides": {
|
|
148
140
|
"type": "stdio",
|
|
149
141
|
"command": "npx",
|
|
150
|
-
"args": ["-y", "@a1-x-tech/mcp-google-slides@latest"]
|
|
151
|
-
"env": {
|
|
152
|
-
"GOOGLE_SLIDES_CLIENT_ID": "your_client_id",
|
|
153
|
-
"GOOGLE_SLIDES_CLIENT_SECRET": "your_client_secret",
|
|
154
|
-
"GOOGLE_SLIDES_REFRESH_TOKEN": "your_refresh_token"
|
|
155
|
-
}
|
|
142
|
+
"args": ["-y", "@a1-x-tech/mcp-google-slides@latest"]
|
|
156
143
|
}
|
|
157
144
|
}
|
|
158
145
|
}
|
|
@@ -175,19 +162,9 @@ Run **MCP: Open User Configuration** and add:
|
|
|
175
162
|
"google-slides": {
|
|
176
163
|
"type": "stdio",
|
|
177
164
|
"command": "npx",
|
|
178
|
-
"args": ["-y", "@a1-x-tech/mcp-google-slides@latest"]
|
|
179
|
-
"env": {
|
|
180
|
-
"GOOGLE_SLIDES_CLIENT_ID": "${input:slides_client_id}",
|
|
181
|
-
"GOOGLE_SLIDES_CLIENT_SECRET": "${input:slides_client_secret}",
|
|
182
|
-
"GOOGLE_SLIDES_REFRESH_TOKEN": "${input:slides_refresh_token}"
|
|
183
|
-
}
|
|
165
|
+
"args": ["-y", "@a1-x-tech/mcp-google-slides@latest"]
|
|
184
166
|
}
|
|
185
|
-
}
|
|
186
|
-
"inputs": [
|
|
187
|
-
{ "type": "promptString", "id": "slides_client_id", "description": "Google OAuth client ID" },
|
|
188
|
-
{ "type": "promptString", "id": "slides_client_secret", "description": "Google OAuth client secret", "password": true },
|
|
189
|
-
{ "type": "promptString", "id": "slides_refresh_token", "description": "Google OAuth refresh token", "password": true }
|
|
190
|
-
]
|
|
167
|
+
}
|
|
191
168
|
}
|
|
192
169
|
```
|
|
193
170
|
|
|
@@ -248,7 +225,20 @@ The AI client controls confirmation prompts. The server marks reads, writes and
|
|
|
248
225
|
|
|
249
226
|
## Getting access
|
|
250
227
|
|
|
251
|
-
Google Slides requires OAuth 2.0; an API key is not enough.
|
|
228
|
+
Google Slides requires OAuth 2.0; an API key is not enough. There are two ways in, and the first one needs no configuration files.
|
|
229
|
+
|
|
230
|
+
### Connect from the chat (recommended)
|
|
231
|
+
|
|
232
|
+
Say "connect Google Slides" and the assistant runs the flow with you:
|
|
233
|
+
|
|
234
|
+
1. `setup_instructions` prints the checklist: create or select a Google Cloud project, enable **Google Slides API**, configure the consent screen and create a **Desktop app** OAuth client.
|
|
235
|
+
2. Download that client's JSON ("Download JSON") and give the assistant its **path** — `set_client` stores it owner-only. The secret never goes through the conversation.
|
|
236
|
+
3. `start_login` returns a Google consent link. Open it **on this machine** and approve; the code comes back to a one-shot listener on `127.0.0.1` (PKCE), never through the chat.
|
|
237
|
+
4. `finish_login` exchanges the code and saves the tokens to `~/.config/mcp-google-slides/credentials.json` (mode 0600).
|
|
238
|
+
|
|
239
|
+
The tokens are re-read on every call, so the connection works immediately — no restart of the AI app. `auth_status` shows what is connected, `logout` revokes and deletes it.
|
|
240
|
+
|
|
241
|
+
### Environment variables (CI, unattended installs)
|
|
252
242
|
|
|
253
243
|
1. Create or select a Google Cloud project and enable **Google Slides API**. Enable **Google Drive API** too if you want comments, export or local-image upload.
|
|
254
244
|
2. Configure the OAuth consent screen and create a **Desktop app** OAuth client.
|
|
@@ -266,12 +256,15 @@ Testing-mode OAuth refresh tokens can expire after seven days. Publish the OAuth
|
|
|
266
256
|
|
|
267
257
|
## Configuration
|
|
268
258
|
|
|
259
|
+
Every variable is optional — with none of them the server connects [from the chat](#connect-from-the-chat-recommended).
|
|
260
|
+
|
|
269
261
|
| Variable | Required | Description |
|
|
270
262
|
|---|---|---|
|
|
271
|
-
| `GOOGLE_SLIDES_CLIENT_ID` |
|
|
272
|
-
| `GOOGLE_SLIDES_CLIENT_SECRET` |
|
|
273
|
-
| `GOOGLE_SLIDES_REFRESH_TOKEN` |
|
|
274
|
-
| `GOOGLE_SLIDES_ACCESS_TOKEN` |
|
|
263
|
+
| `GOOGLE_SLIDES_CLIENT_ID` | No* | OAuth client ID. |
|
|
264
|
+
| `GOOGLE_SLIDES_CLIENT_SECRET` | No* | OAuth client secret. |
|
|
265
|
+
| `GOOGLE_SLIDES_REFRESH_TOKEN` | No* | OAuth refresh token. |
|
|
266
|
+
| `GOOGLE_SLIDES_ACCESS_TOKEN` | No* | Short-lived (~1 h) alternative to the OAuth trio. |
|
|
267
|
+
| `GOOGLE_SLIDES_OAUTH_PORT` | No | Fixed loopback port for the in-chat login; useful over SSH port forwarding. |
|
|
275
268
|
| `GOOGLE_SLIDES_API_BASE` | No | Google Slides API base URL override. |
|
|
276
269
|
| `GOOGLE_SLIDES_DRIVE_API_BASE` | No | Google Drive API base URL override (internal dependency: comments, export, image upload). |
|
|
277
270
|
| `GOOGLE_SLIDES_TIMEOUT_MS` | No | Per-request timeout; default `60000` ms. |
|
package/README.ru.md
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
Сервер работает с Google Slides API через ваш Google-аккаунт. Всё внутри презентации адресуется явными object id, а ограничения Slides API сервер называет прямо, вместо того чтобы делать вид, будто с презентацией возможно всё.
|
|
13
13
|
|
|
14
|
-
- **
|
|
14
|
+
- **32 инструментов.** Проверка презентаций и страниц, редактирование слайдов, текста, фигур, изображений и таблиц, заметки докладчика и комментарии, миниатюры и экспорт файлов.
|
|
15
|
+
- **Подключение из диалога.** Скажите «подключи Google Презентации»: сервер проведёт через создание OAuth-клиента, поймает редирект Google на `127.0.0.1` с PKCE и сам сохранит токены — без конфигов и перезапуска.
|
|
15
16
|
- **Правки атомарны.** Изменения идут через `batchUpdate` API: один неверный запрос отменяет весь пакет, поэтому презентация не остаётся отредактированной наполовину.
|
|
16
17
|
- **Ваш Drive недосягаем.** Комментарии, экспорт и загрузка локальных изображений используют Drive API строго для одного файла презентации — инструментов, чтобы перечислять, расшаривать, переименовывать или удалять файлы Drive, нет.
|
|
17
18
|
- **Минимальные scope Google.** Нужен `presentations`; scope Drive требуются только для локальных изображений, комментариев и экспорта.
|
|
@@ -52,10 +53,10 @@
|
|
|
52
53
|
|
|
53
54
|
## Быстрый старт
|
|
54
55
|
|
|
55
|
-
Нужны Node.js 20
|
|
56
|
+
Нужны Node.js 20+ и Google-аккаунт. Учётные данные при установке не нужны: сервер подключается прямо в диалоге.
|
|
56
57
|
|
|
57
|
-
1.
|
|
58
|
-
2.
|
|
58
|
+
1. Добавьте сервер в AI-приложение.
|
|
59
|
+
2. Скажите «подключи Google Презентации» — ассистент проведёт [создание OAuth-клиента и выдачу доступа](#как-получить-доступ), не трогая конфиги.
|
|
59
60
|
3. Отправьте запрос, который только читает данные.
|
|
60
61
|
|
|
61
62
|
<details open>
|
|
@@ -63,15 +64,12 @@
|
|
|
63
64
|
|
|
64
65
|
<br>
|
|
65
66
|
|
|
66
|
-
**В приложении:** откройте **Settings →
|
|
67
|
+
**В приложении:** откройте **Settings → MCP servers**, нажмите **Add server**, выберите **STDIO**, укажите команду `npx -y @a1-x-tech/mcp-google-slides@latest` и переменные окружения `GOOGLE_SLIDES_CLIENT_ID`, `GOOGLE_SLIDES_CLIENT_SECRET`, `GOOGLE_SLIDES_REFRESH_TOKEN`, затем нажмите **Save**, потом **Restart**.
|
|
67
68
|
|
|
68
69
|
**В командной строке:**
|
|
69
70
|
|
|
70
71
|
```bash
|
|
71
72
|
codex mcp add google-slides \
|
|
72
|
-
--env GOOGLE_SLIDES_CLIENT_ID=your_client_id \
|
|
73
|
-
--env GOOGLE_SLIDES_CLIENT_SECRET=your_client_secret \
|
|
74
|
-
--env GOOGLE_SLIDES_REFRESH_TOKEN=your_refresh_token \
|
|
75
73
|
-- npx -y @a1-x-tech/mcp-google-slides@latest
|
|
76
74
|
```
|
|
77
75
|
|
|
@@ -90,9 +88,6 @@ codex mcp list
|
|
|
90
88
|
|
|
91
89
|
```bash
|
|
92
90
|
claude mcp add \
|
|
93
|
-
--env GOOGLE_SLIDES_CLIENT_ID=your_client_id \
|
|
94
|
-
--env GOOGLE_SLIDES_CLIENT_SECRET=your_client_secret \
|
|
95
|
-
--env GOOGLE_SLIDES_REFRESH_TOKEN=your_refresh_token \
|
|
96
91
|
--transport stdio --scope user google-slides \
|
|
97
92
|
-- npx -y @a1-x-tech/mcp-google-slides@latest
|
|
98
93
|
```
|
|
@@ -110,25 +105,22 @@ claude mcp list
|
|
|
110
105
|
|
|
111
106
|
<br>
|
|
112
107
|
|
|
113
|
-
|
|
108
|
+
Актуальный официальный путь — **Settings → Extensions**. Для пользовательского desktop extension откройте **Advanced settings → Extension Developer → Install Extension…**, выберите файл `.mcpb` и следуйте подсказкам.
|
|
109
|
+
|
|
110
|
+
Этот репозиторий сейчас публикует npm-пакет со stdio и пока не содержит `.mcpb`. Поэтому используйте приведённый ниже JSON stdio-конфиг как fallback только в сборках Claude Desktop, где ещё поддерживается локальная конфигурация:
|
|
114
111
|
|
|
115
112
|
```json
|
|
116
113
|
{
|
|
117
114
|
"mcpServers": {
|
|
118
115
|
"google-slides": {
|
|
119
116
|
"command": "npx",
|
|
120
|
-
"args": ["-y", "@a1-x-tech/mcp-google-slides@latest"]
|
|
121
|
-
"env": {
|
|
122
|
-
"GOOGLE_SLIDES_CLIENT_ID": "your_client_id",
|
|
123
|
-
"GOOGLE_SLIDES_CLIENT_SECRET": "your_client_secret",
|
|
124
|
-
"GOOGLE_SLIDES_REFRESH_TOKEN": "your_refresh_token"
|
|
125
|
-
}
|
|
117
|
+
"args": ["-y", "@a1-x-tech/mcp-google-slides@latest"]
|
|
126
118
|
}
|
|
127
119
|
}
|
|
128
120
|
}
|
|
129
121
|
```
|
|
130
122
|
|
|
131
|
-
|
|
123
|
+
В таких сборках сохраните его в `~/Library/Application Support/Claude/claude_desktop_config.json` на macOS или `%APPDATA%\Claude\claude_desktop_config.json` на Windows.
|
|
132
124
|
|
|
133
125
|
[Документация Claude Desktop MCP](https://support.claude.com/en/articles/10949351-getting-started-with-local-mcp-servers-on-claude-desktop)
|
|
134
126
|
|
|
@@ -147,12 +139,7 @@ claude mcp list
|
|
|
147
139
|
"google-slides": {
|
|
148
140
|
"type": "stdio",
|
|
149
141
|
"command": "npx",
|
|
150
|
-
"args": ["-y", "@a1-x-tech/mcp-google-slides@latest"]
|
|
151
|
-
"env": {
|
|
152
|
-
"GOOGLE_SLIDES_CLIENT_ID": "your_client_id",
|
|
153
|
-
"GOOGLE_SLIDES_CLIENT_SECRET": "your_client_secret",
|
|
154
|
-
"GOOGLE_SLIDES_REFRESH_TOKEN": "your_refresh_token"
|
|
155
|
-
}
|
|
142
|
+
"args": ["-y", "@a1-x-tech/mcp-google-slides@latest"]
|
|
156
143
|
}
|
|
157
144
|
}
|
|
158
145
|
}
|
|
@@ -175,19 +162,9 @@ claude mcp list
|
|
|
175
162
|
"google-slides": {
|
|
176
163
|
"type": "stdio",
|
|
177
164
|
"command": "npx",
|
|
178
|
-
"args": ["-y", "@a1-x-tech/mcp-google-slides@latest"]
|
|
179
|
-
"env": {
|
|
180
|
-
"GOOGLE_SLIDES_CLIENT_ID": "${input:slides_client_id}",
|
|
181
|
-
"GOOGLE_SLIDES_CLIENT_SECRET": "${input:slides_client_secret}",
|
|
182
|
-
"GOOGLE_SLIDES_REFRESH_TOKEN": "${input:slides_refresh_token}"
|
|
183
|
-
}
|
|
165
|
+
"args": ["-y", "@a1-x-tech/mcp-google-slides@latest"]
|
|
184
166
|
}
|
|
185
|
-
}
|
|
186
|
-
"inputs": [
|
|
187
|
-
{ "type": "promptString", "id": "slides_client_id", "description": "Google OAuth client ID" },
|
|
188
|
-
{ "type": "promptString", "id": "slides_client_secret", "description": "Google OAuth client secret", "password": true },
|
|
189
|
-
{ "type": "promptString", "id": "slides_refresh_token", "description": "Google OAuth refresh token", "password": true }
|
|
190
|
-
]
|
|
167
|
+
}
|
|
191
168
|
}
|
|
192
169
|
```
|
|
193
170
|
|
|
@@ -248,7 +225,20 @@ Slides API не может удалить, переименовать или р
|
|
|
248
225
|
|
|
249
226
|
## Как получить доступ
|
|
250
227
|
|
|
251
|
-
Google Slides требует OAuth 2.0: одного API-ключа недостаточно.
|
|
228
|
+
Google Slides требует OAuth 2.0: одного API-ключа недостаточно. Путей два, и первый не требует править конфигурационные файлы.
|
|
229
|
+
|
|
230
|
+
### Подключение из диалога (рекомендуемый путь)
|
|
231
|
+
|
|
232
|
+
Скажите «подключи Google Презентации», и ассистент пройдёт флоу вместе с вами:
|
|
233
|
+
|
|
234
|
+
1. `setup_instructions` выдаёт чек-лист: создать или выбрать проект Google Cloud, включить **Google Slides API**, настроить consent screen и создать OAuth-клиент типа **Desktop app**.
|
|
235
|
+
2. Скачайте JSON этого клиента («Download JSON») и передайте ассистенту **путь** к файлу — `set_client` сохранит его с правами только для владельца. Секрет через переписку не проходит.
|
|
236
|
+
3. `start_login` возвращает ссылку на согласие Google. Откройте её **на этой же машине** и подтвердите доступ: код возвращается на одноразовый слушатель `127.0.0.1` (PKCE), а не в чат.
|
|
237
|
+
4. `finish_login` меняет код на токены и кладёт их в `~/.config/mcp-google-slides/credentials.json` (права 0600).
|
|
238
|
+
|
|
239
|
+
Токены перечитываются на каждый вызов, поэтому подключение действует немедленно — перезапускать AI-приложение не нужно. `auth_status` показывает состояние, `logout` отзывает токен и удаляет его.
|
|
240
|
+
|
|
241
|
+
### Переменные окружения (CI и автоматические установки)
|
|
252
242
|
|
|
253
243
|
1. Создайте или выберите проект Google Cloud и включите **Google Slides API**. Также включите **Google Drive API**, если нужны комментарии, экспорт или загрузка локальных изображений.
|
|
254
244
|
2. Настройте OAuth consent screen и создайте OAuth-клиент типа **Desktop app**.
|
|
@@ -266,12 +256,15 @@ Refresh token OAuth-приложения в режиме Testing может ис
|
|
|
266
256
|
|
|
267
257
|
## Конфигурация
|
|
268
258
|
|
|
259
|
+
Все переменные необязательные — без единой из них сервер подключается [из диалога](#подключение-из-диалога-рекомендуемый-путь).
|
|
260
|
+
|
|
269
261
|
| Переменная | Обязательна | Описание |
|
|
270
262
|
|---|---|---|
|
|
271
|
-
| `GOOGLE_SLIDES_CLIENT_ID` |
|
|
272
|
-
| `GOOGLE_SLIDES_CLIENT_SECRET` |
|
|
273
|
-
| `GOOGLE_SLIDES_REFRESH_TOKEN` |
|
|
274
|
-
| `GOOGLE_SLIDES_ACCESS_TOKEN` |
|
|
263
|
+
| `GOOGLE_SLIDES_CLIENT_ID` | Нет* | OAuth client ID. |
|
|
264
|
+
| `GOOGLE_SLIDES_CLIENT_SECRET` | Нет* | OAuth client secret. |
|
|
265
|
+
| `GOOGLE_SLIDES_REFRESH_TOKEN` | Нет* | OAuth refresh token. |
|
|
266
|
+
| `GOOGLE_SLIDES_ACCESS_TOKEN` | Нет* | Короткоживущая (~1 ч) альтернатива OAuth-тройке. |
|
|
267
|
+
| `GOOGLE_SLIDES_OAUTH_PORT` | Нет | Фиксированный порт loopback-слушателя для входа из диалога; нужен при пробросе портов по SSH. |
|
|
275
268
|
| `GOOGLE_SLIDES_API_BASE` | Нет | Переопределяет базовый URL Google Slides API. |
|
|
276
269
|
| `GOOGLE_SLIDES_DRIVE_API_BASE` | Нет | Переопределяет базовый URL Google Drive API (внутренняя зависимость: комментарии, экспорт, загрузка изображений). |
|
|
277
270
|
| `GOOGLE_SLIDES_TIMEOUT_MS` | Нет | Тайм-аут одного запроса; по умолчанию `60000` мс. |
|
package/dist/client.js
CHANGED
|
@@ -164,6 +164,7 @@ export function extractText(text) {
|
|
|
164
164
|
}
|
|
165
165
|
export class GoogleSlidesClient {
|
|
166
166
|
config;
|
|
167
|
+
tokenProvider;
|
|
167
168
|
base;
|
|
168
169
|
driveBase;
|
|
169
170
|
timeoutMs;
|
|
@@ -173,8 +174,16 @@ export class GoogleSlidesClient {
|
|
|
173
174
|
cachedToken;
|
|
174
175
|
/** In-flight refresh, deduping concurrent token requests. */
|
|
175
176
|
refreshInFlight;
|
|
176
|
-
constructor(config
|
|
177
|
+
constructor(config,
|
|
178
|
+
/**
|
|
179
|
+
* Fallback token source (the in-chat login of @a1-x-tech/mcp-google-auth).
|
|
180
|
+
* Consulted only when the env-derived config carries no credentials —
|
|
181
|
+
* env wins (component invariant 3), so existing refresh-triple and
|
|
182
|
+
* access-token installs behave exactly as before.
|
|
183
|
+
*/
|
|
184
|
+
tokenProvider) {
|
|
177
185
|
this.config = config;
|
|
186
|
+
this.tokenProvider = tokenProvider;
|
|
178
187
|
this.base = config.apiBase.endsWith("/") ? config.apiBase : config.apiBase + "/";
|
|
179
188
|
const drive = config.driveApiBase ?? "https://www.googleapis.com";
|
|
180
189
|
this.driveBase = drive.endsWith("/") ? drive : drive + "/";
|
|
@@ -185,6 +194,19 @@ export class GoogleSlidesClient {
|
|
|
185
194
|
canRefresh() {
|
|
186
195
|
return Boolean(this.config.refreshToken && this.config.clientId && this.config.clientSecret);
|
|
187
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Whether a 401 is worth one re-mint + replay: either the env config can mint
|
|
199
|
+
* from its refresh triple, or the provider holds a refresh token (env or
|
|
200
|
+
* stored login). A static env access token can never be re-minted, so a 401
|
|
201
|
+
* there is final — replaying it would just burn a second request.
|
|
202
|
+
*/
|
|
203
|
+
canReplayOn401() {
|
|
204
|
+
if (this.canRefresh())
|
|
205
|
+
return true;
|
|
206
|
+
if (this.config.accessToken)
|
|
207
|
+
return false;
|
|
208
|
+
return this.tokenProvider?.canRefresh() ?? false;
|
|
209
|
+
}
|
|
188
210
|
/**
|
|
189
211
|
* Returns a valid Bearer token. With the refresh triple configured, mints an
|
|
190
212
|
* access token from the refresh token and caches it until shortly before it
|
|
@@ -196,9 +218,13 @@ export class GoogleSlidesClient {
|
|
|
196
218
|
*/
|
|
197
219
|
async accessToken(forceRefresh = false) {
|
|
198
220
|
if (!this.canRefresh()) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
221
|
+
// Env wins over the provider (component invariant 3): a static
|
|
222
|
+
// GOOGLE_SLIDES_ACCESS_TOKEN keeps behaving exactly as before.
|
|
223
|
+
if (this.config.accessToken)
|
|
224
|
+
return this.config.accessToken;
|
|
225
|
+
if (this.tokenProvider)
|
|
226
|
+
return this.tokenProvider.getAccessToken(forceRefresh);
|
|
227
|
+
throw new CredentialsError();
|
|
202
228
|
}
|
|
203
229
|
if (!forceRefresh && this.cachedToken && Date.now() < this.cachedToken.expiresAt) {
|
|
204
230
|
return this.cachedToken.value;
|
|
@@ -358,7 +384,7 @@ export class GoogleSlidesClient {
|
|
|
358
384
|
// eat into the transient-retry budget (attempt-- cancels the loop's
|
|
359
385
|
// attempt++), so a 429/5xx after the re-mint still gets maxRetries
|
|
360
386
|
// tries; refreshedOn401 guarantees this runs at most once.
|
|
361
|
-
if (res.status === 401 && this.
|
|
387
|
+
if (res.status === 401 && this.canReplayOn401() && !refreshedOn401) {
|
|
362
388
|
refreshedOn401 = true;
|
|
363
389
|
await this.accessToken(true);
|
|
364
390
|
attempt--;
|
|
@@ -395,7 +421,7 @@ export class GoogleSlidesClient {
|
|
|
395
421
|
for (let attempt = 0;; attempt++) {
|
|
396
422
|
const token = await this.accessToken(attempt > 0);
|
|
397
423
|
const { res, bytes } = await this.fetchBytesWithTimeout(url, { method: "GET", headers: { Authorization: `Bearer ${token}` } }, label);
|
|
398
|
-
if (res.status === 401 && this.
|
|
424
|
+
if (res.status === 401 && this.canReplayOn401() && attempt === 0)
|
|
399
425
|
continue;
|
|
400
426
|
if (!res.ok) {
|
|
401
427
|
let parsed;
|
|
@@ -917,7 +943,7 @@ export class GoogleSlidesClient {
|
|
|
917
943
|
},
|
|
918
944
|
body,
|
|
919
945
|
}, "drive image upload");
|
|
920
|
-
if (res.status === 401 && this.
|
|
946
|
+
if (res.status === 401 && this.canReplayOn401() && attempt === 0)
|
|
921
947
|
continue;
|
|
922
948
|
let parsed;
|
|
923
949
|
try {
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { registerNoteTools } from "./tools/notes.js";
|
|
|
14
14
|
import { registerCommentTools } from "./tools/comments.js";
|
|
15
15
|
import { registerExportTools } from "./tools/export.js";
|
|
16
16
|
import { registerRawTool } from "./tools/raw.js";
|
|
17
|
+
import { authUnconfiguredPrefix, hasAuthToken, registerAuthTools } from "./tools/auth.js";
|
|
17
18
|
/**
|
|
18
19
|
* Prose handed to the calling model in the `initialize` result — the only place
|
|
19
20
|
* it learns what the tool list cannot say: which Google product this API is,
|
|
@@ -40,11 +41,6 @@ const INSTRUCTIONS = "Google Slides API v1 builds and edits presentations — no
|
|
|
40
41
|
* rather than with a failed call. There is no in-chat login here: credentials
|
|
41
42
|
* come only from the environment, so the fix is an operator action + restart.
|
|
42
43
|
*/
|
|
43
|
-
const UNCONFIGURED_PREFIX = "ATTENTION: Google Slides is not connected yet — no credentials are configured, so every " +
|
|
44
|
-
"tool call will fail. The operator must set GOOGLE_SLIDES_CLIENT_ID + " +
|
|
45
|
-
"GOOGLE_SLIDES_CLIENT_SECRET + GOOGLE_SLIDES_REFRESH_TOKEN (recommended), or " +
|
|
46
|
-
"GOOGLE_SLIDES_ACCESS_TOKEN with a short-lived access token, in the MCP client's " +
|
|
47
|
-
"server config and restart this server — the variables are read only at startup. ";
|
|
48
44
|
/** Reads the package version so the server reports its real version to MCP clients. */
|
|
49
45
|
function readVersion() {
|
|
50
46
|
try {
|
|
@@ -88,10 +84,9 @@ async function main() {
|
|
|
88
84
|
// credentials can be reported; wired to the server before tools register.
|
|
89
85
|
const telemetry = new Telemetry(readVersion());
|
|
90
86
|
const { config, problem } = loadConfigOrDegraded(telemetry);
|
|
91
|
-
const client = new GoogleSlidesClient(config);
|
|
92
87
|
// Decided once, at startup: credentials come only from the environment, so
|
|
93
88
|
// "restart after setting the variables" is the accurate advice to give.
|
|
94
|
-
const connected = hasCredentials(config);
|
|
89
|
+
const connected = hasCredentials(config) || hasAuthToken();
|
|
95
90
|
const server = new McpServer({
|
|
96
91
|
name: "mcp-google-slides",
|
|
97
92
|
version: readVersion(),
|
|
@@ -100,7 +95,7 @@ async function main() {
|
|
|
100
95
|
{
|
|
101
96
|
instructions: connected
|
|
102
97
|
? INSTRUCTIONS
|
|
103
|
-
:
|
|
98
|
+
: authUnconfiguredPrefix() + (problem ? `Configuration problem: ${problem.message} ` : "") + INSTRUCTIONS,
|
|
104
99
|
});
|
|
105
100
|
instrumentToolCalls(server, telemetry);
|
|
106
101
|
server.server.oninitialized = () => {
|
|
@@ -112,6 +107,11 @@ async function main() {
|
|
|
112
107
|
else
|
|
113
108
|
telemetry.send("unconfigured_start", { reason: problem?.reason ?? "missing_credentials" });
|
|
114
109
|
};
|
|
110
|
+
// The auth tools come first so their TokenProvider exists before the client:
|
|
111
|
+
// the client falls back to it whenever the environment carries no
|
|
112
|
+
// credentials (env always wins — component invariant 3).
|
|
113
|
+
const tokenProvider = registerAuthTools(server);
|
|
114
|
+
const client = new GoogleSlidesClient(config, tokenProvider);
|
|
115
115
|
registerPresentationTools(server, client);
|
|
116
116
|
registerSlideTools(server, client);
|
|
117
117
|
registerTextTools(server, client);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { registerGoogleAuth, TokenProvider, unconfiguredPrefix, } from "@a1-x-tech/mcp-google-auth";
|
|
2
|
+
/**
|
|
3
|
+
* The scopes an in-chat login requests (the per-task table lives in
|
|
4
|
+
* docs/TOOLS.md). The component always adds the identity scopes on top. The full `drive` scope rides along because export and comments reach presentations this server did not create; `drive.file` would silently fail on decks the user opened elsewhere.
|
|
5
|
+
*/
|
|
6
|
+
export const LOGIN_SCOPES = [
|
|
7
|
+
"https://www.googleapis.com/auth/presentations",
|
|
8
|
+
"https://www.googleapis.com/auth/drive",
|
|
9
|
+
];
|
|
10
|
+
/**
|
|
11
|
+
* The single source of the auth wiring: serverName "slides" puts the stored
|
|
12
|
+
* login in ~/.config/mcp-google-slides/credentials.json (the component
|
|
13
|
+
* prefixes "mcp-google-"), envPrefix keeps the provider reading the exact
|
|
14
|
+
* GOOGLE_SLIDES_* variables config.ts documents — which is what preserves the
|
|
15
|
+
* env-beats-stored priority for existing installs.
|
|
16
|
+
*/
|
|
17
|
+
export const AUTH_OPTIONS = {
|
|
18
|
+
serverName: "slides",
|
|
19
|
+
envPrefix: "GOOGLE_SLIDES",
|
|
20
|
+
scopes: LOGIN_SCOPES,
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Registers the six onboarding tools (auth_status, setup_instructions,
|
|
24
|
+
* set_client, start_login, finish_login, logout) and returns the TokenProvider
|
|
25
|
+
* the client plugs in as its fallback token source — the same instance, so a
|
|
26
|
+
* login finished mid-session is visible to the very next API call without a
|
|
27
|
+
* restart.
|
|
28
|
+
*/
|
|
29
|
+
export function registerAuthTools(server) {
|
|
30
|
+
return registerGoogleAuth(server, AUTH_OPTIONS);
|
|
31
|
+
}
|
|
32
|
+
/** True when any token exists — env variables or a stored in-chat login. */
|
|
33
|
+
export function hasAuthToken() {
|
|
34
|
+
return new TokenProvider(AUTH_OPTIONS).hasToken();
|
|
35
|
+
}
|
|
36
|
+
/** The "NOT CONNECTED" prefix for the initialize instructions (names both fixes). */
|
|
37
|
+
export function authUnconfiguredPrefix() {
|
|
38
|
+
return unconfiguredPrefix(AUTH_OPTIONS);
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a1-x-tech/mcp-google-slides",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "MCP server for the Google Slides API — create and edit presentations, slides, text, shapes, tables and images, manage speaker notes, comments, thumbnails and export. For Claude, Cursor, Codex and other AI clients.",
|
|
5
5
|
"mcpName": "io.github.A1-x-Tech/mcp-google-slides",
|
|
6
6
|
"type": "module",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"url": "https://github.com/A1-x-Tech/mcp-google-slides/issues"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
+
"@a1-x-tech/mcp-google-auth": "^0.1.0",
|
|
54
55
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
55
56
|
"zod": "^3.25.0"
|
|
56
57
|
},
|
package/test/dist-smoke.test.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
2
5
|
import test from "node:test";
|
|
3
6
|
import { fileURLToPath } from "node:url";
|
|
4
7
|
|
|
@@ -15,9 +18,16 @@ import { registerNoteTools } from "../dist/tools/notes.js";
|
|
|
15
18
|
import { registerCommentTools } from "../dist/tools/comments.js";
|
|
16
19
|
import { registerExportTools } from "../dist/tools/export.js";
|
|
17
20
|
import { registerRawTool } from "../dist/tools/raw.js";
|
|
21
|
+
import { registerAuthTools } from "../dist/tools/auth.js";
|
|
18
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Sorted, because every assertion compares it against a sorted tool list. The
|
|
25
|
+
* six onboarding tools come from @a1-x-tech/mcp-google-auth, so this list is
|
|
26
|
+
* also the check that the component is wired into the published binary.
|
|
27
|
+
*/
|
|
19
28
|
const ALL_TOOLS = [
|
|
20
29
|
"add_slide",
|
|
30
|
+
"auth_status",
|
|
21
31
|
"batch_update",
|
|
22
32
|
"create_image",
|
|
23
33
|
"create_presentation",
|
|
@@ -27,6 +37,7 @@ const ALL_TOOLS = [
|
|
|
27
37
|
"duplicate_slide",
|
|
28
38
|
"edit_table",
|
|
29
39
|
"export_presentation",
|
|
40
|
+
"finish_login",
|
|
30
41
|
"get_page",
|
|
31
42
|
"get_presentation",
|
|
32
43
|
"get_speaker_notes",
|
|
@@ -34,17 +45,34 @@ const ALL_TOOLS = [
|
|
|
34
45
|
"insert_text",
|
|
35
46
|
"list_layouts",
|
|
36
47
|
"list_slides",
|
|
48
|
+
"logout",
|
|
37
49
|
"manage_comments",
|
|
38
50
|
"move_slides",
|
|
39
51
|
"raw_request",
|
|
40
52
|
"replace_image",
|
|
41
53
|
"replace_text",
|
|
54
|
+
"set_client",
|
|
42
55
|
"set_speaker_notes",
|
|
43
56
|
"set_text",
|
|
57
|
+
"setup_instructions",
|
|
58
|
+
"start_login",
|
|
44
59
|
"update_text_style",
|
|
45
60
|
"update_transform",
|
|
46
61
|
];
|
|
47
62
|
|
|
63
|
+
/**
|
|
64
|
+
* A throwaway $XDG_CONFIG_HOME for the spawned server. The auth component
|
|
65
|
+
* re-reads $XDG_CONFIG_HOME/mcp-google-slides/credentials.json per call, so
|
|
66
|
+
* without this a real login on the developer's machine would make the
|
|
67
|
+
* "unconfigured" case pass for the wrong reason.
|
|
68
|
+
*/
|
|
69
|
+
function isolatedConfigDir(t) {
|
|
70
|
+
const dir = mkdtempSync(join(tmpdir(), "mcp-slides-dist-smoke-"));
|
|
71
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
72
|
+
return dir;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
48
76
|
test("dist client rejects foreign-origin paths before sending the Bearer token", async () => {
|
|
49
77
|
const original = globalThis.fetch;
|
|
50
78
|
let called = false;
|
|
@@ -98,6 +126,7 @@ test("dist registers the expected tools", () => {
|
|
|
98
126
|
};
|
|
99
127
|
const client = {};
|
|
100
128
|
|
|
129
|
+
registerAuthTools(server, client);
|
|
101
130
|
registerPresentationTools(server, client);
|
|
102
131
|
registerSlideTools(server, client);
|
|
103
132
|
registerTextTools(server, client);
|
|
@@ -111,13 +140,14 @@ test("dist registers the expected tools", () => {
|
|
|
111
140
|
assert.deepEqual(names.sort(), ALL_TOOLS);
|
|
112
141
|
});
|
|
113
142
|
|
|
114
|
-
test("dist binary completes a real MCP handshake over stdio and lists every tool", async () => {
|
|
143
|
+
test("dist binary completes a real MCP handshake over stdio and lists every tool", async (t) => {
|
|
115
144
|
const transport = new StdioClientTransport({
|
|
116
145
|
command: process.execPath,
|
|
117
146
|
args: [fileURLToPath(new URL("../dist/index.js", import.meta.url))],
|
|
118
147
|
env: {
|
|
119
148
|
...process.env,
|
|
120
149
|
GOOGLE_SLIDES_ACCESS_TOKEN: "test-token",
|
|
150
|
+
XDG_CONFIG_HOME: isolatedConfigDir(t),
|
|
121
151
|
ASKADS_TELEMETRY: "0", // keep the suite offline
|
|
122
152
|
},
|
|
123
153
|
stderr: "pipe",
|
|
@@ -152,13 +182,14 @@ test("dist binary completes a real MCP handshake over stdio and lists every tool
|
|
|
152
182
|
* tool call with the actionable error — offline: the CredentialsError fires
|
|
153
183
|
* before any fetch, so this test never touches the network.
|
|
154
184
|
*/
|
|
155
|
-
test("dist binary starts without credentials: handshake, tool list, actionable call error", async () => {
|
|
185
|
+
test("dist binary starts without credentials: handshake, tool list, actionable call error", async (t) => {
|
|
156
186
|
const env = Object.fromEntries(
|
|
157
187
|
Object.entries(process.env).filter(
|
|
158
188
|
([key, value]) => value !== undefined && !key.startsWith("GOOGLE_SLIDES_"),
|
|
159
189
|
),
|
|
160
190
|
);
|
|
161
191
|
env.ASKADS_TELEMETRY = "0"; // keep the suite offline
|
|
192
|
+
env.XDG_CONFIG_HOME = isolatedConfigDir(t); // ignore any real login on this machine
|
|
162
193
|
const transport = new StdioClientTransport({
|
|
163
194
|
command: process.execPath,
|
|
164
195
|
args: [fileURLToPath(new URL("../dist/index.js", import.meta.url))],
|
|
@@ -170,7 +201,8 @@ test("dist binary starts without credentials: handshake, tool list, actionable c
|
|
|
170
201
|
try {
|
|
171
202
|
// The model must read the fix before it picks a tool.
|
|
172
203
|
const instructions = client.getInstructions() ?? "";
|
|
173
|
-
assert.match(instructions, /
|
|
204
|
+
assert.match(instructions, /NOT CONNECTED/);
|
|
205
|
+
assert.match(instructions, /start_login/);
|
|
174
206
|
assert.match(instructions, /GOOGLE_SLIDES_CLIENT_ID/);
|
|
175
207
|
assert.match(instructions, /restart/);
|
|
176
208
|
|
|
@@ -181,7 +213,8 @@ test("dist binary starts without credentials: handshake, tool list, actionable c
|
|
|
181
213
|
const result = await client.callTool({ name: "list_slides", arguments: { presentation_id: "smoke-deck" } });
|
|
182
214
|
assert.equal(result.isError, true);
|
|
183
215
|
const text = result.content.map((c) => c.text ?? "").join(" ");
|
|
184
|
-
assert.match(text, /
|
|
216
|
+
assert.match(text, /not connected/i);
|
|
217
|
+
assert.match(text, /start_login/);
|
|
185
218
|
assert.match(text, /restart the server/);
|
|
186
219
|
} finally {
|
|
187
220
|
await client.close();
|