google-tools-mcp 1.2.11 → 2.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/LICENSE +21 -21
- package/README.md +555 -276
- package/dist/cachedToolsList.js +52 -52
- package/dist/googleDocsApiHelpers.js +76 -12
- package/dist/helpers.js +572 -252
- package/dist/httpAuth.js +296 -0
- package/dist/index.js +162 -11
- package/dist/logger.js +87 -87
- package/dist/markdown-transformer/docsToMarkdown.js +222 -21
- package/dist/markdown-transformer/index.js +19 -9
- package/dist/markdown-transformer/markdownToDocs.js +458 -28
- package/dist/readTracker.js +136 -17
- package/dist/sessionContext.js +37 -0
- package/dist/setup.js +187 -9
- package/dist/tools/calendar/getBusy.js +64 -64
- package/dist/tools/calendar/getEvents.js +141 -141
- package/dist/tools/calendar/getFree.js +225 -225
- package/dist/tools/calendar/index.js +19 -19
- package/dist/tools/calendar/listCalendars.js +38 -38
- package/dist/tools/calendar/listRecurringInstances.js +83 -83
- package/dist/tools/calendar/manageCalendar.js +121 -121
- package/dist/tools/calendar/manageEvent.js +258 -258
- package/dist/tools/calendar/moveEvent.js +66 -66
- package/dist/tools/docs/addTab.js +10 -12
- package/dist/tools/docs/appendToGoogleDoc.js +9 -4
- package/dist/tools/docs/deleteRange.js +9 -4
- package/dist/tools/docs/findAndReplace.js +9 -4
- package/dist/tools/docs/formatting/applyParagraphStyle.js +4 -1
- package/dist/tools/docs/formatting/getFormatting.js +171 -171
- package/dist/tools/docs/insertImage.js +15 -2
- package/dist/tools/docs/insertPageBreak.js +4 -1
- package/dist/tools/docs/insertTable.js +4 -1
- package/dist/tools/docs/insertTableWithData.js +4 -1
- package/dist/tools/docs/modifyText.js +14 -4
- package/dist/tools/docs/modifyText.test.js +84 -84
- package/dist/tools/docs/readGoogleDoc.js +71 -11
- package/dist/tools/docs/renameTab.js +12 -14
- package/dist/tools/drafts.js +165 -165
- package/dist/tools/drive/createDocument.js +13 -1
- package/dist/tools/drive/downloadFile.js +268 -268
- package/dist/tools/drive/getFileInfo.js +48 -48
- package/dist/tools/drive/getFilePath.js +50 -50
- package/dist/tools/drive/listDriveFiles.js +112 -112
- package/dist/tools/drive/listSharedDrives.js +52 -52
- package/dist/tools/drive/listSharedWithMe.js +97 -97
- package/dist/tools/drive/uploadFile.js +111 -111
- package/dist/tools/extras/index.js +9 -9
- package/dist/tools/extras/readDriveFile.js +212 -206
- package/dist/tools/extras/readFile.js +84 -84
- package/dist/tools/extras/searchFileContents.js +81 -81
- package/dist/tools/forms/batchUpdateForm.js +81 -81
- package/dist/tools/forms/createForm.js +54 -54
- package/dist/tools/forms/getForm.js +118 -118
- package/dist/tools/forms/getFormResponse.js +45 -45
- package/dist/tools/forms/index.js +15 -15
- package/dist/tools/forms/listFormResponses.js +56 -56
- package/dist/tools/forms/setPublishSettings.js +59 -59
- package/dist/tools/gmail/drafts.js +165 -165
- package/dist/tools/gmail/labels.js +54 -83
- package/dist/tools/gmail/messages.js +444 -441
- package/dist/tools/gmail/settings.js +358 -528
- package/dist/tools/gmail/threads.js +286 -164
- package/dist/tools/index.js +511 -473
- package/dist/tools/labels.js +103 -103
- package/dist/tools/legacyAliases.js +426 -0
- package/dist/tools/maps/directions.js +72 -0
- package/dist/tools/maps/geocode.js +16 -0
- package/dist/tools/maps/index.js +15 -0
- package/dist/tools/maps/mapsClient.js +83 -0
- package/dist/tools/maps/placeDetails.js +16 -0
- package/dist/tools/maps/reverseGeocode.js +19 -0
- package/dist/tools/maps/searchNearby.js +54 -0
- package/dist/tools/maps/searchPlaces.js +24 -0
- package/dist/tools/messages.js +448 -448
- package/dist/tools/settings.js +528 -528
- package/dist/tools/slides/createPresentation.js +104 -104
- package/dist/tools/slides/createShape.js +92 -92
- package/dist/tools/slides/createTextBox.js +82 -82
- package/dist/tools/slides/deleteSlide.js +30 -30
- package/dist/tools/slides/duplicateSlide.js +37 -37
- package/dist/tools/slides/exportThumbnail.js +42 -42
- package/dist/tools/slides/formatParagraph.js +72 -72
- package/dist/tools/slides/formatText.js +84 -84
- package/dist/tools/slides/getPresentation.js +87 -87
- package/dist/tools/slides/index.js +33 -33
- package/dist/tools/slides/reorderSlides.js +41 -41
- package/dist/tools/slides/replaceAllText.js +46 -46
- package/dist/tools/slides/setBackground.js +58 -58
- package/dist/tools/slides/speakerNotes.js +102 -102
- package/dist/tools/slides/styleShape.js +111 -111
- package/dist/tools/slides/updatePresentation.js +128 -128
- package/dist/tools/threads.js +145 -145
- package/dist/tools/utils/appendMarkdownToGoogleDoc.js +31 -7
- package/dist/tools/utils/replaceDocumentWithMarkdown.js +72 -9
- package/dist/updateCheck.js +202 -0
- package/dist/workspace.js +122 -0
- package/package.json +82 -80
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Best-effort check for a newer published version of google-tools-mcp.
|
|
2
|
+
//
|
|
3
|
+
// Why this exists: setup.js points MCP clients directly at a resolved
|
|
4
|
+
// dist/index.js instead of `npx -y google-tools-mcp` (see the fast-launch
|
|
5
|
+
// comment block in setup.js for why). That fixes the startup-timeout race
|
|
6
|
+
// against Claude Code's 30s connection timeout, but it also means nothing
|
|
7
|
+
// ever runs `npm install -g google-tools-mcp@latest` again on its own,
|
|
8
|
+
// unlike npx, which re-resolves to the latest published version on every
|
|
9
|
+
// launch. Left alone, a global-install user would keep running whatever
|
|
10
|
+
// version was installed at setup time forever, silently missing every
|
|
11
|
+
// release after that.
|
|
12
|
+
//
|
|
13
|
+
// This module is the read-only half of the fix: it looks up the latest
|
|
14
|
+
// version published to npm and reports whether the running copy is behind,
|
|
15
|
+
// so index.js can log a clear one-line nudge. It never modifies anything.
|
|
16
|
+
//
|
|
17
|
+
// This intentionally cannot reintroduce the npx timeout bug:
|
|
18
|
+
// - index.js only calls checkForUpdate() AFTER `server.start()` has
|
|
19
|
+
// already resolved, i.e. after the MCP stdio connection is established.
|
|
20
|
+
// Nothing here runs before or during connection setup.
|
|
21
|
+
// - index.js does not await that call. It is fire-and-forget, so even if
|
|
22
|
+
// the network is completely unreachable, the server process itself is
|
|
23
|
+
// never blocked or slowed by this.
|
|
24
|
+
// - The registry fetch is also strictly time-boxed (default 2s, via
|
|
25
|
+
// AbortSignal.timeout) so a hung connection can't leave anything dangling
|
|
26
|
+
// for long, and any failure (timeout, offline, non-2xx, malformed body)
|
|
27
|
+
// resolves to "couldn't check" rather than throwing.
|
|
28
|
+
// - Results are cached to disk and reused for `intervalMs` (default 24h),
|
|
29
|
+
// so most launches skip the network call entirely.
|
|
30
|
+
import * as path from 'path';
|
|
31
|
+
|
|
32
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/google-tools-mcp/latest';
|
|
33
|
+
const DEFAULT_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
34
|
+
const DEFAULT_TIMEOUT_MS = 2000;
|
|
35
|
+
const STATE_FILE_NAME = 'update-check.json';
|
|
36
|
+
|
|
37
|
+
// Exported for testing.
|
|
38
|
+
export function stateFilePath(configDir) {
|
|
39
|
+
return path.join(configDir, STATE_FILE_NAME);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Compares two `major.minor.patch`-style version strings. Anything after a
|
|
43
|
+
// `-` or `+` (pre-release/build metadata) is ignored, since the registry's
|
|
44
|
+
// `dist-tags.latest` for this package is always a plain release version.
|
|
45
|
+
// Missing/non-numeric segments are treated as 0. Returns 1 if a > b, -1 if
|
|
46
|
+
// a < b, 0 if equal or unparseable.
|
|
47
|
+
export function compareVersions(a, b) {
|
|
48
|
+
const parse = (v) => String(v ?? '').split(/[-+]/)[0].split('.').map((n) => parseInt(n, 10));
|
|
49
|
+
const pa = parse(a);
|
|
50
|
+
const pb = parse(b);
|
|
51
|
+
const len = Math.max(pa.length, pb.length);
|
|
52
|
+
for (let i = 0; i < len; i++) {
|
|
53
|
+
const na = Number.isFinite(pa[i]) ? pa[i] : 0;
|
|
54
|
+
const nb = Number.isFinite(pb[i]) ? pb[i] : 0;
|
|
55
|
+
if (na !== nb) return na > nb ? 1 : -1;
|
|
56
|
+
}
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Is `candidate` a real, strictly-newer version than `current`?
|
|
61
|
+
export function isNewerVersion(current, candidate) {
|
|
62
|
+
if (typeof candidate !== 'string' || candidate.length === 0) return false;
|
|
63
|
+
return compareVersions(candidate, current) > 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Pure throttle check: has enough time passed since `lastCheckedAt` (an ISO
|
|
67
|
+
// string or anything `new Date()` accepts) to justify another network call?
|
|
68
|
+
// Missing or unparseable timestamps always say "yes, check" so a corrupt
|
|
69
|
+
// cache file degrades to "check a bit more often" rather than "never check
|
|
70
|
+
// again".
|
|
71
|
+
export function shouldCheckNow(lastCheckedAt, now, intervalMs = DEFAULT_INTERVAL_MS) {
|
|
72
|
+
if (!lastCheckedAt) return true;
|
|
73
|
+
const last = new Date(lastCheckedAt).getTime();
|
|
74
|
+
if (!Number.isFinite(last)) return true;
|
|
75
|
+
return now - last >= intervalMs;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Should the update check be skipped entirely, without touching the network
|
|
79
|
+
// or disk? Three ways to opt out:
|
|
80
|
+
// - NO_UPDATE_NOTIFIER: the npm-ecosystem-standard opt-out env var, used by
|
|
81
|
+
// the widely-adopted `update-notifier` package (the library most CLIs,
|
|
82
|
+
// including npm itself, build this exact "is a newer version out"
|
|
83
|
+
// nudge on top of). Per its README: "Users can also opt-out by setting
|
|
84
|
+
// the environment variable NO_UPDATE_NOTIFIER with any value" (see
|
|
85
|
+
// https://github.com/yeoman/update-notifier#readme, fetched 2026-07-25).
|
|
86
|
+
// That is a presence check, not a truthiness check, so it is honored
|
|
87
|
+
// here the same way: any value, including an empty string, counts.
|
|
88
|
+
// - GOOGLE_MCP_NO_UPDATE_CHECK: this repo's own opt-out, following the
|
|
89
|
+
// existing GOOGLE_MCP_* naming already used for GOOGLE_MCP_PROFILE and
|
|
90
|
+
// GOOGLE_MCP_LOG_FILE. Same presence-check semantics as above.
|
|
91
|
+
// - CI: nearly every CI system sets this, and it is the de facto standard
|
|
92
|
+
// generic "am I running in CI" signal in the npm ecosystem (see the
|
|
93
|
+
// `ci-info` package, which is what `is-ci`/many CLIs use to detect it,
|
|
94
|
+
// and which treats CI=false as an explicit escape hatch back to "not
|
|
95
|
+
// CI"; see https://github.com/watson/ci-info#readme, fetched 2026-07-25).
|
|
96
|
+
// Automated/CI runs should never make an unannounced outbound call.
|
|
97
|
+
//
|
|
98
|
+
// This check is intentionally the very first thing checkForUpdate() does,
|
|
99
|
+
// before reading the cache file or touching the network, and it is a handful
|
|
100
|
+
// of object-property lookups, so it costs nothing even when the check would
|
|
101
|
+
// otherwise run.
|
|
102
|
+
export function isUpdateCheckDisabled(env = process.env) {
|
|
103
|
+
if (!env) return false;
|
|
104
|
+
if ('NO_UPDATE_NOTIFIER' in env) return true;
|
|
105
|
+
if ('GOOGLE_MCP_NO_UPDATE_CHECK' in env) return true;
|
|
106
|
+
if (env.CI && env.CI !== 'false') return true;
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function readState(configDir, { readFile }) {
|
|
111
|
+
try {
|
|
112
|
+
const raw = await readFile(stateFilePath(configDir), 'utf8');
|
|
113
|
+
return JSON.parse(raw);
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function writeState(configDir, state, { mkdir, writeFile }) {
|
|
120
|
+
try {
|
|
121
|
+
await mkdir(configDir, { recursive: true });
|
|
122
|
+
await writeFile(stateFilePath(configDir), JSON.stringify(state));
|
|
123
|
+
} catch {
|
|
124
|
+
// Best-effort cache only. If we can't persist it, we just eat the
|
|
125
|
+
// cost of checking again next launch instead of surfacing an error.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Fetches the latest published version from the npm registry. Never throws:
|
|
130
|
+
// any network error, timeout, non-2xx response, or malformed body resolves
|
|
131
|
+
// to null, so callers can treat "couldn't check" the same as "nothing to
|
|
132
|
+
// report" instead of as a startup failure. `fetchImpl` defaults to the
|
|
133
|
+
// global `fetch` (available on Node 18+; this repo's CI runs 20 and 22).
|
|
134
|
+
export async function fetchLatestVersion({
|
|
135
|
+
fetchImpl = fetch,
|
|
136
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
137
|
+
url = REGISTRY_URL,
|
|
138
|
+
} = {}) {
|
|
139
|
+
try {
|
|
140
|
+
const res = await fetchImpl(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
141
|
+
if (!res.ok) return null;
|
|
142
|
+
const body = await res.json();
|
|
143
|
+
return typeof body?.version === 'string' ? body.version : null;
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Orchestrates the full best-effort check: read the cached state, decide
|
|
150
|
+
// whether enough time has passed to justify a network call, fetch the
|
|
151
|
+
// latest version if so, persist the result, and report whether the running
|
|
152
|
+
// copy is behind. Every dependency is injectable so tests never touch a
|
|
153
|
+
// real filesystem or network.
|
|
154
|
+
//
|
|
155
|
+
// Never throws. Any failure anywhere in this pipeline (corrupt cache,
|
|
156
|
+
// unwritable config dir, unreachable registry) resolves to
|
|
157
|
+
// `{ checked: false, latestVersion: null, updateAvailable: false }`, the
|
|
158
|
+
// safest possible answer, since callers only use this to decide whether to
|
|
159
|
+
// print an informational log line, never to gate startup.
|
|
160
|
+
export async function checkForUpdate({
|
|
161
|
+
currentVersion,
|
|
162
|
+
configDir,
|
|
163
|
+
now = Date.now(),
|
|
164
|
+
intervalMs = DEFAULT_INTERVAL_MS,
|
|
165
|
+
fetchLatest = fetchLatestVersion,
|
|
166
|
+
fetchOptions = {},
|
|
167
|
+
readFile,
|
|
168
|
+
writeFile,
|
|
169
|
+
mkdir,
|
|
170
|
+
env = process.env,
|
|
171
|
+
} = {}) {
|
|
172
|
+
// Opt-out check comes first, before any disk read or network call. See
|
|
173
|
+
// isUpdateCheckDisabled() above for the three ways to trigger this.
|
|
174
|
+
if (isUpdateCheckDisabled(env)) {
|
|
175
|
+
return { checked: false, latestVersion: null, updateAvailable: false, skipped: true };
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const cached = await readState(configDir, { readFile });
|
|
179
|
+
if (cached && !shouldCheckNow(cached.lastCheckedAt, now, intervalMs)) {
|
|
180
|
+
return {
|
|
181
|
+
checked: false,
|
|
182
|
+
latestVersion: cached.latestVersion ?? null,
|
|
183
|
+
updateAvailable: isNewerVersion(currentVersion, cached.latestVersion),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const latestVersion = await fetchLatest(fetchOptions);
|
|
188
|
+
await writeState(
|
|
189
|
+
configDir,
|
|
190
|
+
{ lastCheckedAt: new Date(now).toISOString(), latestVersion },
|
|
191
|
+
{ mkdir, writeFile },
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
checked: true,
|
|
196
|
+
latestVersion,
|
|
197
|
+
updateAvailable: isNewerVersion(currentVersion, latestVersion),
|
|
198
|
+
};
|
|
199
|
+
} catch {
|
|
200
|
+
return { checked: false, latestVersion: null, updateAvailable: false };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Secure local workspace for the Google Docs local-file editing workflow.
|
|
2
|
+
//
|
|
3
|
+
// readDocument saves a markdown working copy here and replaceDocumentWithMarkdown
|
|
4
|
+
// mirrors inline pushes into it. Because these files hold document contents, the
|
|
5
|
+
// storage has to be hardened against the classic shared-/tmp attacks:
|
|
6
|
+
//
|
|
7
|
+
// * Per-user base directory (`google-tools-mcp-<user>`), created 0700, so a
|
|
8
|
+
// different local user cannot read another user's working copies and cannot
|
|
9
|
+
// win a collision by pre-creating the directory.
|
|
10
|
+
// * The base dir is lstat-checked every write: if it exists but is a symlink
|
|
11
|
+
// (or anything other than a directory we own) we refuse to write, defeating
|
|
12
|
+
// a planted-symlink redirect on the directory itself.
|
|
13
|
+
// * Files are opened with O_NOFOLLOW (where supported) plus O_CREAT|O_TRUNC and
|
|
14
|
+
// mode 0600, so an attacker who pre-creates the target path as a symlink
|
|
15
|
+
// causes the write to fail rather than clobber the victim's file.
|
|
16
|
+
// * Filenames are scoped by documentId AND tabId, so reading two tabs of one
|
|
17
|
+
// document keeps two separate working copies instead of silently overwriting.
|
|
18
|
+
import * as fs from 'fs/promises';
|
|
19
|
+
import { constants as fsConstants } from 'fs';
|
|
20
|
+
import * as os from 'os';
|
|
21
|
+
import * as path from 'path';
|
|
22
|
+
|
|
23
|
+
// Restrict an id to characters that are always safe in a filename. documentId
|
|
24
|
+
// and tabId are Google-issued opaque ids, but sanitizing is cheap defense in
|
|
25
|
+
// depth against path traversal if a caller ever passes something unexpected.
|
|
26
|
+
function sanitizeComponent(value) {
|
|
27
|
+
return String(value).replace(/[^a-zA-Z0-9_.-]/g, '_');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Per-user private base directory under the OS temp dir. Including the username
|
|
31
|
+
// scopes working copies per user; on POSIX the directory is additionally locked
|
|
32
|
+
// to 0700 and ownership-verified in ensureSafeBaseDir().
|
|
33
|
+
//
|
|
34
|
+
// GOOGLE_MCP_WORKSPACE_DIR overrides the computed path entirely when set.
|
|
35
|
+
// This exists so tests (and any caller that wants an isolated workspace) can
|
|
36
|
+
// point at a throwaway sandbox instead of the real per-user production
|
|
37
|
+
// directory, without needing a second code path in production. Unset (the
|
|
38
|
+
// normal production case), behavior is unchanged.
|
|
39
|
+
export function getWorkspaceDir() {
|
|
40
|
+
if (process.env.GOOGLE_MCP_WORKSPACE_DIR) {
|
|
41
|
+
return process.env.GOOGLE_MCP_WORKSPACE_DIR;
|
|
42
|
+
}
|
|
43
|
+
let userPart;
|
|
44
|
+
try {
|
|
45
|
+
userPart = sanitizeComponent(os.userInfo().username);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// userInfo can throw if there is no passwd entry (some sandboxes); fall
|
|
49
|
+
// back to the numeric uid, or a constant on platforms without getuid.
|
|
50
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : 'nouid';
|
|
51
|
+
userPart = `uid${uid}`;
|
|
52
|
+
}
|
|
53
|
+
if (!userPart) {
|
|
54
|
+
userPart = 'default';
|
|
55
|
+
}
|
|
56
|
+
return path.join(os.tmpdir(), `google-tools-mcp-${userPart}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Absolute path of the working copy for a document (and optional tab). tabId is
|
|
60
|
+
// part of the identity so distinct tabs never share a file.
|
|
61
|
+
export function getWorkspacePath(documentId, tabId = null) {
|
|
62
|
+
const base = sanitizeComponent(documentId);
|
|
63
|
+
const name = tabId ? `${base}.${sanitizeComponent(tabId)}.md` : `${base}.md`;
|
|
64
|
+
return path.join(getWorkspaceDir(), name);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Create the base dir with private perms and verify it is a real directory we
|
|
68
|
+
// own — not a symlink an attacker planted to redirect our writes. Throws if the
|
|
69
|
+
// directory exists but is unsafe.
|
|
70
|
+
async function ensureSafeBaseDir() {
|
|
71
|
+
const dir = getWorkspaceDir();
|
|
72
|
+
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
73
|
+
// mkdir(recursive) does not reset mode/ownership on a pre-existing entry, so
|
|
74
|
+
// inspect it directly. lstat (not stat) so a symlinked directory is reported
|
|
75
|
+
// as a symlink rather than followed.
|
|
76
|
+
const st = await fs.lstat(dir);
|
|
77
|
+
if (st.isSymbolicLink() || !st.isDirectory()) {
|
|
78
|
+
throw new Error(`Refusing to use workspace directory "${dir}": not a regular directory (possible symlink attack).`);
|
|
79
|
+
}
|
|
80
|
+
if (process.platform !== 'win32') {
|
|
81
|
+
if (typeof process.getuid === 'function' && st.uid !== process.getuid()) {
|
|
82
|
+
throw new Error(`Refusing to use workspace directory "${dir}": owned by another user.`);
|
|
83
|
+
}
|
|
84
|
+
// Re-assert 0700 in case the directory pre-existed with looser perms.
|
|
85
|
+
try {
|
|
86
|
+
await fs.chmod(dir, 0o700);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Non-fatal: best effort tightening.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return dir;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Write content to the workspace file for documentId/tabId without following a
|
|
96
|
+
// symlink at the final path component and with 0600 perms. Returns the absolute
|
|
97
|
+
// path written. Throws on any failure (callers decide whether that is fatal).
|
|
98
|
+
export async function writeWorkspaceFile(documentId, content, tabId = null) {
|
|
99
|
+
await ensureSafeBaseDir();
|
|
100
|
+
const filePath = getWorkspacePath(documentId, tabId);
|
|
101
|
+
// O_NOFOLLOW is a no-op / undefined on some platforms (notably Windows);
|
|
102
|
+
// fall back to 0 there. O_CREAT|O_TRUNC gives create-or-overwrite semantics.
|
|
103
|
+
const noFollow = fsConstants.O_NOFOLLOW ?? 0;
|
|
104
|
+
const flags = fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_TRUNC | noFollow;
|
|
105
|
+
const handle = await fs.open(filePath, flags, 0o600);
|
|
106
|
+
try {
|
|
107
|
+
await handle.writeFile(content, 'utf-8');
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
await handle.close();
|
|
111
|
+
}
|
|
112
|
+
if (process.platform !== 'win32') {
|
|
113
|
+
// Tighten perms even if the file pre-existed with a looser mode.
|
|
114
|
+
try {
|
|
115
|
+
await fs.chmod(filePath, 0o600);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Non-fatal.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return filePath;
|
|
122
|
+
}
|
package/package.json
CHANGED
|
@@ -1,80 +1,82 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "google-tools-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "The easiest MCP server for Google Workspace — Drive, Docs, Sheets, Gmail, Calendar, and Forms. 153 tools with one-click browser auth. Read Word docs, PDFs, and spreadsheets straight from Drive.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"google-tools-mcp": "dist/index.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"dist"
|
|
11
|
-
],
|
|
12
|
-
"keywords": [
|
|
13
|
-
"mcp",
|
|
14
|
-
"mcp-server",
|
|
15
|
-
"model-context-protocol",
|
|
16
|
-
"google",
|
|
17
|
-
"google-workspace",
|
|
18
|
-
"google-drive",
|
|
19
|
-
"google-docs",
|
|
20
|
-
"google-sheets",
|
|
21
|
-
"google-calendar",
|
|
22
|
-
"google-forms",
|
|
23
|
-
"gdrive",
|
|
24
|
-
"gmail",
|
|
25
|
-
"email",
|
|
26
|
-
"send-email",
|
|
27
|
-
"read-email",
|
|
28
|
-
"email-drafts",
|
|
29
|
-
"email-labels",
|
|
30
|
-
"email-filters",
|
|
31
|
-
"sheets",
|
|
32
|
-
"spreadsheet",
|
|
33
|
-
"docs",
|
|
34
|
-
"calendar",
|
|
35
|
-
"forms",
|
|
36
|
-
"drive",
|
|
37
|
-
"pdf",
|
|
38
|
-
"read-pdf",
|
|
39
|
-
"word",
|
|
40
|
-
"docx",
|
|
41
|
-
"read-docx",
|
|
42
|
-
"file-reader",
|
|
43
|
-
"oauth",
|
|
44
|
-
"easy-auth",
|
|
45
|
-
"browser-auth",
|
|
46
|
-
"claude",
|
|
47
|
-
"ai",
|
|
48
|
-
"llm",
|
|
49
|
-
"ai-tools"
|
|
50
|
-
],
|
|
51
|
-
"scripts": {
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "google-tools-mcp",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "The easiest MCP server for Google Workspace — Drive, Docs, Sheets, Gmail, Calendar, and Forms. 153 tools with one-click browser auth. Read Word docs, PDFs, and spreadsheets straight from Drive.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"google-tools-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"mcp",
|
|
14
|
+
"mcp-server",
|
|
15
|
+
"model-context-protocol",
|
|
16
|
+
"google",
|
|
17
|
+
"google-workspace",
|
|
18
|
+
"google-drive",
|
|
19
|
+
"google-docs",
|
|
20
|
+
"google-sheets",
|
|
21
|
+
"google-calendar",
|
|
22
|
+
"google-forms",
|
|
23
|
+
"gdrive",
|
|
24
|
+
"gmail",
|
|
25
|
+
"email",
|
|
26
|
+
"send-email",
|
|
27
|
+
"read-email",
|
|
28
|
+
"email-drafts",
|
|
29
|
+
"email-labels",
|
|
30
|
+
"email-filters",
|
|
31
|
+
"sheets",
|
|
32
|
+
"spreadsheet",
|
|
33
|
+
"docs",
|
|
34
|
+
"calendar",
|
|
35
|
+
"forms",
|
|
36
|
+
"drive",
|
|
37
|
+
"pdf",
|
|
38
|
+
"read-pdf",
|
|
39
|
+
"word",
|
|
40
|
+
"docx",
|
|
41
|
+
"read-docx",
|
|
42
|
+
"file-reader",
|
|
43
|
+
"oauth",
|
|
44
|
+
"easy-auth",
|
|
45
|
+
"browser-auth",
|
|
46
|
+
"claude",
|
|
47
|
+
"ai",
|
|
48
|
+
"llm",
|
|
49
|
+
"ai-tools"
|
|
50
|
+
],
|
|
51
|
+
"scripts": {
|
|
52
|
+
"local:tool": "node scripts/call-local-tool.js",
|
|
53
|
+
"start": "node dist/index.js",
|
|
54
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
55
|
+
"test:ci": "node --experimental-vm-modules node_modules/jest/bin/jest.js --ci --coverage"
|
|
56
|
+
},
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "git+https://github.com/karthikcsq/google-tools-mcp.git"
|
|
60
|
+
},
|
|
61
|
+
"homepage": "https://github.com/karthikcsq/google-tools-mcp#readme",
|
|
62
|
+
"bugs": {
|
|
63
|
+
"url": "https://github.com/karthikcsq/google-tools-mcp/issues"
|
|
64
|
+
},
|
|
65
|
+
"license": "MIT",
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"@clack/prompts": "^1.2.0",
|
|
68
|
+
"chalk": "^5.6.2",
|
|
69
|
+
"diff": "^7.0.0",
|
|
70
|
+
"fastmcp": "^3.24.0",
|
|
71
|
+
"google-auth-library": "^10.5.0",
|
|
72
|
+
"googleapis": "^171.4.0",
|
|
73
|
+
"mammoth": "^1.9.0",
|
|
74
|
+
"markdown-it": "^14.1.0",
|
|
75
|
+
"pdf-parse": "^1.1.1",
|
|
76
|
+
"zod": "^3.24.2"
|
|
77
|
+
},
|
|
78
|
+
"devDependencies": {
|
|
79
|
+
"@jest/globals": "^30.3.0",
|
|
80
|
+
"jest": "^30.3.0"
|
|
81
|
+
}
|
|
82
|
+
}
|