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
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# gdocs-mcp
|
|
2
2
|
|
|
3
|
-
Open-source MCP server for Google Docs and Sheets. Give Claude (or any MCP-compatible AI) the ability to read, create, edit, and
|
|
3
|
+
Open-source MCP server for Google Docs and Sheets. Give Claude (or any MCP-compatible AI) the ability to read, create, edit, search, and style your Google Docs — with OAuth tokens that never leave your machine.
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Open-source. Self-hosted. Your tokens never leave your machine.**
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
@@ -46,7 +46,7 @@ Restart Claude Desktop. Ask Claude: *"List my recent Google Docs"* to verify.
|
|
|
46
46
|
claude mcp add gdocs -- npx gdocs-mcp
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
## Tools
|
|
49
|
+
## Tools (16)
|
|
50
50
|
|
|
51
51
|
### Google Docs
|
|
52
52
|
|
|
@@ -69,21 +69,88 @@ claude mcp add gdocs -- npx gdocs-mcp
|
|
|
69
69
|
|------|-------------|
|
|
70
70
|
| `get_charts` | List all charts in a spreadsheet with IDs and specs |
|
|
71
71
|
|
|
72
|
+
### Style Presets
|
|
73
|
+
|
|
74
|
+
Define how your documents look once, apply everywhere.
|
|
75
|
+
|
|
76
|
+
| Tool | Description |
|
|
77
|
+
|------|-------------|
|
|
78
|
+
| `extract_document_styles` | Extract styles from a reference Google Doc and save as a reusable preset |
|
|
79
|
+
| `apply_style_preset` | Apply a style preset to any document — fonts, colors, spacing, tables |
|
|
80
|
+
| `list_style_presets` | List all available presets (4 built-in + your custom presets) |
|
|
81
|
+
| `set_active_preset` | Set the default preset for new documents |
|
|
82
|
+
| `delete_style_preset` | Delete a custom preset |
|
|
83
|
+
|
|
84
|
+
#### Built-in Presets
|
|
85
|
+
|
|
86
|
+
| Preset | Font | Headings | Body |
|
|
87
|
+
|--------|------|----------|------|
|
|
88
|
+
| `clean` | Inter | Dark charcoal, 22/16/13pt | 11pt, 1.5x spacing |
|
|
89
|
+
| `corporate` | Arial | Dark grey, conservative | 11pt, 1.4x spacing |
|
|
90
|
+
| `classic` | Georgia + Garamond | Dark blue, serif | 11pt, 1.4x spacing |
|
|
91
|
+
| `minimal` | Roboto | Black, light weight | 10.5pt, 1.5x spacing |
|
|
92
|
+
|
|
93
|
+
#### Extract + Apply Workflow
|
|
94
|
+
|
|
95
|
+
The fastest way to use presets: point to a Google Doc that already looks the way you want.
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
You: "Extract styles from my brand doc and save as 'brand'"
|
|
99
|
+
→ extract_document_styles(documentId, saveAs: "brand")
|
|
100
|
+
|
|
101
|
+
You: "Apply brand styles to this report"
|
|
102
|
+
→ apply_style_preset(documentId, presetName: "brand")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
One-step capture, one-step apply. No JSON editing needed.
|
|
106
|
+
|
|
107
|
+
#### Style Properties
|
|
108
|
+
|
|
109
|
+
Presets support every text and paragraph property the Google Docs API exposes:
|
|
110
|
+
|
|
111
|
+
**Text:** font family, font size, bold, italic, underline, strikethrough, small caps, text color, background color, baseline offset
|
|
112
|
+
|
|
113
|
+
**Paragraph:** alignment (left/center/right/justified), line spacing, space above/below, first line indent, start/end indent, keep lines together, keep with next, direction (LTR/RTL), paragraph borders (top/bottom/left/right)
|
|
114
|
+
|
|
115
|
+
**Document:** page margins (top/bottom/left/right), page size (width/height)
|
|
116
|
+
|
|
117
|
+
**Tables:** header row background and text color, bold headers, border color and width, cell padding, alternating row backgrounds
|
|
118
|
+
|
|
119
|
+
Only properties you specify are applied. Omitted properties are left unchanged. Config is stored at `~/.gdocs-mcp/styles.json`.
|
|
120
|
+
|
|
121
|
+
## Limitations
|
|
122
|
+
|
|
123
|
+
These are Google Docs API limitations, not gdocs-mcp limitations:
|
|
124
|
+
|
|
125
|
+
- **Table of Contents styling** is not controllable — TOC entries mirror heading styles
|
|
126
|
+
- **Custom named styles** cannot be created — only the 9 built-in types (Title, Subtitle, Heading 1-6, Normal Text)
|
|
127
|
+
- **Conditional formatting** does not exist in Google Docs
|
|
128
|
+
- **Multi-column layout** is not exposed via the API
|
|
129
|
+
- **Bullet/numbered list glyph types** are per-paragraph, not configurable via style presets
|
|
130
|
+
- **Table header detection** assumes row 0 is the header — multi-row headers are not detected
|
|
131
|
+
|
|
72
132
|
## Security
|
|
73
133
|
|
|
74
|
-
- OAuth tokens
|
|
75
|
-
- Credentials
|
|
134
|
+
- OAuth tokens stored locally at `~/.gdocs-mcp/token.json` with `600` permissions
|
|
135
|
+
- Credentials stored at `~/.gdocs-mcp/credentials.json` with `600` permissions
|
|
136
|
+
- Style presets stored at `~/.gdocs-mcp/styles.json`
|
|
76
137
|
- No telemetry, no data collection, no third-party token storage
|
|
77
138
|
- Tokens refresh automatically; if refresh fails, run `npx gdocs-mcp auth` again
|
|
78
139
|
|
|
79
140
|
## Configuration
|
|
80
141
|
|
|
81
|
-
|
|
142
|
+
Credentials and tokens are stored in `~/.gdocs-mcp/` by default. Override credentials path with:
|
|
82
143
|
|
|
83
144
|
```bash
|
|
84
145
|
GDOCS_CREDENTIALS=/path/to/credentials.json npx gdocs-mcp
|
|
85
146
|
```
|
|
86
147
|
|
|
148
|
+
For faster startup, install globally instead of using npx:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npm install -g gdocs-mcp
|
|
152
|
+
```
|
|
153
|
+
|
|
87
154
|
## Requirements
|
|
88
155
|
|
|
89
156
|
- Node.js 18+
|
package/dist/index.js
CHANGED
|
@@ -12,9 +12,19 @@ import { UpdateDocumentStyleSchema, updateDocumentStyle } from './tools/update-d
|
|
|
12
12
|
import { UpdateDocumentSchema, updateDocument } from './tools/update-document.js';
|
|
13
13
|
import { UnmergeTableCellsSchema, unmergeTableCells } from './tools/unmerge-table-cells.js';
|
|
14
14
|
import { GetChartsSchema, getCharts } from './tools/get-charts.js';
|
|
15
|
+
import { ApplyStylePresetSchema, applyStylePreset } from './tools/apply-style-preset.js';
|
|
16
|
+
import { ExtractDocumentStylesSchema, extractDocumentStyles } from './tools/extract-document-styles.js';
|
|
17
|
+
import { ListStylePresetsSchema, listStylePresets } from './tools/list-style-presets.js';
|
|
18
|
+
import { SetActivePresetSchema, setActivePreset } from './tools/set-active-preset.js';
|
|
19
|
+
import { DeleteStylePresetSchema, deleteStylePreset } from './tools/delete-style-preset.js';
|
|
20
|
+
import { UpdateHeaderFooterSchema, updateHeaderFooter } from './tools/update-header-footer.js';
|
|
21
|
+
import { FormatListSchema, formatList } from './tools/format-list.js';
|
|
22
|
+
import { ExecuteScriptSchema, executeScript } from './tools/execute-script.js';
|
|
23
|
+
const VERSION = '0.3.0';
|
|
24
|
+
const QUIET = process.env.GDOCS_MCP_QUIET === '1';
|
|
15
25
|
const server = new McpServer({
|
|
16
26
|
name: 'gdocs-mcp',
|
|
17
|
-
version:
|
|
27
|
+
version: VERSION,
|
|
18
28
|
});
|
|
19
29
|
function formatError(err) {
|
|
20
30
|
if (err instanceof Error) {
|
|
@@ -31,19 +41,41 @@ function formatError(err) {
|
|
|
31
41
|
if (message.includes('PERMISSION_DENIED') || message.includes('403')) {
|
|
32
42
|
return 'Permission denied. Ensure the document is shared with your Google account.';
|
|
33
43
|
}
|
|
44
|
+
if (message.includes('Apps Script API has not been used')) {
|
|
45
|
+
return 'Apps Script API is not enabled. Enable it at: https://console.cloud.google.com/apis/library/script.googleapis.com';
|
|
46
|
+
}
|
|
34
47
|
return message;
|
|
35
48
|
}
|
|
36
49
|
return String(err);
|
|
37
50
|
}
|
|
38
51
|
function registerTool(name, description, schema, handler) {
|
|
39
52
|
server.tool(name, description, schema.shape, async (args) => {
|
|
53
|
+
const startTime = performance.now();
|
|
40
54
|
try {
|
|
41
55
|
const result = await handler(args);
|
|
56
|
+
if (!QUIET) {
|
|
57
|
+
const totalMs = Math.round(performance.now() - startTime);
|
|
58
|
+
const apiMs = result?._apiMs;
|
|
59
|
+
const requestCount = result?.requestCount;
|
|
60
|
+
delete result?._apiMs;
|
|
61
|
+
let logParts = [`[gdocs-mcp] ${name}: ${totalMs}ms`];
|
|
62
|
+
if (apiMs !== undefined) {
|
|
63
|
+
logParts.push(`(api: ${apiMs}ms${requestCount ? `, ${requestCount} requests` : ''})`);
|
|
64
|
+
}
|
|
65
|
+
else if (totalMs < 10) {
|
|
66
|
+
logParts.push('(local)');
|
|
67
|
+
}
|
|
68
|
+
console.error(logParts.join(' '));
|
|
69
|
+
}
|
|
42
70
|
return {
|
|
43
71
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
44
72
|
};
|
|
45
73
|
}
|
|
46
74
|
catch (err) {
|
|
75
|
+
if (!QUIET) {
|
|
76
|
+
const totalMs = Math.round(performance.now() - startTime);
|
|
77
|
+
console.error(`[gdocs-mcp] ${name}: ${totalMs}ms (error)`);
|
|
78
|
+
}
|
|
47
79
|
return {
|
|
48
80
|
content: [{ type: 'text', text: formatError(err) }],
|
|
49
81
|
isError: true,
|
|
@@ -64,10 +96,20 @@ registerTool('update_document', 'Apply batch updates to a document (insertText,
|
|
|
64
96
|
registerTool('unmerge_table_cells', 'Unmerge previously merged cells in a document table', UnmergeTableCellsSchema, unmergeTableCells);
|
|
65
97
|
// Google Sheets tools
|
|
66
98
|
registerTool('get_charts', 'List all charts in a Google Sheets spreadsheet with IDs and specs', GetChartsSchema, getCharts);
|
|
99
|
+
// Style preset tools
|
|
100
|
+
registerTool('apply_style_preset', 'Apply a style preset to a document. Styles all paragraphs by named type, updates margins, and formats tables.', ApplyStylePresetSchema, applyStylePreset);
|
|
101
|
+
registerTool('extract_document_styles', 'Extract styles from a Google Doc and optionally save as a reusable preset.', ExtractDocumentStylesSchema, extractDocumentStyles);
|
|
102
|
+
registerTool('list_style_presets', 'List all available style presets (built-in + user-defined) with typography summaries.', ListStylePresetsSchema, listStylePresets);
|
|
103
|
+
registerTool('set_active_preset', 'Set the active style preset. Auto-applies when creating new documents.', SetActivePresetSchema, setActivePreset);
|
|
104
|
+
registerTool('delete_style_preset', 'Delete a user-defined style preset. Cannot delete built-in presets.', DeleteStylePresetSchema, deleteStylePreset);
|
|
105
|
+
// v0.3 tools
|
|
106
|
+
registerTool('update_header_footer', 'Create or update header/footer content and styling. Supports page number insertion.', UpdateHeaderFooterSchema, updateHeaderFooter);
|
|
107
|
+
registerTool('format_list', 'Apply bullet, numbered, or remove list formatting on a paragraph range. Supports 6 glyph presets.', FormatListSchema, formatList);
|
|
108
|
+
registerTool('execute_script', 'Execute a function in a deployed Google Apps Script project. Returns JSON result.', ExecuteScriptSchema, executeScript);
|
|
67
109
|
async function main() {
|
|
68
110
|
const transport = new StdioServerTransport();
|
|
69
111
|
await server.connect(transport);
|
|
70
|
-
console.error(
|
|
112
|
+
console.error(`gdocs-mcp v${VERSION} started`);
|
|
71
113
|
}
|
|
72
114
|
main().catch((err) => {
|
|
73
115
|
console.error('Failed to start gdocs-mcp:', err);
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
export const BUILT_IN_PRESETS = {
|
|
2
|
+
clean: {
|
|
3
|
+
document: {
|
|
4
|
+
marginTop: 72,
|
|
5
|
+
marginBottom: 72,
|
|
6
|
+
marginLeft: 72,
|
|
7
|
+
marginRight: 72,
|
|
8
|
+
},
|
|
9
|
+
styles: {
|
|
10
|
+
TITLE: {
|
|
11
|
+
fontFamily: 'Inter',
|
|
12
|
+
fontSize: 28,
|
|
13
|
+
bold: true,
|
|
14
|
+
color: '#1a1a2e',
|
|
15
|
+
spaceBelow: 4,
|
|
16
|
+
},
|
|
17
|
+
SUBTITLE: {
|
|
18
|
+
fontFamily: 'Inter',
|
|
19
|
+
fontSize: 13,
|
|
20
|
+
italic: true,
|
|
21
|
+
color: '#666666',
|
|
22
|
+
spaceBelow: 16,
|
|
23
|
+
},
|
|
24
|
+
HEADING_1: {
|
|
25
|
+
fontFamily: 'Inter',
|
|
26
|
+
fontSize: 22,
|
|
27
|
+
bold: true,
|
|
28
|
+
color: '#1a1a2e',
|
|
29
|
+
spaceAbove: 24,
|
|
30
|
+
spaceBelow: 8,
|
|
31
|
+
keepWithNext: true,
|
|
32
|
+
},
|
|
33
|
+
HEADING_2: {
|
|
34
|
+
fontFamily: 'Inter',
|
|
35
|
+
fontSize: 16,
|
|
36
|
+
bold: true,
|
|
37
|
+
color: '#333333',
|
|
38
|
+
spaceAbove: 18,
|
|
39
|
+
spaceBelow: 6,
|
|
40
|
+
keepWithNext: true,
|
|
41
|
+
},
|
|
42
|
+
HEADING_3: {
|
|
43
|
+
fontFamily: 'Inter',
|
|
44
|
+
fontSize: 13,
|
|
45
|
+
bold: true,
|
|
46
|
+
color: '#555555',
|
|
47
|
+
spaceAbove: 14,
|
|
48
|
+
spaceBelow: 4,
|
|
49
|
+
keepWithNext: true,
|
|
50
|
+
},
|
|
51
|
+
NORMAL_TEXT: {
|
|
52
|
+
fontFamily: 'Inter',
|
|
53
|
+
fontSize: 11,
|
|
54
|
+
color: '#1a1a1a',
|
|
55
|
+
lineSpacing: 150,
|
|
56
|
+
spaceBelow: 6,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
table: {
|
|
60
|
+
headerBackground: '#1a1a2e',
|
|
61
|
+
headerTextColor: '#ffffff',
|
|
62
|
+
headerBold: true,
|
|
63
|
+
borderColor: '#dddddd',
|
|
64
|
+
borderWidth: 0.5,
|
|
65
|
+
alternateRowBackground: '#f9f9f9',
|
|
66
|
+
cellPadding: 5,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
corporate: {
|
|
70
|
+
document: {
|
|
71
|
+
marginTop: 72,
|
|
72
|
+
marginBottom: 72,
|
|
73
|
+
marginLeft: 72,
|
|
74
|
+
marginRight: 72,
|
|
75
|
+
},
|
|
76
|
+
styles: {
|
|
77
|
+
TITLE: {
|
|
78
|
+
fontFamily: 'Arial',
|
|
79
|
+
fontSize: 24,
|
|
80
|
+
bold: true,
|
|
81
|
+
color: '#333333',
|
|
82
|
+
spaceBelow: 4,
|
|
83
|
+
},
|
|
84
|
+
SUBTITLE: {
|
|
85
|
+
fontFamily: 'Arial',
|
|
86
|
+
fontSize: 12,
|
|
87
|
+
italic: true,
|
|
88
|
+
color: '#666666',
|
|
89
|
+
spaceBelow: 14,
|
|
90
|
+
},
|
|
91
|
+
HEADING_1: {
|
|
92
|
+
fontFamily: 'Arial',
|
|
93
|
+
fontSize: 18,
|
|
94
|
+
bold: true,
|
|
95
|
+
color: '#333333',
|
|
96
|
+
spaceAbove: 20,
|
|
97
|
+
spaceBelow: 6,
|
|
98
|
+
keepWithNext: true,
|
|
99
|
+
},
|
|
100
|
+
HEADING_2: {
|
|
101
|
+
fontFamily: 'Arial',
|
|
102
|
+
fontSize: 14,
|
|
103
|
+
bold: true,
|
|
104
|
+
color: '#444444',
|
|
105
|
+
spaceAbove: 16,
|
|
106
|
+
spaceBelow: 4,
|
|
107
|
+
keepWithNext: true,
|
|
108
|
+
},
|
|
109
|
+
HEADING_3: {
|
|
110
|
+
fontFamily: 'Arial',
|
|
111
|
+
fontSize: 12,
|
|
112
|
+
bold: true,
|
|
113
|
+
color: '#555555',
|
|
114
|
+
spaceAbove: 12,
|
|
115
|
+
spaceBelow: 4,
|
|
116
|
+
keepWithNext: true,
|
|
117
|
+
},
|
|
118
|
+
NORMAL_TEXT: {
|
|
119
|
+
fontFamily: 'Arial',
|
|
120
|
+
fontSize: 11,
|
|
121
|
+
color: '#333333',
|
|
122
|
+
lineSpacing: 140,
|
|
123
|
+
spaceBelow: 4,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
table: {
|
|
127
|
+
headerBackground: '#4472C4',
|
|
128
|
+
headerTextColor: '#ffffff',
|
|
129
|
+
headerBold: true,
|
|
130
|
+
borderColor: '#cccccc',
|
|
131
|
+
borderWidth: 0.5,
|
|
132
|
+
alternateRowBackground: '#f2f2f2',
|
|
133
|
+
cellPadding: 5,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
classic: {
|
|
137
|
+
document: {
|
|
138
|
+
marginTop: 72,
|
|
139
|
+
marginBottom: 72,
|
|
140
|
+
marginLeft: 72,
|
|
141
|
+
marginRight: 72,
|
|
142
|
+
},
|
|
143
|
+
styles: {
|
|
144
|
+
TITLE: {
|
|
145
|
+
fontFamily: 'Georgia',
|
|
146
|
+
fontSize: 26,
|
|
147
|
+
bold: true,
|
|
148
|
+
color: '#1a3c6e',
|
|
149
|
+
spaceBelow: 4,
|
|
150
|
+
},
|
|
151
|
+
SUBTITLE: {
|
|
152
|
+
fontFamily: 'Garamond',
|
|
153
|
+
fontSize: 13,
|
|
154
|
+
italic: true,
|
|
155
|
+
color: '#555555',
|
|
156
|
+
spaceBelow: 16,
|
|
157
|
+
},
|
|
158
|
+
HEADING_1: {
|
|
159
|
+
fontFamily: 'Georgia',
|
|
160
|
+
fontSize: 20,
|
|
161
|
+
bold: true,
|
|
162
|
+
color: '#1a3c6e',
|
|
163
|
+
spaceAbove: 22,
|
|
164
|
+
spaceBelow: 8,
|
|
165
|
+
keepWithNext: true,
|
|
166
|
+
},
|
|
167
|
+
HEADING_2: {
|
|
168
|
+
fontFamily: 'Georgia',
|
|
169
|
+
fontSize: 15,
|
|
170
|
+
bold: true,
|
|
171
|
+
color: '#2a5a8e',
|
|
172
|
+
spaceAbove: 18,
|
|
173
|
+
spaceBelow: 6,
|
|
174
|
+
keepWithNext: true,
|
|
175
|
+
},
|
|
176
|
+
HEADING_3: {
|
|
177
|
+
fontFamily: 'Georgia',
|
|
178
|
+
fontSize: 12,
|
|
179
|
+
bold: true,
|
|
180
|
+
italic: true,
|
|
181
|
+
color: '#3a6a9e',
|
|
182
|
+
spaceAbove: 14,
|
|
183
|
+
spaceBelow: 4,
|
|
184
|
+
keepWithNext: true,
|
|
185
|
+
},
|
|
186
|
+
NORMAL_TEXT: {
|
|
187
|
+
fontFamily: 'Garamond',
|
|
188
|
+
fontSize: 11,
|
|
189
|
+
color: '#222222',
|
|
190
|
+
lineSpacing: 140,
|
|
191
|
+
spaceBelow: 6,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
table: {
|
|
195
|
+
headerBackground: '#1a3c6e',
|
|
196
|
+
headerTextColor: '#ffffff',
|
|
197
|
+
headerBold: true,
|
|
198
|
+
borderColor: '#999999',
|
|
199
|
+
borderWidth: 0.75,
|
|
200
|
+
alternateRowBackground: '#eef3f8',
|
|
201
|
+
cellPadding: 5,
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
minimal: {
|
|
205
|
+
document: {
|
|
206
|
+
marginTop: 72,
|
|
207
|
+
marginBottom: 72,
|
|
208
|
+
marginLeft: 72,
|
|
209
|
+
marginRight: 72,
|
|
210
|
+
},
|
|
211
|
+
styles: {
|
|
212
|
+
TITLE: {
|
|
213
|
+
fontFamily: 'Roboto',
|
|
214
|
+
fontSize: 24,
|
|
215
|
+
bold: false,
|
|
216
|
+
color: '#000000',
|
|
217
|
+
spaceBelow: 4,
|
|
218
|
+
},
|
|
219
|
+
SUBTITLE: {
|
|
220
|
+
fontFamily: 'Roboto',
|
|
221
|
+
fontSize: 12,
|
|
222
|
+
color: '#888888',
|
|
223
|
+
spaceBelow: 16,
|
|
224
|
+
},
|
|
225
|
+
HEADING_1: {
|
|
226
|
+
fontFamily: 'Roboto',
|
|
227
|
+
fontSize: 18,
|
|
228
|
+
bold: false,
|
|
229
|
+
color: '#000000',
|
|
230
|
+
spaceAbove: 24,
|
|
231
|
+
spaceBelow: 8,
|
|
232
|
+
keepWithNext: true,
|
|
233
|
+
},
|
|
234
|
+
HEADING_2: {
|
|
235
|
+
fontFamily: 'Roboto',
|
|
236
|
+
fontSize: 14,
|
|
237
|
+
bold: true,
|
|
238
|
+
color: '#333333',
|
|
239
|
+
spaceAbove: 18,
|
|
240
|
+
spaceBelow: 6,
|
|
241
|
+
keepWithNext: true,
|
|
242
|
+
},
|
|
243
|
+
HEADING_3: {
|
|
244
|
+
fontFamily: 'Roboto',
|
|
245
|
+
fontSize: 11,
|
|
246
|
+
bold: true,
|
|
247
|
+
color: '#555555',
|
|
248
|
+
spaceAbove: 14,
|
|
249
|
+
spaceBelow: 4,
|
|
250
|
+
keepWithNext: true,
|
|
251
|
+
},
|
|
252
|
+
NORMAL_TEXT: {
|
|
253
|
+
fontFamily: 'Roboto',
|
|
254
|
+
fontSize: 10.5,
|
|
255
|
+
color: '#222222',
|
|
256
|
+
lineSpacing: 150,
|
|
257
|
+
spaceBelow: 6,
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
table: {
|
|
261
|
+
headerBackground: '#f5f5f5',
|
|
262
|
+
headerTextColor: '#000000',
|
|
263
|
+
headerBold: true,
|
|
264
|
+
borderColor: '#e0e0e0',
|
|
265
|
+
borderWidth: 0.5,
|
|
266
|
+
alternateRowBackground: '#fafafa',
|
|
267
|
+
cellPadding: 5,
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StylePreset } from './types.js';
|
|
2
|
+
export declare function getPreset(presetName?: string): {
|
|
3
|
+
name: string;
|
|
4
|
+
preset: StylePreset;
|
|
5
|
+
};
|
|
6
|
+
export declare function listAllPresets(): Record<string, {
|
|
7
|
+
source: 'built-in' | 'user';
|
|
8
|
+
preset: StylePreset;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function listAllPresetNames(): string[];
|
|
11
|
+
export declare function savePreset(name: string, preset: StylePreset): void;
|
|
12
|
+
export declare function deletePreset(name: string): void;
|
|
13
|
+
export declare function setActivePreset(name: string): void;
|
|
14
|
+
export declare function getActivePresetName(): string | undefined;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { BUILT_IN_PRESETS } from './built-in.js';
|
|
4
|
+
const CONFIG_DIR = path.join(process.env.HOME || '~', '.gdocs-mcp');
|
|
5
|
+
const STYLES_PATH = path.join(CONFIG_DIR, 'styles.json');
|
|
6
|
+
const BUILT_IN_NAMES = new Set(Object.keys(BUILT_IN_PRESETS));
|
|
7
|
+
function readConfig() {
|
|
8
|
+
if (!fs.existsSync(STYLES_PATH)) {
|
|
9
|
+
return { presets: {} };
|
|
10
|
+
}
|
|
11
|
+
return JSON.parse(fs.readFileSync(STYLES_PATH, 'utf8'));
|
|
12
|
+
}
|
|
13
|
+
function writeConfig(config) {
|
|
14
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
15
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
16
|
+
}
|
|
17
|
+
fs.writeFileSync(STYLES_PATH, JSON.stringify(config, null, 2));
|
|
18
|
+
fs.chmodSync(STYLES_PATH, 0o600);
|
|
19
|
+
}
|
|
20
|
+
export function getPreset(presetName) {
|
|
21
|
+
const config = readConfig();
|
|
22
|
+
const name = presetName || config.activePreset;
|
|
23
|
+
if (!name) {
|
|
24
|
+
throw new Error('No preset specified and no activePreset set. Run set_active_preset or pass a presetName.');
|
|
25
|
+
}
|
|
26
|
+
// User presets override built-ins
|
|
27
|
+
const preset = config.presets[name] || BUILT_IN_PRESETS[name];
|
|
28
|
+
if (!preset) {
|
|
29
|
+
const available = listAllPresetNames();
|
|
30
|
+
throw new Error(`Preset "${name}" not found. Available: ${available.join(', ')}`);
|
|
31
|
+
}
|
|
32
|
+
return { name, preset };
|
|
33
|
+
}
|
|
34
|
+
export function listAllPresets() {
|
|
35
|
+
const config = readConfig();
|
|
36
|
+
const result = {};
|
|
37
|
+
for (const [name, preset] of Object.entries(BUILT_IN_PRESETS)) {
|
|
38
|
+
result[name] = { source: 'built-in', preset };
|
|
39
|
+
}
|
|
40
|
+
// User presets override built-ins
|
|
41
|
+
for (const [name, preset] of Object.entries(config.presets)) {
|
|
42
|
+
result[name] = { source: BUILT_IN_NAMES.has(name) ? 'built-in' : 'user', preset };
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
export function listAllPresetNames() {
|
|
47
|
+
return Object.keys(listAllPresets());
|
|
48
|
+
}
|
|
49
|
+
export function savePreset(name, preset) {
|
|
50
|
+
const config = readConfig();
|
|
51
|
+
config.presets = { ...config.presets, [name]: preset };
|
|
52
|
+
writeConfig(config);
|
|
53
|
+
}
|
|
54
|
+
export function deletePreset(name) {
|
|
55
|
+
if (BUILT_IN_NAMES.has(name)) {
|
|
56
|
+
const config = readConfig();
|
|
57
|
+
if (!config.presets[name]) {
|
|
58
|
+
throw new Error(`Cannot delete built-in preset "${name}". You can override it by creating a preset with the same name.`);
|
|
59
|
+
}
|
|
60
|
+
// User override of built-in — remove the override, built-in remains
|
|
61
|
+
const { [name]: _, ...rest } = config.presets;
|
|
62
|
+
writeConfig({ ...config, presets: rest });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const config = readConfig();
|
|
66
|
+
if (!config.presets[name]) {
|
|
67
|
+
throw new Error(`Preset "${name}" not found.`);
|
|
68
|
+
}
|
|
69
|
+
const { [name]: _, ...rest } = config.presets;
|
|
70
|
+
const updated = { ...config, presets: rest };
|
|
71
|
+
// Clear activePreset if it was the deleted one
|
|
72
|
+
if (config.activePreset === name) {
|
|
73
|
+
delete updated.activePreset;
|
|
74
|
+
}
|
|
75
|
+
writeConfig(updated);
|
|
76
|
+
}
|
|
77
|
+
export function setActivePreset(name) {
|
|
78
|
+
const allNames = listAllPresetNames();
|
|
79
|
+
if (!allNames.includes(name)) {
|
|
80
|
+
throw new Error(`Preset "${name}" not found. Available: ${allNames.join(', ')}`);
|
|
81
|
+
}
|
|
82
|
+
const config = readConfig();
|
|
83
|
+
writeConfig({ ...config, activePreset: name });
|
|
84
|
+
}
|
|
85
|
+
export function getActivePresetName() {
|
|
86
|
+
return readConfig().activePreset;
|
|
87
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { NamedStyleConfig, DocumentStyleConfig, TableStyleConfig } from './types.js';
|
|
2
|
+
export declare function buildNamedStyleRequest(styles: Record<string, NamedStyleConfig>): {
|
|
3
|
+
updateNamedStyles: any;
|
|
4
|
+
};
|
|
5
|
+
export declare function buildDocumentStyleRequest(config: DocumentStyleConfig): {
|
|
6
|
+
updateDocumentStyle: any;
|
|
7
|
+
};
|
|
8
|
+
export declare function buildTableStyleRequests(tableConfig: TableStyleConfig, tables: Array<{
|
|
9
|
+
startIndex: number;
|
|
10
|
+
rows: number;
|
|
11
|
+
cols: number;
|
|
12
|
+
}>): any[];
|