google-workspace-api-mcp 1.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.
Files changed (3) hide show
  1. package/README.md +309 -0
  2. package/build/index.js +513 -0
  3. package/package.json +42 -0
package/README.md ADDED
@@ -0,0 +1,309 @@
1
+ # Google Workspace API MCP
2
+
3
+ Google Workspace API를 사용해 Google Drive 파일 검색, Google Docs 문서 조회 및 편집, Google Sheets 스프레드시트 조회 및 수정을 제공하는 MCP 서버입니다.
4
+
5
+ ## 주요 기능
6
+
7
+ - `google_drive_search_files`: Google Drive에서 파일 이름 또는 본문 검색으로 문서, 스프레드시트, 기타 파일을 찾습니다.
8
+ - `google_drive_get_file`: Drive 파일 ID로 파일 메타데이터, 웹 링크, 소유자, 권한 정보를 조회합니다.
9
+ - `google_docs_get_document`: Google Docs 문서 구조와 추출된 일반 텍스트를 조회합니다.
10
+ - `google_docs_create_document`: 새 Google Docs 문서를 만들고 선택적으로 초기 텍스트를 삽입합니다.
11
+ - `google_docs_append_text`: 기존 Google Docs 문서 끝에 텍스트를 추가합니다.
12
+ - `google_docs_insert_text`: 지정한 Google Docs 구조 인덱스에 텍스트를 삽입합니다.
13
+ - `google_docs_replace_text`: 문서 안의 특정 텍스트를 전체 치환합니다.
14
+ - `google_sheets_create_spreadsheet`: 새 Google Sheets 스프레드시트를 만들고 선택적으로 시트 이름을 지정합니다.
15
+ - `google_sheets_get_values`: A1 표기법 범위의 셀 값을 읽습니다.
16
+ - `google_sheets_update_values`: 지정한 범위의 셀 값을 덮어씁니다.
17
+ - `google_sheets_append_values`: 지정한 범위에 행 데이터를 추가합니다.
18
+
19
+ ## 사전 준비
20
+
21
+ 1. Google Cloud Console에서 프로젝트를 생성합니다.
22
+ 2. 다음 API를 사용 설정합니다.
23
+ - Google Drive API
24
+ - Google Docs API
25
+ - Google Sheets API
26
+ 3. 서비스 계정을 만들고 JSON 키를 발급합니다.
27
+ 4. 접근하려는 문서, 스프레드시트, 폴더를 서비스 계정 이메일에 공유합니다.
28
+ 5. MCP 실행 환경에 인증 정보를 설정합니다.
29
+
30
+ 서비스 계정은 명시적으로 공유받은 파일과 폴더에만 접근할 수 있습니다.
31
+
32
+ ## 인증 설정
33
+
34
+ 이 서버는 세 가지 인증 방식을 지원합니다.
35
+
36
+ ### 1. MCP 설정에 직접 입력
37
+
38
+ JSON 키 파일을 따로 두기 싫다면 서비스 계정 JSON에서 `client_email`, `private_key`, `project_id` 값을 꺼내 MCP 설정의 환경 변수로 넣을 수 있습니다.
39
+
40
+ ```json
41
+ {
42
+ "mcpServers": {
43
+ "google-workspace": {
44
+ "command": "npx",
45
+ "args": ["-y", "google-workspace-api-mcp"],
46
+ "env": {
47
+ "GOOGLE_CLIENT_EMAIL": "your-service-account@your-project.iam.gserviceaccount.com",
48
+ "GOOGLE_PRIVATE_KEY": "-----BEGIN PRIVATE KEY-----\\nYOUR_PRIVATE_KEY_CONTENT\\n-----END PRIVATE KEY-----\\n",
49
+ "GOOGLE_PROJECT_ID": "your-google-cloud-project-id"
50
+ }
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ `GOOGLE_PRIVATE_KEY`의 줄바꿈은 위 예시처럼 `\\n`으로 입력하면 됩니다. 서버가 실행 시 실제 개행 문자로 변환합니다.
57
+
58
+ ### 2. 서비스 계정 JSON 전체 입력
59
+
60
+ 서비스 계정 JSON 전체를 환경 변수에 직접 넣을 수도 있습니다. MCP 설정 JSON 안에 다시 JSON 문자열을 넣어야 하므로 따옴표 이스케이프가 번거롭습니다. 가능하면 base64 인코딩 값을 권장합니다.
61
+
62
+ ```json
63
+ {
64
+ "mcpServers": {
65
+ "google-workspace": {
66
+ "command": "npx",
67
+ "args": ["-y", "google-workspace-api-mcp"],
68
+ "env": {
69
+ "GOOGLE_SERVICE_ACCOUNT_JSON": "base64_encoded_service_account_json"
70
+ }
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ PowerShell에서 base64 값을 만드는 예시:
77
+
78
+ ```powershell
79
+ [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes((Get-Content .\service-account.json -Raw)))
80
+ ```
81
+
82
+ ### 3. 키 파일 경로 사용
83
+
84
+ 로컬 파일 경로를 사용하는 방식도 계속 지원합니다.
85
+
86
+ ```json
87
+ {
88
+ "mcpServers": {
89
+ "google-workspace": {
90
+ "command": "npx",
91
+ "args": ["-y", "google-workspace-api-mcp"],
92
+ "env": {
93
+ "GOOGLE_SERVICE_ACCOUNT_KEY_FILE": "/path/to/service-account.json"
94
+ }
95
+ }
96
+ }
97
+ }
98
+ ```
99
+
100
+ `GOOGLE_APPLICATION_CREDENTIALS`도 동일하게 사용할 수 있습니다.
101
+
102
+ 필요한 OAuth scope:
103
+
104
+ - `https://www.googleapis.com/auth/documents`
105
+ - `https://www.googleapis.com/auth/drive`
106
+ - `https://www.googleapis.com/auth/spreadsheets`
107
+
108
+ 서비스 계정 JSON, private key, 키 파일은 README, 소스 코드, 공개 저장소에 커밋하지 마세요. MCP 클라이언트의 secret/env 관리 기능이나 로컬 환경 변수를 사용하세요.
109
+
110
+ ## 설치해서 사용하기
111
+
112
+ MCP 클라이언트 설정 예시:
113
+
114
+ ```json
115
+ {
116
+ "mcpServers": {
117
+ "google-workspace": {
118
+ "command": "npx",
119
+ "args": ["-y", "google-workspace-api-mcp"],
120
+ "env": {
121
+ "GOOGLE_CLIENT_EMAIL": "your-service-account@your-project.iam.gserviceaccount.com",
122
+ "GOOGLE_PRIVATE_KEY": "-----BEGIN PRIVATE KEY-----\\nYOUR_PRIVATE_KEY_CONTENT\\n-----END PRIVATE KEY-----\\n",
123
+ "GOOGLE_PROJECT_ID": "your-google-cloud-project-id"
124
+ }
125
+ }
126
+ }
127
+ }
128
+ ```
129
+
130
+ ## 로컬 개발
131
+
132
+ 저장소를 받은 뒤 패키지 디렉터리에서 실행합니다.
133
+
134
+ ```bash
135
+ cd google-workspace-api-mcp
136
+ npm install
137
+ npm run build
138
+ npm start
139
+ ```
140
+
141
+ 로컬 빌드 결과물을 직접 실행하는 MCP 설정 예시:
142
+
143
+ ```json
144
+ {
145
+ "mcpServers": {
146
+ "google-workspace": {
147
+ "command": "node",
148
+ "args": ["./build/index.js"],
149
+ "cwd": "/path/to/google-workspace-api-mcp",
150
+ "env": {
151
+ "GOOGLE_CLIENT_EMAIL": "your-service-account@your-project.iam.gserviceaccount.com",
152
+ "GOOGLE_PRIVATE_KEY": "-----BEGIN PRIVATE KEY-----\\nYOUR_PRIVATE_KEY_CONTENT\\n-----END PRIVATE KEY-----\\n",
153
+ "GOOGLE_PROJECT_ID": "your-google-cloud-project-id"
154
+ }
155
+ }
156
+ }
157
+ }
158
+ ```
159
+
160
+ `cwd`는 사용자의 실제 프로젝트 디렉터리로 바꾸면 됩니다. Windows라면 예를 들어 `C:\\path\\to\\google-workspace-api-mcp`처럼 적습니다.
161
+
162
+ ## Docker
163
+
164
+ 이미지 빌드:
165
+
166
+ ```bash
167
+ docker build -t google-workspace-api-mcp .
168
+ ```
169
+
170
+ Docker 기반 MCP 설정 예시:
171
+
172
+ ```json
173
+ {
174
+ "mcpServers": {
175
+ "google-workspace": {
176
+ "command": "docker",
177
+ "args": [
178
+ "run",
179
+ "-i",
180
+ "--rm",
181
+ "-e",
182
+ "GOOGLE_CLIENT_EMAIL=your-service-account@your-project.iam.gserviceaccount.com",
183
+ "-e",
184
+ "GOOGLE_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----\\nYOUR_PRIVATE_KEY_CONTENT\\n-----END PRIVATE KEY-----\\n",
185
+ "-e",
186
+ "GOOGLE_PROJECT_ID=your-google-cloud-project-id",
187
+ "google-workspace-api-mcp"
188
+ ]
189
+ }
190
+ }
191
+ }
192
+ ```
193
+
194
+ ## JSON 호출 예시
195
+
196
+ ### Drive 파일 검색
197
+
198
+ ```json
199
+ {
200
+ "query": "회의록",
201
+ "mime_type": "application/vnd.google-apps.document",
202
+ "page_size": 10
203
+ }
204
+ ```
205
+
206
+ ### Google Docs 문서 생성
207
+
208
+ ```json
209
+ {
210
+ "title": "주간 업무 보고",
211
+ "text": "이번 주 진행 상황\n- MCP 서버 구성\n- Google Docs 연동 테스트\n"
212
+ }
213
+ ```
214
+
215
+ ### Google Docs 텍스트 추가
216
+
217
+ ```json
218
+ {
219
+ "document_id": "google_docs_document_id",
220
+ "text": "\n다음 주 계획\n- 배포 검증\n- 사용자 문서 보강\n"
221
+ }
222
+ ```
223
+
224
+ ### Google Sheets 값 읽기
225
+
226
+ ```json
227
+ {
228
+ "spreadsheet_id": "google_sheets_spreadsheet_id",
229
+ "range": "Sheet1!A1:D20"
230
+ }
231
+ ```
232
+
233
+ ### Google Sheets 행 추가
234
+
235
+ ```json
236
+ {
237
+ "spreadsheet_id": "google_sheets_spreadsheet_id",
238
+ "range": "Sheet1!A:D",
239
+ "values": [
240
+ ["날짜", "항목", "상태", "메모"],
241
+ ["2026-06-27", "MCP 배포 준비", "완료", "npx 실행 구성"]
242
+ ],
243
+ "value_input_option": "USER_ENTERED"
244
+ }
245
+ ```
246
+
247
+ ## LLM 채팅 프롬프트 예시
248
+
249
+ MCP가 연결된 LLM 채팅에서 아래처럼 자연어로 요청할 수 있습니다.
250
+
251
+ ```text
252
+ 내 Google Drive에서 "분기 보고서"가 들어간 Google Docs 문서를 찾아서 최근 수정일 기준으로 정리해줘.
253
+ ```
254
+
255
+ ```text
256
+ 이 문서 ID의 내용을 읽고 핵심 결정 사항과 할 일을 요약해줘.
257
+ ```
258
+
259
+ ```text
260
+ 새 Google Docs 문서를 만들고 제목은 "프로젝트 킥오프 메모"로 해줘. 본문에는 참석자, 목표, 다음 액션 섹션을 넣어줘.
261
+ ```
262
+
263
+ ```text
264
+ 이 스프레드시트의 Sheet1!A1:F50 범위를 읽고 상태가 지연인 항목만 표로 정리해줘.
265
+ ```
266
+
267
+ ```text
268
+ 이 스프레드시트 Sheet1에 오늘 날짜, 작업명, 상태, 메모를 한 행으로 추가해줘.
269
+ ```
270
+
271
+ ## 배포
272
+
273
+ ```bash
274
+ npm run build
275
+ npm pack --dry-run
276
+ npm login
277
+ npm publish
278
+ ```
279
+
280
+ scope 패키지로 이름을 바꾸는 경우:
281
+
282
+ ```bash
283
+ npm publish --access public
284
+ ```
285
+
286
+ 배포 후 MCP 클라이언트에서는 다음처럼 실행할 수 있습니다.
287
+
288
+ ```json
289
+ {
290
+ "mcpServers": {
291
+ "google-workspace": {
292
+ "command": "npx",
293
+ "args": ["-y", "google-workspace-api-mcp"],
294
+ "env": {
295
+ "GOOGLE_CLIENT_EMAIL": "your-service-account@your-project.iam.gserviceaccount.com",
296
+ "GOOGLE_PRIVATE_KEY": "-----BEGIN PRIVATE KEY-----\\nYOUR_PRIVATE_KEY_CONTENT\\n-----END PRIVATE KEY-----\\n",
297
+ "GOOGLE_PROJECT_ID": "your-google-cloud-project-id"
298
+ }
299
+ }
300
+ }
301
+ }
302
+ ```
303
+
304
+ ## 주의 사항
305
+
306
+ - Google Workspace API 사용량과 조직 정책에 따라 할당량 제한이나 접근 제한이 발생할 수 있습니다.
307
+ - 서비스 계정은 명시적으로 공유받은 파일과 폴더에만 접근할 수 있습니다.
308
+ - 문서 편집 도구는 Google Docs API의 구조 인덱스를 사용합니다. 일반적인 문서 시작 위치는 `1`입니다.
309
+ - 민감한 문서와 스프레드시트를 다룰 때는 MCP 클라이언트 로그, 터미널 기록, 환경 변수 노출 범위를 확인하세요.
package/build/index.js ADDED
@@ -0,0 +1,513 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
5
+ import { google } from "googleapis";
6
+ import { z } from "zod";
7
+ const SERVER_NAME = "google-workspace-api-mcp";
8
+ const SERVER_VERSION = "1.0.0";
9
+ const scopes = [
10
+ "https://www.googleapis.com/auth/documents",
11
+ "https://www.googleapis.com/auth/drive",
12
+ "https://www.googleapis.com/auth/spreadsheets",
13
+ ];
14
+ const optionalString = z.string().min(1).optional();
15
+ const valueMatrixSchema = z.array(z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])));
16
+ const searchFilesSchema = z.object({
17
+ query: optionalString,
18
+ mime_type: optionalString,
19
+ page_size: z.number().int().min(1).max(100).default(10),
20
+ include_trashed: z.boolean().default(false),
21
+ });
22
+ const fileIdSchema = z.object({
23
+ file_id: z.string().min(1),
24
+ });
25
+ const createDocumentSchema = z.object({
26
+ title: z.string().min(1),
27
+ text: z.string().optional(),
28
+ });
29
+ const appendDocumentTextSchema = z.object({
30
+ document_id: z.string().min(1),
31
+ text: z.string(),
32
+ });
33
+ const insertDocumentTextSchema = z.object({
34
+ document_id: z.string().min(1),
35
+ index: z.number().int().min(1),
36
+ text: z.string(),
37
+ });
38
+ const replaceDocumentTextSchema = z.object({
39
+ document_id: z.string().min(1),
40
+ search_text: z.string().min(1),
41
+ replace_text: z.string(),
42
+ match_case: z.boolean().default(false),
43
+ });
44
+ const createSpreadsheetSchema = z.object({
45
+ title: z.string().min(1),
46
+ sheets: z.array(z.string().min(1)).default([]),
47
+ });
48
+ const sheetRangeSchema = z.object({
49
+ spreadsheet_id: z.string().min(1),
50
+ range: z.string().min(1),
51
+ });
52
+ const updateValuesSchema = sheetRangeSchema.extend({
53
+ values: valueMatrixSchema,
54
+ value_input_option: z.enum(["RAW", "USER_ENTERED"]).default("USER_ENTERED"),
55
+ });
56
+ const appendValuesSchema = updateValuesSchema.extend({
57
+ insert_data_option: z.enum(["INSERT_ROWS", "OVERWRITE"]).default("INSERT_ROWS"),
58
+ });
59
+ class GoogleWorkspaceApiMcpServer {
60
+ server;
61
+ docs;
62
+ drive;
63
+ sheets;
64
+ constructor() {
65
+ this.server = new Server({
66
+ name: SERVER_NAME,
67
+ version: SERVER_VERSION,
68
+ }, {
69
+ capabilities: {
70
+ tools: {},
71
+ },
72
+ });
73
+ const auth = this.createAuthClient();
74
+ this.docs = google.docs({ version: "v1", auth });
75
+ this.drive = google.drive({ version: "v3", auth });
76
+ this.sheets = google.sheets({ version: "v4", auth });
77
+ this.registerToolListHandler();
78
+ this.registerToolCallHandler();
79
+ }
80
+ createAuthClient() {
81
+ const serviceAccountJson = process.env.GOOGLE_SERVICE_ACCOUNT_JSON;
82
+ const keyFile = process.env.GOOGLE_SERVICE_ACCOUNT_KEY_FILE || process.env.GOOGLE_APPLICATION_CREDENTIALS;
83
+ const inlineCredentials = this.getInlineServiceAccountCredentials();
84
+ if (serviceAccountJson) {
85
+ return new google.auth.GoogleAuth({
86
+ credentials: this.parseServiceAccountJson(serviceAccountJson),
87
+ scopes,
88
+ });
89
+ }
90
+ if (inlineCredentials) {
91
+ return new google.auth.GoogleAuth({
92
+ credentials: inlineCredentials,
93
+ scopes,
94
+ });
95
+ }
96
+ return new google.auth.GoogleAuth({
97
+ keyFile,
98
+ scopes,
99
+ });
100
+ }
101
+ getInlineServiceAccountCredentials() {
102
+ const clientEmail = process.env.GOOGLE_CLIENT_EMAIL;
103
+ const privateKey = process.env.GOOGLE_PRIVATE_KEY;
104
+ if (!clientEmail && !privateKey) {
105
+ return undefined;
106
+ }
107
+ if (!clientEmail || !privateKey) {
108
+ throw new McpError(ErrorCode.InvalidRequest, "GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY must be set together.");
109
+ }
110
+ return {
111
+ type: "service_account",
112
+ project_id: process.env.GOOGLE_PROJECT_ID,
113
+ private_key_id: process.env.GOOGLE_PRIVATE_KEY_ID,
114
+ client_email: clientEmail,
115
+ private_key: privateKey.replace(/\\n/g, "\n"),
116
+ };
117
+ }
118
+ parseServiceAccountJson(raw) {
119
+ try {
120
+ return JSON.parse(raw);
121
+ }
122
+ catch {
123
+ try {
124
+ return JSON.parse(Buffer.from(raw, "base64").toString("utf8"));
125
+ }
126
+ catch {
127
+ throw new McpError(ErrorCode.InvalidRequest, "GOOGLE_SERVICE_ACCOUNT_JSON must be a JSON object string or base64-encoded JSON.");
128
+ }
129
+ }
130
+ }
131
+ registerToolListHandler() {
132
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
133
+ tools: [
134
+ {
135
+ name: "google_drive_search_files",
136
+ description: "Search Google Drive files visible to the configured Google account.",
137
+ inputSchema: {
138
+ type: "object",
139
+ properties: {
140
+ query: { type: "string", description: "Drive search text matched against file name or full text." },
141
+ mime_type: { type: "string", description: "Optional exact MIME type filter." },
142
+ page_size: { type: "number", description: "Maximum results, 1-100. Default: 10." },
143
+ include_trashed: { type: "boolean", description: "Whether to include trashed files. Default: false." },
144
+ },
145
+ },
146
+ },
147
+ {
148
+ name: "google_drive_get_file",
149
+ description: "Get Google Drive file metadata by file id.",
150
+ inputSchema: {
151
+ type: "object",
152
+ properties: {
153
+ file_id: { type: "string", description: "Google Drive file id." },
154
+ },
155
+ required: ["file_id"],
156
+ },
157
+ },
158
+ {
159
+ name: "google_docs_get_document",
160
+ description: "Read a Google Docs document structure and extracted plain text.",
161
+ inputSchema: {
162
+ type: "object",
163
+ properties: {
164
+ file_id: { type: "string", description: "Google Docs document id." },
165
+ },
166
+ required: ["file_id"],
167
+ },
168
+ },
169
+ {
170
+ name: "google_docs_create_document",
171
+ description: "Create a new Google Docs document, optionally with initial plain text.",
172
+ inputSchema: {
173
+ type: "object",
174
+ properties: {
175
+ title: { type: "string", description: "Document title." },
176
+ text: { type: "string", description: "Optional initial plain text." },
177
+ },
178
+ required: ["title"],
179
+ },
180
+ },
181
+ {
182
+ name: "google_docs_append_text",
183
+ description: "Append plain text to the end of a Google Docs document.",
184
+ inputSchema: {
185
+ type: "object",
186
+ properties: {
187
+ document_id: { type: "string", description: "Google Docs document id." },
188
+ text: { type: "string", description: "Plain text to append." },
189
+ },
190
+ required: ["document_id", "text"],
191
+ },
192
+ },
193
+ {
194
+ name: "google_docs_insert_text",
195
+ description: "Insert plain text at a specific Google Docs structural index.",
196
+ inputSchema: {
197
+ type: "object",
198
+ properties: {
199
+ document_id: { type: "string", description: "Google Docs document id." },
200
+ index: { type: "number", description: "Google Docs insertion index. Use 1 for the start of the body." },
201
+ text: { type: "string", description: "Plain text to insert." },
202
+ },
203
+ required: ["document_id", "index", "text"],
204
+ },
205
+ },
206
+ {
207
+ name: "google_docs_replace_text",
208
+ description: "Replace all matching text in a Google Docs document.",
209
+ inputSchema: {
210
+ type: "object",
211
+ properties: {
212
+ document_id: { type: "string", description: "Google Docs document id." },
213
+ search_text: { type: "string", description: "Text to find." },
214
+ replace_text: { type: "string", description: "Replacement text." },
215
+ match_case: { type: "boolean", description: "Whether matching is case-sensitive. Default: false." },
216
+ },
217
+ required: ["document_id", "search_text", "replace_text"],
218
+ },
219
+ },
220
+ {
221
+ name: "google_sheets_create_spreadsheet",
222
+ description: "Create a Google Sheets spreadsheet, optionally with named sheets.",
223
+ inputSchema: {
224
+ type: "object",
225
+ properties: {
226
+ title: { type: "string", description: "Spreadsheet title." },
227
+ sheets: { type: "array", items: { type: "string" }, description: "Optional sheet names." },
228
+ },
229
+ required: ["title"],
230
+ },
231
+ },
232
+ {
233
+ name: "google_sheets_get_values",
234
+ description: "Read cell values from a Google Sheets range.",
235
+ inputSchema: {
236
+ type: "object",
237
+ properties: {
238
+ spreadsheet_id: { type: "string", description: "Spreadsheet id." },
239
+ range: { type: "string", description: "A1 notation range, for example Sheet1!A1:D20." },
240
+ },
241
+ required: ["spreadsheet_id", "range"],
242
+ },
243
+ },
244
+ {
245
+ name: "google_sheets_update_values",
246
+ description: "Overwrite values in a Google Sheets range.",
247
+ inputSchema: {
248
+ type: "object",
249
+ properties: {
250
+ spreadsheet_id: { type: "string", description: "Spreadsheet id." },
251
+ range: { type: "string", description: "A1 notation range." },
252
+ values: { type: "array", items: { type: "array", items: {} }, description: "Two-dimensional value array." },
253
+ value_input_option: { type: "string", enum: ["RAW", "USER_ENTERED"], description: "Default: USER_ENTERED." },
254
+ },
255
+ required: ["spreadsheet_id", "range", "values"],
256
+ },
257
+ },
258
+ {
259
+ name: "google_sheets_append_values",
260
+ description: "Append rows to a Google Sheets range.",
261
+ inputSchema: {
262
+ type: "object",
263
+ properties: {
264
+ spreadsheet_id: { type: "string", description: "Spreadsheet id." },
265
+ range: { type: "string", description: "A1 notation range." },
266
+ values: { type: "array", items: { type: "array", items: {} }, description: "Two-dimensional value array." },
267
+ value_input_option: { type: "string", enum: ["RAW", "USER_ENTERED"], description: "Default: USER_ENTERED." },
268
+ insert_data_option: { type: "string", enum: ["INSERT_ROWS", "OVERWRITE"], description: "Default: INSERT_ROWS." },
269
+ },
270
+ required: ["spreadsheet_id", "range", "values"],
271
+ },
272
+ },
273
+ ],
274
+ }));
275
+ }
276
+ registerToolCallHandler() {
277
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
278
+ const { name, arguments: args } = request.params;
279
+ try {
280
+ switch (name) {
281
+ case "google_drive_search_files":
282
+ return this.textResponse(await this.searchFiles(searchFilesSchema.parse(args)));
283
+ case "google_drive_get_file":
284
+ return this.textResponse(await this.getFile(fileIdSchema.parse(args).file_id));
285
+ case "google_docs_get_document":
286
+ return this.textResponse(await this.getDocument(fileIdSchema.parse(args).file_id));
287
+ case "google_docs_create_document":
288
+ return this.textResponse(await this.createDocument(createDocumentSchema.parse(args)));
289
+ case "google_docs_append_text":
290
+ return this.textResponse(await this.appendDocumentText(appendDocumentTextSchema.parse(args)));
291
+ case "google_docs_insert_text":
292
+ return this.textResponse(await this.insertDocumentText(insertDocumentTextSchema.parse(args)));
293
+ case "google_docs_replace_text":
294
+ return this.textResponse(await this.replaceDocumentText(replaceDocumentTextSchema.parse(args)));
295
+ case "google_sheets_create_spreadsheet":
296
+ return this.textResponse(await this.createSpreadsheet(createSpreadsheetSchema.parse(args)));
297
+ case "google_sheets_get_values":
298
+ return this.textResponse(await this.getSheetValues(sheetRangeSchema.parse(args)));
299
+ case "google_sheets_update_values":
300
+ return this.textResponse(await this.updateSheetValues(updateValuesSchema.parse(args)));
301
+ case "google_sheets_append_values":
302
+ return this.textResponse(await this.appendSheetValues(appendValuesSchema.parse(args)));
303
+ default:
304
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
305
+ }
306
+ }
307
+ catch (error) {
308
+ if (error instanceof McpError) {
309
+ throw error;
310
+ }
311
+ if (error instanceof z.ZodError) {
312
+ throw new McpError(ErrorCode.InvalidParams, `Invalid arguments: ${error.issues.map((issue) => issue.message).join(", ")}`);
313
+ }
314
+ throw new McpError(ErrorCode.InternalError, this.getErrorMessage(error));
315
+ }
316
+ });
317
+ }
318
+ async searchFiles(input) {
319
+ const filters = input.include_trashed ? [] : ["trashed = false"];
320
+ if (input.query) {
321
+ const escaped = input.query.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
322
+ filters.push(`(name contains '${escaped}' or fullText contains '${escaped}')`);
323
+ }
324
+ if (input.mime_type) {
325
+ const escapedMimeType = input.mime_type.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
326
+ filters.push(`mimeType = '${escapedMimeType}'`);
327
+ }
328
+ const response = await this.drive.files.list({
329
+ q: filters.length > 0 ? filters.join(" and ") : undefined,
330
+ pageSize: input.page_size,
331
+ fields: "files(id,name,mimeType,webViewLink,createdTime,modifiedTime,owners(displayName,emailAddress))",
332
+ orderBy: "modifiedTime desc",
333
+ supportsAllDrives: true,
334
+ includeItemsFromAllDrives: true,
335
+ });
336
+ return response.data.files ?? [];
337
+ }
338
+ async getFile(fileId) {
339
+ const response = await this.drive.files.get({
340
+ fileId,
341
+ fields: "id,name,mimeType,webViewLink,createdTime,modifiedTime,size,owners(displayName,emailAddress),permissions(id,type,role,emailAddress,displayName)",
342
+ supportsAllDrives: true,
343
+ });
344
+ return response.data;
345
+ }
346
+ async getDocument(documentId) {
347
+ const response = await this.docs.documents.get({ documentId });
348
+ return {
349
+ documentId: response.data.documentId,
350
+ title: response.data.title,
351
+ revisionId: response.data.revisionId,
352
+ text: this.extractDocumentText(response.data.body?.content ?? []),
353
+ document: response.data,
354
+ };
355
+ }
356
+ async createDocument(input) {
357
+ const response = await this.docs.documents.create({
358
+ requestBody: {
359
+ title: input.title,
360
+ },
361
+ });
362
+ const documentId = response.data.documentId;
363
+ if (documentId && input.text) {
364
+ await this.insertDocumentText({
365
+ document_id: documentId,
366
+ index: 1,
367
+ text: input.text,
368
+ });
369
+ }
370
+ return {
371
+ documentId,
372
+ title: response.data.title,
373
+ webViewLink: documentId ? `https://docs.google.com/document/d/${documentId}/edit` : undefined,
374
+ };
375
+ }
376
+ async appendDocumentText(input) {
377
+ const document = await this.docs.documents.get({ documentId: input.document_id });
378
+ const endIndex = document.data.body?.content?.at(-1)?.endIndex;
379
+ if (!endIndex || endIndex < 2) {
380
+ throw new McpError(ErrorCode.InternalError, "Could not determine document end index.");
381
+ }
382
+ return this.insertDocumentText({
383
+ document_id: input.document_id,
384
+ index: endIndex - 1,
385
+ text: input.text,
386
+ });
387
+ }
388
+ async insertDocumentText(input) {
389
+ const response = await this.docs.documents.batchUpdate({
390
+ documentId: input.document_id,
391
+ requestBody: {
392
+ requests: [
393
+ {
394
+ insertText: {
395
+ location: { index: input.index },
396
+ text: input.text,
397
+ },
398
+ },
399
+ ],
400
+ },
401
+ });
402
+ return {
403
+ documentId: input.document_id,
404
+ replies: response.data.replies ?? [],
405
+ };
406
+ }
407
+ async replaceDocumentText(input) {
408
+ const response = await this.docs.documents.batchUpdate({
409
+ documentId: input.document_id,
410
+ requestBody: {
411
+ requests: [
412
+ {
413
+ replaceAllText: {
414
+ containsText: {
415
+ text: input.search_text,
416
+ matchCase: input.match_case,
417
+ },
418
+ replaceText: input.replace_text,
419
+ },
420
+ },
421
+ ],
422
+ },
423
+ });
424
+ return {
425
+ documentId: input.document_id,
426
+ replies: response.data.replies ?? [],
427
+ };
428
+ }
429
+ async createSpreadsheet(input) {
430
+ const response = await this.sheets.spreadsheets.create({
431
+ requestBody: {
432
+ properties: {
433
+ title: input.title,
434
+ },
435
+ sheets: input.sheets.map((title) => ({
436
+ properties: { title },
437
+ })),
438
+ },
439
+ });
440
+ return {
441
+ spreadsheetId: response.data.spreadsheetId,
442
+ spreadsheetUrl: response.data.spreadsheetUrl,
443
+ properties: response.data.properties,
444
+ sheets: response.data.sheets?.map((sheet) => sheet.properties),
445
+ };
446
+ }
447
+ async getSheetValues(input) {
448
+ const response = await this.sheets.spreadsheets.values.get({
449
+ spreadsheetId: input.spreadsheet_id,
450
+ range: input.range,
451
+ });
452
+ return {
453
+ range: response.data.range,
454
+ majorDimension: response.data.majorDimension,
455
+ values: response.data.values ?? [],
456
+ };
457
+ }
458
+ async updateSheetValues(input) {
459
+ const response = await this.sheets.spreadsheets.values.update({
460
+ spreadsheetId: input.spreadsheet_id,
461
+ range: input.range,
462
+ valueInputOption: input.value_input_option,
463
+ requestBody: {
464
+ values: input.values,
465
+ },
466
+ });
467
+ return response.data;
468
+ }
469
+ async appendSheetValues(input) {
470
+ const response = await this.sheets.spreadsheets.values.append({
471
+ spreadsheetId: input.spreadsheet_id,
472
+ range: input.range,
473
+ valueInputOption: input.value_input_option,
474
+ insertDataOption: input.insert_data_option,
475
+ requestBody: {
476
+ values: input.values,
477
+ },
478
+ });
479
+ return response.data;
480
+ }
481
+ extractDocumentText(content) {
482
+ return content
483
+ .flatMap((element) => element.paragraph?.elements ?? [])
484
+ .map((element) => element.textRun?.content ?? "")
485
+ .join("");
486
+ }
487
+ textResponse(data) {
488
+ return {
489
+ content: [
490
+ {
491
+ type: "text",
492
+ text: JSON.stringify(data, null, 2),
493
+ },
494
+ ],
495
+ };
496
+ }
497
+ getErrorMessage(error) {
498
+ if (error instanceof Error) {
499
+ return error.message;
500
+ }
501
+ return String(error);
502
+ }
503
+ async run() {
504
+ const transport = new StdioServerTransport();
505
+ await this.server.connect(transport);
506
+ console.error(`${SERVER_NAME} running on stdio`);
507
+ }
508
+ }
509
+ const server = new GoogleWorkspaceApiMcpServer();
510
+ server.run().catch((error) => {
511
+ console.error(error);
512
+ process.exit(1);
513
+ });
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "google-workspace-api-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Google Workspace MCP server for Google Drive, Docs, and Sheets",
5
+ "type": "module",
6
+ "main": "build/index.js",
7
+ "bin": {
8
+ "google-workspace-api-mcp": "./build/index.js"
9
+ },
10
+ "files": [
11
+ "build",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "start": "node build/index.js",
17
+ "dev": "ts-node --esm src/index.ts",
18
+ "prepack": "npm run build"
19
+ },
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "keywords": [
24
+ "mcp",
25
+ "google-docs",
26
+ "google-sheets",
27
+ "google-drive",
28
+ "google-workspace"
29
+ ],
30
+ "author": "",
31
+ "license": "ISC",
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.29.0",
34
+ "googleapis": "^144.0.0",
35
+ "zod": "^3.23.8"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^22.13.4",
39
+ "ts-node": "^10.9.2",
40
+ "typescript": "^5.7.3"
41
+ }
42
+ }