@alanse/mcp-server-google-workspace 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/dist/auth.js +135 -0
  4. package/dist/index.js +116 -0
  5. package/dist/tools/basic/gsheets_add_sheet.js +65 -0
  6. package/dist/tools/basic/gsheets_copy_sheet.js +56 -0
  7. package/dist/tools/basic/gsheets_copy_to.js +113 -0
  8. package/dist/tools/basic/gsheets_create_spreadsheet.js +88 -0
  9. package/dist/tools/basic/gsheets_delete_columns.js +69 -0
  10. package/dist/tools/basic/gsheets_delete_rows.js +69 -0
  11. package/dist/tools/basic/gsheets_delete_sheet.js +56 -0
  12. package/dist/tools/basic/gsheets_duplicate_sheet.js +72 -0
  13. package/dist/tools/basic/gsheets_insert_columns.js +69 -0
  14. package/dist/tools/basic/gsheets_insert_rows.js +69 -0
  15. package/dist/tools/basic/gsheets_list_sheets.js +53 -0
  16. package/dist/tools/basic/gsheets_read.js +120 -0
  17. package/dist/tools/basic/gsheets_rename_sheet.js +64 -0
  18. package/dist/tools/charts/gsheets_add_bubble.js +176 -0
  19. package/dist/tools/charts/gsheets_add_candlestick.js +192 -0
  20. package/dist/tools/charts/gsheets_add_chart.js +162 -0
  21. package/dist/tools/charts/gsheets_add_combo.js +169 -0
  22. package/dist/tools/charts/gsheets_add_histogram.js +143 -0
  23. package/dist/tools/charts/gsheets_add_org_chart.js +160 -0
  24. package/dist/tools/charts/gsheets_add_treemap.js +177 -0
  25. package/dist/tools/charts/gsheets_add_waterfall.js +155 -0
  26. package/dist/tools/charts/gsheets_delete_chart.js +56 -0
  27. package/dist/tools/charts/gsheets_update_chart.js +118 -0
  28. package/dist/tools/data/gsheets_append_data.js +68 -0
  29. package/dist/tools/data/gsheets_batch_clear.js +53 -0
  30. package/dist/tools/data/gsheets_batch_update.js +81 -0
  31. package/dist/tools/data/gsheets_clear_data.js +53 -0
  32. package/dist/tools/data/gsheets_create_filter.js +81 -0
  33. package/dist/tools/data/gsheets_find_replace.js +124 -0
  34. package/dist/tools/data/gsheets_set_data_validation.js +153 -0
  35. package/dist/tools/data/gsheets_sort_range.js +102 -0
  36. package/dist/tools/data/gsheets_update_cell.js +44 -0
  37. package/dist/tools/formatting/gsheets_auto_resize.js +75 -0
  38. package/dist/tools/formatting/gsheets_format_cells.js +161 -0
  39. package/dist/tools/formatting/gsheets_freeze_columns.js +67 -0
  40. package/dist/tools/formatting/gsheets_freeze_rows.js +67 -0
  41. package/dist/tools/formatting/gsheets_merge_cells.js +85 -0
  42. package/dist/tools/formatting/gsheets_set_number_format.js +116 -0
  43. package/dist/tools/formatting/gsheets_unmerge_cells.js +79 -0
  44. package/dist/tools/formatting/gsheets_update_borders.js +212 -0
  45. package/dist/tools/gdrive/gdrive_read_file.js +77 -0
  46. package/dist/tools/gdrive/gdrive_search.js +71 -0
  47. package/dist/tools/gdrive_read_file.js +77 -0
  48. package/dist/tools/gdrive_search.js +71 -0
  49. package/dist/tools/gsheets_add_bubble.js +176 -0
  50. package/dist/tools/gsheets_add_candlestick.js +192 -0
  51. package/dist/tools/gsheets_add_chart.js +162 -0
  52. package/dist/tools/gsheets_add_combo.js +169 -0
  53. package/dist/tools/gsheets_add_conditional_format.js +175 -0
  54. package/dist/tools/gsheets_add_histogram.js +143 -0
  55. package/dist/tools/gsheets_add_named_range.js +87 -0
  56. package/dist/tools/gsheets_add_org_chart.js +160 -0
  57. package/dist/tools/gsheets_add_protected_range.js +127 -0
  58. package/dist/tools/gsheets_add_sheet.js +65 -0
  59. package/dist/tools/gsheets_add_treemap.js +177 -0
  60. package/dist/tools/gsheets_add_waterfall.js +155 -0
  61. package/dist/tools/gsheets_append_data.js +68 -0
  62. package/dist/tools/gsheets_auto_resize.js +75 -0
  63. package/dist/tools/gsheets_batch_clear.js +53 -0
  64. package/dist/tools/gsheets_batch_update.js +81 -0
  65. package/dist/tools/gsheets_clear_data.js +53 -0
  66. package/dist/tools/gsheets_copy_sheet.js +56 -0
  67. package/dist/tools/gsheets_copy_to.js +113 -0
  68. package/dist/tools/gsheets_create_filter.js +81 -0
  69. package/dist/tools/gsheets_create_spreadsheet.js +88 -0
  70. package/dist/tools/gsheets_delete_chart.js +56 -0
  71. package/dist/tools/gsheets_delete_columns.js +69 -0
  72. package/dist/tools/gsheets_delete_named_range.js +56 -0
  73. package/dist/tools/gsheets_delete_protected_range.js +56 -0
  74. package/dist/tools/gsheets_delete_rows.js +69 -0
  75. package/dist/tools/gsheets_delete_sheet.js +56 -0
  76. package/dist/tools/gsheets_duplicate_sheet.js +72 -0
  77. package/dist/tools/gsheets_find_replace.js +124 -0
  78. package/dist/tools/gsheets_format_cells.js +161 -0
  79. package/dist/tools/gsheets_freeze_columns.js +67 -0
  80. package/dist/tools/gsheets_freeze_rows.js +67 -0
  81. package/dist/tools/gsheets_insert_columns.js +69 -0
  82. package/dist/tools/gsheets_insert_rows.js +69 -0
  83. package/dist/tools/gsheets_list_sheets.js +53 -0
  84. package/dist/tools/gsheets_merge_cells.js +85 -0
  85. package/dist/tools/gsheets_read.js +120 -0
  86. package/dist/tools/gsheets_rename_sheet.js +64 -0
  87. package/dist/tools/gsheets_set_data_validation.js +153 -0
  88. package/dist/tools/gsheets_set_number_format.js +116 -0
  89. package/dist/tools/gsheets_sort_range.js +102 -0
  90. package/dist/tools/gsheets_unmerge_cells.js +79 -0
  91. package/dist/tools/gsheets_update_borders.js +212 -0
  92. package/dist/tools/gsheets_update_cell.js +44 -0
  93. package/dist/tools/gsheets_update_chart.js +118 -0
  94. package/dist/tools/gsheets_update_named_range.js +112 -0
  95. package/dist/tools/gsheets_update_protected_range.js +110 -0
  96. package/dist/tools/index.js +294 -0
  97. package/dist/tools/protection/gsheets_add_conditional_format.js +175 -0
  98. package/dist/tools/protection/gsheets_add_named_range.js +87 -0
  99. package/dist/tools/protection/gsheets_add_protected_range.js +127 -0
  100. package/dist/tools/protection/gsheets_delete_named_range.js +56 -0
  101. package/dist/tools/protection/gsheets_delete_protected_range.js +56 -0
  102. package/dist/tools/protection/gsheets_update_named_range.js +112 -0
  103. package/dist/tools/protection/gsheets_update_protected_range.js +110 -0
  104. package/dist/tools/sheets/advanced/gsheets_add_dimension_group.js +75 -0
  105. package/dist/tools/sheets/advanced/gsheets_add_filter_view.js +86 -0
  106. package/dist/tools/sheets/advanced/gsheets_add_pivot_table.js +188 -0
  107. package/dist/tools/sheets/advanced/gsheets_create_developer_metadata.js +121 -0
  108. package/dist/tools/sheets/advanced/gsheets_delete_developer_metadata.js +61 -0
  109. package/dist/tools/sheets/advanced/gsheets_delete_dimension_group.js +75 -0
  110. package/dist/tools/sheets/advanced/gsheets_update_developer_metadata.js +94 -0
  111. package/dist/tools/sheets/advanced/gsheets_update_dimension_group.js +83 -0
  112. package/dist/tools/sheets/basic/gsheets_add_sheet.js +65 -0
  113. package/dist/tools/sheets/basic/gsheets_copy_sheet.js +56 -0
  114. package/dist/tools/sheets/basic/gsheets_copy_to.js +113 -0
  115. package/dist/tools/sheets/basic/gsheets_create_spreadsheet.js +88 -0
  116. package/dist/tools/sheets/basic/gsheets_delete_columns.js +69 -0
  117. package/dist/tools/sheets/basic/gsheets_delete_rows.js +69 -0
  118. package/dist/tools/sheets/basic/gsheets_delete_sheet.js +56 -0
  119. package/dist/tools/sheets/basic/gsheets_duplicate_sheet.js +72 -0
  120. package/dist/tools/sheets/basic/gsheets_insert_columns.js +69 -0
  121. package/dist/tools/sheets/basic/gsheets_insert_rows.js +69 -0
  122. package/dist/tools/sheets/basic/gsheets_list_sheets.js +53 -0
  123. package/dist/tools/sheets/basic/gsheets_read.js +120 -0
  124. package/dist/tools/sheets/basic/gsheets_rename_sheet.js +64 -0
  125. package/dist/tools/sheets/charts/gsheets_add_bubble.js +176 -0
  126. package/dist/tools/sheets/charts/gsheets_add_candlestick.js +192 -0
  127. package/dist/tools/sheets/charts/gsheets_add_chart.js +162 -0
  128. package/dist/tools/sheets/charts/gsheets_add_combo.js +169 -0
  129. package/dist/tools/sheets/charts/gsheets_add_histogram.js +143 -0
  130. package/dist/tools/sheets/charts/gsheets_add_org_chart.js +160 -0
  131. package/dist/tools/sheets/charts/gsheets_add_treemap.js +177 -0
  132. package/dist/tools/sheets/charts/gsheets_add_waterfall.js +155 -0
  133. package/dist/tools/sheets/charts/gsheets_delete_chart.js +56 -0
  134. package/dist/tools/sheets/charts/gsheets_update_chart.js +118 -0
  135. package/dist/tools/sheets/data/gsheets_append_data.js +68 -0
  136. package/dist/tools/sheets/data/gsheets_batch_clear.js +53 -0
  137. package/dist/tools/sheets/data/gsheets_batch_update.js +81 -0
  138. package/dist/tools/sheets/data/gsheets_clear_data.js +53 -0
  139. package/dist/tools/sheets/data/gsheets_create_filter.js +81 -0
  140. package/dist/tools/sheets/data/gsheets_find_replace.js +124 -0
  141. package/dist/tools/sheets/data/gsheets_set_data_validation.js +153 -0
  142. package/dist/tools/sheets/data/gsheets_sort_range.js +102 -0
  143. package/dist/tools/sheets/data/gsheets_update_cell.js +44 -0
  144. package/dist/tools/sheets/formatting/gsheets_auto_resize.js +75 -0
  145. package/dist/tools/sheets/formatting/gsheets_format_cells.js +161 -0
  146. package/dist/tools/sheets/formatting/gsheets_freeze_columns.js +67 -0
  147. package/dist/tools/sheets/formatting/gsheets_freeze_rows.js +67 -0
  148. package/dist/tools/sheets/formatting/gsheets_merge_cells.js +85 -0
  149. package/dist/tools/sheets/formatting/gsheets_set_number_format.js +116 -0
  150. package/dist/tools/sheets/formatting/gsheets_unmerge_cells.js +79 -0
  151. package/dist/tools/sheets/formatting/gsheets_update_borders.js +212 -0
  152. package/dist/tools/sheets/protection/gsheets_add_conditional_format.js +175 -0
  153. package/dist/tools/sheets/protection/gsheets_add_named_range.js +87 -0
  154. package/dist/tools/sheets/protection/gsheets_add_protected_range.js +127 -0
  155. package/dist/tools/sheets/protection/gsheets_delete_named_range.js +56 -0
  156. package/dist/tools/sheets/protection/gsheets_delete_protected_range.js +56 -0
  157. package/dist/tools/sheets/protection/gsheets_update_named_range.js +112 -0
  158. package/dist/tools/sheets/protection/gsheets_update_protected_range.js +110 -0
  159. package/dist/tools/types.js +1 -0
  160. package/dist/vitest.config.js +18 -0
  161. package/package.json +57 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alanse inc.
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,173 @@
1
+ # MCP Server - Google Workspace
2
+
3
+ **The most comprehensive MCP server for Google Workspace** - Complete programmatic control over Sheets, Drive, and future Docs, Calendar, Forms integration.
4
+
5
+ 🚀 **Current Status**: 59 tools (Drive: 2, Sheets: 57)
6
+ 📅 **Roadmap**: Google Docs, Calendar, Forms, Gmail, Slides
7
+
8
+ This is an extended version of [@isaacphi/mcp-gdrive](https://github.com/isaacphi/mcp-gdrive), adding **53 additional Google Sheets API tools** for complete spreadsheet management and preparing for comprehensive Google Workspace coverage.
9
+
10
+ ## Credits
11
+
12
+ This project is based on:
13
+ - Original code by **Anthropic, PBC** from [MCP Servers](https://github.com/modelcontextprotocol/servers/tree/main/src/gdrive) (MIT License)
14
+ - Enhanced by **Phil Isaac** in [@isaacphi/mcp-gdrive](https://github.com/isaacphi/mcp-gdrive) (MIT License)
15
+ - **Extended implementation** by **Alanse株式会社 / Daichi Okazaki** (2026)
16
+
17
+ All contributions are licensed under the MIT License.
18
+
19
+ ## 🚀 Features
20
+
21
+ ### 📊 Extended Google Sheets API Implementation
22
+
23
+ **59 Total Tools** = 2 GDrive + **57 Google Sheets** operations
24
+
25
+ #### Tool Categories
26
+
27
+ **Google Drive Operations (2 tools)**
28
+ - Search & File Management
29
+
30
+ **Sheet Management (13 tools)**
31
+ - Create, Read, List, Add, Delete, Rename Sheets
32
+ - Insert/Delete Rows & Columns
33
+ - Copy, Duplicate, Copy Between Spreadsheets
34
+
35
+ **Data Operations (9 tools)**
36
+ - Cell Update, Batch Update, Append Data
37
+ - Clear Data, Batch Clear
38
+ - Sort, Find & Replace
39
+ - Data Validation, Filter Creation
40
+
41
+ **Formatting (8 tools)**
42
+ - Cell Formatting (Font, Color, Alignment)
43
+ - Merge/Unmerge Cells
44
+ - Borders, Auto-resize
45
+ - Number Formats, Freeze Rows/Columns
46
+
47
+ **Charts (10 tools)**
48
+ - Basic Charts (Column, Bar, Line, Area, Pie, Scatter)
49
+ - Advanced Charts (Histogram, Waterfall, Candlestick, Combo, Bubble, Treemap, Org Chart)
50
+ - Chart Update & Delete
51
+
52
+ **Protection & Named Ranges (7 tools)**
53
+ - Protected Ranges (Add/Update/Delete)
54
+ - Named Ranges (Add/Update/Delete)
55
+ - Conditional Formatting
56
+
57
+ **Advanced Features (8 tools)**
58
+ - Filter Views & Pivot Tables
59
+ - Dimension Groups (Row/Column Grouping)
60
+ - Developer Metadata (Custom Metadata Storage)
61
+
62
+ For complete tool documentation, see [docs/api-comparison.md](./docs/api-comparison.md)
63
+
64
+ ### Resources
65
+
66
+ The server provides access to Google Drive files:
67
+
68
+ - **Files** (`gdrive:///<file_id>`)
69
+ - Supports all file types
70
+ - Google Workspace files are automatically exported:
71
+ - Docs → Markdown
72
+ - Sheets → CSV
73
+ - Presentations → Plain text
74
+ - Drawings → PNG
75
+ - Other files are provided in their native format
76
+
77
+ ## Getting started
78
+
79
+ 1. [Create a new Google Cloud project](https://console.cloud.google.com/projectcreate)
80
+ 2. [Enable the Google Drive API](https://console.cloud.google.com/workspace-api/products)
81
+ 3. [Configure an OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent) ("internal" is fine for testing)
82
+ 4. Add OAuth scopes `https://www.googleapis.com/auth/drive.readonly`, `https://www.googleapis.com/auth/spreadsheets`
83
+ 5. In order to allow interaction with sheets and docs you will also need to enable the [Google Sheets API](https://console.cloud.google.com/apis/api/sheets.googleapis.com/) and [Google Docs API](https://console.cloud.google.com/marketplace/product/google/docs.googleapis.com) in your workspaces Enabled API and Services section.
84
+ 6. [Create an OAuth Client ID](https://console.cloud.google.com/apis/credentials/oauthclient) for application type "Desktop App"
85
+ 7. Download the JSON file of your client's OAuth keys
86
+ 8. Rename the key file to `gcp-oauth.keys.json` and place into the path you specify with `GDRIVE_CREDS_DIR` (i.e. `/Users/username/.config/mcp-gdrive`)
87
+ 9. Note your OAuth Client ID and Client Secret. They must be provided as environment variables along with your configuration directory.
88
+ 10. You will also need to setup a .env file within the project with the following fields. You can find the Client ID and Client Secret in the Credentials section of the Google Cloud Console.
89
+
90
+ ```
91
+ GDRIVE_CREDS_DIR=/path/to/config/directory
92
+ CLIENT_ID=<CLIENT_ID>
93
+ CLIENT_SECRET=<CLIENT_SECRET>
94
+ ```
95
+
96
+ Make sure to build the server with either `npm run build` or `npm run watch`.
97
+
98
+ ### Authentication
99
+
100
+ Next you will need to run `node ./dist/index.js` to trigger the authentication step
101
+
102
+ You will be prompted to authenticate with your browser. You must authenticate with an account in the same organization as your Google Cloud project.
103
+
104
+ Your OAuth token is saved in the directory specified by the `GDRIVE_CREDS_DIR` environment variable.
105
+
106
+ ![Authentication Prompt](https://i.imgur.com/TbyV6Yq.png)
107
+
108
+ ### Usage with Desktop App
109
+
110
+ To integrate this server with the desktop app, add the following to your app's server configuration:
111
+
112
+ ```json
113
+ {
114
+ "mcpServers": {
115
+ "google-workspace": {
116
+ "command": "npx",
117
+ "args": ["-y", "@alanse/mcp-server-google-workspace"],
118
+ "env": {
119
+ "CLIENT_ID": "<CLIENT_ID>",
120
+ "CLIENT_SECRET": "<CLIENT_SECRET>",
121
+ "GDRIVE_CREDS_DIR": "/path/to/config/directory"
122
+ }
123
+ }
124
+ }
125
+ }
126
+ ```
127
+
128
+ ## 📈 Project Status
129
+
130
+ ✅ **Phase 1-12 Complete** (2026-01-06)
131
+ - **59 Tools Implemented** (57 Sheets + 2 GDrive)
132
+ - **155 Tests** (100% Pass Rate)
133
+ - **Production Ready**
134
+
135
+ ## 🗺️ Roadmap
136
+
137
+ ### Current: Google Sheets & Drive ✅
138
+
139
+ - ✅ **Google Drive**: 2 tools (Search, Read)
140
+ - ✅ **Google Sheets**: 57 tools (Complete API coverage)
141
+
142
+ ### Coming Soon
143
+
144
+ - 📝 **Google Docs**: Document creation, editing, formatting (~15-20 tools)
145
+ - 📅 **Google Calendar**: Event management, scheduling (~10-15 tools)
146
+ - 📋 **Google Forms**: Form creation, response management (~10-15 tools)
147
+ - 📧 **Gmail**: Email operations, thread management (~10-15 tools)
148
+ - 🎬 **Google Slides**: Presentation creation, editing (~15-20 tools)
149
+
150
+ **Goal**: 100+ tools covering the entire Google Workspace ecosystem
151
+
152
+ ## 📄 License
153
+
154
+ This project is licensed under the **MIT License**.
155
+
156
+ ### Copyright Notices
157
+
158
+ - Original code: © 2024 Anthropic, PBC
159
+ - Base implementation: © 2024 Phil Isaac
160
+ - Extended implementation: © 2026 Alanse株式会社 / Daichi Okazaki
161
+
162
+ You are free to use, modify, and distribute this software under the terms of the MIT License. See the [LICENSE](./LICENSE) file for details.
163
+
164
+ ## 🙏 Acknowledgments
165
+
166
+ Special thanks to:
167
+ - **Anthropic, PBC** for the original MCP server implementation
168
+ - **Phil Isaac** for the enhanced Google Drive & Sheets integration
169
+ - The **Model Context Protocol** team for the excellent framework
170
+
171
+ ## 📚 Related Documentation
172
+
173
+ - [docs/api-comparison.md](./docs/api-comparison.md) - MCP vs Google Sheets API comparison
package/dist/auth.js ADDED
@@ -0,0 +1,135 @@
1
+ import { authenticate } from "@google-cloud/local-auth";
2
+ import { google } from "googleapis";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ export const SCOPES = [
6
+ "https://www.googleapis.com/auth/drive.readonly",
7
+ "https://www.googleapis.com/auth/spreadsheets",
8
+ ];
9
+ // Get credentials directory from environment variable or use default
10
+ const CREDS_DIR = process.env.GDRIVE_CREDS_DIR ||
11
+ path.join(path.dirname(new URL(import.meta.url).pathname), "../../../");
12
+ // Ensure the credentials directory exists
13
+ function ensureCredsDirectory() {
14
+ try {
15
+ fs.mkdirSync(CREDS_DIR, { recursive: true });
16
+ console.error(`Ensured credentials directory exists at: ${CREDS_DIR}`);
17
+ }
18
+ catch (error) {
19
+ console.error(`Failed to create credentials directory: ${CREDS_DIR}`, error);
20
+ throw error;
21
+ }
22
+ }
23
+ const credentialsPath = path.join(CREDS_DIR, ".gdrive-server-credentials.json");
24
+ async function authenticateWithTimeout(keyfilePath, SCOPES, timeoutMs = 30000) {
25
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Authentication timed out")), timeoutMs));
26
+ const authPromise = authenticate({
27
+ keyfilePath,
28
+ scopes: SCOPES,
29
+ });
30
+ try {
31
+ return await Promise.race([authPromise, timeoutPromise]);
32
+ }
33
+ catch (error) {
34
+ console.error(error);
35
+ return null;
36
+ }
37
+ }
38
+ async function authenticateAndSaveCredentials() {
39
+ console.error("Launching auth flow…");
40
+ console.error("Using credentials path:", credentialsPath);
41
+ const keyfilePath = path.join(CREDS_DIR, "gcp-oauth.keys.json");
42
+ console.error("Using keyfile path:", keyfilePath);
43
+ const auth = await authenticateWithTimeout(keyfilePath, SCOPES);
44
+ if (auth) {
45
+ const newAuth = new google.auth.OAuth2();
46
+ newAuth.setCredentials(auth.credentials);
47
+ }
48
+ try {
49
+ const { credentials } = await auth.refreshAccessToken();
50
+ console.error("Received new credentials with scopes:", credentials.scope);
51
+ // Ensure directory exists before saving
52
+ ensureCredsDirectory();
53
+ fs.writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2));
54
+ console.error("Credentials saved successfully with refresh token to:", credentialsPath);
55
+ auth.setCredentials(credentials);
56
+ return auth;
57
+ }
58
+ catch (error) {
59
+ console.error("Error refreshing token during initial auth:", error);
60
+ return auth;
61
+ }
62
+ }
63
+ // Try to load credentials without prompting for auth
64
+ export async function loadCredentialsQuietly() {
65
+ console.error("Attempting to load credentials from:", credentialsPath);
66
+ const oauth2Client = new google.auth.OAuth2(process.env.CLIENT_ID, process.env.CLIENT_SECRET);
67
+ if (!fs.existsSync(credentialsPath)) {
68
+ console.error("No credentials file found");
69
+ return null;
70
+ }
71
+ try {
72
+ const savedCreds = JSON.parse(fs.readFileSync(credentialsPath, "utf-8"));
73
+ console.error("Loaded existing credentials with scopes:", savedCreds.scope);
74
+ oauth2Client.setCredentials(savedCreds);
75
+ const expiryDate = new Date(savedCreds.expiry_date);
76
+ const now = new Date();
77
+ const fiveMinutes = 5 * 60 * 1000;
78
+ const timeToExpiry = expiryDate.getTime() - now.getTime();
79
+ console.error("Token expiry status:", {
80
+ expiryDate: expiryDate.toISOString(),
81
+ timeToExpiryMinutes: Math.floor(timeToExpiry / (60 * 1000)),
82
+ hasRefreshToken: !!savedCreds.refresh_token,
83
+ });
84
+ if (timeToExpiry < fiveMinutes && savedCreds.refresh_token) {
85
+ console.error("Attempting to refresh token using refresh_token");
86
+ try {
87
+ const response = await oauth2Client.refreshAccessToken();
88
+ const newCreds = response.credentials;
89
+ ensureCredsDirectory();
90
+ fs.writeFileSync(credentialsPath, JSON.stringify(newCreds, null, 2));
91
+ oauth2Client.setCredentials(newCreds);
92
+ console.error("Token refreshed and saved successfully");
93
+ }
94
+ catch (error) {
95
+ console.error("Failed to refresh token:", error);
96
+ return null;
97
+ }
98
+ }
99
+ return oauth2Client;
100
+ }
101
+ catch (error) {
102
+ console.error("Error loading credentials:", error);
103
+ return null;
104
+ }
105
+ }
106
+ // Get valid credentials, prompting for auth if necessary
107
+ export async function getValidCredentials(forceAuth = false) {
108
+ if (!forceAuth) {
109
+ const quietAuth = await loadCredentialsQuietly();
110
+ if (quietAuth) {
111
+ return quietAuth;
112
+ }
113
+ }
114
+ return await authenticateAndSaveCredentials();
115
+ }
116
+ // Background refresh that never prompts for auth
117
+ export function setupTokenRefresh() {
118
+ console.error("Setting up automatic token refresh interval (45 minutes)");
119
+ return setInterval(async () => {
120
+ try {
121
+ console.error("Running scheduled token refresh check");
122
+ const auth = await loadCredentialsQuietly();
123
+ if (auth) {
124
+ google.options({ auth });
125
+ console.error("Completed scheduled token refresh");
126
+ }
127
+ else {
128
+ console.error("Skipping token refresh - no valid credentials");
129
+ }
130
+ }
131
+ catch (error) {
132
+ console.error("Error in automatic token refresh:", error);
133
+ }
134
+ }, 45 * 60 * 1000);
135
+ }
package/dist/index.js ADDED
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
6
+ import { google } from "googleapis";
7
+ import { getValidCredentials, setupTokenRefresh, loadCredentialsQuietly, } from "./auth.js";
8
+ import { tools } from "./tools/index.js";
9
+ const drive = google.drive("v3");
10
+ const server = new Server({
11
+ name: "example-servers/gdrive",
12
+ version: "0.1.0",
13
+ }, {
14
+ capabilities: {
15
+ resources: {
16
+ schemes: ["gdrive"], // Declare that we handle gdrive:/// URIs
17
+ listable: true, // Support listing available resources
18
+ readable: true, // Support reading resource contents
19
+ },
20
+ tools: {},
21
+ },
22
+ });
23
+ // Ensure we have valid credentials before making API calls
24
+ async function ensureAuth() {
25
+ const auth = await getValidCredentials();
26
+ google.options({ auth });
27
+ return auth;
28
+ }
29
+ async function ensureAuthQuietly() {
30
+ const auth = await loadCredentialsQuietly();
31
+ if (auth) {
32
+ google.options({ auth });
33
+ }
34
+ return auth;
35
+ }
36
+ server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
37
+ await ensureAuthQuietly();
38
+ const pageSize = 10;
39
+ const params = {
40
+ pageSize,
41
+ fields: "nextPageToken, files(id, name, mimeType)",
42
+ };
43
+ if (request.params?.cursor) {
44
+ params.pageToken = request.params.cursor;
45
+ }
46
+ const res = await drive.files.list(params);
47
+ const files = res.data.files;
48
+ return {
49
+ resources: files.map((file) => ({
50
+ uri: `gdrive:///${file.id}`,
51
+ mimeType: file.mimeType,
52
+ name: file.name,
53
+ })),
54
+ nextCursor: res.data.nextPageToken,
55
+ };
56
+ });
57
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
58
+ await ensureAuthQuietly();
59
+ const fileId = request.params.uri.replace("gdrive:///", "");
60
+ const readFileTool = tools[1]; // gdrive_read_file is the second tool
61
+ const result = await readFileTool.handler({ fileId });
62
+ // Extract the file contents from the tool response
63
+ const fileContents = result.content[0].text.split("\n\n")[1]; // Skip the "Contents of file:" prefix
64
+ return {
65
+ contents: [
66
+ {
67
+ uri: request.params.uri,
68
+ mimeType: "text/plain", // You might want to determine this dynamically
69
+ text: fileContents,
70
+ },
71
+ ],
72
+ };
73
+ });
74
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
75
+ return {
76
+ tools: tools.map(({ name, description, inputSchema }) => ({
77
+ name,
78
+ description,
79
+ inputSchema,
80
+ })),
81
+ };
82
+ });
83
+ // Helper function to convert internal tool response to SDK format
84
+ function convertToolResponse(response) {
85
+ return {
86
+ _meta: {},
87
+ content: response.content,
88
+ isError: response.isError,
89
+ };
90
+ }
91
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
92
+ await ensureAuth();
93
+ const tool = tools.find((t) => t.name === request.params.name);
94
+ if (!tool) {
95
+ throw new Error("Tool not found");
96
+ }
97
+ const result = await tool.handler(request.params.arguments);
98
+ return convertToolResponse(result);
99
+ });
100
+ async function startServer() {
101
+ try {
102
+ console.error("Starting server");
103
+ // Add this line to force authentication at startup
104
+ await ensureAuth(); // This will trigger the auth flow if no valid credentials exist
105
+ const transport = new StdioServerTransport();
106
+ await server.connect(transport);
107
+ // Set up periodic token refresh that never prompts for auth
108
+ setupTokenRefresh();
109
+ }
110
+ catch (error) {
111
+ console.error("Error starting server:", error);
112
+ process.exit(1);
113
+ }
114
+ }
115
+ // Start server immediately
116
+ startServer().catch(console.error);
@@ -0,0 +1,65 @@
1
+ import { google } from "googleapis";
2
+ export const schema = {
3
+ name: "gsheets_add_sheet",
4
+ description: "Add a new sheet (tab) to a Google Spreadsheet",
5
+ inputSchema: {
6
+ type: "object",
7
+ properties: {
8
+ spreadsheetId: {
9
+ type: "string",
10
+ description: "The ID of the spreadsheet",
11
+ },
12
+ title: {
13
+ type: "string",
14
+ description: "The name of the new sheet",
15
+ },
16
+ index: {
17
+ type: "number",
18
+ description: "Optional position index (0-based) where the sheet should be inserted",
19
+ },
20
+ },
21
+ required: ["spreadsheetId", "title"],
22
+ },
23
+ };
24
+ export async function addSheet(args) {
25
+ try {
26
+ const sheets = google.sheets("v4");
27
+ const request = {
28
+ addSheet: {
29
+ properties: {
30
+ title: args.title,
31
+ },
32
+ },
33
+ };
34
+ if (args.index !== undefined) {
35
+ request.addSheet.properties.index = args.index;
36
+ }
37
+ const response = await sheets.spreadsheets.batchUpdate({
38
+ spreadsheetId: args.spreadsheetId,
39
+ requestBody: {
40
+ requests: [request],
41
+ },
42
+ });
43
+ const addedSheet = response.data.replies?.[0]?.addSheet?.properties;
44
+ return {
45
+ content: [
46
+ {
47
+ type: "text",
48
+ text: `Successfully added sheet "${addedSheet?.title}" (ID: ${addedSheet?.sheetId}, Index: ${addedSheet?.index})`,
49
+ },
50
+ ],
51
+ isError: false,
52
+ };
53
+ }
54
+ catch (error) {
55
+ return {
56
+ content: [
57
+ {
58
+ type: "text",
59
+ text: `Error adding sheet: ${error.message}`,
60
+ },
61
+ ],
62
+ isError: true,
63
+ };
64
+ }
65
+ }
@@ -0,0 +1,56 @@
1
+ import { google } from "googleapis";
2
+ export const schema = {
3
+ name: "gsheets_copy_sheet",
4
+ description: "Copy a sheet to another spreadsheet or within the same spreadsheet",
5
+ inputSchema: {
6
+ type: "object",
7
+ properties: {
8
+ sourceSpreadsheetId: {
9
+ type: "string",
10
+ description: "The ID of the source spreadsheet",
11
+ },
12
+ sourceSheetId: {
13
+ type: "number",
14
+ description: "The ID of the sheet to copy",
15
+ },
16
+ destinationSpreadsheetId: {
17
+ type: "string",
18
+ description: "The ID of the destination spreadsheet",
19
+ },
20
+ },
21
+ required: ["sourceSpreadsheetId", "sourceSheetId", "destinationSpreadsheetId"],
22
+ },
23
+ };
24
+ export async function copySheet(args) {
25
+ const sheets = google.sheets("v4");
26
+ try {
27
+ const response = await sheets.spreadsheets.sheets.copyTo({
28
+ spreadsheetId: args.sourceSpreadsheetId,
29
+ sheetId: args.sourceSheetId,
30
+ requestBody: {
31
+ destinationSpreadsheetId: args.destinationSpreadsheetId,
32
+ },
33
+ });
34
+ const copiedSheet = response.data;
35
+ return {
36
+ content: [
37
+ {
38
+ type: "text",
39
+ text: `Successfully copied sheet to destination. New sheet ID: ${copiedSheet.sheetId}, Title: "${copiedSheet.title}"`,
40
+ },
41
+ ],
42
+ isError: false,
43
+ };
44
+ }
45
+ catch (error) {
46
+ return {
47
+ content: [
48
+ {
49
+ type: "text",
50
+ text: `Error copying sheet: ${error.message}`,
51
+ },
52
+ ],
53
+ isError: true,
54
+ };
55
+ }
56
+ }
@@ -0,0 +1,113 @@
1
+ import { google } from "googleapis";
2
+ export const schema = {
3
+ name: "gsheets_copy_to",
4
+ description: "Copy data from one range to another range using cut/paste",
5
+ inputSchema: {
6
+ type: "object",
7
+ properties: {
8
+ spreadsheetId: {
9
+ type: "string",
10
+ description: "The ID of the spreadsheet",
11
+ },
12
+ sourceSheetId: {
13
+ type: "number",
14
+ description: "The ID of the source sheet",
15
+ },
16
+ sourceStartRow: {
17
+ type: "number",
18
+ description: "Source starting row index (0-based)",
19
+ },
20
+ sourceStartColumn: {
21
+ type: "number",
22
+ description: "Source starting column index (0-based)",
23
+ },
24
+ sourceEndRow: {
25
+ type: "number",
26
+ description: "Source ending row index (exclusive, 0-based)",
27
+ },
28
+ sourceEndColumn: {
29
+ type: "number",
30
+ description: "Source ending column index (exclusive, 0-based)",
31
+ },
32
+ destinationSheetId: {
33
+ type: "number",
34
+ description: "The ID of the destination sheet",
35
+ },
36
+ destinationStartRow: {
37
+ type: "number",
38
+ description: "Destination starting row index (0-based)",
39
+ },
40
+ destinationStartColumn: {
41
+ type: "number",
42
+ description: "Destination starting column index (0-based)",
43
+ },
44
+ pasteType: {
45
+ type: "string",
46
+ enum: ["NORMAL", "VALUES", "FORMAT", "FORMULA"],
47
+ description: "What to paste (default: NORMAL - all data)",
48
+ },
49
+ },
50
+ required: [
51
+ "spreadsheetId",
52
+ "sourceSheetId",
53
+ "sourceStartRow",
54
+ "sourceStartColumn",
55
+ "sourceEndRow",
56
+ "sourceEndColumn",
57
+ "destinationSheetId",
58
+ "destinationStartRow",
59
+ "destinationStartColumn",
60
+ ],
61
+ },
62
+ };
63
+ export async function copyTo(args) {
64
+ const sheets = google.sheets("v4");
65
+ try {
66
+ await sheets.spreadsheets.batchUpdate({
67
+ spreadsheetId: args.spreadsheetId,
68
+ requestBody: {
69
+ requests: [
70
+ {
71
+ copyPaste: {
72
+ source: {
73
+ sheetId: args.sourceSheetId,
74
+ startRowIndex: args.sourceStartRow,
75
+ endRowIndex: args.sourceEndRow,
76
+ startColumnIndex: args.sourceStartColumn,
77
+ endColumnIndex: args.sourceEndColumn,
78
+ },
79
+ destination: {
80
+ sheetId: args.destinationSheetId,
81
+ startRowIndex: args.destinationStartRow,
82
+ startColumnIndex: args.destinationStartColumn,
83
+ },
84
+ pasteType: args.pasteType || "NORMAL",
85
+ },
86
+ },
87
+ ],
88
+ },
89
+ });
90
+ const sourceRange = `R${args.sourceStartRow}C${args.sourceStartColumn}:R${args.sourceEndRow - 1}C${args.sourceEndColumn - 1}`;
91
+ const destRange = `R${args.destinationStartRow}C${args.destinationStartColumn}`;
92
+ return {
93
+ content: [
94
+ {
95
+ type: "text",
96
+ text: `Successfully copied data from sheet ${args.sourceSheetId} (${sourceRange}) to sheet ${args.destinationSheetId} (${destRange})`,
97
+ },
98
+ ],
99
+ isError: false,
100
+ };
101
+ }
102
+ catch (error) {
103
+ return {
104
+ content: [
105
+ {
106
+ type: "text",
107
+ text: `Error copying data: ${error.message}`,
108
+ },
109
+ ],
110
+ isError: true,
111
+ };
112
+ }
113
+ }