@lmdat/google-sheets-oauth-mcp 1.0.2 → 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 +29 -34
- package/dist/index.js +5 -5
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -32,11 +32,11 @@ npm install
|
|
|
32
32
|
|
|
33
33
|
## 3. Build
|
|
34
34
|
|
|
35
|
-
| Lệnh
|
|
36
|
-
|
|
37
|
-
| `npm run build`
|
|
38
|
-
| `npm run build:min`
|
|
39
|
-
| `npm run build:obfuscate` | `dist/index.js` (đã obfuscate)
|
|
35
|
+
| Lệnh | Output | Dùng khi nào |
|
|
36
|
+
| --------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
37
|
+
| `npm run build` | `build/index.js` + `build/auth.js` | Phát triển/debug, code dễ đọc |
|
|
38
|
+
| `npm run build:min` | `dist/index.js` (1 file, đã gộp + minify) | Dùng để chạy thật / publish |
|
|
39
|
+
| `npm run build:obfuscate` | `dist/index.js` (đã obfuscate) | Muốn code khó đọc hơn khi chia sẻ (không phải mã hóa, chỉ gây khó đọc) |
|
|
40
40
|
|
|
41
41
|
Build tối thiểu 1 lần trước khi cấu hình opencode ở bước tiếp theo.
|
|
42
42
|
|
|
@@ -52,10 +52,10 @@ ra shell.
|
|
|
52
52
|
|
|
53
53
|
**Biến môi trường optional:**
|
|
54
54
|
|
|
55
|
-
| Var
|
|
56
|
-
|
|
57
|
-
| `GOOGLE_OAUTH_REDIRECT_PORT` | `53682`
|
|
58
|
-
| `MCP_GSHEETS_TOKEN_DIR`
|
|
55
|
+
| Var | Default | Khi nào cần đổi |
|
|
56
|
+
| ------------------------------ | ------------------------ | ------------------------------------------- |
|
|
57
|
+
| `GOOGLE_OAUTH_REDIRECT_PORT` | `53682` | Port bị chương trình khác chiếm dụng |
|
|
58
|
+
| `MCP_GSHEETS_TOKEN_DIR` | `~/.mcp-google-sheets` | Muốn lưu file token ở vị trí khác |
|
|
59
59
|
|
|
60
60
|
## 5. Cấu hình trong opencode
|
|
61
61
|
|
|
@@ -68,7 +68,7 @@ Có 2 cách trỏ tới MCP server, chọn 1 trong 2.
|
|
|
68
68
|
"type": "local",
|
|
69
69
|
"command": [
|
|
70
70
|
"node",
|
|
71
|
-
"/duong-dan-tuyet-doi/
|
|
71
|
+
"/duong-dan-tuyet-doi/google-sheets-oauth-mcp/dist/index.js"
|
|
72
72
|
],
|
|
73
73
|
"enabled": true,
|
|
74
74
|
"environment": {
|
|
@@ -92,7 +92,7 @@ Nếu package đã được publish lên npm registry (xem mục 8), cấu hình
|
|
|
92
92
|
"command": [
|
|
93
93
|
"npx",
|
|
94
94
|
"-y",
|
|
95
|
-
"
|
|
95
|
+
"@lmdat/google-sheets-oauth-mcp@latest"
|
|
96
96
|
],
|
|
97
97
|
"enabled": true,
|
|
98
98
|
"environment": {
|
|
@@ -102,10 +102,6 @@ Nếu package đã được publish lên npm registry (xem mục 8), cấu hình
|
|
|
102
102
|
}
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
Thay `ten-package` bằng tên thật trong `package.json` (ví dụ `@username/mcp-google-sheets-oauth`
|
|
106
|
-
nếu dùng scoped package). Phù hợp khi muốn dùng trên nhiều máy hoặc chia sẻ cho người khác mà
|
|
107
|
-
không cần gửi kèm source code.
|
|
108
|
-
|
|
109
105
|
## 6. Lần đầu sử dụng — cần đăng nhập 1 lần
|
|
110
106
|
|
|
111
107
|
Khi opencode gọi tool đầu tiên (ví dụ `drive_list_spreadsheets`), server sẽ:
|
|
@@ -144,17 +140,17 @@ npm run build:min && npm pack --dry-run
|
|
|
144
140
|
|
|
145
141
|
## 9. Danh sách tool
|
|
146
142
|
|
|
147
|
-
| Tool
|
|
148
|
-
|
|
149
|
-
| `drive_list_spreadsheets`
|
|
150
|
-
| `sheets_create`
|
|
151
|
-
| `sheets_read`
|
|
152
|
-
| `sheets_write`
|
|
153
|
-
| `sheets_append`
|
|
154
|
-
| `sheets_clear`
|
|
155
|
-
| `sheets_list_tabs`
|
|
156
|
-
| `sheets_create_chart`
|
|
157
|
-
| `sheets_create_candlestick_chart` | Tạo chart nến (OHLC) cho dữ liệu giá
|
|
143
|
+
| Tool | Chức năng |
|
|
144
|
+
| ----------------------------------- | ------------------------------------------------------------------------------- |
|
|
145
|
+
| `drive_list_spreadsheets` | Tìm sheet theo tên (Drive API), không cần biết ID trước |
|
|
146
|
+
| `sheets_create` | Tạo sheet mới, tùy chọn đặt vào folder cụ thể và tạo sẵn nhiều tab |
|
|
147
|
+
| `sheets_read` | Đọc 1 range, trả về mảng 2 chiều |
|
|
148
|
+
| `sheets_write` | Ghi đè giá trị trong 1 range |
|
|
149
|
+
| `sheets_append` | Thêm hàng vào cuối bảng dữ liệu hiện có |
|
|
150
|
+
| `sheets_clear` | Xóa giá trị trong 1 range (giữ nguyên format) |
|
|
151
|
+
| `sheets_list_tabs` | Liệt kê tên các tab trong file |
|
|
152
|
+
| `sheets_create_chart` | Tạo chart COLUMN/BAR/LINE/AREA/SCATTER/PIE từ dữ liệu có sẵn |
|
|
153
|
+
| `sheets_create_candlestick_chart` | Tạo chart nến (OHLC) cho dữ liệu giá |
|
|
158
154
|
|
|
159
155
|
Mọi tool đọc/ghi (trừ `drive_list_spreadsheets` và `sheets_create`) đều cần `spreadsheet_id` —
|
|
160
156
|
lấy từ URL của sheet hoặc dùng `drive_list_spreadsheets` để tìm theo tên.
|
|
@@ -198,11 +194,10 @@ Tạo candlestick chart "Giá VNM Q2 2026" từ range Data!A2:E50
|
|
|
198
194
|
|
|
199
195
|
## 12. So sánh với bản dùng Service Account
|
|
200
196
|
|
|
201
|
-
|
|
|
202
|
-
|
|
203
|
-
| Truy cập sheet có sẵn | Phải share thủ công email service account
|
|
204
|
-
| Tìm sheet theo tên
|
|
205
|
-
| File mới tạo
|
|
206
|
-
| Setup ban đầu
|
|
207
|
-
| Phù hợp
|
|
208
|
-
|
|
197
|
+
| | Service Account | OAuth (bản này) |
|
|
198
|
+
| ------------------------ | --------------------------------------------- | ----------------------------------------------------------------------- |
|
|
199
|
+
| Truy cập sheet có sẵn | Phải share thủ công email service account | Tự thấy mọi sheet user đã có quyền |
|
|
200
|
+
| Tìm sheet theo tên | Không hỗ trợ | Có, qua`drive_list_spreadsheets` |
|
|
201
|
+
| File mới tạo | Thuộc Drive của service account | Thuộc Drive của chính user |
|
|
202
|
+
| Setup ban đầu | Nhanh, không cần đăng nhập trình duyệt | Cần đăng nhập trình duyệt 1 lần, cấu hình OAuth consent screen |
|
|
203
|
+
| Phù hợp | Vài sheet cố định, biết trước ID | Cần linh hoạt nhiều sheet, tạo/tìm theo tên |
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{McpServer as ee}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as te}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as a}from"zod";import{OAuth2Client as V}from"google-auth-library";import
|
|
2
|
+
import{McpServer as ee}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as te}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as a}from"zod";import{OAuth2Client as V}from"google-auth-library";import w from"node:fs";import A from"node:path";import K from"node:os";import q from"node:http";import{URL as j}from"node:url";import{exec as J}from"node:child_process";var _=process.env.MCP_GSHEETS_TOKEN_DIR||A.join(K.homedir(),".mcp-google-sheets"),T=A.join(_,"token.json"),$=process.env.GOOGLE_OAUTH_CLIENT_ID,v=process.env.GOOGLE_OAUTH_CLIENT_SECRET,b=Number(process.env.GOOGLE_OAUTH_REDIRECT_PORT||53682),O=`http://127.0.0.1:${b}/oauth2callback`,z=["https://www.googleapis.com/auth/spreadsheets","https://www.googleapis.com/auth/drive.readonly","https://www.googleapis.com/auth/drive.file"];function W(){if(!$||!v)throw new Error("Thi\u1EBFu GOOGLE_OAUTH_CLIENT_ID ho\u1EB7c GOOGLE_OAUTH_CLIENT_SECRET trong env. L\u1EA5y 2 gi\xE1 tr\u1ECB n\xE0y t\u1EEB OAuth Client (lo\u1EA1i 'Desktop app') trong Google Cloud Console.");return new V({clientId:$,clientSecret:v,redirectUri:O})}function X(e){if(!w.existsSync(T))return!1;let t=JSON.parse(w.readFileSync(T,"utf-8"));return e.setCredentials(t),!0}function L(e){w.mkdirSync(_,{recursive:!0}),w.writeFileSync(T,JSON.stringify(e,null,2),{mode:384})}function Z(e){let t=process.platform,n=t==="darwin"?`open "${e}"`:t==="win32"?`start "" "${e}"`:`xdg-open "${e}"`;J(n,()=>{})}async function Q(e){let t=e.generateAuthUrl({access_type:"offline",scope:z,prompt:"consent"});console.error(`
|
|
3
3
|
[mcp-google-sheets-oauth] Ch\u01B0a c\xF3 token. M\u1EDF URL sau \u0111\u1EC3 \u0111\u0103ng nh\u1EADp (\u0111ang t\u1EF1 m\u1EDF browser...):
|
|
4
4
|
${t}
|
|
5
|
-
`),Z(t);let n=await new Promise((o,c)=>{let s=
|
|
5
|
+
`),Z(t);let n=await new Promise((o,c)=>{let s=q.createServer((d,i)=>{i.setHeader("Content-Type","text/html; charset=utf-8");let h=new j(d.url||"",O),u=h.searchParams.get("code"),I=h.searchParams.get("error");if(I){i.end("\u0110\u0103ng nh\u1EADp b\u1ECB t\u1EEB ch\u1ED1i ho\u1EB7c l\u1ED7i. \u0110\xF3ng tab n\xE0y, xem log \u1EDF terminal."),s.close(),c(new Error(`Google tr\u1EA3 l\u1ED7i OAuth: ${I}`));return}if(u){i.end("<h2>\u0110\u0103ng nh\u1EADp th\xE0nh c\xF4ng!</h2><p>\u0110\xF3ng tab n\xE0y v\xE0 quay l\u1EA1i terminal/opencode.</p>"),s.close(),o(u);return}i.end("Thi\u1EBFu tham s\u1ED1 code/error trong redirect.")});s.listen(b,"127.0.0.1")}),{tokens:r}=await e.getToken(n);e.setCredentials(r),L(r),console.error(`[mcp-google-sheets-oauth] \u0110\xE3 l\u01B0u token v\xE0o: ${T}`)}var f=null;async function Y(){if(f)return f;let e=W();return X(e)||await Q(e),e.on("tokens",n=>{let r={...e.credentials,...n};L(r)}),f=e,e}async function k(){let e=await Y(),{token:t}=await e.getAccessToken();if(!t)throw new Error("Kh\xF4ng l\u1EA5y \u0111\u01B0\u1EE3c access token t\u1EEB OAuth client.");return t}import{createRequire as ne}from"node:module";var re=ne(import.meta.url),oe=re("../package.json"),se="google-sheets-oauth-mcp",ae=oe.version,ie="https://sheets.googleapis.com/v4/spreadsheets",G="https://www.googleapis.com/drive/v3";async function ce(e){try{let t=await e.json();if(t?.error?.message){let n=`Google API l\u1ED7i (${e.status}): ${t.error.message}`;return e.status===403&&(n+=`
|
|
6
6
|
\u2192 Kh\u1EA3 n\u0103ng cao: t\xE0i kho\u1EA3n anh ch\u01B0a c\xF3 quy\u1EC1n Edit/Viewer tr\xEAn sheet n\xE0y, ho\u1EB7c scope OAuth \u0111\xE3 xin ch\u01B0a \u0111\u1EE7 (c\u1EA7n re-consent v\u1EDBi scope m\u1EDBi).`),e.status===404&&(n+=`
|
|
7
|
-
\u2192 Ki\u1EC3m tra l\u1EA1i spreadsheet_id, c\xF3 th\u1EC3 sai ho\u1EB7c anh kh\xF4ng c\xF3 quy\u1EC1n truy c\u1EADp.`),n}return`Google API l\u1ED7i (${e.status}): ${JSON.stringify(t)}`}catch{return`Google API l\u1ED7i (${e.status}): ${e.statusText}`}}async function C(e,t,n){let r=await
|
|
7
|
+
\u2192 Ki\u1EC3m tra l\u1EA1i spreadsheet_id, c\xF3 th\u1EC3 sai ho\u1EB7c anh kh\xF4ng c\xF3 quy\u1EC1n truy c\u1EADp.`),n}return`Google API l\u1ED7i (${e.status}): ${JSON.stringify(t)}`}catch{return`Google API l\u1ED7i (${e.status}): ${e.statusText}`}}async function C(e,t,n){let r=await k(),o=await fetch(e,{method:t,headers:{Authorization:`Bearer ${r}`,"Content-Type":"application/json"},body:n!==void 0?JSON.stringify(n):void 0});if(!o.ok)throw new Error(await ce(o));return o.json()}var l=(e,t,n)=>C(`${ie}${e}`,t,n),de=a.union([a.string(),a.number(),a.boolean(),a.null()]),N=a.array(a.array(de));function m(){return a.string().min(1).describe("ID c\u1EE7a Google Sheet \u2014 l\u1EA5y t\u1EEB URL (docs.google.com/spreadsheets/d/<ID>/edit) ho\u1EB7c d\xF9ng tool drive_list_spreadsheets \u0111\u1EC3 t\xECm theo t\xEAn.")}function E(e){return a.string().min(1).describe(`Range theo A1 notation, v\xED d\u1EE5 "${e}". C\xF3 th\u1EC3 ch\u1EC9 ghi t\xEAn sheet (vd "Sheet1") \u0111\u1EC3 l\u1EA5y/ghi c\u1EA3 sheet.`)}function y(e){let t=0;for(let n of e.toUpperCase())t=t*26+(n.charCodeAt(0)-64);return t-1}function he(e){let t="",n=e+1;for(;n>0;){let r=(n-1)%26;t=String.fromCharCode(65+r)+t,n=Math.floor((n-1)/26)}return t}function le(e){let t=e.match(/^([^!]+)!([A-Za-z]+)(\d+):([A-Za-z]+)(\d+)$/);if(!t)throw new Error(`Range "${e}" kh\xF4ng \u0111\xFAng format. C\u1EA7n d\u1EA1ng "TenSheet!A1:E20" (b\u1EAFt bu\u1ED9c c\xF3 t\xEAn sheet, \u0111\u1EE7 2 g\xF3c range).`);let[,n,r,o,c,s]=t;return{sheetName:n,startRowIndex:parseInt(o,10)-1,endRowIndex:parseInt(s,10),startColumnIndex:y(r),endColumnIndex:y(c)+1}}function ge(e){let t=e.match(/^([^!]+)!([A-Za-z]+)(\d+)$/);if(!t)throw new Error(`Cell "${e}" kh\xF4ng \u0111\xFAng format. C\u1EA7n d\u1EA1ng "TenSheet!F2".`);let[,n,r,o]=t;return{sheetName:n,rowIndex:parseInt(o,10)-1,columnIndex:y(r)}}async function P(e,t,n){let r=n.get(t);if(r!==void 0)return r;let c=((await l(`/${e}?fields=sheets.properties`,"GET")).sheets||[]).find(s=>s.properties.title===t);if(!c)throw new Error(`Kh\xF4ng t\xECm th\u1EA5y tab "${t}" trong spreadsheet. D\xF9ng tool sheets_list_tabs \u0111\u1EC3 xem t\xEAn tab \u0111\xFAng.`);return n.set(t,c.properties.sheetId),c.properties.sheetId}async function x(e,t,n){let r=le(t),o=await P(e,r.sheetName,n);return{sheetName:r.sheetName,sheetId:o,gridRange:{sheetId:o,startRowIndex:r.startRowIndex,endRowIndex:r.endRowIndex,startColumnIndex:r.startColumnIndex,endColumnIndex:r.endColumnIndex}}}async function U(e,t,n){let r=ge(t);return{sheetId:await P(e,r.sheetName,n),rowIndex:r.rowIndex,columnIndex:r.columnIndex}}async function ue(e,t,n){let r=he(n.startColumnIndex),o=n.startRowIndex+1,c=n.endRowIndex,s=`${t}!${r}${o}:${r}${c}`,i=(await l(`/${e}/values/${encodeURIComponent(s)}?valueRenderOption=FORMATTED_VALUE`,"GET")).values||[];i.length!==0&&(await l(`/${e}/values/${encodeURIComponent(s)}?valueInputOption=RAW`,"PUT",{values:i}),await l(`/${e}:batchUpdate`,"POST",{requests:[{repeatCell:{range:n,cell:{userEnteredFormat:{numberFormat:{type:"TEXT"}}},fields:"userEnteredFormat.numberFormat"}}]}))}async function D(e,t,n){let r={requests:[{addChart:{chart:{spec:t,position:{overlayPosition:{anchorCell:{sheetId:n.sheetId,rowIndex:n.rowIndex,columnIndex:n.columnIndex},widthPixels:600,heightPixels:371}}}}}]},c=(await l(`/${e}:batchUpdate`,"POST",r)).replies?.[0]?.addChart?.chart?.chartId;if(c===void 0)throw new Error("T\u1EA1o chart kh\xF4ng tr\u1EA3 v\u1EC1 chartId \u2014 ki\u1EC3m tra l\u1EA1i response Google API.");return c}var g=new ee({name:se,version:ae});g.registerTool("drive_list_spreadsheets",{title:"T\xECm Google Sheet theo t\xEAn",description:"List c\xE1c Google Sheet trong Drive c\u1EE7a ch\xEDnh user \u0111ang login (kh\xF4ng c\u1EA7n share th\u1EE7 c\xF4ng, v\xEC OAuth \u0111\u1EA1i di\u1EC7n ch\xEDnh user \u0111\xF3). D\xF9ng \u0111\u1EC3 t\xECm spreadsheet_id theo t\xEAn thay v\xEC copy URL. Tr\u1EA3 v\u1EC1 JSON array, m\u1ED7i item: {name, id, url, modifiedTime, owner}.",inputSchema:{name_contains:a.string().optional().describe('L\u1ECDc theo t\xEAn file ch\u1EE9a chu\u1ED7i n\xE0y (vd "Ng\xE2n s\xE1ch"). B\u1ECF tr\u1ED1ng \u0111\u1EC3 l\u1EA5y g\u1EA7n \u0111\xE2y nh\u1EA5t.'),max_results:a.number().int().min(1).max(50).default(10)}},async({name_contains:e,max_results:t})=>{try{let n=["mimeType='application/vnd.google-apps.spreadsheet'","trashed=false"];if(e){let i=e.replace(/'/g,"\\'");n.push(`name contains '${i}'`)}let r=encodeURIComponent(n.join(" and ")),o=encodeURIComponent("files(id,name,modifiedTime,webViewLink,owners(displayName))"),d=((await C(`${G}/files?q=${r}&fields=${o}&orderBy=modifiedTime desc&pageSize=${t}`,"GET")).files||[]).map(i=>({name:i.name,id:i.id,url:i.webViewLink,modifiedTime:i.modifiedTime,owner:i.owners?.[0]?.displayName??null}));return{content:[{type:"text",text:JSON.stringify(d,null,2)}]}}catch(n){return{content:[{type:"text",text:`L\u1ED7i: ${n.message}`}],isError:!0}}});g.registerTool("sheets_create",{title:"T\u1EA1o Google Sheet m\u1EDBi",description:"T\u1EA1o 1 spreadsheet m\u1EDBi (r\u1ED7ng). M\u1EB7c \u0111\u1ECBnh n\u1EB1m \u1EDF root My Drive c\u1EE7a account \u0111ang login. Truy\u1EC1n folder_id n\u1EBFu mu\u1ED1n \u0111\u1EB7t lu\xF4n v\xE0o 1 folder c\u1EE5 th\u1EC3.",inputSchema:{title:a.string().min(1).describe("T\xEAn file Google Sheet m\u1EDBi."),folder_id:a.string().optional().describe("ID folder Drive mu\u1ED1n \u0111\u1EB7t file v\xE0o (l\u1EA5y t\u1EEB URL folder tr\xEAn Drive). B\u1ECF tr\u1ED1ng -> file n\u1EB1m \u1EDF root My Drive."),sheet_titles:a.array(a.string()).optional().describe('T\xEAn c\xE1c tab mu\u1ED1n t\u1EA1o s\u1EB5n, v\xED d\u1EE5 ["Thu","Chi"]. B\u1ECF tr\u1ED1ng -> 1 tab m\u1EB7c \u0111\u1ECBnh "Sheet1".')}},async({title:e,folder_id:t,sheet_titles:n})=>{try{let r={properties:{title:e}};n&&n.length>0&&(r.sheets=n.map(d=>({properties:{title:d}})));let o=await l("","POST",r),c=o.spreadsheetId,s=o.spreadsheetUrl;return t&&await C(`${G}/files/${c}?addParents=${t}&removeParents=root&fields=id,parents`,"PATCH"),{content:[{type:"text",text:`\u0110\xE3 t\u1EA1o sheet "${e}".
|
|
8
8
|
spreadsheet_id: ${c}
|
|
9
9
|
URL: ${s}${t?`
|
|
10
|
-
\u0110\xE3 move v\xE0o folder: ${t}`:""}`}]}}catch(r){return{content:[{type:"text",text:`L\u1ED7i: ${r.message}`}],isError:!0}}});g.registerTool("sheets_read",{title:"\u0110\u1ECDc d\u1EEF li\u1EC7u t\u1EEB Google Sheet",description:"\u0110\u1ECDc gi\xE1 tr\u1ECB 1 v\xF9ng (range) trong Google Sheet, tr\u1EA3 v\u1EC1 d\u1EA1ng m\u1EA3ng 2 chi\u1EC1u (h\xE0ng x c\u1ED9t).",inputSchema:{spreadsheet_id:m(),range:
|
|
10
|
+
\u0110\xE3 move v\xE0o folder: ${t}`:""}`}]}}catch(r){return{content:[{type:"text",text:`L\u1ED7i: ${r.message}`}],isError:!0}}});g.registerTool("sheets_read",{title:"\u0110\u1ECDc d\u1EEF li\u1EC7u t\u1EEB Google Sheet",description:"\u0110\u1ECDc gi\xE1 tr\u1ECB 1 v\xF9ng (range) trong Google Sheet, tr\u1EA3 v\u1EC1 d\u1EA1ng m\u1EA3ng 2 chi\u1EC1u (h\xE0ng x c\u1ED9t).",inputSchema:{spreadsheet_id:m(),range:E("Sheet1!A1:D10")}},async({spreadsheet_id:e,range:t})=>{try{let r=(await l(`/${e}/values/${encodeURIComponent(t)}`,"GET")).values||[];return{content:[{type:"text",text:r.length===0?"Range tr\u1ED1ng, kh\xF4ng c\xF3 d\u1EEF li\u1EC7u.":JSON.stringify(r,null,2)}]}}catch(n){return{content:[{type:"text",text:`L\u1ED7i: ${n.message}`}],isError:!0}}});g.registerTool("sheets_write",{title:"Ghi \u0111\xE8 d\u1EEF li\u1EC7u v\xE0o Google Sheet",description:"Ghi \u0111\xE8 gi\xE1 tr\u1ECB v\xE0o 1 range c\u1EE5 th\u1EC3. D\u1EEF li\u1EC7u c\u0169 trong range s\u1EBD b\u1ECB thay th\u1EBF ho\xE0n to\xE0n.",inputSchema:{spreadsheet_id:m(),range:E("Sheet1!A1:C3"),values:N.describe('M\u1EA3ng 2 chi\u1EC1u, m\u1ED7i m\u1EA3ng con l\xE0 1 h\xE0ng. V\xED d\u1EE5: [["T\xEAn","Tu\u1ED5i"],["\u0110\u1EA1t",30]]'),value_input_option:a.enum(["RAW","USER_ENTERED"]).default("USER_ENTERED").describe("USER_ENTERED: Sheet t\u1EF1 parse nh\u01B0 khi anh g\xF5 tay (c\xF4ng th\u1EE9c, ng\xE0y th\xE1ng...). RAW: gi\u1EEF nguy\xEAn string.")}},async({spreadsheet_id:e,range:t,values:n,value_input_option:r})=>{try{let o=await l(`/${e}/values/${encodeURIComponent(t)}?valueInputOption=${r}`,"PUT",{values:n});return{content:[{type:"text",text:`\u0110\xE3 ghi ${o.updatedCells??"?"} cell v\xE0o range ${o.updatedRange}.`}]}}catch(o){return{content:[{type:"text",text:`L\u1ED7i: ${o.message}`}],isError:!0}}});g.registerTool("sheets_append",{title:"Append h\xE0ng m\u1EDBi v\xE0o Google Sheet",description:"Th\xEAm h\xE0ng m\u1EDBi v\xE0o cu\u1ED1i b\u1EA3ng d\u1EEF li\u1EC7u hi\u1EC7n c\xF3 (kh\xF4ng \u0111\xE8 d\u1EEF li\u1EC7u c\u0169).",inputSchema:{spreadsheet_id:m(),range:E("Sheet1 (ch\u1EC9 c\u1EA7n t\xEAn sheet)"),values:N.describe("M\u1EA3ng 2 chi\u1EC1u, m\u1ED7i m\u1EA3ng con l\xE0 1 h\xE0ng c\u1EA7n th\xEAm."),value_input_option:a.enum(["RAW","USER_ENTERED"]).default("USER_ENTERED")}},async({spreadsheet_id:e,range:t,values:n,value_input_option:r})=>{try{let c=(await l(`/${e}/values/${encodeURIComponent(t)}:append?valueInputOption=${r}&insertDataOption=INSERT_ROWS`,"POST",{values:n})).updates;return{content:[{type:"text",text:`\u0110\xE3 append ${c?.updatedRows??n.length} h\xE0ng v\xE0o ${c?.updatedRange??t}.`}]}}catch(o){return{content:[{type:"text",text:`L\u1ED7i: ${o.message}`}],isError:!0}}});g.registerTool("sheets_clear",{title:"X\xF3a gi\xE1 tr\u1ECB trong 1 range",description:"X\xF3a n\u1ED9i dung (gi\xE1 tr\u1ECB) trong 1 range, kh\xF4ng x\xF3a format/border.",inputSchema:{spreadsheet_id:m(),range:E("Sheet1!A2:D100")}},async({spreadsheet_id:e,range:t})=>{try{return await l(`/${e}/values/${encodeURIComponent(t)}:clear`,"POST",{}),{content:[{type:"text",text:`\u0110\xE3 x\xF3a d\u1EEF li\u1EC7u trong range ${t}.`}]}}catch(n){return{content:[{type:"text",text:`L\u1ED7i: ${n.message}`}],isError:!0}}});g.registerTool("sheets_list_tabs",{title:"List c\xE1c tab/sheet trong file",description:"Li\u1EC7t k\xEA t\xEAn + ID c\xE1c tab (sheet con) trong 1 Google Sheet file.",inputSchema:{spreadsheet_id:m()}},async({spreadsheet_id:e})=>{try{let t=await l(`/${e}?fields=properties.title,sheets.properties`,"GET"),n=(t.sheets||[]).map(r=>({title:r.properties.title,sheetId:r.properties.sheetId,rowCount:r.properties.gridProperties?.rowCount,columnCount:r.properties.gridProperties?.columnCount}));return{content:[{type:"text",text:`File: ${t.properties?.title}
|
|
11
11
|
|
|
12
12
|
Tabs:
|
|
13
|
-
${JSON.stringify(n,null,2)}`}]}}catch(t){return{content:[{type:"text",text:`L\u1ED7i: ${t.message}`}],isError:!0}}});g.registerTool("sheets_create_chart",{title:"T\u1EA1o chart c\u01A1 b\u1EA3n trong Google Sheet",description:'T\u1EA1o 1 chart COLUMN/BAR/LINE/AREA/SCATTER/PIE t\u1EEB data c\xF3 s\u1EB5n, chart \u0111\u01B0\u1EE3c embed th\u1EB3ng v\xE0o tab. M\u1ECDi range PH\u1EA2I ghi \u0111\u1EE7 t\xEAn sheet, d\u1EA1ng "TenSheet!A2:A10" \u2014 kh\xF4ng h\u1ED7 tr\u1EE3 range thi\u1EBFu t\xEAn sheet.',inputSchema:{spreadsheet_id:m(),chart_type:a.enum(["COLUMN","BAR","LINE","AREA","SCATTER","PIE"]),title:a.string().min(1).describe("Ti\xEAu \u0111\u1EC1 chart."),domain_range:a.string().describe('Range nh\xE3n tr\u1EE5c X (category/labels), d\u1EA1ng "TenSheet!A2:A10". V\u1EDBi PIE \u0111\xE2y l\xE0 nh\xE3n t\u1EEBng ph\u1EA7n.'),series_ranges:a.array(a.string()).min(1).describe('Range gi\xE1 tr\u1ECB, m\u1ED7i string l\xE0 1 series, d\u1EA1ng "TenSheet!B2:B10". PIE ch\u1EC9 nh\u1EADn \u0111\xFAng 1 series.'),stacked:a.boolean().default(!1).describe("Ch\u1EC9 c\xF3 \xFD ngh\u0129a v\u1EDBi COLUMN/BAR/AREA. true = stacked chart."),anchor_cell:a.string().optional().describe('V\u1ECB tr\xED g\xF3c tr\xEAn-tr\xE1i \u0111\u1EB7t chart, d\u1EA1ng "TenSheet!F2". B\u1ECF tr\u1ED1ng -> t\u1EF1 \u0111\u1EB7t ngay b\xEAn ph\u1EA3i domain_range.')}},async({spreadsheet_id:e,chart_type:t,title:n,domain_range:r,series_ranges:o,stacked:c,anchor_cell:s})=>{try{if(t==="PIE"&&o.length!==1)throw new Error("PIE chart ch\u1EC9 nh\u1EADn \u0111\xFAng 1 series_ranges, \u0111ang truy\u1EC1n "+o.length+".");let d=new Map,i=await x(e,r,d),h=await Promise.all(o.map(p=>x(e,p,d))),u;t==="PIE"?u={title:n,pieChart:{legendPosition:"BOTTOM_LEGEND",domain:{sourceRange:{sources:[i.gridRange]}},series:{sourceRange:{sources:[h[0].gridRange]}}}}:u={title:n,basicChart:{chartType:t,legendPosition:"BOTTOM_LEGEND",domains:[{domain:{sourceRange:{sources:[i.gridRange]}}}],series:h.map(p=>({series:{sourceRange:{sources:[p.gridRange]}}})),...["COLUMN","BAR","AREA"].includes(t)&&c?{stackedType:"STACKED"}:{}}};let
|
|
13
|
+
${JSON.stringify(n,null,2)}`}]}}catch(t){return{content:[{type:"text",text:`L\u1ED7i: ${t.message}`}],isError:!0}}});g.registerTool("sheets_create_chart",{title:"T\u1EA1o chart c\u01A1 b\u1EA3n trong Google Sheet",description:'T\u1EA1o 1 chart COLUMN/BAR/LINE/AREA/SCATTER/PIE t\u1EEB data c\xF3 s\u1EB5n, chart \u0111\u01B0\u1EE3c embed th\u1EB3ng v\xE0o tab. M\u1ECDi range PH\u1EA2I ghi \u0111\u1EE7 t\xEAn sheet, d\u1EA1ng "TenSheet!A2:A10" \u2014 kh\xF4ng h\u1ED7 tr\u1EE3 range thi\u1EBFu t\xEAn sheet.',inputSchema:{spreadsheet_id:m(),chart_type:a.enum(["COLUMN","BAR","LINE","AREA","SCATTER","PIE"]),title:a.string().min(1).describe("Ti\xEAu \u0111\u1EC1 chart."),domain_range:a.string().describe('Range nh\xE3n tr\u1EE5c X (category/labels), d\u1EA1ng "TenSheet!A2:A10". V\u1EDBi PIE \u0111\xE2y l\xE0 nh\xE3n t\u1EEBng ph\u1EA7n.'),series_ranges:a.array(a.string()).min(1).describe('Range gi\xE1 tr\u1ECB, m\u1ED7i string l\xE0 1 series, d\u1EA1ng "TenSheet!B2:B10". PIE ch\u1EC9 nh\u1EADn \u0111\xFAng 1 series.'),stacked:a.boolean().default(!1).describe("Ch\u1EC9 c\xF3 \xFD ngh\u0129a v\u1EDBi COLUMN/BAR/AREA. true = stacked chart."),anchor_cell:a.string().optional().describe('V\u1ECB tr\xED g\xF3c tr\xEAn-tr\xE1i \u0111\u1EB7t chart, d\u1EA1ng "TenSheet!F2". B\u1ECF tr\u1ED1ng -> t\u1EF1 \u0111\u1EB7t ngay b\xEAn ph\u1EA3i domain_range.')}},async({spreadsheet_id:e,chart_type:t,title:n,domain_range:r,series_ranges:o,stacked:c,anchor_cell:s})=>{try{if(t==="PIE"&&o.length!==1)throw new Error("PIE chart ch\u1EC9 nh\u1EADn \u0111\xFAng 1 series_ranges, \u0111ang truy\u1EC1n "+o.length+".");let d=new Map,i=await x(e,r,d),h=await Promise.all(o.map(p=>x(e,p,d))),u;t==="PIE"?u={title:n,pieChart:{legendPosition:"BOTTOM_LEGEND",domain:{sourceRange:{sources:[i.gridRange]}},series:{sourceRange:{sources:[h[0].gridRange]}}}}:u={title:n,basicChart:{chartType:t,legendPosition:"BOTTOM_LEGEND",domains:[{domain:{sourceRange:{sources:[i.gridRange]}}}],series:h.map(p=>({series:{sourceRange:{sources:[p.gridRange]}}})),...["COLUMN","BAR","AREA"].includes(t)&&c?{stackedType:"STACKED"}:{}}};let I=s?await U(e,s,d):{sheetId:i.sheetId,rowIndex:i.gridRange.startRowIndex,columnIndex:Math.max(i.gridRange.endColumnIndex,...h.map(p=>p.gridRange.endColumnIndex))+1},R=await D(e,u,I);return{content:[{type:"text",text:`\u0110\xE3 t\u1EA1o ${t} chart "${n}" (chartId: ${R}) trong tab "${i.sheetName}".`}]}}catch(d){return{content:[{type:"text",text:`L\u1ED7i: ${d.message}`}],isError:!0}}});g.registerTool("sheets_create_candlestick_chart",{title:"T\u1EA1o candlestick chart (n\u1EBFn) trong Google Sheet",description:'T\u1EA1o chart n\u1EBFn cho d\u1EEF li\u1EC7u gi\xE1 (OHLC). data_range PH\u1EA2I c\xF3 \u0110\xDANG 5 c\u1ED9t li\xEAn ti\u1EBFp theo th\u1EE9 t\u1EF1 c\u1ED1 \u0111\u1ECBnh: Date, Open, High, Low, Close \u2014 \u0111\xFAng convention chu\u1EA9n c\u1EE7a Google Sheets (gi\u1ED1ng khi t\u1EF1 ch\u1ECDn data tr\xEAn Insert > Chart b\u1EB1ng tay). V\xED d\u1EE5: "Data!A2:E50" (kh\xF4ng g\u1ED3m d\xF2ng header).',inputSchema:{spreadsheet_id:m(),title:a.string().min(1).describe("Ti\xEAu \u0111\u1EC1 chart."),data_range:a.string().describe('Range 5 c\u1ED9t li\xEAn ti\u1EBFp, th\u1EE9 t\u1EF1 C\u1ED0 \u0110\u1ECANH Date-Open-High-Low-Close, d\u1EA1ng "TenSheet!A2:E50". Kh\xF4ng g\u1ED3m d\xF2ng header.'),anchor_cell:a.string().optional().describe('V\u1ECB tr\xED \u0111\u1EB7t chart, d\u1EA1ng "TenSheet!G2". B\u1ECF tr\u1ED1ng -> t\u1EF1 \u0111\u1EB7t b\xEAn ph\u1EA3i data_range.')}},async({spreadsheet_id:e,title:t,data_range:n,anchor_cell:r})=>{try{let o=new Map,c=await x(e,n,o),{gridRange:s,sheetName:d}=c,i=s.endColumnIndex-s.startColumnIndex;if(i!==5)throw new Error(`data_range ph\u1EA3i c\xF3 \u0110\xDANG 5 c\u1ED9t theo th\u1EE9 t\u1EF1 Date, Open, High, Low, Close \u2014 hi\u1EC7n t\u1EA1i \u0111ang c\xF3 ${i} c\u1ED9t. Ki\u1EC3m tra l\u1EA1i range "${n}".`);let h=S=>({sheetId:s.sheetId,startRowIndex:s.startRowIndex,endRowIndex:s.endRowIndex,startColumnIndex:s.startColumnIndex+S,endColumnIndex:s.startColumnIndex+S+1}),u=h(0),I=h(1),R=h(2),p=h(3),M=h(4);await ue(e,d,u);let B={title:t,candlestickChart:{domain:{data:{sourceRange:{sources:[u]}}},data:[{lowSeries:{data:{sourceRange:{sources:[p]}}},openSeries:{data:{sourceRange:{sources:[I]}}},closeSeries:{data:{sourceRange:{sources:[M]}}},highSeries:{data:{sourceRange:{sources:[R]}}}}]}},H=r?await U(e,r,o):{sheetId:s.sheetId,rowIndex:s.startRowIndex,columnIndex:s.endColumnIndex+1},F=await D(e,B,H);return{content:[{type:"text",text:`\u0110\xE3 t\u1EA1o candlestick chart "${t}" (chartId: ${F}) trong tab "${d}".`}]}}catch(o){return{content:[{type:"text",text:`L\u1ED7i: ${o.message}`}],isError:!0}}});async function pe(){let e=new te;await g.connect(e),console.error("[mcp-google-sheets-oauth] Server \u0111ang ch\u1EA1y (stdio).")}pe().catch(e=>{console.error("[mcp-google-sheets-oauth] Fatal error:",e),process.exit(1)});
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmdat/google-sheets-oauth-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"author": "Lê Minh Đạt <sirminhdat@gmail.com>",
|
|
4
5
|
"description": "Local MCP server đọc/ghi/tạo Google Sheets qua OAuth (đại diện chính user, không cần share thủ công).",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "dist/index.js",
|
|
@@ -28,8 +29,7 @@
|
|
|
28
29
|
"google-sheets",
|
|
29
30
|
"oauth"
|
|
30
31
|
],
|
|
31
|
-
"
|
|
32
|
-
"license": "ISC",
|
|
32
|
+
"license": "MIT",
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
35
35
|
"google-auth-library": "^10.9.0",
|