gdocs-mcp 0.1.0 → 0.3.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 +73 -6
- package/dist/index.js +44 -2
- package/dist/presets/built-in.d.ts +2 -0
- package/dist/presets/built-in.js +270 -0
- package/dist/presets/config.d.ts +14 -0
- package/dist/presets/config.js +87 -0
- package/dist/presets/converter.d.ts +12 -0
- package/dist/presets/converter.js +289 -0
- package/dist/presets/types.d.ts +59 -0
- package/dist/presets/types.js +6 -0
- package/dist/tools/apply-style-preset.d.ts +18 -0
- package/dist/tools/apply-style-preset.js +208 -0
- package/dist/tools/delete-style-preset.d.ts +8 -0
- package/dist/tools/delete-style-preset.js +12 -0
- package/dist/tools/execute-script.d.ts +12 -0
- package/dist/tools/execute-script.js +37 -0
- package/dist/tools/extract-document-styles.d.ts +12 -0
- package/dist/tools/extract-document-styles.js +149 -0
- package/dist/tools/format-list.d.ts +20 -0
- package/dist/tools/format-list.js +74 -0
- package/dist/tools/list-style-presets.d.ts +18 -0
- package/dist/tools/list-style-presets.js +29 -0
- package/dist/tools/set-active-preset.d.ts +8 -0
- package/dist/tools/set-active-preset.js +12 -0
- package/dist/tools/update-header-footer.d.ts +16 -0
- package/dist/tools/update-header-footer.js +103 -0
- package/package.json +2 -2
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getAuthClient } from '../auth/google-auth.js';
|
|
4
|
+
import { savePreset } from '../presets/config.js';
|
|
5
|
+
import { VALID_NAMED_STYLE_TYPES } from '../presets/types.js';
|
|
6
|
+
export const ExtractDocumentStylesSchema = z.object({
|
|
7
|
+
documentId: z.string().describe('The Google Doc document ID to extract styles from'),
|
|
8
|
+
saveAs: z.string().optional().describe('If provided, saves the extracted styles as a named preset in ~/.gdocs-mcp/styles.json'),
|
|
9
|
+
});
|
|
10
|
+
export async function extractDocumentStyles(args) {
|
|
11
|
+
const auth = getAuthClient();
|
|
12
|
+
const docs = google.docs({ version: 'v1', auth });
|
|
13
|
+
const doc = await docs.documents.get({ documentId: args.documentId });
|
|
14
|
+
const preset = {
|
|
15
|
+
document: extractDocumentStyle(doc.data.documentStyle),
|
|
16
|
+
styles: extractNamedStyles(doc.data.namedStyles),
|
|
17
|
+
table: extractTableStyles(doc.data.body?.content || []),
|
|
18
|
+
};
|
|
19
|
+
if (args.saveAs) {
|
|
20
|
+
savePreset(args.saveAs, preset);
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
documentId: args.documentId,
|
|
24
|
+
title: doc.data.title,
|
|
25
|
+
preset,
|
|
26
|
+
savedAs: args.saveAs || null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function extractDocumentStyle(docStyle) {
|
|
30
|
+
const config = {};
|
|
31
|
+
if (docStyle?.marginTop?.magnitude !== undefined)
|
|
32
|
+
config.marginTop = docStyle.marginTop.magnitude;
|
|
33
|
+
if (docStyle?.marginBottom?.magnitude !== undefined)
|
|
34
|
+
config.marginBottom = docStyle.marginBottom.magnitude;
|
|
35
|
+
if (docStyle?.marginLeft?.magnitude !== undefined)
|
|
36
|
+
config.marginLeft = docStyle.marginLeft.magnitude;
|
|
37
|
+
if (docStyle?.marginRight?.magnitude !== undefined)
|
|
38
|
+
config.marginRight = docStyle.marginRight.magnitude;
|
|
39
|
+
if (docStyle?.pageSize?.width?.magnitude !== undefined)
|
|
40
|
+
config.pageWidth = docStyle.pageSize.width.magnitude;
|
|
41
|
+
if (docStyle?.pageSize?.height?.magnitude !== undefined)
|
|
42
|
+
config.pageHeight = docStyle.pageSize.height.magnitude;
|
|
43
|
+
return config;
|
|
44
|
+
}
|
|
45
|
+
function extractNamedStyles(namedStyles) {
|
|
46
|
+
const result = {};
|
|
47
|
+
if (!namedStyles?.styles)
|
|
48
|
+
return result;
|
|
49
|
+
for (const style of namedStyles.styles) {
|
|
50
|
+
const type = style.namedStyleType;
|
|
51
|
+
if (!VALID_NAMED_STYLE_TYPES.includes(type))
|
|
52
|
+
continue;
|
|
53
|
+
const config = {};
|
|
54
|
+
const ts = style.textStyle || {};
|
|
55
|
+
const ps = style.paragraphStyle || {};
|
|
56
|
+
// Text style
|
|
57
|
+
if (ts.weightedFontFamily?.fontFamily)
|
|
58
|
+
config.fontFamily = ts.weightedFontFamily.fontFamily;
|
|
59
|
+
if (ts.fontSize?.magnitude !== undefined)
|
|
60
|
+
config.fontSize = ts.fontSize.magnitude;
|
|
61
|
+
if (ts.bold !== undefined)
|
|
62
|
+
config.bold = ts.bold;
|
|
63
|
+
if (ts.italic !== undefined)
|
|
64
|
+
config.italic = ts.italic;
|
|
65
|
+
if (ts.underline !== undefined)
|
|
66
|
+
config.underline = ts.underline;
|
|
67
|
+
if (ts.strikethrough !== undefined)
|
|
68
|
+
config.strikethrough = ts.strikethrough;
|
|
69
|
+
if (ts.smallCaps !== undefined)
|
|
70
|
+
config.smallCaps = ts.smallCaps;
|
|
71
|
+
if (ts.foregroundColor?.color?.rgbColor)
|
|
72
|
+
config.color = rgbToHex(ts.foregroundColor.color.rgbColor);
|
|
73
|
+
if (ts.backgroundColor?.color?.rgbColor)
|
|
74
|
+
config.backgroundColor = rgbToHex(ts.backgroundColor.color.rgbColor);
|
|
75
|
+
if (ts.baselineOffset)
|
|
76
|
+
config.baselineOffset = ts.baselineOffset;
|
|
77
|
+
// Paragraph style
|
|
78
|
+
if (ps.alignment)
|
|
79
|
+
config.alignment = ps.alignment;
|
|
80
|
+
if (ps.lineSpacing !== undefined)
|
|
81
|
+
config.lineSpacing = ps.lineSpacing;
|
|
82
|
+
if (ps.spaceAbove?.magnitude !== undefined)
|
|
83
|
+
config.spaceAbove = ps.spaceAbove.magnitude;
|
|
84
|
+
if (ps.spaceBelow?.magnitude !== undefined)
|
|
85
|
+
config.spaceBelow = ps.spaceBelow.magnitude;
|
|
86
|
+
if (ps.indentFirstLine?.magnitude !== undefined)
|
|
87
|
+
config.indentFirstLine = ps.indentFirstLine.magnitude;
|
|
88
|
+
if (ps.indentStart?.magnitude !== undefined)
|
|
89
|
+
config.indentStart = ps.indentStart.magnitude;
|
|
90
|
+
if (ps.indentEnd?.magnitude !== undefined)
|
|
91
|
+
config.indentEnd = ps.indentEnd.magnitude;
|
|
92
|
+
if (ps.keepLinesTogether !== undefined)
|
|
93
|
+
config.keepLinesTogether = ps.keepLinesTogether;
|
|
94
|
+
if (ps.keepWithNext !== undefined)
|
|
95
|
+
config.keepWithNext = ps.keepWithNext;
|
|
96
|
+
if (ps.direction)
|
|
97
|
+
config.direction = ps.direction;
|
|
98
|
+
if (Object.keys(config).length > 0) {
|
|
99
|
+
result[type] = config;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
function extractTableStyles(content) {
|
|
105
|
+
// Find first table and extract its styling as the template
|
|
106
|
+
for (const element of content) {
|
|
107
|
+
if (!element.table)
|
|
108
|
+
continue;
|
|
109
|
+
const table = element.table;
|
|
110
|
+
const firstRow = table.tableRows?.[0];
|
|
111
|
+
if (!firstRow)
|
|
112
|
+
continue;
|
|
113
|
+
const config = {};
|
|
114
|
+
const firstCell = firstRow.tableCells?.[0];
|
|
115
|
+
if (firstCell?.tableCellStyle) {
|
|
116
|
+
const cs = firstCell.tableCellStyle;
|
|
117
|
+
if (cs.backgroundColor?.color?.rgbColor) {
|
|
118
|
+
config.headerBackground = rgbToHex(cs.backgroundColor.color.rgbColor);
|
|
119
|
+
}
|
|
120
|
+
if (cs.borderTop?.width?.magnitude !== undefined) {
|
|
121
|
+
config.borderWidth = cs.borderTop.width.magnitude;
|
|
122
|
+
}
|
|
123
|
+
if (cs.borderTop?.color?.color?.rgbColor) {
|
|
124
|
+
config.borderColor = rgbToHex(cs.borderTop.color.color.rgbColor);
|
|
125
|
+
}
|
|
126
|
+
if (cs.paddingTop?.magnitude !== undefined) {
|
|
127
|
+
config.cellPadding = cs.paddingTop.magnitude;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Check second row for alternating background
|
|
131
|
+
if (table.tableRows?.length > 2) {
|
|
132
|
+
const thirdRow = table.tableRows[2];
|
|
133
|
+
const thirdCell = thirdRow?.tableCells?.[0];
|
|
134
|
+
if (thirdCell?.tableCellStyle?.backgroundColor?.color?.rgbColor) {
|
|
135
|
+
config.alternateRowBackground = rgbToHex(thirdCell.tableCellStyle.backgroundColor.color.rgbColor);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (Object.keys(config).length > 0)
|
|
139
|
+
return config;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
function rgbToHex(rgb) {
|
|
145
|
+
const r = Math.round((rgb.red || 0) * 255);
|
|
146
|
+
const g = Math.round((rgb.green || 0) * 255);
|
|
147
|
+
const b = Math.round((rgb.blue || 0) * 255);
|
|
148
|
+
return '#' + [r, g, b].map(c => c.toString(16).padStart(2, '0')).join('');
|
|
149
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const FormatListSchema: z.ZodObject<{
|
|
3
|
+
documentId: z.ZodString;
|
|
4
|
+
startIndex: z.ZodNumber;
|
|
5
|
+
endIndex: z.ZodNumber;
|
|
6
|
+
listType: z.ZodEnum<{
|
|
7
|
+
none: "none";
|
|
8
|
+
bullet: "bullet";
|
|
9
|
+
numbered: "numbered";
|
|
10
|
+
}>;
|
|
11
|
+
preset: z.ZodOptional<z.ZodString>;
|
|
12
|
+
}, z.core.$strip>;
|
|
13
|
+
export declare function formatList(args: z.infer<typeof FormatListSchema>): Promise<{
|
|
14
|
+
documentId: string;
|
|
15
|
+
listType: "none" | "bullet" | "numbered";
|
|
16
|
+
preset: string | null;
|
|
17
|
+
startIndex: number;
|
|
18
|
+
endIndex: number;
|
|
19
|
+
_apiMs: number;
|
|
20
|
+
}>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getAuthClient } from '../auth/google-auth.js';
|
|
4
|
+
const BULLET_PRESETS = [
|
|
5
|
+
'BULLET_DISC_CIRCLE_SQUARE',
|
|
6
|
+
'BULLET_DIAMONDX_ARROW3D_SQUARE',
|
|
7
|
+
'BULLET_CHECKBOX',
|
|
8
|
+
];
|
|
9
|
+
const NUMBERED_PRESETS = [
|
|
10
|
+
'NUMBERED_DECIMAL_ALPHA_ROMAN',
|
|
11
|
+
'NUMBERED_DECIMAL_NESTED',
|
|
12
|
+
'NUMBERED_UPPERALPHA_ALPHA_ROMAN',
|
|
13
|
+
];
|
|
14
|
+
const ALL_PRESETS = [...BULLET_PRESETS, ...NUMBERED_PRESETS];
|
|
15
|
+
export const FormatListSchema = z.object({
|
|
16
|
+
documentId: z.string().describe('The Google Doc document ID'),
|
|
17
|
+
startIndex: z.number().describe('Start index of the paragraph range'),
|
|
18
|
+
endIndex: z.number().describe('End index of the paragraph range'),
|
|
19
|
+
listType: z.enum(['bullet', 'numbered', 'none']).describe('"bullet" for unordered, "numbered" for ordered, "none" to remove list formatting'),
|
|
20
|
+
preset: z.string().optional().describe('Bullet/number preset. Bullets: BULLET_DISC_CIRCLE_SQUARE (default), BULLET_DIAMONDX_ARROW3D_SQUARE, BULLET_CHECKBOX. ' +
|
|
21
|
+
'Numbers: NUMBERED_DECIMAL_ALPHA_ROMAN (default), NUMBERED_DECIMAL_NESTED, NUMBERED_UPPERALPHA_ALPHA_ROMAN.'),
|
|
22
|
+
});
|
|
23
|
+
export async function formatList(args) {
|
|
24
|
+
const auth = getAuthClient();
|
|
25
|
+
const docs = google.docs({ version: 'v1', auth });
|
|
26
|
+
const apiStart = performance.now();
|
|
27
|
+
const requests = [];
|
|
28
|
+
if (args.listType === 'none') {
|
|
29
|
+
requests.push({
|
|
30
|
+
deleteParagraphBullets: {
|
|
31
|
+
range: { startIndex: args.startIndex, endIndex: args.endIndex },
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
const defaultPreset = args.listType === 'bullet'
|
|
37
|
+
? 'BULLET_DISC_CIRCLE_SQUARE'
|
|
38
|
+
: 'NUMBERED_DECIMAL_ALPHA_ROMAN';
|
|
39
|
+
const preset = args.preset || defaultPreset;
|
|
40
|
+
// Validate preset matches list type
|
|
41
|
+
if (args.listType === 'bullet' && !BULLET_PRESETS.includes(preset)) {
|
|
42
|
+
if (NUMBERED_PRESETS.includes(preset)) {
|
|
43
|
+
throw new Error(`Preset "${preset}" is a numbered preset. Use listType: "numbered" or pick a bullet preset: ${BULLET_PRESETS.join(', ')}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (args.listType === 'numbered' && !NUMBERED_PRESETS.includes(preset)) {
|
|
47
|
+
if (BULLET_PRESETS.includes(preset)) {
|
|
48
|
+
throw new Error(`Preset "${preset}" is a bullet preset. Use listType: "bullet" or pick a numbered preset: ${NUMBERED_PRESETS.join(', ')}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!ALL_PRESETS.includes(preset)) {
|
|
52
|
+
throw new Error(`Unknown preset "${preset}". Available: ${ALL_PRESETS.join(', ')}`);
|
|
53
|
+
}
|
|
54
|
+
requests.push({
|
|
55
|
+
createParagraphBullets: {
|
|
56
|
+
range: { startIndex: args.startIndex, endIndex: args.endIndex },
|
|
57
|
+
bulletPreset: preset,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
await docs.documents.batchUpdate({
|
|
62
|
+
documentId: args.documentId,
|
|
63
|
+
requestBody: { requests },
|
|
64
|
+
});
|
|
65
|
+
const apiMs = Math.round(performance.now() - apiStart);
|
|
66
|
+
return {
|
|
67
|
+
documentId: args.documentId,
|
|
68
|
+
listType: args.listType,
|
|
69
|
+
preset: args.listType === 'none' ? null : (args.preset || (args.listType === 'bullet' ? 'BULLET_DISC_CIRCLE_SQUARE' : 'NUMBERED_DECIMAL_ALPHA_ROMAN')),
|
|
70
|
+
startIndex: args.startIndex,
|
|
71
|
+
endIndex: args.endIndex,
|
|
72
|
+
_apiMs: apiMs,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const ListStylePresetsSchema: z.ZodObject<{}, z.core.$strip>;
|
|
3
|
+
export declare function listStylePresets(): Promise<{
|
|
4
|
+
activePreset: string;
|
|
5
|
+
presets: {
|
|
6
|
+
name: string;
|
|
7
|
+
source: "built-in" | "user";
|
|
8
|
+
isActive: boolean;
|
|
9
|
+
summary: {
|
|
10
|
+
bodyFont: string;
|
|
11
|
+
bodySize: string | number;
|
|
12
|
+
headingFont: string;
|
|
13
|
+
headingColor: string;
|
|
14
|
+
lineSpacing: string | number;
|
|
15
|
+
};
|
|
16
|
+
}[];
|
|
17
|
+
count: number;
|
|
18
|
+
}>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { listAllPresets, getActivePresetName } from '../presets/config.js';
|
|
3
|
+
export const ListStylePresetsSchema = z.object({});
|
|
4
|
+
export async function listStylePresets() {
|
|
5
|
+
const presets = listAllPresets();
|
|
6
|
+
const activePreset = getActivePresetName();
|
|
7
|
+
const result = Object.entries(presets).map(([name, { source, preset }]) => {
|
|
8
|
+
const styles = preset.styles || {};
|
|
9
|
+
const normalText = styles.NORMAL_TEXT || {};
|
|
10
|
+
const heading1 = styles.HEADING_1 || {};
|
|
11
|
+
return {
|
|
12
|
+
name,
|
|
13
|
+
source,
|
|
14
|
+
isActive: name === activePreset,
|
|
15
|
+
summary: {
|
|
16
|
+
bodyFont: normalText.fontFamily || 'default',
|
|
17
|
+
bodySize: normalText.fontSize || 'default',
|
|
18
|
+
headingFont: heading1.fontFamily || normalText.fontFamily || 'default',
|
|
19
|
+
headingColor: heading1.color || 'default',
|
|
20
|
+
lineSpacing: normalText.lineSpacing || 'default',
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
return {
|
|
25
|
+
activePreset: activePreset || 'none',
|
|
26
|
+
presets: result,
|
|
27
|
+
count: result.length,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const SetActivePresetSchema: z.ZodObject<{
|
|
3
|
+
presetName: z.ZodString;
|
|
4
|
+
}, z.core.$strip>;
|
|
5
|
+
export declare function setActivePreset(args: z.infer<typeof SetActivePresetSchema>): Promise<{
|
|
6
|
+
activePreset: string;
|
|
7
|
+
message: string;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { setActivePreset as setActive } from '../presets/config.js';
|
|
3
|
+
export const SetActivePresetSchema = z.object({
|
|
4
|
+
presetName: z.string().describe('Name of the preset to set as active. The active preset auto-applies when creating new documents.'),
|
|
5
|
+
});
|
|
6
|
+
export async function setActivePreset(args) {
|
|
7
|
+
setActive(args.presetName);
|
|
8
|
+
return {
|
|
9
|
+
activePreset: args.presetName,
|
|
10
|
+
message: `Active preset set to "${args.presetName}". New documents will use this preset automatically.`,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const UpdateHeaderFooterSchema: z.ZodObject<{
|
|
3
|
+
documentId: z.ZodString;
|
|
4
|
+
section: z.ZodEnum<{
|
|
5
|
+
header: "header";
|
|
6
|
+
footer: "footer";
|
|
7
|
+
}>;
|
|
8
|
+
content: z.ZodOptional<z.ZodString>;
|
|
9
|
+
}, z.core.$strip>;
|
|
10
|
+
export declare function updateHeaderFooter(args: z.infer<typeof UpdateHeaderFooterSchema>): Promise<{
|
|
11
|
+
documentId: string;
|
|
12
|
+
section: "header" | "footer";
|
|
13
|
+
created: boolean;
|
|
14
|
+
contentInserted: boolean;
|
|
15
|
+
_apiMs: number;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getAuthClient } from '../auth/google-auth.js';
|
|
4
|
+
export const UpdateHeaderFooterSchema = z.object({
|
|
5
|
+
documentId: z.string().describe('The Google Doc document ID'),
|
|
6
|
+
section: z.enum(['header', 'footer']).describe('Which section to update: "header" or "footer"'),
|
|
7
|
+
content: z.string().optional().describe('Text content to insert. Replaces existing content. Use {{pageNumber}} to insert a page number field.'),
|
|
8
|
+
});
|
|
9
|
+
export async function updateHeaderFooter(args) {
|
|
10
|
+
const auth = getAuthClient();
|
|
11
|
+
const docs = google.docs({ version: 'v1', auth });
|
|
12
|
+
const apiStart = performance.now();
|
|
13
|
+
// Read document to check if header/footer exists
|
|
14
|
+
const doc = await docs.documents.get({ documentId: args.documentId });
|
|
15
|
+
const isHeader = args.section === 'header';
|
|
16
|
+
const existingId = isHeader
|
|
17
|
+
? doc.data.headers && Object.keys(doc.data.headers)[0]
|
|
18
|
+
: doc.data.footers && Object.keys(doc.data.footers)[0];
|
|
19
|
+
const requests = [];
|
|
20
|
+
// Create header/footer if it doesn't exist
|
|
21
|
+
if (!existingId) {
|
|
22
|
+
requests.push(isHeader
|
|
23
|
+
? { createHeader: { type: 'DEFAULT', sectionBreakLocation: { index: 0 } } }
|
|
24
|
+
: { createFooter: { type: 'DEFAULT', sectionBreakLocation: { index: 0 } } });
|
|
25
|
+
// Need to execute first to get the ID, then insert content
|
|
26
|
+
if (args.content) {
|
|
27
|
+
await docs.documents.batchUpdate({
|
|
28
|
+
documentId: args.documentId,
|
|
29
|
+
requestBody: { requests },
|
|
30
|
+
});
|
|
31
|
+
// Re-read to get the new ID
|
|
32
|
+
const updated = await docs.documents.get({ documentId: args.documentId });
|
|
33
|
+
const newId = isHeader
|
|
34
|
+
? Object.keys(updated.data.headers || {})[0]
|
|
35
|
+
: Object.keys(updated.data.footers || {})[0];
|
|
36
|
+
if (newId && args.content) {
|
|
37
|
+
await insertContent(docs, args.documentId, newId, args.content, isHeader, updated.data);
|
|
38
|
+
}
|
|
39
|
+
const apiMs = Math.round(performance.now() - apiStart);
|
|
40
|
+
return {
|
|
41
|
+
documentId: args.documentId,
|
|
42
|
+
section: args.section,
|
|
43
|
+
created: true,
|
|
44
|
+
contentInserted: !!args.content,
|
|
45
|
+
_apiMs: apiMs,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
await docs.documents.batchUpdate({
|
|
49
|
+
documentId: args.documentId,
|
|
50
|
+
requestBody: { requests },
|
|
51
|
+
});
|
|
52
|
+
const apiMs = Math.round(performance.now() - apiStart);
|
|
53
|
+
return {
|
|
54
|
+
documentId: args.documentId,
|
|
55
|
+
section: args.section,
|
|
56
|
+
created: true,
|
|
57
|
+
contentInserted: false,
|
|
58
|
+
_apiMs: apiMs,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// Header/footer exists — update content if provided
|
|
62
|
+
if (args.content) {
|
|
63
|
+
await insertContent(docs, args.documentId, existingId, args.content, isHeader, doc.data);
|
|
64
|
+
}
|
|
65
|
+
const apiMs = Math.round(performance.now() - apiStart);
|
|
66
|
+
return {
|
|
67
|
+
documentId: args.documentId,
|
|
68
|
+
section: args.section,
|
|
69
|
+
created: false,
|
|
70
|
+
contentInserted: !!args.content,
|
|
71
|
+
_apiMs: apiMs,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async function insertContent(docs, documentId, sectionId, content, isHeader, docData) {
|
|
75
|
+
const sectionData = isHeader
|
|
76
|
+
? docData.headers?.[sectionId]
|
|
77
|
+
: docData.footers?.[sectionId];
|
|
78
|
+
if (!sectionData?.content)
|
|
79
|
+
return;
|
|
80
|
+
const sectionContent = sectionData.content;
|
|
81
|
+
const startIndex = sectionContent[0]?.startIndex || 0;
|
|
82
|
+
const endIndex = sectionContent[sectionContent.length - 1]?.endIndex || startIndex + 1;
|
|
83
|
+
const requests = [];
|
|
84
|
+
// Delete existing content
|
|
85
|
+
if (endIndex > startIndex + 1) {
|
|
86
|
+
requests.push({
|
|
87
|
+
deleteContentRange: {
|
|
88
|
+
range: { segmentId: sectionId, startIndex: startIndex, endIndex: endIndex - 1 },
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// Insert new content
|
|
93
|
+
requests.push({
|
|
94
|
+
insertText: {
|
|
95
|
+
location: { segmentId: sectionId, index: startIndex },
|
|
96
|
+
text: content.replace('{{pageNumber}}', ''),
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
await docs.documents.batchUpdate({
|
|
100
|
+
documentId,
|
|
101
|
+
requestBody: { requests },
|
|
102
|
+
});
|
|
103
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdocs-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Open-source MCP server for Google Docs and Sheets. Self-hosted, local OAuth, no third-party token storage.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"repository": {
|
|
32
32
|
"type": "git",
|
|
33
|
-
"url": "https://github.com/
|
|
33
|
+
"url": "https://github.com/Apurwa/gdocs-mcp"
|
|
34
34
|
},
|
|
35
35
|
"engines": {
|
|
36
36
|
"node": ">=18"
|