@triffon/google-docs-mcp 1.11.3-triffon

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 (165) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +567 -0
  3. package/dist/auth.js +327 -0
  4. package/dist/cachedToolsList.js +38 -0
  5. package/dist/clients.js +180 -0
  6. package/dist/config.js +9 -0
  7. package/dist/downloadProxy.js +82 -0
  8. package/dist/driveQueryUtils.js +7 -0
  9. package/dist/firestoreTokenStorage.js +32 -0
  10. package/dist/googleDocsApiHelpers.js +1257 -0
  11. package/dist/googleSheetsApiHelpers.js +849 -0
  12. package/dist/index.js +240 -0
  13. package/dist/landingPage.js +103 -0
  14. package/dist/logger.js +58 -0
  15. package/dist/markdown-transformer/docsToMarkdown.js +259 -0
  16. package/dist/markdown-transformer/index.js +128 -0
  17. package/dist/markdown-transformer/markdownToDocs.js +834 -0
  18. package/dist/remoteWrapper.js +72 -0
  19. package/dist/tools/calendar/createEvent.js +89 -0
  20. package/dist/tools/calendar/deleteEvent.js +49 -0
  21. package/dist/tools/calendar/helpers.js +19 -0
  22. package/dist/tools/calendar/index.js +12 -0
  23. package/dist/tools/calendar/listEvents.js +84 -0
  24. package/dist/tools/calendar/quickAddEvent.js +55 -0
  25. package/dist/tools/calendar/updateEvent.js +84 -0
  26. package/dist/tools/docs/addTab.js +84 -0
  27. package/dist/tools/docs/appendTableRows.js +89 -0
  28. package/dist/tools/docs/appendToGoogleDoc.js +85 -0
  29. package/dist/tools/docs/cloneTable.js +159 -0
  30. package/dist/tools/docs/comments/addComment.js +83 -0
  31. package/dist/tools/docs/comments/deleteComment.js +30 -0
  32. package/dist/tools/docs/comments/getComment.js +45 -0
  33. package/dist/tools/docs/comments/index.js +14 -0
  34. package/dist/tools/docs/comments/listComments.js +43 -0
  35. package/dist/tools/docs/comments/replyToComment.js +35 -0
  36. package/dist/tools/docs/comments/resolveComment.js +55 -0
  37. package/dist/tools/docs/deleteRange.js +61 -0
  38. package/dist/tools/docs/deleteTableRows.js +62 -0
  39. package/dist/tools/docs/findAndReplace.js +54 -0
  40. package/dist/tools/docs/findElement.js +43 -0
  41. package/dist/tools/docs/findSectionsByHeading.js +46 -0
  42. package/dist/tools/docs/formatting/applyParagraphStyle.js +83 -0
  43. package/dist/tools/docs/formatting/applyTextStyle.js +49 -0
  44. package/dist/tools/docs/formatting/batchApplyTextStyle.js +86 -0
  45. package/dist/tools/docs/formatting/index.js +16 -0
  46. package/dist/tools/docs/formatting/updateTableBorders.js +81 -0
  47. package/dist/tools/docs/formatting/updateTableCellStyle.js +85 -0
  48. package/dist/tools/docs/formatting/updateTableColumnWidth.js +51 -0
  49. package/dist/tools/docs/formatting/updateTableRowStyle.js +74 -0
  50. package/dist/tools/docs/getTableStructure.js +48 -0
  51. package/dist/tools/docs/index.js +66 -0
  52. package/dist/tools/docs/insertDateChip.js +82 -0
  53. package/dist/tools/docs/insertImage.js +112 -0
  54. package/dist/tools/docs/insertPageBreak.js +47 -0
  55. package/dist/tools/docs/insertPerson.js +53 -0
  56. package/dist/tools/docs/insertRichLink.js +58 -0
  57. package/dist/tools/docs/insertSectionBreak.js +60 -0
  58. package/dist/tools/docs/insertTable.js +42 -0
  59. package/dist/tools/docs/insertTableWithData.js +125 -0
  60. package/dist/tools/docs/insertText.js +49 -0
  61. package/dist/tools/docs/listDocumentTables.js +47 -0
  62. package/dist/tools/docs/listDocumentTabs.js +59 -0
  63. package/dist/tools/docs/listSmartChips.js +41 -0
  64. package/dist/tools/docs/modifyText.js +147 -0
  65. package/dist/tools/docs/readGoogleDoc.js +164 -0
  66. package/dist/tools/docs/renameTab.js +47 -0
  67. package/dist/tools/docs/replaceTableRowData.js +55 -0
  68. package/dist/tools/docs/smartChipHelpers.js +71 -0
  69. package/dist/tools/docs/structureHelpers.js +250 -0
  70. package/dist/tools/docs/tabFieldMasks.js +48 -0
  71. package/dist/tools/docs/tableRowDataHelpers.js +53 -0
  72. package/dist/tools/docs/updateSectionStyle.js +148 -0
  73. package/dist/tools/drive/copyFile.js +63 -0
  74. package/dist/tools/drive/createDocument.js +105 -0
  75. package/dist/tools/drive/createFolder.js +48 -0
  76. package/dist/tools/drive/createFromTemplate.js +82 -0
  77. package/dist/tools/drive/deleteFile.js +72 -0
  78. package/dist/tools/drive/downloadFile.js +266 -0
  79. package/dist/tools/drive/getDocumentInfo.js +48 -0
  80. package/dist/tools/drive/getFolderInfo.js +48 -0
  81. package/dist/tools/drive/index.js +34 -0
  82. package/dist/tools/drive/listDriveFiles.js +129 -0
  83. package/dist/tools/drive/listFolderContents.js +83 -0
  84. package/dist/tools/drive/listGoogleDocs.js +70 -0
  85. package/dist/tools/drive/moveFile.js +54 -0
  86. package/dist/tools/drive/renameFile.js +39 -0
  87. package/dist/tools/drive/savePathGuard.js +86 -0
  88. package/dist/tools/drive/searchDriveFiles.js +148 -0
  89. package/dist/tools/drive/searchGoogleDocs.js +77 -0
  90. package/dist/tools/drive/setFilePermission.js +68 -0
  91. package/dist/tools/gmail/createDraft.js +58 -0
  92. package/dist/tools/gmail/deleteDraft.js +37 -0
  93. package/dist/tools/gmail/getDraft.js +52 -0
  94. package/dist/tools/gmail/getMessage.js +92 -0
  95. package/dist/tools/gmail/helpers.js +113 -0
  96. package/dist/tools/gmail/index.js +28 -0
  97. package/dist/tools/gmail/listDrafts.js +74 -0
  98. package/dist/tools/gmail/listLabels.js +31 -0
  99. package/dist/tools/gmail/listMessages.js +87 -0
  100. package/dist/tools/gmail/modifyMessageLabels.js +54 -0
  101. package/dist/tools/gmail/sendDraft.js +42 -0
  102. package/dist/tools/gmail/sendEmail.js +56 -0
  103. package/dist/tools/gmail/trashMessage.js +37 -0
  104. package/dist/tools/gmail/triageInbox.js +147 -0
  105. package/dist/tools/gmail/updateDraft.js +60 -0
  106. package/dist/tools/index.js +64 -0
  107. package/dist/tools/script/appsScriptShared.js +96 -0
  108. package/dist/tools/script/createAppsScriptProject.js +74 -0
  109. package/dist/tools/script/getAppsScriptContent.js +56 -0
  110. package/dist/tools/script/index.js +14 -0
  111. package/dist/tools/script/updateAppsScriptContent.js +56 -0
  112. package/dist/tools/sheets/addConditionalFormatting.js +143 -0
  113. package/dist/tools/sheets/addSpreadsheetSheet.js +34 -0
  114. package/dist/tools/sheets/appendSpreadsheetRows.js +44 -0
  115. package/dist/tools/sheets/appendTableRows.js +51 -0
  116. package/dist/tools/sheets/autoResizeColumns.js +67 -0
  117. package/dist/tools/sheets/autoResizeRows.js +63 -0
  118. package/dist/tools/sheets/batchWrite.js +61 -0
  119. package/dist/tools/sheets/clearSpreadsheetRange.js +31 -0
  120. package/dist/tools/sheets/comments/commentAnchor.js +95 -0
  121. package/dist/tools/sheets/comments/createSheetsCellNote.js +33 -0
  122. package/dist/tools/sheets/comments/createSheetsComment.js +168 -0
  123. package/dist/tools/sheets/comments/deleteSheetsComment.js +32 -0
  124. package/dist/tools/sheets/comments/getSheetsComment.js +51 -0
  125. package/dist/tools/sheets/comments/index.js +16 -0
  126. package/dist/tools/sheets/comments/listSheetsComments.js +177 -0
  127. package/dist/tools/sheets/comments/replyToSheetsComment.js +37 -0
  128. package/dist/tools/sheets/comments/resolveSheetsComment.js +53 -0
  129. package/dist/tools/sheets/copyFormatting.js +59 -0
  130. package/dist/tools/sheets/copySheetTo.js +36 -0
  131. package/dist/tools/sheets/createSpreadsheet.js +72 -0
  132. package/dist/tools/sheets/createTable.js +120 -0
  133. package/dist/tools/sheets/deleteChart.js +41 -0
  134. package/dist/tools/sheets/deleteConditionalFormatting.js +46 -0
  135. package/dist/tools/sheets/deleteSheet.js +43 -0
  136. package/dist/tools/sheets/deleteTable.js +56 -0
  137. package/dist/tools/sheets/duplicateSheet.js +53 -0
  138. package/dist/tools/sheets/formatCells.js +122 -0
  139. package/dist/tools/sheets/freezeRowsAndColumns.js +58 -0
  140. package/dist/tools/sheets/getConditionalFormatting.js +98 -0
  141. package/dist/tools/sheets/getSpreadsheetInfo.js +44 -0
  142. package/dist/tools/sheets/getTable.js +48 -0
  143. package/dist/tools/sheets/groupRows.js +62 -0
  144. package/dist/tools/sheets/index.js +84 -0
  145. package/dist/tools/sheets/insertChart.js +225 -0
  146. package/dist/tools/sheets/listGoogleSheets.js +66 -0
  147. package/dist/tools/sheets/listTables.js +55 -0
  148. package/dist/tools/sheets/protectRange.js +59 -0
  149. package/dist/tools/sheets/readCellFormat.js +143 -0
  150. package/dist/tools/sheets/readSpreadsheet.js +36 -0
  151. package/dist/tools/sheets/renameSheet.js +48 -0
  152. package/dist/tools/sheets/setCellBorders.js +94 -0
  153. package/dist/tools/sheets/setColumnWidths.js +43 -0
  154. package/dist/tools/sheets/setDropdownValidation.js +51 -0
  155. package/dist/tools/sheets/setRowHeights.js +63 -0
  156. package/dist/tools/sheets/ungroupAllRows.js +66 -0
  157. package/dist/tools/sheets/updateTableRange.js +51 -0
  158. package/dist/tools/sheets/writeSpreadsheet.js +45 -0
  159. package/dist/tools/utils/appendMarkdownToGoogleDoc.js +95 -0
  160. package/dist/tools/utils/index.js +8 -0
  161. package/dist/tools/utils/replaceDocumentWithMarkdown.js +161 -0
  162. package/dist/tools/utils/replaceRangeWithMarkdown.js +72 -0
  163. package/dist/types.js +208 -0
  164. package/dist/upstreamAuth.js +62 -0
  165. package/package.json +51 -0
package/dist/index.js ADDED
@@ -0,0 +1,240 @@
1
+ #!/usr/bin/env node
2
+ // src/index.ts
3
+ //
4
+ // Single entry point for the Google Docs MCP Server.
5
+ //
6
+ // Usage:
7
+ // @a-bonus/google-docs-mcp Start the MCP server (default)
8
+ // @a-bonus/google-docs-mcp auth Run the interactive OAuth flow
9
+ //
10
+ // Remote mode (env vars):
11
+ // MCP_TRANSPORT=httpStream Use Streamable HTTP instead of stdio
12
+ // BASE_URL=https://... Public URL for OAuth redirects
13
+ // ALLOWED_DOMAINS=scio.cz,... Restrict to specific Google Workspace domains
14
+ import { FastMCP } from 'fastmcp';
15
+ import { OAuthProxy } from 'fastmcp/auth';
16
+ import { buildCachedToolsListPayload, collectToolsWhileRegistering, installCachedToolsListHandler, } from './cachedToolsList.js';
17
+ import { initializeGoogleClient } from './clients.js';
18
+ import { parseEnabledToolGroups, registerAllTools } from './tools/index.js';
19
+ import { wrapServerForRemote } from './remoteWrapper.js';
20
+ import { registerLandingPage } from './landingPage.js';
21
+ import { registerDownloadRoute } from './downloadProxy.js';
22
+ import { FirestoreTokenStorage } from './firestoreTokenStorage.js';
23
+ import { parseStatelessFlag } from './config.js';
24
+ import { enableUpstreamOfflineAccess } from './upstreamAuth.js';
25
+ import { logger } from './logger.js';
26
+ // --- Auth subcommand ---
27
+ if (process.argv[2] === 'auth') {
28
+ const { runAuthFlow } = await import('./auth.js');
29
+ try {
30
+ await runAuthFlow();
31
+ logger.info('Authorization complete. You can now start the MCP server.');
32
+ process.exit(0);
33
+ }
34
+ catch (error) {
35
+ logger.error('Authorization failed:', error.message || error);
36
+ process.exit(1);
37
+ }
38
+ }
39
+ // --- Server startup ---
40
+ process.on('uncaughtException', (error) => {
41
+ logger.error('Uncaught Exception:', error);
42
+ if (error.code === 'EPIPE' || error.code === 'ERR_STREAM_DESTROYED') {
43
+ process.exit(1);
44
+ }
45
+ });
46
+ process.on('unhandledRejection', (reason, _promise) => {
47
+ logger.error('Unhandled Promise Rejection:', reason);
48
+ });
49
+ process.stdin.on('end', () => {
50
+ logger.info('stdin closed — MCP host disconnected. Shutting down.');
51
+ process.exit(0);
52
+ });
53
+ process.stdin.on('error', () => {
54
+ process.exit(0);
55
+ });
56
+ // Graceful shutdown on termination signals.
57
+ // Without these handlers, a host that sends SIGTERM (rather than closing
58
+ // stdin) leaves the server running and accumulating in the background.
59
+ const cleanShutdown = (signal) => {
60
+ logger.info(`Received ${signal} — shutting down.`);
61
+ process.exit(0);
62
+ };
63
+ process.on('SIGTERM', () => cleanShutdown('SIGTERM'));
64
+ process.on('SIGINT', () => cleanShutdown('SIGINT'));
65
+ process.on('SIGHUP', () => cleanShutdown('SIGHUP'));
66
+ // Orphan-process watchdog (stdio mode only).
67
+ // In practice, some MCP clients exit without sending SIGTERM, and the
68
+ // stdin 'end' event can be swallowed by the transport's internal read
69
+ // loop — leaving the server running as a zombie that consumes CPU and
70
+ // memory indefinitely. As a backstop, detect reparenting to init
71
+ // (PID 1) and exit. The check runs every 10 s and is unref()'d so it
72
+ // does not keep the event loop alive on its own.
73
+ if (process.env.MCP_TRANSPORT !== 'httpStream') {
74
+ const initialPpid = process.ppid;
75
+ const watchdog = setInterval(() => {
76
+ if (process.ppid !== initialPpid && process.ppid === 1) {
77
+ logger.info(`Parent process (was PID ${initialPpid}) exited; reparented to init. Shutting down.`);
78
+ process.exit(0);
79
+ }
80
+ }, 10_000);
81
+ watchdog.unref();
82
+ }
83
+ const isRemote = process.env.MCP_TRANSPORT === 'httpStream';
84
+ if (isRemote) {
85
+ const missing = ['BASE_URL', 'GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET'].filter((k) => !process.env[k]);
86
+ if (missing.length > 0) {
87
+ logger.error(`FATAL: Missing required env vars for httpStream mode: ${missing.join(', ')}`);
88
+ process.exit(1);
89
+ }
90
+ }
91
+ const GOOGLE_API_SCOPES = [
92
+ 'openid',
93
+ 'email',
94
+ 'https://www.googleapis.com/auth/documents',
95
+ 'https://www.googleapis.com/auth/spreadsheets',
96
+ 'https://www.googleapis.com/auth/drive',
97
+ 'https://www.googleapis.com/auth/script.external_request',
98
+ 'https://www.googleapis.com/auth/gmail.modify',
99
+ 'https://www.googleapis.com/auth/calendar.events',
100
+ ];
101
+ const oauthProxy = isRemote
102
+ ? new OAuthProxy({
103
+ upstreamAuthorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
104
+ upstreamTokenEndpoint: 'https://oauth2.googleapis.com/token',
105
+ upstreamClientId: process.env.GOOGLE_CLIENT_ID,
106
+ upstreamClientSecret: process.env.GOOGLE_CLIENT_SECRET,
107
+ baseUrl: process.env.BASE_URL,
108
+ scopes: GOOGLE_API_SCOPES,
109
+ allowedRedirectUriPatterns: [
110
+ 'http://localhost:*',
111
+ `${process.env.BASE_URL}/*`,
112
+ 'cursor://*',
113
+ 'https://claude.ai/*',
114
+ 'https://claude.com/*',
115
+ 'claude://*',
116
+ ],
117
+ jwtSigningKey: process.env.JWT_SIGNING_KEY,
118
+ encryptionKey: process.env.TOKEN_ENCRYPTION_KEY,
119
+ consentRequired: false,
120
+ accessTokenTtl: 2592000,
121
+ refreshTokenTtl: 2592000,
122
+ ...(process.env.TOKEN_STORE === 'firestore' && {
123
+ tokenStorage: new FirestoreTokenStorage(process.env.GCLOUD_PROJECT),
124
+ }),
125
+ })
126
+ : undefined;
127
+ if (oauthProxy) {
128
+ // issueSwappedTokens() inlines the TTL logic: it checks
129
+ // upstreamTokens.expiresIn > 0 BEFORE config.accessTokenTtl.
130
+ // Google always returns expiresIn=3600, so our 30-day config is
131
+ // never reached. Zero it out so the config fallback is used.
132
+ const origIssue = oauthProxy.issueSwappedTokens.bind(oauthProxy);
133
+ oauthProxy.issueSwappedTokens = async function (clientId, upstreamTokens) {
134
+ if (this.config.accessTokenTtl) {
135
+ return origIssue(clientId, { ...upstreamTokens, expiresIn: 0 });
136
+ }
137
+ return origIssue(clientId, upstreamTokens);
138
+ };
139
+ enableUpstreamOfflineAccess(oauthProxy);
140
+ }
141
+ const server = new FastMCP({
142
+ name: 'Ultimate Google Docs & Sheets MCP Server',
143
+ version: '1.0.0',
144
+ ...(isRemote &&
145
+ oauthProxy && {
146
+ authenticate: async (request) => {
147
+ if (!request)
148
+ return undefined;
149
+ const authHeader = request.headers?.authorization;
150
+ if (!authHeader?.startsWith('Bearer '))
151
+ return undefined;
152
+ const token = authHeader.slice(7);
153
+ const upstreamTokens = await oauthProxy.loadUpstreamTokens(token);
154
+ if (!upstreamTokens)
155
+ return undefined;
156
+ return {
157
+ accessToken: upstreamTokens.accessToken,
158
+ refreshToken: upstreamTokens.refreshToken,
159
+ expiresAt: upstreamTokens.expiresIn
160
+ ? Math.floor(Date.now() / 1000) + upstreamTokens.expiresIn
161
+ : undefined,
162
+ idToken: upstreamTokens.idToken,
163
+ scopes: upstreamTokens.scope,
164
+ };
165
+ },
166
+ oauth: {
167
+ enabled: true,
168
+ authorizationServer: oauthProxy.getAuthorizationServerMetadata(),
169
+ protectedResource: {
170
+ authorizationServers: [process.env.BASE_URL],
171
+ resource: process.env.BASE_URL,
172
+ scopesSupported: GOOGLE_API_SCOPES,
173
+ },
174
+ proxy: oauthProxy,
175
+ },
176
+ }),
177
+ });
178
+ const registeredTools = [];
179
+ collectToolsWhileRegistering(server, registeredTools);
180
+ if (isRemote)
181
+ wrapServerForRemote(server);
182
+ let enabledToolGroups;
183
+ try {
184
+ enabledToolGroups = parseEnabledToolGroups();
185
+ registerAllTools(server, enabledToolGroups);
186
+ logger.info(`Registered tool groups: ${enabledToolGroups.join(', ')}.`);
187
+ }
188
+ catch (error) {
189
+ logger.error(`FATAL: Invalid MCP_TOOL_GROUPS: ${error.message || error}`);
190
+ process.exit(1);
191
+ }
192
+ try {
193
+ if (isRemote) {
194
+ logger.info('Starting in remote mode (httpStream + MCP OAuth 2.1)...');
195
+ registerLandingPage(server, registeredTools.length);
196
+ registerDownloadRoute(server);
197
+ const port = parseInt(process.env.PORT || '8080');
198
+ await server.start({
199
+ transportType: 'httpStream',
200
+ httpStream: {
201
+ port,
202
+ host: '0.0.0.0',
203
+ stateless: parseStatelessFlag(),
204
+ },
205
+ });
206
+ logger.info(`MCP Server running at ${process.env.BASE_URL || `http://0.0.0.0:${port}`}/mcp`);
207
+ }
208
+ else {
209
+ logger.info('Starting Ultimate Google Docs & Sheets MCP server...');
210
+ const cachedToolsList = await buildCachedToolsListPayload(registeredTools);
211
+ await server.start({ transportType: 'stdio' });
212
+ installCachedToolsListHandler(server, cachedToolsList);
213
+ logger.info('MCP Server running using stdio. Awaiting client connection...');
214
+ // Warm up the Google client *after* the transport is live, and never let it
215
+ // stop the server from running.
216
+ //
217
+ // Authorizing before server.start() meant a host could never complete the
218
+ // MCP `initialize` handshake when credentials were unavailable: missing
219
+ // credentials threw and the catch below called process.exit(1), while a
220
+ // missing saved token sent authorize() into the interactive browser flow,
221
+ // which blocks on a local callback server and hangs startup indefinitely.
222
+ // Either way the client saw "server exited" instead of a handshake.
223
+ //
224
+ // Tools already resolve the client lazily (getDocsClient() and friends call
225
+ // initializeGoogleClient() per request and raise a UserError when it fails),
226
+ // so deferring this costs nothing and keeps auth problems scoped to the
227
+ // calls that actually need Google.
228
+ void initializeGoogleClient().catch((authError) => {
229
+ logger.error(`Google authorization is not available yet: ${authError?.message || authError} ` +
230
+ 'The server is running and will answer initialize and tools/list; individual tool ' +
231
+ 'calls will fail until authorization succeeds. Run `npx @a-bonus/google-docs-mcp auth` ' +
232
+ 'to complete the OAuth flow.');
233
+ });
234
+ }
235
+ logger.info('Process-level error handling configured to prevent crashes from timeout errors.');
236
+ }
237
+ catch (startError) {
238
+ logger.error('FATAL: Server failed to start:', startError.message || startError);
239
+ process.exit(1);
240
+ }
@@ -0,0 +1,103 @@
1
+ const TEXTS = {
2
+ en: {
3
+ title: 'Google Docs MCP Server',
4
+ subtitle: 'Model Context Protocol server for Google Docs, Sheets, Drive, Gmail & Calendar',
5
+ setup: 'Setup',
6
+ step1: 'Copy the configuration below',
7
+ step2: 'Open <strong>Cursor Settings → Tools &amp; MCP → Add new MCP server</strong>',
8
+ step3: 'Paste the JSON and save',
9
+ step4: 'Cursor will prompt you to sign in with Google on first use',
10
+ config: 'Cursor MCP Configuration',
11
+ copy: 'Copy',
12
+ copied: 'Copied!',
13
+ tools: 'tools available',
14
+ footer: 'Powered by <a href="https://github.com/a-bonus/google-docs-mcp">a-bonus/google-docs-mcp</a>',
15
+ },
16
+ cs: {
17
+ title: 'Google Docs MCP Server',
18
+ subtitle: 'Model Context Protocol server pro Google Docs, Sheets, Drive, Gmail &amp; Calendar',
19
+ setup: 'Nastavení',
20
+ step1: 'Zkopírujte konfiguraci níže',
21
+ step2: 'Otevřete <strong>Cursor Settings → Tools &amp; MCP → Add new MCP server</strong>',
22
+ step3: 'Vložte JSON a uložte',
23
+ step4: 'Cursor vás při prvním použití vyzve k přihlášení přes Google',
24
+ config: 'Konfigurace pro Cursor',
25
+ copy: 'Kopírovat',
26
+ copied: 'Zkopírováno!',
27
+ tools: 'dostupných nástrojů',
28
+ footer: 'Poháněno <a href="https://github.com/a-bonus/google-docs-mcp">a-bonus/google-docs-mcp</a>',
29
+ },
30
+ };
31
+ function pickLang(acceptLanguage) {
32
+ if (acceptLanguage?.toLowerCase().startsWith('cs'))
33
+ return 'cs';
34
+ return 'en';
35
+ }
36
+ function renderPage(lang, mcpUrl, toolCount) {
37
+ const t = TEXTS[lang];
38
+ const configJson = JSON.stringify({ mcpServers: { 'google-docs': { type: 'streamableHttp', url: mcpUrl } } }, null, 2);
39
+ return `<!DOCTYPE html>
40
+ <html lang="${lang}">
41
+ <head>
42
+ <meta charset="utf-8">
43
+ <meta name="viewport" content="width=device-width, initial-scale=1">
44
+ <title>${t.title}</title>
45
+ <style>
46
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
47
+ body { font-family: system-ui, -apple-system, sans-serif; background: #f8f9fa; color: #1a1a1a; line-height: 1.6; padding: 2rem 1rem; }
48
+ .container { max-width: 640px; margin: 0 auto; }
49
+ h1 { font-size: 1.75rem; font-weight: 700; margin-bottom: 0.25rem; }
50
+ .subtitle { color: #666; margin-bottom: 2rem; }
51
+ .badge { display: inline-block; background: #e8f5e9; color: #2e7d32; font-size: 0.8rem; font-weight: 600; padding: 0.2rem 0.6rem; border-radius: 99px; margin-left: 0.5rem; vertical-align: middle; }
52
+ h2 { font-size: 1.1rem; font-weight: 600; margin: 1.5rem 0 0.75rem; }
53
+ ol { padding-left: 1.5rem; margin-bottom: 1.5rem; }
54
+ li { margin-bottom: 0.4rem; }
55
+ .code-block { position: relative; background: #1e1e1e; color: #d4d4d4; border-radius: 8px; padding: 1rem; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 0.85rem; overflow-x: auto; white-space: pre; margin-bottom: 1.5rem; }
56
+ .copy-btn { position: absolute; top: 0.5rem; right: 0.5rem; background: #333; color: #ccc; border: none; border-radius: 4px; padding: 0.3rem 0.6rem; font-size: 0.75rem; cursor: pointer; transition: background 0.2s; }
57
+ .copy-btn:hover { background: #555; }
58
+ .footer { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid #ddd; color: #888; font-size: 0.8rem; }
59
+ .footer a { color: #666; }
60
+ </style>
61
+ </head>
62
+ <body>
63
+ <div class="container">
64
+ <h1>${t.title} <span class="badge">${toolCount} ${t.tools}</span></h1>
65
+ <p class="subtitle">${t.subtitle}</p>
66
+
67
+ <h2>${t.setup}</h2>
68
+ <ol>
69
+ <li>${t.step1}</li>
70
+ <li>${t.step2}</li>
71
+ <li>${t.step3}</li>
72
+ <li>${t.step4}</li>
73
+ </ol>
74
+
75
+ <h2>${t.config}</h2>
76
+ <div class="code-block"><button class="copy-btn" onclick="copyConfig()">${t.copy}</button><code id="config">${escapeHtml(configJson)}</code></div>
77
+
78
+ <p class="footer">${t.footer}</p>
79
+ </div>
80
+ <script>
81
+ var _cfg = ${JSON.stringify(configJson).replace(/</g, '\\u003c')};
82
+ function copyConfig() {
83
+ navigator.clipboard.writeText(_cfg);
84
+ var btn = document.querySelector('.copy-btn');
85
+ btn.textContent = '${t.copied}';
86
+ setTimeout(function() { btn.textContent = '${t.copy}'; }, 2000);
87
+ }
88
+ </script>
89
+ </body>
90
+ </html>`;
91
+ }
92
+ function escapeHtml(s) {
93
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
94
+ }
95
+ export function registerLandingPage(server, toolCount) {
96
+ const baseUrl = process.env.BASE_URL || '';
97
+ const mcpUrl = `${baseUrl}/mcp`;
98
+ const app = server.getApp();
99
+ app.get('/', (c) => {
100
+ const lang = pickLang(c.req.header('accept-language'));
101
+ return c.html(renderPage(lang, mcpUrl, toolCount));
102
+ });
103
+ }
package/dist/logger.js ADDED
@@ -0,0 +1,58 @@
1
+ // src/logger.ts
2
+ //
3
+ // Centralized logger with LOG_LEVEL support.
4
+ //
5
+ // Log levels (from most to least verbose):
6
+ // debug - Detailed diagnostic info (text range mapping, request building, etc.)
7
+ // info - General operational messages (server started, auth succeeded, etc.)
8
+ // warn - Potentially harmful situations (missing content, fallback behavior)
9
+ // error - Error conditions (API failures, auth failures, etc.)
10
+ //
11
+ // Set via the LOG_LEVEL environment variable. Defaults to "info".
12
+ // Example: LOG_LEVEL=debug npm start
13
+ //
14
+ // MCP servers communicate over stdout, so all log output goes to stderr.
15
+ const LOG_LEVELS = {
16
+ debug: 0,
17
+ info: 1,
18
+ warn: 2,
19
+ error: 3,
20
+ silent: 4,
21
+ };
22
+ function resolveLevel() {
23
+ const env = process.env.LOG_LEVEL?.toLowerCase();
24
+ if (env && env in LOG_LEVELS) {
25
+ return env;
26
+ }
27
+ return 'info';
28
+ }
29
+ let currentLevel = resolveLevel();
30
+ /** Re-read LOG_LEVEL from the environment (useful for testing). */
31
+ export function refreshLogLevel() {
32
+ currentLevel = resolveLevel();
33
+ }
34
+ function shouldLog(level) {
35
+ return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
36
+ }
37
+ export const logger = {
38
+ debug(...args) {
39
+ if (shouldLog('debug')) {
40
+ console.error('[DEBUG]', ...args);
41
+ }
42
+ },
43
+ info(...args) {
44
+ if (shouldLog('info')) {
45
+ console.error('[INFO]', ...args);
46
+ }
47
+ },
48
+ warn(...args) {
49
+ if (shouldLog('warn')) {
50
+ console.error('[WARN]', ...args);
51
+ }
52
+ },
53
+ error(...args) {
54
+ if (shouldLog('error')) {
55
+ console.error('[ERROR]', ...args);
56
+ }
57
+ },
58
+ };
@@ -0,0 +1,259 @@
1
+ // src/markdown-transformer/docsToMarkdown.ts
2
+ /**
3
+ * Font families used by the markdown-to-docs direction for code styling.
4
+ * When these are detected on a text run, we render backtick code in markdown.
5
+ */
6
+ const CODE_FONT_FAMILIES = new Set(['Roboto Mono', 'Courier New', 'Consolas', 'monospace']);
7
+ // --- Main Conversion ---
8
+ /**
9
+ * Converts Google Docs JSON structure to a markdown string.
10
+ *
11
+ * Accepts the raw response from `docs.documents.get()`, or a subset with
12
+ * `{ body, lists }` (e.g. when extracting a specific tab).
13
+ *
14
+ * Handles headings, paragraphs, text formatting (bold, italic, strikethrough,
15
+ * underline, links, code), ordered & unordered lists with nesting, tables,
16
+ * and section breaks.
17
+ */
18
+ export function docsJsonToMarkdown(docData) {
19
+ const body = docData.body;
20
+ if (!body?.content) {
21
+ return '';
22
+ }
23
+ const lists = docData.lists ?? {};
24
+ let markdown = '';
25
+ for (const element of body.content) {
26
+ if (element.paragraph) {
27
+ markdown += convertParagraph(element.paragraph, lists);
28
+ }
29
+ else if (element.table) {
30
+ markdown += convertTable(element.table);
31
+ }
32
+ else if (element.sectionBreak) {
33
+ markdown += '\n---\n\n';
34
+ }
35
+ }
36
+ return markdown.trim();
37
+ }
38
+ // --- Paragraph Conversion ---
39
+ function convertParagraph(paragraph, lists) {
40
+ // 1. Determine paragraph type
41
+ const headingLevel = getHeadingLevel(paragraph);
42
+ const listInfo = getListInfo(paragraph, lists);
43
+ // 2. Extract text content with inline formatting
44
+ const elements = paragraph.elements ?? [];
45
+ const text = extractFormattedText(elements);
46
+ // 3. Format based on type
47
+ if (headingLevel && text.trim()) {
48
+ const hashes = '#'.repeat(Math.min(headingLevel, 6));
49
+ return `${hashes} ${text.trim()}\n\n`;
50
+ }
51
+ if (listInfo && text.trim()) {
52
+ const indent = ' '.repeat(listInfo.nestingLevel);
53
+ const marker = listInfo.ordered ? `1.` : `-`;
54
+ return `${indent}${marker} ${text.trim()}\n`;
55
+ }
56
+ if (text.trim()) {
57
+ return `${text.trim()}\n\n`;
58
+ }
59
+ return '\n';
60
+ }
61
+ // --- Heading Detection ---
62
+ function getHeadingLevel(paragraph) {
63
+ const styleType = paragraph.paragraphStyle?.namedStyleType;
64
+ if (!styleType)
65
+ return null;
66
+ if (styleType === 'TITLE')
67
+ return 1;
68
+ if (styleType === 'SUBTITLE')
69
+ return 2;
70
+ const match = styleType.match(/^HEADING_(\d)$/);
71
+ return match ? parseInt(match[1], 10) : null;
72
+ }
73
+ function getListInfo(paragraph, lists) {
74
+ if (!paragraph.bullet)
75
+ return null;
76
+ const nestingLevel = paragraph.bullet.nestingLevel ?? 0;
77
+ const listId = paragraph.bullet.listId;
78
+ let ordered = false;
79
+ if (listId && lists[listId]?.listProperties?.nestingLevels) {
80
+ const nestingLevels = lists[listId].listProperties.nestingLevels;
81
+ const level = nestingLevels[nestingLevel];
82
+ if (level) {
83
+ // glyphType is set for ordered lists (e.g., DECIMAL, ALPHA, ROMAN)
84
+ // glyphSymbol is set for unordered lists (e.g., bullet characters)
85
+ // If glyphType is present and not empty, it's ordered
86
+ if (level.glyphType && level.glyphType !== 'GLYPH_TYPE_UNSPECIFIED') {
87
+ ordered = true;
88
+ }
89
+ }
90
+ }
91
+ return { ordered, nestingLevel };
92
+ }
93
+ // --- Text Run Conversion ---
94
+ function extractFormattedText(elements) {
95
+ let result = '';
96
+ for (const element of elements) {
97
+ if (element.textRun) {
98
+ result += convertTextRun(element.textRun);
99
+ }
100
+ }
101
+ return result;
102
+ }
103
+ function convertTextRun(textRun) {
104
+ let text = textRun.content ?? '';
105
+ const style = textRun.textStyle;
106
+ if (!style)
107
+ return text;
108
+ // Detect code-styled text (monospace font) -- wrap in backticks and skip
109
+ // other formatting since markdown code spans don't support nested formatting.
110
+ if (isCodeStyled(style)) {
111
+ const trimmed = text.replace(/\n$/, '');
112
+ if (trimmed) {
113
+ return `\`${trimmed}\`${text.endsWith('\n') ? '\n' : ''}`;
114
+ }
115
+ return text;
116
+ }
117
+ // Strip trailing newline before applying formatting markers, then re-add.
118
+ // This prevents markers from wrapping the newline (e.g., "**text\n**").
119
+ const trailingNewline = text.endsWith('\n');
120
+ const content = trailingNewline ? text.slice(0, -1) : text;
121
+ if (!content)
122
+ return text;
123
+ let formatted = content;
124
+ // Apply inline formatting (bold + italic combined, or individually)
125
+ if (style.bold && style.italic) {
126
+ formatted = `***${formatted}***`;
127
+ }
128
+ else if (style.bold) {
129
+ formatted = `**${formatted}**`;
130
+ }
131
+ else if (style.italic) {
132
+ formatted = `*${formatted}*`;
133
+ }
134
+ if (style.strikethrough) {
135
+ formatted = `~~${formatted}~~`;
136
+ }
137
+ if (style.underline && !style.link) {
138
+ formatted = `<u>${formatted}</u>`;
139
+ }
140
+ if (style.link?.url) {
141
+ formatted = `[${formatted}](${style.link.url})`;
142
+ }
143
+ return formatted + (trailingNewline ? '\n' : '');
144
+ }
145
+ function isCodeStyled(style) {
146
+ const fontFamily = style.weightedFontFamily?.fontFamily;
147
+ return typeof fontFamily === 'string' && CODE_FONT_FAMILIES.has(fontFamily);
148
+ }
149
+ // --- Table Conversion ---
150
+ function convertTable(table) {
151
+ if (!table.tableRows || table.tableRows.length === 0) {
152
+ return '';
153
+ }
154
+ // Detect code block tables (1x1 table with monospace font or gray background)
155
+ if (isCodeBlockTable(table)) {
156
+ return convertCodeBlockTable(table);
157
+ }
158
+ let markdown = '\n';
159
+ let isFirstRow = true;
160
+ for (const row of table.tableRows) {
161
+ if (!row.tableCells)
162
+ continue;
163
+ let rowText = '|';
164
+ for (const cell of row.tableCells) {
165
+ const cellText = extractCellText(cell);
166
+ rowText += ` ${cellText} |`;
167
+ }
168
+ markdown += rowText + '\n';
169
+ // Add header separator after the first row
170
+ if (isFirstRow) {
171
+ let separator = '|';
172
+ for (let i = 0; i < row.tableCells.length; i++) {
173
+ separator += ' --- |';
174
+ }
175
+ markdown += separator + '\n';
176
+ isFirstRow = false;
177
+ }
178
+ }
179
+ return markdown + '\n';
180
+ }
181
+ /**
182
+ * Detects if a table is a code block (1x1 table with monospace font or gray background).
183
+ * Google Docs "Code Block" building blocks are represented as styled 1x1 tables.
184
+ */
185
+ function isCodeBlockTable(table) {
186
+ // Must be a 1x1 table
187
+ if (!table.tableRows || table.tableRows.length !== 1)
188
+ return false;
189
+ const row = table.tableRows[0];
190
+ if (!row.tableCells || row.tableCells.length !== 1)
191
+ return false;
192
+ const cell = row.tableCells[0];
193
+ // Check for gray/colored background on the cell
194
+ const cellStyle = cell.tableCellStyle;
195
+ if (cellStyle?.backgroundColor?.color?.rgbColor) {
196
+ const bg = cellStyle.backgroundColor.color.rgbColor;
197
+ // Detect light gray backgrounds (typical of code blocks)
198
+ // Allow a range of light grays
199
+ const r = bg.red ?? 0;
200
+ const g = bg.green ?? 0;
201
+ const b = bg.blue ?? 0;
202
+ if (r > 0.85 && g > 0.85 && b > 0.85 && r < 1 && g < 1 && b < 1) {
203
+ return true;
204
+ }
205
+ }
206
+ // Check for monospace font in cell content
207
+ if (cell.content) {
208
+ for (const element of cell.content) {
209
+ if (element.paragraph?.elements) {
210
+ for (const pe of element.paragraph.elements) {
211
+ if (pe.textRun?.textStyle) {
212
+ if (isCodeStyled(pe.textRun.textStyle)) {
213
+ return true;
214
+ }
215
+ }
216
+ }
217
+ }
218
+ }
219
+ }
220
+ return false;
221
+ }
222
+ /**
223
+ * Converts a code block table (1x1 table) to a fenced markdown code block.
224
+ */
225
+ function convertCodeBlockTable(table) {
226
+ const cell = table.tableRows[0].tableCells[0];
227
+ let codeText = '';
228
+ if (cell.content) {
229
+ for (const element of cell.content) {
230
+ if (element.paragraph?.elements) {
231
+ for (const pe of element.paragraph.elements) {
232
+ if (pe.textRun?.content) {
233
+ codeText += pe.textRun.content;
234
+ }
235
+ }
236
+ }
237
+ }
238
+ }
239
+ // Remove trailing newline (cells always end with one)
240
+ if (codeText.endsWith('\n')) {
241
+ codeText = codeText.slice(0, -1);
242
+ }
243
+ return '\n```\n' + codeText + '\n```\n\n';
244
+ }
245
+ function extractCellText(cell) {
246
+ let text = '';
247
+ if (!cell.content)
248
+ return text;
249
+ for (const element of cell.content) {
250
+ if (element.paragraph?.elements) {
251
+ for (const pe of element.paragraph.elements) {
252
+ if (pe.textRun?.content) {
253
+ text += pe.textRun.content.replace(/\n/g, ' ').trim();
254
+ }
255
+ }
256
+ }
257
+ }
258
+ return text;
259
+ }