@starfysh/gdrive-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.
- package/LICENSE +7 -0
- package/README.md +521 -0
- package/dist/auth.js +148 -0
- package/dist/googleDocsApiHelpers.js +618 -0
- package/dist/googleSheetsApiHelpers.js +356 -0
- package/dist/server.js +2451 -0
- package/dist/types.js +107 -0
- package/package.json +53 -0
package/dist/types.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
// --- Helper function for hex color validation ---
|
|
4
|
+
export const hexColorRegex = /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/;
|
|
5
|
+
export const validateHexColor = (color) => hexColorRegex.test(color);
|
|
6
|
+
// --- Helper function for Hex to RGB conversion ---
|
|
7
|
+
export function hexToRgbColor(hex) {
|
|
8
|
+
if (!hex)
|
|
9
|
+
return null;
|
|
10
|
+
let hexClean = hex.startsWith('#') ? hex.slice(1) : hex;
|
|
11
|
+
if (hexClean.length === 3) {
|
|
12
|
+
hexClean = hexClean[0] + hexClean[0] + hexClean[1] + hexClean[1] + hexClean[2] + hexClean[2];
|
|
13
|
+
}
|
|
14
|
+
if (hexClean.length !== 6)
|
|
15
|
+
return null;
|
|
16
|
+
const bigint = parseInt(hexClean, 16);
|
|
17
|
+
if (isNaN(bigint))
|
|
18
|
+
return null;
|
|
19
|
+
const r = ((bigint >> 16) & 255) / 255;
|
|
20
|
+
const g = ((bigint >> 8) & 255) / 255;
|
|
21
|
+
const b = (bigint & 255) / 255;
|
|
22
|
+
return { red: r, green: g, blue: b };
|
|
23
|
+
}
|
|
24
|
+
// --- Zod Schema Fragments for Reusability ---
|
|
25
|
+
export const DocumentIdParameter = z.object({
|
|
26
|
+
documentId: z.string().describe('The ID of the Google Document (from the URL).'),
|
|
27
|
+
});
|
|
28
|
+
export const RangeParameters = z.object({
|
|
29
|
+
startIndex: z.number().int().min(1).describe('The starting index of the text range (inclusive, starts from 1).'),
|
|
30
|
+
endIndex: z.number().int().min(1).describe('The ending index of the text range (exclusive).'),
|
|
31
|
+
}).refine(data => data.endIndex > data.startIndex, {
|
|
32
|
+
message: "endIndex must be greater than startIndex",
|
|
33
|
+
path: ["endIndex"],
|
|
34
|
+
});
|
|
35
|
+
export const OptionalRangeParameters = z.object({
|
|
36
|
+
startIndex: z.number().int().min(1).optional().describe('Optional: The starting index of the text range (inclusive, starts from 1). If omitted, might apply to a found element or whole paragraph.'),
|
|
37
|
+
endIndex: z.number().int().min(1).optional().describe('Optional: The ending index of the text range (exclusive). If omitted, might apply to a found element or whole paragraph.'),
|
|
38
|
+
}).refine(data => !data.startIndex || !data.endIndex || data.endIndex > data.startIndex, {
|
|
39
|
+
message: "If both startIndex and endIndex are provided, endIndex must be greater than startIndex",
|
|
40
|
+
path: ["endIndex"],
|
|
41
|
+
});
|
|
42
|
+
export const TextFindParameter = z.object({
|
|
43
|
+
textToFind: z.string().min(1).describe('The exact text string to locate.'),
|
|
44
|
+
matchInstance: z.number().int().min(1).optional().default(1).describe('Which instance of the text to target (1st, 2nd, etc.). Defaults to 1.'),
|
|
45
|
+
});
|
|
46
|
+
// --- Style Parameter Schemas ---
|
|
47
|
+
export const TextStyleParameters = z.object({
|
|
48
|
+
bold: z.boolean().optional().describe('Apply bold formatting.'),
|
|
49
|
+
italic: z.boolean().optional().describe('Apply italic formatting.'),
|
|
50
|
+
underline: z.boolean().optional().describe('Apply underline formatting.'),
|
|
51
|
+
strikethrough: z.boolean().optional().describe('Apply strikethrough formatting.'),
|
|
52
|
+
fontSize: z.number().min(1).optional().describe('Set font size (in points, e.g., 12).'),
|
|
53
|
+
fontFamily: z.string().optional().describe('Set font family (e.g., "Arial", "Times New Roman").'),
|
|
54
|
+
foregroundColor: z.string()
|
|
55
|
+
.refine(validateHexColor, { message: "Invalid hex color format (e.g., #FF0000 or #F00)" })
|
|
56
|
+
.optional()
|
|
57
|
+
.describe('Set text color using hex format (e.g., "#FF0000").'),
|
|
58
|
+
backgroundColor: z.string()
|
|
59
|
+
.refine(validateHexColor, { message: "Invalid hex color format (e.g., #00FF00 or #0F0)" })
|
|
60
|
+
.optional()
|
|
61
|
+
.describe('Set text background color using hex format (e.g., "#FFFF00").'),
|
|
62
|
+
linkUrl: z.string().url().optional().describe('Make the text a hyperlink pointing to this URL.'),
|
|
63
|
+
// clearDirectFormatting: z.boolean().optional().describe('If true, attempts to clear all direct text formatting within the range before applying new styles.') // Harder to implement perfectly
|
|
64
|
+
}).describe("Parameters for character-level text formatting.");
|
|
65
|
+
export const ParagraphStyleParameters = z.object({
|
|
66
|
+
alignment: z.enum(['START', 'END', 'CENTER', 'JUSTIFIED']).optional().describe('Paragraph alignment. START=left for LTR languages, END=right for LTR languages.'),
|
|
67
|
+
indentStart: z.number().min(0).optional().describe('Left indentation in points.'),
|
|
68
|
+
indentEnd: z.number().min(0).optional().describe('Right indentation in points.'),
|
|
69
|
+
spaceAbove: z.number().min(0).optional().describe('Space before the paragraph in points.'),
|
|
70
|
+
spaceBelow: z.number().min(0).optional().describe('Space after the paragraph in points.'),
|
|
71
|
+
namedStyleType: z.enum([
|
|
72
|
+
'NORMAL_TEXT', 'TITLE', 'SUBTITLE',
|
|
73
|
+
'HEADING_1', 'HEADING_2', 'HEADING_3', 'HEADING_4', 'HEADING_5', 'HEADING_6'
|
|
74
|
+
]).optional().describe('Apply a built-in named paragraph style (e.g., HEADING_1).'),
|
|
75
|
+
keepWithNext: z.boolean().optional().describe('Keep this paragraph together with the next one on the same page.'),
|
|
76
|
+
// Borders are more complex, might need separate objects/tools
|
|
77
|
+
// clearDirectFormatting: z.boolean().optional().describe('If true, attempts to clear all direct paragraph formatting within the range before applying new styles.') // Harder to implement perfectly
|
|
78
|
+
}).describe("Parameters for paragraph-level formatting.");
|
|
79
|
+
// --- Combination Schemas for Tools ---
|
|
80
|
+
export const ApplyTextStyleToolParameters = DocumentIdParameter.extend({
|
|
81
|
+
// Target EITHER by range OR by finding text
|
|
82
|
+
target: z.union([
|
|
83
|
+
RangeParameters,
|
|
84
|
+
TextFindParameter
|
|
85
|
+
]).describe("Specify the target range either by start/end indices or by finding specific text."),
|
|
86
|
+
style: TextStyleParameters.refine(styleArgs => Object.values(styleArgs).some(v => v !== undefined), { message: "At least one text style option must be provided." }).describe("The text styling to apply.")
|
|
87
|
+
});
|
|
88
|
+
export const ApplyParagraphStyleToolParameters = DocumentIdParameter.extend({
|
|
89
|
+
// Target EITHER by range OR by finding text (tool logic needs to find paragraph boundaries)
|
|
90
|
+
target: z.union([
|
|
91
|
+
RangeParameters, // User provides paragraph start/end (less likely)
|
|
92
|
+
TextFindParameter, // Find text within paragraph to apply style
|
|
93
|
+
z.object({
|
|
94
|
+
indexWithinParagraph: z.number().int().min(1).describe("An index located anywhere within the target paragraph.")
|
|
95
|
+
})
|
|
96
|
+
]).describe("Specify the target paragraph either by start/end indices, by finding text within it, or by providing an index within it."),
|
|
97
|
+
style: ParagraphStyleParameters.refine(styleArgs => Object.values(styleArgs).some(v => v !== undefined), { message: "At least one paragraph style option must be provided." }).describe("The paragraph styling to apply.")
|
|
98
|
+
});
|
|
99
|
+
// --- Error Class ---
|
|
100
|
+
// Use FastMCP's UserError for client-facing issues
|
|
101
|
+
// Define a custom error for internal issues if needed
|
|
102
|
+
export class NotImplementedError extends Error {
|
|
103
|
+
constructor(message = "This feature is not yet implemented.") {
|
|
104
|
+
super(message);
|
|
105
|
+
this.name = "NotImplementedError";
|
|
106
|
+
}
|
|
107
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@starfysh/gdrive-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "MCP server for Google Docs, Sheets, and Drive integration with Claude",
|
|
6
|
+
"main": "dist/server.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"gdrive-mcp": "dist/server.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test tests/",
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mcp",
|
|
22
|
+
"claude",
|
|
23
|
+
"google-docs",
|
|
24
|
+
"google-sheets",
|
|
25
|
+
"google-drive",
|
|
26
|
+
"anthropic",
|
|
27
|
+
"ai"
|
|
28
|
+
],
|
|
29
|
+
"author": "Starfysh <hello@starfysh.net>",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/starfysh-tech/gdrive-mcp.git"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/starfysh-tech/gdrive-mcp#readme",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/starfysh-tech/gdrive-mcp/issues"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"fastmcp": "^3.24.0",
|
|
44
|
+
"google-auth-library": "^9.15.1",
|
|
45
|
+
"googleapis": "^148.0.0",
|
|
46
|
+
"zod": "^3.24.2"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^22.14.1",
|
|
50
|
+
"tsx": "^4.19.3",
|
|
51
|
+
"typescript": "^5.8.3"
|
|
52
|
+
}
|
|
53
|
+
}
|