google-tools-mcp 1.0.1 → 1.0.3
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 +6 -9
- package/dist/auth.js +4 -0
- package/dist/clients.js +16 -0
- package/dist/index.js +3 -8
- package/dist/tools/forms/batchUpdateForm.js +80 -0
- package/dist/tools/forms/createForm.js +54 -0
- package/dist/tools/forms/getForm.js +118 -0
- package/dist/tools/forms/getFormResponse.js +45 -0
- package/dist/tools/forms/index.js +15 -0
- package/dist/tools/forms/listFormResponses.js +56 -0
- package/dist/tools/forms/setPublishSettings.js +58 -0
- package/dist/tools/index.js +15 -82
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
# google-tools-mcp
|
|
2
2
|
|
|
3
|
-
A unified MCP server for Google Workspace — Drive, Docs, Sheets, Gmail, and Calendar — with **
|
|
3
|
+
A unified MCP server for Google Workspace — Drive, Docs, Sheets, Gmail, and Calendar — with **146 tools** across 8 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
|
|
|
7
7
|
## Why This Exists
|
|
8
8
|
|
|
9
|
-
Most Google MCP servers
|
|
10
|
-
|
|
11
|
-
This server starts with **2 tools** and loads categories on demand — so you only pay the context cost for what you actually use.
|
|
9
|
+
Most Google MCP servers split functionality across separate packages. This server combines everything into one — single auth token, single process, single config.
|
|
12
10
|
|
|
13
11
|
## Features
|
|
14
12
|
|
|
15
|
-
- **
|
|
13
|
+
- **146 tools** across 8 categories, all available immediately
|
|
16
14
|
- **Single auth token** — one OAuth flow covers Drive, Docs, Sheets, Gmail, and Calendar
|
|
17
15
|
- **Lazy-loading auth** — no browser popup until your first tool call
|
|
16
|
+
- **No lazy tool loading** — all 146 tools are registered eagerly at startup since most MCP clients (including Claude Code) don't support `notifications/tools/list_changed`
|
|
18
17
|
- **Multi-profile support** — separate tokens per Google account
|
|
19
18
|
- **No telemetry**
|
|
20
19
|
|
|
@@ -169,8 +168,6 @@ This stores tokens in `~/.config/google-tools-mcp/work/` instead of the default
|
|
|
169
168
|
|
|
170
169
|
## Tool Categories
|
|
171
170
|
|
|
172
|
-
Call `load_google_tools` with one or more category names to load them. You can load multiple at once.
|
|
173
|
-
|
|
174
171
|
### `files` (16 tools)
|
|
175
172
|
Google Drive file management and content reading.
|
|
176
173
|
|
|
@@ -230,7 +227,7 @@ This package replaces both [`gdrive-tools-mcp`](https://www.npmjs.com/package/gd
|
|
|
230
227
|
|
|
231
228
|
1. Replace both MCP server entries with a single `google-tools-mcp` entry
|
|
232
229
|
2. Re-authenticate (the combined server uses its own config dir at `~/.config/google-tools-mcp/`)
|
|
233
|
-
3. All
|
|
230
|
+
3. All tools are available immediately — no discovery step needed
|
|
234
231
|
|
|
235
232
|
## License
|
|
236
233
|
|
package/dist/auth.js
CHANGED
|
@@ -48,6 +48,10 @@ const SCOPES = [
|
|
|
48
48
|
'https://www.googleapis.com/auth/gmail.settings.sharing',
|
|
49
49
|
// Calendar
|
|
50
50
|
'https://www.googleapis.com/auth/calendar',
|
|
51
|
+
// Forms
|
|
52
|
+
'https://www.googleapis.com/auth/forms.body',
|
|
53
|
+
'https://www.googleapis.com/auth/forms.body.readonly',
|
|
54
|
+
'https://www.googleapis.com/auth/forms.responses.readonly',
|
|
51
55
|
];
|
|
52
56
|
|
|
53
57
|
// ---------------------------------------------------------------------------
|
package/dist/clients.js
CHANGED
|
@@ -12,6 +12,7 @@ let googleSheets = null;
|
|
|
12
12
|
let googleScript = null;
|
|
13
13
|
let gmailClient = null;
|
|
14
14
|
let calendarClient = null;
|
|
15
|
+
let formsClient = null;
|
|
15
16
|
|
|
16
17
|
async function ensureAuth() {
|
|
17
18
|
if (authClient) return;
|
|
@@ -58,6 +59,14 @@ export async function initializeCalendarClient() {
|
|
|
58
59
|
return { authClient, calendarClient };
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
// --- Forms client ---
|
|
63
|
+
export async function initializeFormsClient() {
|
|
64
|
+
if (formsClient) return { authClient, formsClient };
|
|
65
|
+
await ensureAuth();
|
|
66
|
+
if (!formsClient) formsClient = google.forms({ version: 'v1', auth: authClient });
|
|
67
|
+
return { authClient, formsClient };
|
|
68
|
+
}
|
|
69
|
+
|
|
61
70
|
// --- Reset all clients (used by logout) ---
|
|
62
71
|
export function resetClients() {
|
|
63
72
|
authClient = null;
|
|
@@ -67,6 +76,7 @@ export function resetClients() {
|
|
|
67
76
|
googleScript = null;
|
|
68
77
|
gmailClient = null;
|
|
69
78
|
calendarClient = null;
|
|
79
|
+
formsClient = null;
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
// --- Individual client getters ---
|
|
@@ -111,3 +121,9 @@ export async function getCalendarClient() {
|
|
|
111
121
|
if (!calendar) throw new UserError('Google Calendar client is not initialized.');
|
|
112
122
|
return calendar;
|
|
113
123
|
}
|
|
124
|
+
|
|
125
|
+
export async function getFormsClient() {
|
|
126
|
+
const { formsClient: forms } = await initializeFormsClient();
|
|
127
|
+
if (!forms) throw new UserError('Google Forms client is not initialized.');
|
|
128
|
+
return forms;
|
|
129
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// google-tools-mcp — Combined Google Workspace MCP server
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// All tool categories (Drive, Docs, Sheets, Gmail, Calendar) are loaded at
|
|
5
|
+
// startup so they're available in the initial tools/list response.
|
|
6
6
|
//
|
|
7
7
|
// Usage:
|
|
8
8
|
// google-tools-mcp Start the MCP server (default)
|
|
9
9
|
// google-tools-mcp auth Run the interactive OAuth flow
|
|
10
10
|
import { FastMCP } from 'fastmcp';
|
|
11
|
-
import { collectToolsWhileRegistering, installCachedToolsListHandler } from './cachedToolsList.js';
|
|
12
11
|
import { registerAllTools } from './tools/index.js';
|
|
13
12
|
import { logger } from './logger.js';
|
|
14
13
|
|
|
@@ -38,17 +37,13 @@ const server = new FastMCP({
|
|
|
38
37
|
version: '1.0.0',
|
|
39
38
|
});
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
collectToolsWhileRegistering(server, registeredTools);
|
|
43
|
-
registerAllTools(server);
|
|
40
|
+
await registerAllTools(server);
|
|
44
41
|
|
|
45
42
|
try {
|
|
46
43
|
logger.info('Starting google-tools-mcp server...');
|
|
47
44
|
await server.start({ transportType: 'stdio' });
|
|
48
|
-
installCachedToolsListHandler(server, registeredTools);
|
|
49
45
|
logger.info('MCP Server running using stdio. Awaiting client connection...');
|
|
50
46
|
logger.info('Google auth will run automatically on first tool call.');
|
|
51
|
-
logger.info(`${registeredTools.length} tools registered at startup (discovery + logout). Call load_google_tools to load more.`);
|
|
52
47
|
} catch (startError) {
|
|
53
48
|
logger.error('FATAL: Server failed to start:', startError.message || startError);
|
|
54
49
|
process.exit(1);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getFormsClient } from '../../clients.js';
|
|
4
|
+
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.addTool({
|
|
7
|
+
name: 'batch_update_form',
|
|
8
|
+
description:
|
|
9
|
+
'Applies one or more updates to a Google Form. Supports creating, updating, deleting, and moving items, as well as updating form info and settings. ' +
|
|
10
|
+
'Each request in the array should be an object with exactly one key: createItem, updateItem, deleteItem, moveItem, updateFormInfo, or updateSettings. ' +
|
|
11
|
+
'See the Google Forms API batchUpdate documentation for the full request schema.',
|
|
12
|
+
parameters: z.object({
|
|
13
|
+
formId: z.string().describe('The ID of the Google Form to update'),
|
|
14
|
+
requests: z
|
|
15
|
+
.array(z.record(z.any()))
|
|
16
|
+
.min(1)
|
|
17
|
+
.describe(
|
|
18
|
+
'Array of update requests. Each object should have one key: createItem, updateItem, deleteItem, moveItem, updateFormInfo, or updateSettings',
|
|
19
|
+
),
|
|
20
|
+
}),
|
|
21
|
+
execute: async (args, { log }) => {
|
|
22
|
+
const forms = await getFormsClient();
|
|
23
|
+
log.info(`Batch updating form ${args.formId} with ${args.requests.length} request(s)`);
|
|
24
|
+
|
|
25
|
+
const validKeys = new Set([
|
|
26
|
+
'createItem',
|
|
27
|
+
'updateItem',
|
|
28
|
+
'deleteItem',
|
|
29
|
+
'moveItem',
|
|
30
|
+
'updateFormInfo',
|
|
31
|
+
'updateSettings',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
for (const req of args.requests) {
|
|
35
|
+
const keys = Object.keys(req);
|
|
36
|
+
if (keys.length !== 1 || !validKeys.has(keys[0])) {
|
|
37
|
+
throw new UserError(
|
|
38
|
+
`Invalid request object. Each request must have exactly one key from: ${[...validKeys].join(', ')}. Got: ${keys.join(', ')}`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const response = await forms.forms.batchUpdate({
|
|
45
|
+
formId: args.formId,
|
|
46
|
+
requestBody: { requests: args.requests },
|
|
47
|
+
});
|
|
48
|
+
const data = response.data;
|
|
49
|
+
const replies = data.replies || [];
|
|
50
|
+
|
|
51
|
+
const result = {
|
|
52
|
+
requestsApplied: args.requests.length,
|
|
53
|
+
repliesReceived: replies.length,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const createdItems = [];
|
|
57
|
+
for (const reply of replies) {
|
|
58
|
+
if (reply.createItem) {
|
|
59
|
+
const ci = reply.createItem;
|
|
60
|
+
createdItems.push({
|
|
61
|
+
itemId: ci.itemId,
|
|
62
|
+
questionId: ci.questionId?.[0] || null,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (createdItems.length > 0) {
|
|
67
|
+
result.createdItems = createdItems;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return JSON.stringify(result, null, 2);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
log.error(`Error batch updating form: ${error.message || error}`);
|
|
73
|
+
if (error.code === 401)
|
|
74
|
+
throw new UserError('Authentication failed. Try logging out and re-authenticating.');
|
|
75
|
+
if (error.code === 404) throw new UserError(`Form not found: ${args.formId}`);
|
|
76
|
+
throw new UserError(`Failed to batch update form: ${error.message || 'Unknown error'}`);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getFormsClient } from '../../clients.js';
|
|
4
|
+
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.addTool({
|
|
7
|
+
name: 'create_form',
|
|
8
|
+
description:
|
|
9
|
+
'Creates a new Google Form with a title and optional description. Returns the form ID, edit URL, and responder URL.',
|
|
10
|
+
parameters: z.object({
|
|
11
|
+
title: z.string().describe('The title of the form (shown to respondents)'),
|
|
12
|
+
description: z.string().optional().describe('A description displayed at the top of the form'),
|
|
13
|
+
documentTitle: z
|
|
14
|
+
.string()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('The document title (shown in Drive). Defaults to the form title if not provided'),
|
|
17
|
+
}),
|
|
18
|
+
execute: async (args, { log }) => {
|
|
19
|
+
const forms = await getFormsClient();
|
|
20
|
+
log.info(`Creating form: ${args.title}`);
|
|
21
|
+
|
|
22
|
+
const body = {
|
|
23
|
+
info: {
|
|
24
|
+
title: args.title,
|
|
25
|
+
documentTitle: args.documentTitle || args.title,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
if (args.description) {
|
|
29
|
+
body.info.description = args.description;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const response = await forms.forms.create({ requestBody: body });
|
|
34
|
+
const form = response.data;
|
|
35
|
+
|
|
36
|
+
return JSON.stringify(
|
|
37
|
+
{
|
|
38
|
+
formId: form.formId,
|
|
39
|
+
title: form.info?.title,
|
|
40
|
+
editUrl: `https://docs.google.com/forms/d/${form.formId}/edit`,
|
|
41
|
+
responderUrl: form.responderUri || `https://docs.google.com/forms/d/${form.formId}/viewform`,
|
|
42
|
+
},
|
|
43
|
+
null,
|
|
44
|
+
2,
|
|
45
|
+
);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
log.error(`Error creating form: ${error.message || error}`);
|
|
48
|
+
if (error.code === 401)
|
|
49
|
+
throw new UserError('Authentication failed. Try logging out and re-authenticating.');
|
|
50
|
+
throw new UserError(`Failed to create form: ${error.message || 'Unknown error'}`);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getFormsClient } from '../../clients.js';
|
|
4
|
+
|
|
5
|
+
function extractOptionValues(options) {
|
|
6
|
+
if (!options) return [];
|
|
7
|
+
return options.filter((o) => o.value).map((o) => {
|
|
8
|
+
const opt = { value: o.value };
|
|
9
|
+
if (o.isOther) opt.isOther = true;
|
|
10
|
+
if (o.image) opt.image = o.image;
|
|
11
|
+
if (o.goToAction) opt.goToAction = o.goToAction;
|
|
12
|
+
if (o.goToSectionId) opt.goToSectionId = o.goToSectionId;
|
|
13
|
+
return opt;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getQuestionType(question) {
|
|
18
|
+
if (question.choiceQuestion) return question.choiceQuestion.type || 'RADIO';
|
|
19
|
+
if (question.textQuestion) {
|
|
20
|
+
return question.textQuestion.paragraph ? 'PARAGRAPH' : 'TEXT';
|
|
21
|
+
}
|
|
22
|
+
if (question.scaleQuestion) return 'SCALE';
|
|
23
|
+
if (question.dateQuestion) return 'DATE';
|
|
24
|
+
if (question.timeQuestion) return 'TIME';
|
|
25
|
+
if (question.fileUploadQuestion) return 'FILE_UPLOAD';
|
|
26
|
+
if (question.ratingQuestion) return 'RATING';
|
|
27
|
+
if (question.rowQuestion) return 'GRID_ROW';
|
|
28
|
+
return 'QUESTION';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function serializeFormItem(item, index) {
|
|
32
|
+
const result = {
|
|
33
|
+
index,
|
|
34
|
+
itemId: item.itemId,
|
|
35
|
+
title: item.title || '',
|
|
36
|
+
description: item.description || '',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
if (item.questionItem) {
|
|
40
|
+
const q = item.questionItem.question;
|
|
41
|
+
result.type = getQuestionType(q);
|
|
42
|
+
result.required = q.required || false;
|
|
43
|
+
result.questionId = q.questionId || null;
|
|
44
|
+
if (q.choiceQuestion) {
|
|
45
|
+
result.options = extractOptionValues(q.choiceQuestion.options);
|
|
46
|
+
}
|
|
47
|
+
} else if (item.questionGroupItem) {
|
|
48
|
+
const group = item.questionGroupItem;
|
|
49
|
+
result.type = 'GRID';
|
|
50
|
+
result.grid = {
|
|
51
|
+
columns: group.grid?.columns?.options
|
|
52
|
+
? extractOptionValues(group.grid.columns.options)
|
|
53
|
+
: [],
|
|
54
|
+
};
|
|
55
|
+
if (group.questions) {
|
|
56
|
+
result.grid.rows = group.questions.map((q) => ({
|
|
57
|
+
questionId: q.questionId,
|
|
58
|
+
title: q.rowQuestion?.title || '',
|
|
59
|
+
required: q.required || false,
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
} else if (item.pageBreakItem !== undefined) {
|
|
63
|
+
result.type = 'PAGE_BREAK';
|
|
64
|
+
} else if (item.textItem !== undefined) {
|
|
65
|
+
result.type = 'TEXT_ITEM';
|
|
66
|
+
} else if (item.imageItem) {
|
|
67
|
+
result.type = 'IMAGE';
|
|
68
|
+
} else if (item.videoItem) {
|
|
69
|
+
result.type = 'VIDEO';
|
|
70
|
+
} else {
|
|
71
|
+
result.type = 'UNKNOWN';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function register(server) {
|
|
78
|
+
server.addTool({
|
|
79
|
+
name: 'get_form',
|
|
80
|
+
description:
|
|
81
|
+
'Retrieves a Google Form by ID. Returns the form title, description, URLs, and all items (questions, sections, images, etc.) with their types, options, and metadata.',
|
|
82
|
+
parameters: z.object({
|
|
83
|
+
formId: z.string().describe('The ID of the Google Form to retrieve'),
|
|
84
|
+
}),
|
|
85
|
+
execute: async (args, { log }) => {
|
|
86
|
+
const forms = await getFormsClient();
|
|
87
|
+
log.info(`Getting form: ${args.formId}`);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const response = await forms.forms.get({ formId: args.formId });
|
|
91
|
+
const form = response.data;
|
|
92
|
+
const items = (form.items || []).map((item, i) => serializeFormItem(item, i));
|
|
93
|
+
|
|
94
|
+
return JSON.stringify(
|
|
95
|
+
{
|
|
96
|
+
formId: form.formId,
|
|
97
|
+
title: form.info?.title || '',
|
|
98
|
+
description: form.info?.description || '',
|
|
99
|
+
documentTitle: form.info?.documentTitle || '',
|
|
100
|
+
editUrl: `https://docs.google.com/forms/d/${form.formId}/edit`,
|
|
101
|
+
responderUrl:
|
|
102
|
+
form.responderUri || `https://docs.google.com/forms/d/${form.formId}/viewform`,
|
|
103
|
+
itemCount: items.length,
|
|
104
|
+
items,
|
|
105
|
+
},
|
|
106
|
+
null,
|
|
107
|
+
2,
|
|
108
|
+
);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
log.error(`Error getting form: ${error.message || error}`);
|
|
111
|
+
if (error.code === 401)
|
|
112
|
+
throw new UserError('Authentication failed. Try logging out and re-authenticating.');
|
|
113
|
+
if (error.code === 404) throw new UserError(`Form not found: ${args.formId}`);
|
|
114
|
+
throw new UserError(`Failed to get form: ${error.message || 'Unknown error'}`);
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getFormsClient } from '../../clients.js';
|
|
4
|
+
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.addTool({
|
|
7
|
+
name: 'get_form_response',
|
|
8
|
+
description:
|
|
9
|
+
'Retrieves a single response from a Google Form by response ID. Returns the response timestamps and all answers keyed by question ID.',
|
|
10
|
+
parameters: z.object({
|
|
11
|
+
formId: z.string().describe('The ID of the Google Form'),
|
|
12
|
+
responseId: z.string().describe('The ID of the specific response to retrieve'),
|
|
13
|
+
}),
|
|
14
|
+
execute: async (args, { log }) => {
|
|
15
|
+
const forms = await getFormsClient();
|
|
16
|
+
log.info(`Getting response ${args.responseId} for form: ${args.formId}`);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const response = await forms.forms.responses.get({
|
|
20
|
+
formId: args.formId,
|
|
21
|
+
responseId: args.responseId,
|
|
22
|
+
});
|
|
23
|
+
const r = response.data;
|
|
24
|
+
|
|
25
|
+
return JSON.stringify(
|
|
26
|
+
{
|
|
27
|
+
responseId: r.responseId,
|
|
28
|
+
createTime: r.createTime,
|
|
29
|
+
lastSubmittedTime: r.lastSubmittedTime,
|
|
30
|
+
answers: r.answers || {},
|
|
31
|
+
},
|
|
32
|
+
null,
|
|
33
|
+
2,
|
|
34
|
+
);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
log.error(`Error getting form response: ${error.message || error}`);
|
|
37
|
+
if (error.code === 401)
|
|
38
|
+
throw new UserError('Authentication failed. Try logging out and re-authenticating.');
|
|
39
|
+
if (error.code === 404)
|
|
40
|
+
throw new UserError(`Form or response not found: ${args.formId} / ${args.responseId}`);
|
|
41
|
+
throw new UserError(`Failed to get form response: ${error.message || 'Unknown error'}`);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { register as createForm } from './createForm.js';
|
|
2
|
+
import { register as getForm } from './getForm.js';
|
|
3
|
+
import { register as listFormResponses } from './listFormResponses.js';
|
|
4
|
+
import { register as getFormResponse } from './getFormResponse.js';
|
|
5
|
+
import { register as batchUpdateForm } from './batchUpdateForm.js';
|
|
6
|
+
import { register as setPublishSettings } from './setPublishSettings.js';
|
|
7
|
+
|
|
8
|
+
export function registerFormsTools(server) {
|
|
9
|
+
createForm(server);
|
|
10
|
+
getForm(server);
|
|
11
|
+
listFormResponses(server);
|
|
12
|
+
getFormResponse(server);
|
|
13
|
+
batchUpdateForm(server);
|
|
14
|
+
setPublishSettings(server);
|
|
15
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getFormsClient } from '../../clients.js';
|
|
4
|
+
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.addTool({
|
|
7
|
+
name: 'list_form_responses',
|
|
8
|
+
description:
|
|
9
|
+
'Lists responses submitted to a Google Form. Returns a paginated list of responses with timestamps and answers.',
|
|
10
|
+
parameters: z.object({
|
|
11
|
+
formId: z.string().describe('The ID of the Google Form'),
|
|
12
|
+
pageSize: z
|
|
13
|
+
.number()
|
|
14
|
+
.min(1)
|
|
15
|
+
.max(5000)
|
|
16
|
+
.optional()
|
|
17
|
+
.describe('Number of responses to return per page (default 10, max 5000)'),
|
|
18
|
+
pageToken: z.string().optional().describe('Token for fetching the next page of results'),
|
|
19
|
+
}),
|
|
20
|
+
execute: async (args, { log }) => {
|
|
21
|
+
const forms = await getFormsClient();
|
|
22
|
+
log.info(`Listing responses for form: ${args.formId}`);
|
|
23
|
+
|
|
24
|
+
const params = { formId: args.formId };
|
|
25
|
+
if (args.pageSize) params.pageSize = args.pageSize;
|
|
26
|
+
if (args.pageToken) params.pageToken = args.pageToken;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const response = await forms.forms.responses.list(params);
|
|
30
|
+
const data = response.data;
|
|
31
|
+
const responses = (data.responses || []).map((r) => ({
|
|
32
|
+
responseId: r.responseId,
|
|
33
|
+
createTime: r.createTime,
|
|
34
|
+
lastSubmittedTime: r.lastSubmittedTime,
|
|
35
|
+
answers: r.answers || {},
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
return JSON.stringify(
|
|
39
|
+
{
|
|
40
|
+
totalResponses: responses.length,
|
|
41
|
+
nextPageToken: data.nextPageToken || null,
|
|
42
|
+
responses,
|
|
43
|
+
},
|
|
44
|
+
null,
|
|
45
|
+
2,
|
|
46
|
+
);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
log.error(`Error listing form responses: ${error.message || error}`);
|
|
49
|
+
if (error.code === 401)
|
|
50
|
+
throw new UserError('Authentication failed. Try logging out and re-authenticating.');
|
|
51
|
+
if (error.code === 404) throw new UserError(`Form not found: ${args.formId}`);
|
|
52
|
+
throw new UserError(`Failed to list form responses: ${error.message || 'Unknown error'}`);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getFormsClient } from '../../clients.js';
|
|
4
|
+
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.addTool({
|
|
7
|
+
name: 'set_publish_settings',
|
|
8
|
+
description:
|
|
9
|
+
'Updates the publish settings of a Google Form. Controls whether the form is published as a template and whether respondents must be authenticated.',
|
|
10
|
+
parameters: z.object({
|
|
11
|
+
formId: z.string().describe('The ID of the Google Form'),
|
|
12
|
+
publishAsTemplate: z
|
|
13
|
+
.boolean()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe('Whether to publish the form as a reusable template'),
|
|
16
|
+
requireAuthentication: z
|
|
17
|
+
.boolean()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe('Whether respondents must sign in with a Google account to submit the form'),
|
|
20
|
+
}),
|
|
21
|
+
execute: async (args, { log }) => {
|
|
22
|
+
const forms = await getFormsClient();
|
|
23
|
+
log.info(`Setting publish settings for form: ${args.formId}`);
|
|
24
|
+
|
|
25
|
+
const body = {};
|
|
26
|
+
if (args.publishAsTemplate !== undefined) {
|
|
27
|
+
body.publishAsTemplate = args.publishAsTemplate;
|
|
28
|
+
}
|
|
29
|
+
if (args.requireAuthentication !== undefined) {
|
|
30
|
+
body.requireAuthentication = args.requireAuthentication;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (Object.keys(body).length === 0) {
|
|
34
|
+
throw new UserError('At least one setting (publishAsTemplate or requireAuthentication) must be provided.');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
await forms.forms.setPublishSettings({
|
|
39
|
+
formId: args.formId,
|
|
40
|
+
requestBody: body,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return JSON.stringify({
|
|
44
|
+
success: true,
|
|
45
|
+
formId: args.formId,
|
|
46
|
+
message: 'Publish settings updated successfully.',
|
|
47
|
+
appliedSettings: body,
|
|
48
|
+
}, null, 2);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
log.error(`Error setting publish settings: ${error.message || error}`);
|
|
51
|
+
if (error.code === 401)
|
|
52
|
+
throw new UserError('Authentication failed. Try logging out and re-authenticating.');
|
|
53
|
+
if (error.code === 404) throw new UserError(`Form not found: ${args.formId}`);
|
|
54
|
+
throw new UserError(`Failed to set publish settings: ${error.message || 'Unknown error'}`);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
// Tool
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// When a category is loaded, its tools are dynamically registered and the
|
|
5
|
-
// client is notified via tools/list_changed so it picks them up.
|
|
1
|
+
// Tool registration — all categories loaded eagerly at startup.
|
|
2
|
+
// (Claude Code doesn't support notifications/tools/list_changed,
|
|
3
|
+
// so lazy-loading doesn't work.)
|
|
6
4
|
import { z } from 'zod';
|
|
7
5
|
import * as fs from 'fs/promises';
|
|
8
6
|
import { getTokenPath } from '../auth.js';
|
|
@@ -10,11 +8,8 @@ import { resetClients } from '../clients.js';
|
|
|
10
8
|
import { logger } from '../logger.js';
|
|
11
9
|
|
|
12
10
|
// --- Category registry ---
|
|
13
|
-
// Maps category names to { loader, description, toolCount }
|
|
14
11
|
const CATEGORIES = {
|
|
15
12
|
files: {
|
|
16
|
-
description: 'Google Drive file management — list, search, create, copy, move, rename, delete files/folders, read file contents (pdf, docx), search within files',
|
|
17
|
-
toolCount: 16,
|
|
18
13
|
async loader(server) {
|
|
19
14
|
const { registerDriveTools } = await import('./drive/index.js');
|
|
20
15
|
const { registerExtrasTools } = await import('./extras/index.js');
|
|
@@ -23,8 +18,6 @@ const CATEGORIES = {
|
|
|
23
18
|
},
|
|
24
19
|
},
|
|
25
20
|
documents: {
|
|
26
|
-
description: 'Google Docs — read, write, insert text/tables/images, formatting, comments, tabs, markdown conversion',
|
|
27
|
-
toolCount: 23,
|
|
28
21
|
async loader(server) {
|
|
29
22
|
const { registerDocsTools } = await import('./docs/index.js');
|
|
30
23
|
const { registerUtilsTools } = await import('./utils/index.js');
|
|
@@ -33,16 +26,12 @@ const CATEGORIES = {
|
|
|
33
26
|
},
|
|
34
27
|
},
|
|
35
28
|
spreadsheets: {
|
|
36
|
-
description: 'Google Sheets — read, write, format cells, charts, tables, conditional formatting, data validation',
|
|
37
|
-
toolCount: 30,
|
|
38
29
|
async loader(server) {
|
|
39
30
|
const { registerSheetsTools } = await import('./sheets/index.js');
|
|
40
31
|
registerSheetsTools(server);
|
|
41
32
|
},
|
|
42
33
|
},
|
|
43
34
|
email: {
|
|
44
|
-
description: 'Gmail — send, reply, forward, get, list, delete, trash messages, create/update/send drafts, attachments, batch operations',
|
|
45
|
-
toolCount: 19,
|
|
46
35
|
async loader(server) {
|
|
47
36
|
const { register: registerMessages } = await import('./gmail/messages.js');
|
|
48
37
|
const { register: registerDrafts } = await import('./gmail/drafts.js');
|
|
@@ -51,102 +40,46 @@ const CATEGORIES = {
|
|
|
51
40
|
},
|
|
52
41
|
},
|
|
53
42
|
email_threads: {
|
|
54
|
-
description: 'Gmail threads — get, list, batch get, modify, delete, trash/untrash conversation threads',
|
|
55
|
-
toolCount: 7,
|
|
56
43
|
async loader(server) {
|
|
57
44
|
const { register } = await import('./gmail/threads.js');
|
|
58
45
|
register(server);
|
|
59
46
|
},
|
|
60
47
|
},
|
|
61
48
|
email_labels: {
|
|
62
|
-
description: 'Gmail labels — create, delete, get, list, update labels for organizing email',
|
|
63
|
-
toolCount: 6,
|
|
64
49
|
async loader(server) {
|
|
65
50
|
const { register } = await import('./gmail/labels.js');
|
|
66
51
|
register(server);
|
|
67
52
|
},
|
|
68
53
|
},
|
|
69
54
|
email_settings: {
|
|
70
|
-
description: 'Gmail settings — auto-forwarding, IMAP, POP, vacation responder, delegates, filters, forwarding addresses, send-as aliases, S/MIME, profile, mailbox watch',
|
|
71
|
-
toolCount: 37,
|
|
72
55
|
async loader(server) {
|
|
73
56
|
const { register } = await import('./gmail/settings.js');
|
|
74
57
|
register(server);
|
|
75
58
|
},
|
|
76
59
|
},
|
|
77
60
|
calendar: {
|
|
78
|
-
description: 'Google Calendar — list calendars, get/create/update/delete events, check busy times, find free slots, move events, recurring event instances, manage calendars',
|
|
79
|
-
toolCount: 8,
|
|
80
61
|
async loader(server) {
|
|
81
62
|
const { registerCalendarTools } = await import('./calendar/index.js');
|
|
82
63
|
registerCalendarTools(server);
|
|
83
64
|
},
|
|
84
65
|
},
|
|
66
|
+
forms: {
|
|
67
|
+
async loader(server) {
|
|
68
|
+
const { registerFormsTools } = await import('./forms/index.js');
|
|
69
|
+
registerFormsTools(server);
|
|
70
|
+
},
|
|
71
|
+
},
|
|
85
72
|
};
|
|
86
73
|
|
|
87
|
-
// Track which categories have been loaded
|
|
88
|
-
const loadedCategories = new Set();
|
|
89
|
-
|
|
90
74
|
// ---------------------------------------------------------------------------
|
|
91
|
-
//
|
|
75
|
+
// Public: register all tools eagerly, plus the logout utility.
|
|
92
76
|
// ---------------------------------------------------------------------------
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
session.server.notification({ method: 'notifications/tools/list_changed' });
|
|
98
|
-
logger.debug('Sent tools/list_changed notification.');
|
|
99
|
-
} else {
|
|
100
|
-
logger.debug('No session available for tools/list_changed notification.');
|
|
101
|
-
}
|
|
102
|
-
} catch (err) {
|
|
103
|
-
logger.warn('Failed to send tools/list_changed notification:', err.message);
|
|
77
|
+
export async function registerAllTools(server) {
|
|
78
|
+
// Load every category
|
|
79
|
+
for (const [name, { loader }] of Object.entries(CATEGORIES)) {
|
|
80
|
+
await loader(server);
|
|
104
81
|
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ---------------------------------------------------------------------------
|
|
108
|
-
// Public: register the discovery tool (and logout)
|
|
109
|
-
// ---------------------------------------------------------------------------
|
|
110
|
-
export function registerAllTools(server) {
|
|
111
|
-
// --- Discovery tool ---
|
|
112
|
-
server.addTool({
|
|
113
|
-
name: 'load_google_tools',
|
|
114
|
-
description:
|
|
115
|
-
'Load Google Workspace tools by category. Call this first before using any Google service.\n\n' +
|
|
116
|
-
'Categories:\n' +
|
|
117
|
-
Object.entries(CATEGORIES)
|
|
118
|
-
.map(([name, { description, toolCount }]) => ` • ${name} (${toolCount} tools) — ${description}`)
|
|
119
|
-
.join('\n') +
|
|
120
|
-
'\n\nYou can load multiple categories at once by passing an array.',
|
|
121
|
-
parameters: z.object({
|
|
122
|
-
categories: z
|
|
123
|
-
.array(z.enum(Object.keys(CATEGORIES)))
|
|
124
|
-
.describe('One or more category names to load'),
|
|
125
|
-
}),
|
|
126
|
-
execute: async ({ categories }, { log }) => {
|
|
127
|
-
const results = [];
|
|
128
|
-
for (const cat of categories) {
|
|
129
|
-
if (loadedCategories.has(cat)) {
|
|
130
|
-
results.push({ category: cat, status: 'already_loaded' });
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
log.info(`Loading category: ${cat}`);
|
|
134
|
-
await CATEGORIES[cat].loader(server);
|
|
135
|
-
loadedCategories.add(cat);
|
|
136
|
-
results.push({
|
|
137
|
-
category: cat,
|
|
138
|
-
status: 'loaded',
|
|
139
|
-
toolCount: CATEGORIES[cat].toolCount,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
notifyToolsChanged(server);
|
|
143
|
-
return JSON.stringify({
|
|
144
|
-
loaded: results,
|
|
145
|
-
message: 'Tools are now available. You can call them directly.',
|
|
146
|
-
all_loaded_categories: [...loadedCategories],
|
|
147
|
-
});
|
|
148
|
-
},
|
|
149
|
-
});
|
|
82
|
+
logger.info(`Loaded all ${Object.keys(CATEGORIES).length} categories at startup.`);
|
|
150
83
|
|
|
151
84
|
// --- Logout tool (always available) ---
|
|
152
85
|
server.addTool({
|