@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 CHANGED
@@ -3,15 +3,16 @@
3
3
  **English** | [Русский](./README.ru.md)
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/%40a1-x-tech%2Fmcp-google-slides)](https://www.npmjs.com/package/@a1-x-tech/mcp-google-slides)
6
- [![CI](https://github.com/A1-x-Tech/mcp-google-slides/actions/workflows/ci.yml/badge.svg)](https://github.com/A1-x-Tech/mcp-google-slides/actions/workflows/ci.yml)
7
6
  [![Glama](https://glama.ai/mcp/servers/A1-x-Tech/mcp-google-slides/badges/score.svg)](https://glama.ai/mcp/servers/A1-x-Tech/mcp-google-slides)
7
+ [![CI](https://github.com/A1-x-Tech/mcp-google-slides/actions/workflows/ci.yml/badge.svg)](https://github.com/A1-x-Tech/mcp-google-slides/actions/workflows/ci.yml)
8
8
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./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
- - **26 tools.** Inspect decks and pages, edit slides, text, shapes, images and tables, manage speaker notes and comments, render thumbnails and export files.
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+, a Google account and OAuth credentials from a Google Cloud project with the Google Slides API enabled.
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. [Prepare Google OAuth access](#getting-access).
58
- 2. Add the server to your AI app.
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 → Plugins → MCP servers**, select **Add server**, then add `npx -y @a1-x-tech/mcp-google-slides@latest` with `GOOGLE_SLIDES_CLIENT_ID`, `GOOGLE_SLIDES_CLIENT_SECRET` and `GOOGLE_SLIDES_REFRESH_TOKEN`.
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
- Open **Settings → Developer → Edit Config** and add:
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
- If **Edit Config** is unavailable, edit `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS or `%APPDATA%\Claude\claude_desktop_config.json` on Windows.
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` | Yes* | OAuth client ID. |
272
- | `GOOGLE_SLIDES_CLIENT_SECRET` | Yes* | OAuth client secret. |
273
- | `GOOGLE_SLIDES_REFRESH_TOKEN` | Yes* | OAuth refresh token. |
274
- | `GOOGLE_SLIDES_ACCESS_TOKEN` | Yes* | Short-lived (~1 h) alternative to the OAuth trio. |
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
- - **26 инструментов.** Проверка презентаций и страниц, редактирование слайдов, текста, фигур, изображений и таблиц, заметки докладчика и комментарии, миниатюры и экспорт файлов.
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+, Google-аккаунт и OAuth-данные из проекта Google Cloud с включённым Google Slides API.
56
+ Нужны Node.js 20+ и Google-аккаунт. Учётные данные при установке не нужны: сервер подключается прямо в диалоге.
56
57
 
57
- 1. [Подготовьте Google OAuth-доступ](#как-получить-доступ).
58
- 2. Добавьте сервер в AI-приложение.
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 → Plugins → MCP servers**, нажмите **Add server**, затем добавьте `npx -y @a1-x-tech/mcp-google-slides@latest` с `GOOGLE_SLIDES_CLIENT_ID`, `GOOGLE_SLIDES_CLIENT_SECRET` и `GOOGLE_SLIDES_REFRESH_TOKEN`.
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
- Откройте **Settings → Developer → Edit Config** и добавьте:
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
- Если **Edit Config** недоступна, отредактируйте `~/Library/Application Support/Claude/claude_desktop_config.json` на macOS или `%APPDATA%\Claude\claude_desktop_config.json` на Windows.
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` | Да* | OAuth client ID. |
272
- | `GOOGLE_SLIDES_CLIENT_SECRET` | Да* | OAuth client secret. |
273
- | `GOOGLE_SLIDES_REFRESH_TOKEN` | Да* | OAuth refresh token. |
274
- | `GOOGLE_SLIDES_ACCESS_TOKEN` | Да* | Короткоживущая (~1 ч) альтернатива OAuth-тройке. |
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
- if (!this.config.accessToken)
200
- throw new CredentialsError();
201
- return this.config.accessToken;
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.canRefresh() && !refreshedOn401) {
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.canRefresh() && attempt === 0)
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.canRefresh() && attempt === 0)
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
- : UNCONFIGURED_PREFIX + (problem ? `Configuration problem: ${problem.message} ` : "") + INSTRUCTIONS,
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": "0.1.0",
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
  },
@@ -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, /not connected/);
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, /Google OAuth credentials are required: set GOOGLE_SLIDES_CLIENT_ID/);
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();