gogcli-mcp-drive 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/README.md +69 -0
- package/SKILL.md +51 -0
- package/dist/index.js +30591 -0
- package/manifest.json +179 -0
- package/package.json +40 -0
- package/src/index.ts +12 -0
- package/src/tools/drive-extra.ts +190 -0
- package/tests/tools/drive-extra.test.ts +222 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +17 -0
package/manifest.json
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/anthropics/dxt/main/dist/mcpb-manifest.schema.json",
|
|
3
|
+
"manifest_version": "0.3",
|
|
4
|
+
"name": "gogcli-mcp-drive",
|
|
5
|
+
"display_name": "gogcli (Drive)",
|
|
6
|
+
"version": "2.0.0",
|
|
7
|
+
"description": "Extended Google Drive for Claude via gogcli — auth + full Drive support (upload, download, permissions, comments, shared drives)",
|
|
8
|
+
"author": {
|
|
9
|
+
"name": "Chris Hall",
|
|
10
|
+
"email": "chris.c.hall@gmail.com",
|
|
11
|
+
"url": "https://github.com/chrischall"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/chrischall/gogcli-mcp"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/chrischall/gogcli-mcp",
|
|
18
|
+
"support": "https://github.com/chrischall/gogcli-mcp/issues",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"keywords": [
|
|
21
|
+
"google",
|
|
22
|
+
"drive",
|
|
23
|
+
"gogcli",
|
|
24
|
+
"gog",
|
|
25
|
+
"upload",
|
|
26
|
+
"download",
|
|
27
|
+
"permissions",
|
|
28
|
+
"comments",
|
|
29
|
+
"shared-drives"
|
|
30
|
+
],
|
|
31
|
+
"server": {
|
|
32
|
+
"type": "node",
|
|
33
|
+
"entry_point": "dist/index.js",
|
|
34
|
+
"mcp_config": {
|
|
35
|
+
"command": "node",
|
|
36
|
+
"args": [
|
|
37
|
+
"${__dirname}/dist/index.js"
|
|
38
|
+
],
|
|
39
|
+
"env": {
|
|
40
|
+
"GOG_ACCOUNT": "${user_config.gog_account}",
|
|
41
|
+
"GOG_PATH": "${user_config.gog_path}"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"user_config": {
|
|
46
|
+
"gog_account": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"title": "Google Account",
|
|
49
|
+
"description": "Default Google account email (optional — uses gogcli's configured default if omitted)",
|
|
50
|
+
"required": false
|
|
51
|
+
},
|
|
52
|
+
"gog_path": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"title": "gog Executable Path",
|
|
55
|
+
"description": "Path to the gog executable (optional — defaults to 'gog' on your PATH)",
|
|
56
|
+
"required": false
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"tools": [
|
|
60
|
+
{
|
|
61
|
+
"name": "gog_auth_add",
|
|
62
|
+
"description": "Authorize a Google account via browser-based OAuth"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "gog_auth_list",
|
|
66
|
+
"description": "List all Google accounts stored in gogcli"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "gog_auth_status",
|
|
70
|
+
"description": "Show gogcli auth configuration (keyring, credential files)"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "gog_auth_services",
|
|
74
|
+
"description": "List Google services and OAuth scopes supported by gogcli"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "gog_auth_run",
|
|
78
|
+
"description": "Run any gog auth subcommand (escape hatch)"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"name": "gog_drive_ls",
|
|
82
|
+
"description": "List files in a Google Drive folder (with pagination and query filters)"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"name": "gog_drive_search",
|
|
86
|
+
"description": "Full-text search across Drive"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"name": "gog_drive_get",
|
|
90
|
+
"description": "Get file metadata"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"name": "gog_drive_mkdir",
|
|
94
|
+
"description": "Create a new folder"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"name": "gog_drive_rename",
|
|
98
|
+
"description": "Rename a file or folder"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"name": "gog_drive_move",
|
|
102
|
+
"description": "Move a file to a different folder"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"name": "gog_drive_delete",
|
|
106
|
+
"description": "Delete a file (trash by default, or permanent=true for irreversible delete)"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"name": "gog_drive_share",
|
|
110
|
+
"description": "Share a file or folder"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"name": "gog_drive_run",
|
|
114
|
+
"description": "Run any gog drive subcommand (escape hatch)"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"name": "gog_drive_download",
|
|
118
|
+
"description": "Download a file (Google Docs formats export as pdf/docx/xlsx/etc.)"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"name": "gog_drive_upload",
|
|
122
|
+
"description": "Upload a local file, optionally replacing an existing file or converting to Google format"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"name": "gog_drive_copy",
|
|
126
|
+
"description": "Copy a file to a new file"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"name": "gog_drive_url",
|
|
130
|
+
"description": "Print shareable web URLs for one or more files"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"name": "gog_drive_permissions",
|
|
134
|
+
"description": "List permissions on a file"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"name": "gog_drive_unshare",
|
|
138
|
+
"description": "Remove a permission from a file"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"name": "gog_drive_drives_list",
|
|
142
|
+
"description": "List shared drives (Team Drives)"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"name": "gog_drive_comments_list",
|
|
146
|
+
"description": "List comments on a file"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"name": "gog_drive_comments_get",
|
|
150
|
+
"description": "Get a single comment by ID"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"name": "gog_drive_comments_add",
|
|
154
|
+
"description": "Add a new comment"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"name": "gog_drive_comments_update",
|
|
158
|
+
"description": "Update a comment's text"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"name": "gog_drive_comments_delete",
|
|
162
|
+
"description": "Delete a comment"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"name": "gog_drive_comments_reply",
|
|
166
|
+
"description": "Reply to an existing comment"
|
|
167
|
+
}
|
|
168
|
+
],
|
|
169
|
+
"compatibility": {
|
|
170
|
+
"platforms": [
|
|
171
|
+
"darwin",
|
|
172
|
+
"linux",
|
|
173
|
+
"win32"
|
|
174
|
+
],
|
|
175
|
+
"runtimes": {
|
|
176
|
+
"node": ">=18.0.0"
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gogcli-mcp-drive",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Extended Google Drive MCP server via gogcli — auth + full Drive support (upload/download/permissions/comments/shared drives)",
|
|
5
|
+
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/chrischall/gogcli-mcp"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"bin": {
|
|
12
|
+
"gogcli-mcp-drive": "dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc --noEmit && npm run bundle",
|
|
19
|
+
"bundle": "node ../../scripts/bundle.js src/index.ts dist/index.js",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"test:coverage": "vitest run --coverage"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
27
|
+
"zod": "^4.3.6"
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"keywords": [
|
|
31
|
+
"mcp",
|
|
32
|
+
"model-context-protocol",
|
|
33
|
+
"claude",
|
|
34
|
+
"ai",
|
|
35
|
+
"google",
|
|
36
|
+
"gogcli",
|
|
37
|
+
"google-drive",
|
|
38
|
+
"drive"
|
|
39
|
+
]
|
|
40
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { createServer, registerAuthTools, registerDriveTools } from '../../gogcli-mcp/src/lib.js';
|
|
4
|
+
import { registerExtraDriveTools } from './tools/drive-extra.js';
|
|
5
|
+
|
|
6
|
+
const server = createServer({ name: 'gogcli-drive' });
|
|
7
|
+
registerAuthTools(server);
|
|
8
|
+
registerDriveTools(server);
|
|
9
|
+
registerExtraDriveTools(server);
|
|
10
|
+
|
|
11
|
+
const transport = new StdioServerTransport();
|
|
12
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { accountParam, runOrDiagnose } from '../../../gogcli-mcp/src/lib.js';
|
|
4
|
+
|
|
5
|
+
export function registerExtraDriveTools(server: McpServer): void {
|
|
6
|
+
server.registerTool('gog_drive_download', {
|
|
7
|
+
description: 'Download a Drive file to the local filesystem. For Google Docs formats, specify an export format (pdf, csv, xlsx, pptx, txt, png, docx, md).',
|
|
8
|
+
annotations: { readOnlyHint: true },
|
|
9
|
+
inputSchema: {
|
|
10
|
+
fileId: z.string().describe('File ID to download'),
|
|
11
|
+
out: z.string().optional().describe('Output file path (default: gogcli config dir)'),
|
|
12
|
+
format: z.string().optional().describe('Export format for Google Docs: pdf, csv, xlsx, pptx, txt, png, docx, md (default: inferred)'),
|
|
13
|
+
account: accountParam,
|
|
14
|
+
},
|
|
15
|
+
}, async ({ fileId, out, format, account }) => {
|
|
16
|
+
const args = ['drive', 'download', fileId];
|
|
17
|
+
if (out) args.push(`--out=${out}`);
|
|
18
|
+
if (format) args.push(`--format=${format}`);
|
|
19
|
+
return runOrDiagnose(args, { account });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
server.registerTool('gog_drive_upload', {
|
|
23
|
+
description: 'Upload a local file to Drive. Use --replace to replace the content of an existing file (preserves link/permissions), or --convert to auto-convert to a Google format.',
|
|
24
|
+
annotations: { destructiveHint: true },
|
|
25
|
+
inputSchema: {
|
|
26
|
+
localPath: z.string().describe('Path to the local file to upload'),
|
|
27
|
+
name: z.string().optional().describe('Override filename (create) or rename target (replace)'),
|
|
28
|
+
parent: z.string().optional().describe('Destination folder ID (create only)'),
|
|
29
|
+
replace: z.string().optional().describe('Replace content of an existing Drive file ID (preserves link/permissions)'),
|
|
30
|
+
mimeType: z.string().optional().describe('Override MIME type inference'),
|
|
31
|
+
keepRevisionForever: z.boolean().optional().describe('Keep the new head revision forever (binary files only)'),
|
|
32
|
+
convert: z.boolean().optional().describe('Auto-convert to native Google format based on file extension (create only)'),
|
|
33
|
+
convertTo: z.string().optional().describe('Convert to a specific Google format: doc | sheet | slides (create only)'),
|
|
34
|
+
account: accountParam,
|
|
35
|
+
},
|
|
36
|
+
}, async ({ localPath, name, parent, replace, mimeType, keepRevisionForever, convert, convertTo, account }) => {
|
|
37
|
+
const args = ['drive', 'upload', localPath];
|
|
38
|
+
if (name) args.push(`--name=${name}`);
|
|
39
|
+
if (parent) args.push(`--parent=${parent}`);
|
|
40
|
+
if (replace) args.push(`--replace=${replace}`);
|
|
41
|
+
if (mimeType) args.push(`--mime-type=${mimeType}`);
|
|
42
|
+
if (keepRevisionForever) args.push('--keep-revision-forever');
|
|
43
|
+
if (convert) args.push('--convert');
|
|
44
|
+
if (convertTo) args.push(`--convert-to=${convertTo}`);
|
|
45
|
+
return runOrDiagnose(args, { account });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
server.registerTool('gog_drive_copy', {
|
|
49
|
+
description: 'Copy a Drive file to a new file with the given name.',
|
|
50
|
+
inputSchema: {
|
|
51
|
+
fileId: z.string().describe('File ID to copy'),
|
|
52
|
+
name: z.string().describe('Name for the new copy'),
|
|
53
|
+
parent: z.string().optional().describe('Destination folder ID'),
|
|
54
|
+
account: accountParam,
|
|
55
|
+
},
|
|
56
|
+
}, async ({ fileId, name, parent, account }) => {
|
|
57
|
+
const args = ['drive', 'copy', fileId, name];
|
|
58
|
+
if (parent) args.push(`--parent=${parent}`);
|
|
59
|
+
return runOrDiagnose(args, { account });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
server.registerTool('gog_drive_url', {
|
|
63
|
+
description: 'Print shareable web URLs for one or more Drive files.',
|
|
64
|
+
annotations: { readOnlyHint: true },
|
|
65
|
+
inputSchema: {
|
|
66
|
+
fileIds: z.array(z.string()).min(1).describe('One or more file IDs to get URLs for'),
|
|
67
|
+
account: accountParam,
|
|
68
|
+
},
|
|
69
|
+
}, async ({ fileIds, account }) => {
|
|
70
|
+
return runOrDiagnose(['drive', 'url', ...fileIds], { account });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
server.registerTool('gog_drive_permissions', {
|
|
74
|
+
description: 'List permissions on a Drive file.',
|
|
75
|
+
annotations: { readOnlyHint: true },
|
|
76
|
+
inputSchema: {
|
|
77
|
+
fileId: z.string().describe('File ID'),
|
|
78
|
+
max: z.number().optional().describe('Max results (default: 100)'),
|
|
79
|
+
page: z.string().optional().describe('Page token'),
|
|
80
|
+
account: accountParam,
|
|
81
|
+
},
|
|
82
|
+
}, async ({ fileId, max, page, account }) => {
|
|
83
|
+
const args = ['drive', 'permissions', fileId];
|
|
84
|
+
if (max !== undefined) args.push(`--max=${max}`);
|
|
85
|
+
if (page) args.push(`--page=${page}`);
|
|
86
|
+
return runOrDiagnose(args, { account });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
server.registerTool('gog_drive_unshare', {
|
|
90
|
+
description: 'Remove a permission from a Drive file. Get the permissionId from gog_drive_permissions.',
|
|
91
|
+
annotations: { destructiveHint: true },
|
|
92
|
+
inputSchema: {
|
|
93
|
+
fileId: z.string().describe('File ID'),
|
|
94
|
+
permissionId: z.string().describe('Permission ID to remove'),
|
|
95
|
+
account: accountParam,
|
|
96
|
+
},
|
|
97
|
+
}, async ({ fileId, permissionId, account }) => {
|
|
98
|
+
return runOrDiagnose(['drive', 'unshare', fileId, permissionId], { account });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
server.registerTool('gog_drive_drives_list', {
|
|
102
|
+
description: 'List shared drives (Team Drives) accessible to the account.',
|
|
103
|
+
annotations: { readOnlyHint: true },
|
|
104
|
+
inputSchema: {
|
|
105
|
+
max: z.number().optional().describe('Max results (default: 100, max allowed: 100)'),
|
|
106
|
+
page: z.string().optional().describe('Page token'),
|
|
107
|
+
all: z.boolean().optional().describe('Fetch all pages'),
|
|
108
|
+
query: z.string().optional().describe('Search query for filtering shared drives'),
|
|
109
|
+
account: accountParam,
|
|
110
|
+
},
|
|
111
|
+
}, async ({ max, page, all, query, account }) => {
|
|
112
|
+
const args = ['drive', 'drives'];
|
|
113
|
+
if (max !== undefined) args.push(`--max=${max}`);
|
|
114
|
+
if (page) args.push(`--page=${page}`);
|
|
115
|
+
if (all) args.push('--all');
|
|
116
|
+
if (query) args.push(`--query=${query}`);
|
|
117
|
+
return runOrDiagnose(args, { account });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
server.registerTool('gog_drive_comments_list', {
|
|
121
|
+
description: 'List comments on a Drive file.',
|
|
122
|
+
annotations: { readOnlyHint: true },
|
|
123
|
+
inputSchema: {
|
|
124
|
+
fileId: z.string().describe('File ID'),
|
|
125
|
+
account: accountParam,
|
|
126
|
+
},
|
|
127
|
+
}, async ({ fileId, account }) => {
|
|
128
|
+
return runOrDiagnose(['drive', 'comments', 'list', fileId], { account });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
server.registerTool('gog_drive_comments_get', {
|
|
132
|
+
description: 'Get a single comment on a Drive file by ID.',
|
|
133
|
+
annotations: { readOnlyHint: true },
|
|
134
|
+
inputSchema: {
|
|
135
|
+
fileId: z.string().describe('File ID'),
|
|
136
|
+
commentId: z.string().describe('Comment ID'),
|
|
137
|
+
account: accountParam,
|
|
138
|
+
},
|
|
139
|
+
}, async ({ fileId, commentId, account }) => {
|
|
140
|
+
return runOrDiagnose(['drive', 'comments', 'get', fileId, commentId], { account });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
server.registerTool('gog_drive_comments_add', {
|
|
144
|
+
description: 'Add a new comment to a Drive file.',
|
|
145
|
+
inputSchema: {
|
|
146
|
+
fileId: z.string().describe('File ID'),
|
|
147
|
+
content: z.string().describe('Comment text'),
|
|
148
|
+
account: accountParam,
|
|
149
|
+
},
|
|
150
|
+
}, async ({ fileId, content, account }) => {
|
|
151
|
+
return runOrDiagnose(['drive', 'comments', 'create', fileId, content], { account });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
server.registerTool('gog_drive_comments_update', {
|
|
155
|
+
description: 'Update the text of an existing comment.',
|
|
156
|
+
annotations: { destructiveHint: true },
|
|
157
|
+
inputSchema: {
|
|
158
|
+
fileId: z.string().describe('File ID'),
|
|
159
|
+
commentId: z.string().describe('Comment ID to update'),
|
|
160
|
+
content: z.string().describe('New comment text'),
|
|
161
|
+
account: accountParam,
|
|
162
|
+
},
|
|
163
|
+
}, async ({ fileId, commentId, content, account }) => {
|
|
164
|
+
return runOrDiagnose(['drive', 'comments', 'update', fileId, commentId, content], { account });
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
server.registerTool('gog_drive_comments_delete', {
|
|
168
|
+
description: 'Delete a comment from a Drive file.',
|
|
169
|
+
annotations: { destructiveHint: true },
|
|
170
|
+
inputSchema: {
|
|
171
|
+
fileId: z.string().describe('File ID'),
|
|
172
|
+
commentId: z.string().describe('Comment ID to delete'),
|
|
173
|
+
account: accountParam,
|
|
174
|
+
},
|
|
175
|
+
}, async ({ fileId, commentId, account }) => {
|
|
176
|
+
return runOrDiagnose(['drive', 'comments', 'delete', fileId, commentId], { account });
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
server.registerTool('gog_drive_comments_reply', {
|
|
180
|
+
description: 'Reply to an existing comment on a Drive file.',
|
|
181
|
+
inputSchema: {
|
|
182
|
+
fileId: z.string().describe('File ID'),
|
|
183
|
+
commentId: z.string().describe('Comment ID to reply to'),
|
|
184
|
+
content: z.string().describe('Reply text'),
|
|
185
|
+
account: accountParam,
|
|
186
|
+
},
|
|
187
|
+
}, async ({ fileId, commentId, content, account }) => {
|
|
188
|
+
return runOrDiagnose(['drive', 'comments', 'reply', fileId, commentId, content], { account });
|
|
189
|
+
});
|
|
190
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { registerExtraDriveTools } from '../../src/tools/drive-extra.js';
|
|
4
|
+
import * as lib from '../../../gogcli-mcp/src/lib.js';
|
|
5
|
+
|
|
6
|
+
vi.mock('../../../gogcli-mcp/src/lib.js', async (importOriginal) => {
|
|
7
|
+
const actual = await importOriginal<typeof lib>();
|
|
8
|
+
return {
|
|
9
|
+
...actual,
|
|
10
|
+
runOrDiagnose: vi.fn(),
|
|
11
|
+
};
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
type ToolHandler = (args: Record<string, unknown>) => Promise<{ content: Array<{ type: string; text: string }> }>;
|
|
15
|
+
|
|
16
|
+
function toText(text: string) {
|
|
17
|
+
return { content: [{ type: 'text', text }] };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function setupHandlers(): Map<string, ToolHandler> {
|
|
21
|
+
const server = new McpServer({ name: 'test', version: '0.0.0' });
|
|
22
|
+
const handlers = new Map<string, ToolHandler>();
|
|
23
|
+
vi.spyOn(server, 'registerTool').mockImplementation((name, _config, cb) => {
|
|
24
|
+
handlers.set(name, cb as ToolHandler);
|
|
25
|
+
return undefined as never;
|
|
26
|
+
});
|
|
27
|
+
registerExtraDriveTools(server);
|
|
28
|
+
return handlers;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let handlers: Map<string, ToolHandler>;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
36
|
+
handlers = setupHandlers();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('gog_drive_download', () => {
|
|
40
|
+
it('calls runOrDiagnose with fileId only', async () => {
|
|
41
|
+
await handlers.get('gog_drive_download')!({ fileId: 'f1' });
|
|
42
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'download', 'f1'], { account: undefined });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('passes --out and --format when provided', async () => {
|
|
46
|
+
await handlers.get('gog_drive_download')!({ fileId: 'f1', out: '/tmp/file.pdf', format: 'pdf', account: 'a@b.com' });
|
|
47
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
48
|
+
['drive', 'download', 'f1', '--out=/tmp/file.pdf', '--format=pdf'],
|
|
49
|
+
{ account: 'a@b.com' },
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('gog_drive_upload', () => {
|
|
55
|
+
it('calls runOrDiagnose with localPath only', async () => {
|
|
56
|
+
await handlers.get('gog_drive_upload')!({ localPath: '/tmp/x.txt' });
|
|
57
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'upload', '/tmp/x.txt'], { account: undefined });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('passes all upload flags', async () => {
|
|
61
|
+
await handlers.get('gog_drive_upload')!({
|
|
62
|
+
localPath: '/tmp/x.txt',
|
|
63
|
+
name: 'renamed.txt',
|
|
64
|
+
parent: 'folder1',
|
|
65
|
+
replace: 'file2',
|
|
66
|
+
mimeType: 'text/plain',
|
|
67
|
+
keepRevisionForever: true,
|
|
68
|
+
convert: true,
|
|
69
|
+
convertTo: 'doc',
|
|
70
|
+
});
|
|
71
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
72
|
+
[
|
|
73
|
+
'drive', 'upload', '/tmp/x.txt',
|
|
74
|
+
'--name=renamed.txt',
|
|
75
|
+
'--parent=folder1',
|
|
76
|
+
'--replace=file2',
|
|
77
|
+
'--mime-type=text/plain',
|
|
78
|
+
'--keep-revision-forever',
|
|
79
|
+
'--convert',
|
|
80
|
+
'--convert-to=doc',
|
|
81
|
+
],
|
|
82
|
+
{ account: undefined },
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('omits boolean flags when false', async () => {
|
|
87
|
+
await handlers.get('gog_drive_upload')!({ localPath: '/tmp/x.txt', keepRevisionForever: false, convert: false });
|
|
88
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'upload', '/tmp/x.txt'], { account: undefined });
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('gog_drive_copy', () => {
|
|
93
|
+
it('calls runOrDiagnose with fileId and name', async () => {
|
|
94
|
+
await handlers.get('gog_drive_copy')!({ fileId: 'f1', name: 'Copy' });
|
|
95
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'copy', 'f1', 'Copy'], { account: undefined });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('passes --parent when provided', async () => {
|
|
99
|
+
await handlers.get('gog_drive_copy')!({ fileId: 'f1', name: 'Copy', parent: 'folder1' });
|
|
100
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
101
|
+
['drive', 'copy', 'f1', 'Copy', '--parent=folder1'],
|
|
102
|
+
{ account: undefined },
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('gog_drive_url', () => {
|
|
108
|
+
it('calls runOrDiagnose with single fileId', async () => {
|
|
109
|
+
await handlers.get('gog_drive_url')!({ fileIds: ['f1'] });
|
|
110
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'url', 'f1'], { account: undefined });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('calls runOrDiagnose with multiple fileIds', async () => {
|
|
114
|
+
await handlers.get('gog_drive_url')!({ fileIds: ['f1', 'f2', 'f3'] });
|
|
115
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'url', 'f1', 'f2', 'f3'], { account: undefined });
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('gog_drive_permissions', () => {
|
|
120
|
+
it('calls runOrDiagnose with fileId', async () => {
|
|
121
|
+
await handlers.get('gog_drive_permissions')!({ fileId: 'f1' });
|
|
122
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'permissions', 'f1'], { account: undefined });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('passes --max and --page when provided', async () => {
|
|
126
|
+
await handlers.get('gog_drive_permissions')!({ fileId: 'f1', max: 50, page: 'tok' });
|
|
127
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
128
|
+
['drive', 'permissions', 'f1', '--max=50', '--page=tok'],
|
|
129
|
+
{ account: undefined },
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('gog_drive_unshare', () => {
|
|
135
|
+
it('calls runOrDiagnose with fileId and permissionId', async () => {
|
|
136
|
+
await handlers.get('gog_drive_unshare')!({ fileId: 'f1', permissionId: 'p1' });
|
|
137
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
138
|
+
['drive', 'unshare', 'f1', 'p1'],
|
|
139
|
+
{ account: undefined },
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('gog_drive_drives_list', () => {
|
|
145
|
+
it('calls runOrDiagnose with no flags', async () => {
|
|
146
|
+
await handlers.get('gog_drive_drives_list')!({});
|
|
147
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'drives'], { account: undefined });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('passes all listing flags', async () => {
|
|
151
|
+
await handlers.get('gog_drive_drives_list')!({ max: 50, page: 'tok', all: true, query: 'engineering' });
|
|
152
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
153
|
+
['drive', 'drives', '--max=50', '--page=tok', '--all', '--query=engineering'],
|
|
154
|
+
{ account: undefined },
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('omits --all when false', async () => {
|
|
159
|
+
await handlers.get('gog_drive_drives_list')!({ all: false });
|
|
160
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'drives'], { account: undefined });
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('gog_drive_comments_list', () => {
|
|
165
|
+
it('calls runOrDiagnose with fileId', async () => {
|
|
166
|
+
await handlers.get('gog_drive_comments_list')!({ fileId: 'f1' });
|
|
167
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
168
|
+
['drive', 'comments', 'list', 'f1'],
|
|
169
|
+
{ account: undefined },
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('gog_drive_comments_get', () => {
|
|
175
|
+
it('calls runOrDiagnose with fileId and commentId', async () => {
|
|
176
|
+
await handlers.get('gog_drive_comments_get')!({ fileId: 'f1', commentId: 'c1' });
|
|
177
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
178
|
+
['drive', 'comments', 'get', 'f1', 'c1'],
|
|
179
|
+
{ account: undefined },
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('gog_drive_comments_add', () => {
|
|
185
|
+
it('calls runOrDiagnose with fileId and content', async () => {
|
|
186
|
+
await handlers.get('gog_drive_comments_add')!({ fileId: 'f1', content: 'LGTM' });
|
|
187
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
188
|
+
['drive', 'comments', 'create', 'f1', 'LGTM'],
|
|
189
|
+
{ account: undefined },
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('gog_drive_comments_update', () => {
|
|
195
|
+
it('calls runOrDiagnose with fileId, commentId, and content', async () => {
|
|
196
|
+
await handlers.get('gog_drive_comments_update')!({ fileId: 'f1', commentId: 'c1', content: 'edited' });
|
|
197
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
198
|
+
['drive', 'comments', 'update', 'f1', 'c1', 'edited'],
|
|
199
|
+
{ account: undefined },
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('gog_drive_comments_delete', () => {
|
|
205
|
+
it('calls runOrDiagnose with fileId and commentId', async () => {
|
|
206
|
+
await handlers.get('gog_drive_comments_delete')!({ fileId: 'f1', commentId: 'c1' });
|
|
207
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
208
|
+
['drive', 'comments', 'delete', 'f1', 'c1'],
|
|
209
|
+
{ account: undefined },
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('gog_drive_comments_reply', () => {
|
|
215
|
+
it('calls runOrDiagnose with fileId, commentId, and content', async () => {
|
|
216
|
+
await handlers.get('gog_drive_comments_reply')!({ fileId: 'f1', commentId: 'c1', content: 'thanks' });
|
|
217
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
218
|
+
['drive', 'comments', 'reply', 'f1', 'c1', 'thanks'],
|
|
219
|
+
{ account: undefined },
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
});
|
package/tsconfig.json
ADDED
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
coverage: {
|
|
6
|
+
provider: 'v8',
|
|
7
|
+
include: ['src/**/*.ts'],
|
|
8
|
+
exclude: ['src/index.ts'],
|
|
9
|
+
thresholds: {
|
|
10
|
+
lines: 100,
|
|
11
|
+
functions: 100,
|
|
12
|
+
branches: 100,
|
|
13
|
+
statements: 100,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
});
|