google-tools-mcp 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# google-tools-mcp
|
|
2
2
|
|
|
3
|
-
A unified MCP server for Google Workspace — Drive, Docs, Sheets, Gmail, and
|
|
3
|
+
A unified MCP server for Google Workspace — Drive, Docs, Sheets, Gmail, Calendar, and Forms — with **152 tools** across 9 categories.
|
|
4
4
|
|
|
5
5
|
All tools are loaded at startup so they're immediately available to your AI agent. No discovery step needed.
|
|
6
6
|
|
|
@@ -10,10 +10,10 @@ Most Google MCP servers split functionality across separate packages. This serve
|
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
|
-
- **
|
|
14
|
-
- **Single auth token** — one OAuth flow covers Drive, Docs, Sheets, Gmail, and
|
|
13
|
+
- **152 tools** across 9 categories, all available immediately
|
|
14
|
+
- **Single auth token** — one OAuth flow covers Drive, Docs, Sheets, Gmail, Calendar, and Forms
|
|
15
15
|
- **Lazy-loading auth** — no browser popup until your first tool call
|
|
16
|
-
- **No lazy tool loading** — all
|
|
16
|
+
- **No lazy tool loading** — all 150 tools are registered eagerly at startup since most MCP clients (including Claude Code) don't support `notifications/tools/list_changed`
|
|
17
17
|
- **Multi-profile support** — separate tokens per Google account
|
|
18
18
|
- **No telemetry**
|
|
19
19
|
|
|
@@ -23,10 +23,11 @@ Most Google MCP servers split functionality across separate packages. This serve
|
|
|
23
23
|
|
|
24
24
|
1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
|
|
25
25
|
2. Create a project (or use an existing one)
|
|
26
|
-
3. Enable the **Google Docs API**, **Google Sheets API**, **Google Drive API**, **Gmail API**, and **Google
|
|
27
|
-
4. Go to **
|
|
28
|
-
5.
|
|
29
|
-
6.
|
|
26
|
+
3. Enable the **Google Docs API**, **Google Sheets API**, **Google Drive API**, **Gmail API**, **Google Calendar API**, and **Google Forms API**
|
|
27
|
+
4. Go to **OAuth consent screen** and configure it (External is fine for personal use). If your app's publishing status is **Testing** (the default), you must add your Google account as a test user — go to **OAuth consent screen** → **Test users** → **Add users** and enter your email. Without this, Google will block the OAuth flow with an "Access denied" or "app not verified" error.
|
|
28
|
+
5. Go to **Credentials** → **Create Credentials** → **OAuth Client ID**
|
|
29
|
+
6. Select **Desktop application** as the application type
|
|
30
|
+
7. Download the credentials or note your **Client ID** and **Client Secret**
|
|
30
31
|
|
|
31
32
|
### Step 2: Provide Your Credentials
|
|
32
33
|
|
|
@@ -168,20 +169,20 @@ This stores tokens in `~/.config/google-tools-mcp/work/` instead of the default
|
|
|
168
169
|
|
|
169
170
|
## Tool Categories
|
|
170
171
|
|
|
171
|
-
### `files` (
|
|
172
|
+
### `files` (17 tools)
|
|
172
173
|
Google Drive file management and content reading.
|
|
173
174
|
|
|
174
|
-
`
|
|
175
|
+
`listDriveFiles`, `searchDocuments`, `getFileInfo`, `createFolder`, `listFolderContents`, `getFolderInfo`, `moveFile`, `copyFile`, `renameFile`, `deleteFile`, `createDocument`, `createDocumentFromTemplate`, `listSharedDrives`, `listSharedWithMe`, `downloadFile`, `readFile`, `searchFileContents`
|
|
175
176
|
|
|
176
|
-
### `documents` (
|
|
177
|
+
### `documents` (22 tools)
|
|
177
178
|
Google Docs read/write/format with markdown support.
|
|
178
179
|
|
|
179
|
-
`readDocument`, `appendText`, `
|
|
180
|
+
`readDocument`, `appendText`, `deleteRange`, `modifyText`, `findAndReplace`, `insertTable`, `insertTableWithData`, `insertPageBreak`, `insertImage`, `listTabs`, `addTab`, `renameTab`, `applyParagraphStyle`, `getFormatting`, `addComment`, `deleteComment`, `getComment`, `listComments`, `replyToComment`, `resolveComment`, `appendMarkdown`, `replaceDocumentWithMarkdown`
|
|
180
181
|
|
|
181
|
-
### `spreadsheets` (
|
|
182
|
+
### `spreadsheets` (29 tools)
|
|
182
183
|
Google Sheets operations.
|
|
183
184
|
|
|
184
|
-
`readSpreadsheet`, `writeSpreadsheet`, `batchWrite`, `appendRows`, `clearRange`, `createSpreadsheet`, `getSpreadsheetInfo`, `addSheet`, `deleteSheet`, `duplicateSheet`, `renameSheet`, `formatCells`, `readCellFormat`, `autoResizeColumns`, `freezeRowsAndColumns`, `setColumnWidths`, `addConditionalFormatting`, `copyFormatting`, `setDropdownValidation`, `createTable`, `deleteTable`, `getTable`, `listTables`, `appendTableRows`, `updateTableRange`, `insertChart`, `deleteChart`, `groupRows`, `ungroupAllRows
|
|
185
|
+
`readSpreadsheet`, `writeSpreadsheet`, `batchWrite`, `appendRows`, `clearRange`, `createSpreadsheet`, `getSpreadsheetInfo`, `addSheet`, `deleteSheet`, `duplicateSheet`, `renameSheet`, `formatCells`, `readCellFormat`, `autoResizeColumns`, `freezeRowsAndColumns`, `setColumnWidths`, `addConditionalFormatting`, `copyFormatting`, `setDropdownValidation`, `createTable`, `deleteTable`, `getTable`, `listTables`, `appendTableRows`, `updateTableRange`, `insertChart`, `deleteChart`, `groupRows`, `ungroupAllRows`
|
|
185
186
|
|
|
186
187
|
### `email` (19 tools)
|
|
187
188
|
Gmail messages and drafts.
|
|
@@ -208,6 +209,11 @@ Google Calendar — events, availability, and calendar management.
|
|
|
208
209
|
|
|
209
210
|
`list_calendars`, `get_events`, `manage_event`, `get_busy`, `get_free`, `move_event`, `list_recurring_event_instances`, `manage_calendar`
|
|
210
211
|
|
|
212
|
+
### `forms` (6 tools)
|
|
213
|
+
Google Forms — create/read forms, manage responses, and publish settings.
|
|
214
|
+
|
|
215
|
+
`create_form`, `get_form`, `batch_update_form`, `get_form_response`, `list_form_responses`, `set_publish_settings`
|
|
216
|
+
|
|
211
217
|
## Environment Variables
|
|
212
218
|
|
|
213
219
|
| Variable | Required | Description |
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDocsClient } from '../../../clients.js';
|
|
4
|
+
import { DocumentIdParameter, TextFindParameter } from '../../../types.js';
|
|
5
|
+
import * as GDocsHelpers from '../../../googleDocsApiHelpers.js';
|
|
6
|
+
|
|
7
|
+
const RangeTarget = z.object({
|
|
8
|
+
startIndex: z.number().int().min(1).describe('Start of range (inclusive, 1-based).'),
|
|
9
|
+
endIndex: z.number().int().min(1).describe('End of range (exclusive).'),
|
|
10
|
+
}).refine((d) => d.endIndex > d.startIndex, {
|
|
11
|
+
message: 'endIndex must be greater than startIndex',
|
|
12
|
+
path: ['endIndex'],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const GetFormattingParameters = DocumentIdParameter.extend({
|
|
16
|
+
target: z
|
|
17
|
+
.union([RangeTarget, TextFindParameter])
|
|
18
|
+
.describe('Target by range indices or text search.'),
|
|
19
|
+
tabId: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe('The ID of the specific tab to read from. If not specified, reads from the first tab.'),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Extracts text style and paragraph style info from document elements
|
|
27
|
+
* that overlap the given range.
|
|
28
|
+
*/
|
|
29
|
+
function extractFormattingFromContent(bodyContent, startIndex, endIndex) {
|
|
30
|
+
const textStyles = [];
|
|
31
|
+
const paragraphStyles = [];
|
|
32
|
+
|
|
33
|
+
for (const element of bodyContent) {
|
|
34
|
+
if (!element.paragraph) continue;
|
|
35
|
+
|
|
36
|
+
const para = element.paragraph;
|
|
37
|
+
// Check if this paragraph overlaps our range
|
|
38
|
+
const paraElements = para.elements || [];
|
|
39
|
+
let paragraphOverlaps = false;
|
|
40
|
+
|
|
41
|
+
for (const pe of paraElements) {
|
|
42
|
+
const elStart = pe.startIndex ?? 0;
|
|
43
|
+
const elEnd = pe.endIndex ?? 0;
|
|
44
|
+
|
|
45
|
+
// Check overlap with our target range
|
|
46
|
+
if (elEnd > startIndex && elStart < endIndex) {
|
|
47
|
+
paragraphOverlaps = true;
|
|
48
|
+
if (pe.textRun) {
|
|
49
|
+
const text = pe.textRun.content || '';
|
|
50
|
+
const style = pe.textRun.textStyle || {};
|
|
51
|
+
// Only include non-empty style properties
|
|
52
|
+
const cleanStyle = {};
|
|
53
|
+
if (style.bold) cleanStyle.bold = true;
|
|
54
|
+
if (style.italic) cleanStyle.italic = true;
|
|
55
|
+
if (style.underline) cleanStyle.underline = true;
|
|
56
|
+
if (style.strikethrough) cleanStyle.strikethrough = true;
|
|
57
|
+
if (style.fontSize) cleanStyle.fontSize = style.fontSize;
|
|
58
|
+
if (style.foregroundColor?.color?.rgbColor) cleanStyle.foregroundColor = style.foregroundColor.color.rgbColor;
|
|
59
|
+
if (style.backgroundColor?.color?.rgbColor) cleanStyle.backgroundColor = style.backgroundColor.color.rgbColor;
|
|
60
|
+
if (style.weightedFontFamily) cleanStyle.fontFamily = style.weightedFontFamily.fontFamily;
|
|
61
|
+
if (style.link) cleanStyle.link = style.link;
|
|
62
|
+
if (style.baselineOffset && style.baselineOffset !== 'BASELINE_OFFSET_UNSPECIFIED') cleanStyle.baselineOffset = style.baselineOffset;
|
|
63
|
+
|
|
64
|
+
textStyles.push({
|
|
65
|
+
startIndex: elStart,
|
|
66
|
+
endIndex: elEnd,
|
|
67
|
+
text: text.replace(/\n$/, ''),
|
|
68
|
+
style: cleanStyle,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (paragraphOverlaps && para.paragraphStyle) {
|
|
75
|
+
const ps = para.paragraphStyle;
|
|
76
|
+
const cleanStyle = {};
|
|
77
|
+
if (ps.namedStyleType && ps.namedStyleType !== 'NORMAL_TEXT') cleanStyle.namedStyleType = ps.namedStyleType;
|
|
78
|
+
if (ps.alignment && ps.alignment !== 'START') cleanStyle.alignment = ps.alignment;
|
|
79
|
+
if (ps.indentStart?.magnitude) cleanStyle.indentStart = ps.indentStart.magnitude;
|
|
80
|
+
if (ps.indentEnd?.magnitude) cleanStyle.indentEnd = ps.indentEnd.magnitude;
|
|
81
|
+
if (ps.spaceAbove?.magnitude) cleanStyle.spaceAbove = ps.spaceAbove.magnitude;
|
|
82
|
+
if (ps.spaceBelow?.magnitude) cleanStyle.spaceBelow = ps.spaceBelow.magnitude;
|
|
83
|
+
if (ps.keepWithNext) cleanStyle.keepWithNext = true;
|
|
84
|
+
if (ps.lineSpacing) cleanStyle.lineSpacing = ps.lineSpacing;
|
|
85
|
+
if (ps.direction && ps.direction !== 'LEFT_TO_RIGHT') cleanStyle.direction = ps.direction;
|
|
86
|
+
|
|
87
|
+
// Always include namedStyleType even if NORMAL_TEXT for clarity
|
|
88
|
+
if (!cleanStyle.namedStyleType) cleanStyle.namedStyleType = 'NORMAL_TEXT';
|
|
89
|
+
|
|
90
|
+
paragraphStyles.push({
|
|
91
|
+
paragraphStartIndex: element.startIndex,
|
|
92
|
+
paragraphEndIndex: element.endIndex,
|
|
93
|
+
style: cleanStyle,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { textStyles, paragraphStyles };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function register(server) {
|
|
102
|
+
server.addTool({
|
|
103
|
+
name: 'getFormatting',
|
|
104
|
+
description: 'Returns the text styling (bold, italic, font, colors, etc.) and paragraph styling ' +
|
|
105
|
+
'(alignment, headings, spacing, indentation) for a specific range or found text in a Google Doc. ' +
|
|
106
|
+
'Returns per-run text styles and per-paragraph styles, so mixed formatting (e.g. bold + italic spans) is fully represented.',
|
|
107
|
+
parameters: GetFormattingParameters,
|
|
108
|
+
execute: async (args, { log }) => {
|
|
109
|
+
const docs = await getDocsClient();
|
|
110
|
+
log.info(`getFormatting on doc ${args.documentId}: target=${JSON.stringify(args.target)}` +
|
|
111
|
+
`${args.tabId ? `, tab=${args.tabId}` : ''}`);
|
|
112
|
+
try {
|
|
113
|
+
// Resolve target to numeric indices
|
|
114
|
+
let startIndex;
|
|
115
|
+
let endIndex;
|
|
116
|
+
|
|
117
|
+
if ('textToFind' in args.target) {
|
|
118
|
+
const range = await GDocsHelpers.findTextRange(
|
|
119
|
+
docs, args.documentId, args.target.textToFind,
|
|
120
|
+
args.target.matchInstance, args.tabId
|
|
121
|
+
);
|
|
122
|
+
if (!range) {
|
|
123
|
+
throw new UserError(
|
|
124
|
+
`Could not find instance ${args.target.matchInstance ?? 1} of text "${args.target.textToFind}"${args.tabId ? ` in tab ${args.tabId}` : ''}.`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
startIndex = range.startIndex;
|
|
128
|
+
endIndex = range.endIndex;
|
|
129
|
+
} else {
|
|
130
|
+
startIndex = args.target.startIndex;
|
|
131
|
+
endIndex = args.target.endIndex;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Fetch document with full content
|
|
135
|
+
const needsTabsContent = !!args.tabId;
|
|
136
|
+
const res = await docs.documents.get({
|
|
137
|
+
documentId: args.documentId,
|
|
138
|
+
...(needsTabsContent && { includeTabsContent: true }),
|
|
139
|
+
fields: needsTabsContent ? '*' : 'body(content)',
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
let bodyContent;
|
|
143
|
+
if (args.tabId) {
|
|
144
|
+
const targetTab = GDocsHelpers.findTabById(res.data, args.tabId);
|
|
145
|
+
if (!targetTab) {
|
|
146
|
+
throw new UserError(`Tab with ID "${args.tabId}" not found in document.`);
|
|
147
|
+
}
|
|
148
|
+
bodyContent = targetTab.documentTab?.body?.content;
|
|
149
|
+
} else {
|
|
150
|
+
bodyContent = res.data.body?.content;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!bodyContent) {
|
|
154
|
+
throw new UserError('Document has no content.');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const result = extractFormattingFromContent(bodyContent, startIndex, endIndex);
|
|
158
|
+
|
|
159
|
+
return JSON.stringify({
|
|
160
|
+
range: { startIndex, endIndex },
|
|
161
|
+
textStyles: result.textStyles,
|
|
162
|
+
paragraphStyles: result.paragraphStyles,
|
|
163
|
+
}, null, 2);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
log.error(`Error in getFormatting for doc ${args.documentId}: ${error.message || error}`);
|
|
166
|
+
if (error instanceof UserError) throw error;
|
|
167
|
+
throw new UserError(`Failed to get formatting: ${error.message || 'Unknown error'}`);
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { UserError } from 'fastmcp';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { getDocsClient } from '../../clients.js';
|
|
4
|
-
import { DocumentIdParameter, TextFindParameter, TextStyleParameters } from '../../types.js';
|
|
4
|
+
import { DocumentIdParameter, TextFindParameter, TextStyleParameters, ParagraphStyleParameters } from '../../types.js';
|
|
5
5
|
import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
|
|
6
6
|
const RangeTarget = z
|
|
7
7
|
.object({
|
|
@@ -20,14 +20,15 @@ const ModifyTextParameters = DocumentIdParameter.extend({
|
|
|
20
20
|
.union([RangeTarget, TextFindParameter, InsertionTarget])
|
|
21
21
|
.describe('Target by range indices, text search, or insertion index.'),
|
|
22
22
|
text: z.string().optional().describe('New text to insert or replace with.'),
|
|
23
|
-
style: TextStyleParameters.optional().describe('Text formatting to apply.'),
|
|
23
|
+
style: TextStyleParameters.optional().describe('Text formatting to apply (bold, italic, font size, etc.).'),
|
|
24
|
+
paragraphStyle: ParagraphStyleParameters.optional().describe('Paragraph formatting to apply (alignment, indentation, headings, spacing, etc.).'),
|
|
24
25
|
tabId: z
|
|
25
26
|
.string()
|
|
26
27
|
.optional()
|
|
27
28
|
.describe('The ID of the specific tab to operate on. If not specified, operates on the first tab.'),
|
|
28
29
|
})
|
|
29
|
-
.refine((args) => args.text !== undefined || args.style !== undefined, {
|
|
30
|
-
message: 'At least one of text or
|
|
30
|
+
.refine((args) => args.text !== undefined || args.style !== undefined || args.paragraphStyle !== undefined, {
|
|
31
|
+
message: 'At least one of text, style, or paragraphStyle must be provided.',
|
|
31
32
|
})
|
|
32
33
|
.refine((args) => {
|
|
33
34
|
if ('insertionIndex' in args.target && args.text === undefined)
|
|
@@ -39,9 +40,9 @@ const ModifyTextParameters = DocumentIdParameter.extend({
|
|
|
39
40
|
* modifyText operation. Indices must already be resolved (no text-search here).
|
|
40
41
|
*/
|
|
41
42
|
export function buildModifyTextRequests(opts) {
|
|
42
|
-
const { startIndex, endIndex, text, style, tabId } = opts;
|
|
43
|
+
const { startIndex, endIndex, text, style, paragraphStyle, tabId } = opts;
|
|
43
44
|
const requests = [];
|
|
44
|
-
if (!text && !style)
|
|
45
|
+
if (!text && !style && !paragraphStyle)
|
|
45
46
|
return requests;
|
|
46
47
|
// 1. Delete existing content (only when replacing, not insert-only)
|
|
47
48
|
if (endIndex !== undefined && text !== undefined) {
|
|
@@ -57,7 +58,7 @@ export function buildModifyTextRequests(opts) {
|
|
|
57
58
|
location.tabId = tabId;
|
|
58
59
|
requests.push({ insertText: { location, text } });
|
|
59
60
|
}
|
|
60
|
-
// 3. Apply formatting
|
|
61
|
+
// 3. Apply text formatting
|
|
61
62
|
if (style) {
|
|
62
63
|
const formatStart = startIndex;
|
|
63
64
|
const formatEnd = text !== undefined
|
|
@@ -72,20 +73,36 @@ export function buildModifyTextRequests(opts) {
|
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
}
|
|
76
|
+
// 4. Apply paragraph formatting
|
|
77
|
+
if (paragraphStyle) {
|
|
78
|
+
const formatStart = startIndex;
|
|
79
|
+
const formatEnd = text !== undefined
|
|
80
|
+
? startIndex + text.length
|
|
81
|
+
: endIndex !== undefined
|
|
82
|
+
? endIndex
|
|
83
|
+
: startIndex;
|
|
84
|
+
if (formatEnd > formatStart) {
|
|
85
|
+
const requestInfo = GDocsHelpers.buildUpdateParagraphStyleRequest(formatStart, formatEnd, paragraphStyle, tabId);
|
|
86
|
+
if (requestInfo) {
|
|
87
|
+
requests.push(requestInfo.request);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
75
91
|
return requests;
|
|
76
92
|
}
|
|
77
93
|
export function register(server) {
|
|
78
94
|
server.addTool({
|
|
79
95
|
name: 'modifyText',
|
|
80
96
|
description: 'Combines text replacement/insertion and formatting in one atomic operation. ' +
|
|
81
|
-
'Can insert text at a position, replace a range or found text, apply
|
|
82
|
-
"or any combination. Use readGoogleDoc with format='json' to determine indices.",
|
|
97
|
+
'Can insert text at a position, replace a range or found text, apply text styling (bold, italic, etc.), ' +
|
|
98
|
+
"apply paragraph styling (alignment, headings, spacing, etc.), or any combination. Use readGoogleDoc with format='json' to determine indices.",
|
|
83
99
|
parameters: ModifyTextParameters,
|
|
84
100
|
execute: async (args, { log }) => {
|
|
85
101
|
const docs = await getDocsClient();
|
|
86
102
|
log.info(`modifyText on doc ${args.documentId}: target=${JSON.stringify(args.target)}` +
|
|
87
103
|
`${args.text !== undefined ? `, text="${args.text.substring(0, 50)}"` : ''}` +
|
|
88
104
|
`${args.style ? `, style=${JSON.stringify(args.style)}` : ''}` +
|
|
105
|
+
`${args.paragraphStyle ? `, paragraphStyle=${JSON.stringify(args.paragraphStyle)}` : ''}` +
|
|
89
106
|
`${args.tabId ? `, tab=${args.tabId}` : ''}`);
|
|
90
107
|
try {
|
|
91
108
|
// Verify tab exists if specified
|
|
@@ -131,6 +148,7 @@ export function register(server) {
|
|
|
131
148
|
endIndex,
|
|
132
149
|
text: args.text,
|
|
133
150
|
style: args.style,
|
|
151
|
+
paragraphStyle: args.paragraphStyle,
|
|
134
152
|
tabId: args.tabId,
|
|
135
153
|
});
|
|
136
154
|
if (requests.length === 0) {
|
|
@@ -144,7 +162,9 @@ export function register(server) {
|
|
|
144
162
|
else if (args.text !== undefined)
|
|
145
163
|
actions.push('inserted text');
|
|
146
164
|
if (args.style)
|
|
147
|
-
actions.push('applied formatting');
|
|
165
|
+
actions.push('applied text formatting');
|
|
166
|
+
if (args.paragraphStyle)
|
|
167
|
+
actions.push('applied paragraph formatting');
|
|
148
168
|
return `Successfully ${actions.join(' and ')} at range ${startIndex}-${endIndex ?? startIndex + (args.text?.length ?? 0)}${args.tabId ? ` in tab ${args.tabId}` : ''}.`;
|
|
149
169
|
}
|
|
150
170
|
catch (error) {
|
package/dist/tools/index.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// so lazy-loading doesn't work.)
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import * as fs from 'fs/promises';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
6
8
|
import { getTokenPath } from '../auth.js';
|
|
7
9
|
import { resetClients } from '../clients.js';
|
|
8
10
|
import { logger } from '../logger.js';
|
|
@@ -81,6 +83,23 @@ export async function registerAllTools(server) {
|
|
|
81
83
|
}
|
|
82
84
|
logger.info(`Loaded all ${Object.keys(CATEGORIES).length} categories at startup.`);
|
|
83
85
|
|
|
86
|
+
// --- Help tool (always available) ---
|
|
87
|
+
server.addTool({
|
|
88
|
+
name: 'help',
|
|
89
|
+
description:
|
|
90
|
+
'Show documentation for google-tools-mcp: setup instructions, available tool categories, environment variables, and troubleshooting. Call this when you need guidance on how to use the Google Workspace tools.',
|
|
91
|
+
parameters: z.object({}),
|
|
92
|
+
execute: async () => {
|
|
93
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
94
|
+
const readmePath = path.resolve(__dirname, '..', '..', 'README.md');
|
|
95
|
+
try {
|
|
96
|
+
return await fs.readFile(readmePath, 'utf-8');
|
|
97
|
+
} catch {
|
|
98
|
+
return 'README not found. Visit https://www.npmjs.com/package/google-tools-mcp for documentation.';
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
84
103
|
// --- Logout tool (always available) ---
|
|
85
104
|
server.addTool({
|
|
86
105
|
name: 'logout',
|