gdocs-mcp 0.4.1 → 0.4.2

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/dist/index.js CHANGED
@@ -29,7 +29,8 @@ import { CopyDocumentSchema, copyDocument } from './tools/copy-document.js';
29
29
  import { DeleteDocumentSchema, deleteDocument } from './tools/delete-document.js';
30
30
  import { GetCommentsSchema, getComments } from './tools/get-comments.js';
31
31
  import { AddCommentSchema, addComment } from './tools/add-comment.js';
32
- const VERSION = '0.4.0';
32
+ import { ReadDocumentStructureSchema, readDocumentStructure } from './tools/read-document-structure.js';
33
+ const VERSION = '0.4.2';
33
34
  const QUIET = process.env.GDOCS_MCP_QUIET === '1';
34
35
  const server = new McpServer({
35
36
  name: 'gdocs-mcp',
@@ -125,6 +126,8 @@ registerTool('copy_document', 'Create a copy of a Google Doc with a new title. I
125
126
  registerTool('delete_document', 'Move a Google Doc to trash. Requires confirmTitle matching the exact document title.', DeleteDocumentSchema, deleteDocument);
126
127
  registerTool('get_comments', 'List comments on a document with author, content, resolved status, and replies.', GetCommentsSchema, getComments);
127
128
  registerTool('add_comment', 'Add a comment anchored to specific text in the document. Anchors to first occurrence of quotedText.', AddCommentSchema, addComment);
129
+ // v0.4.2 structure tool
130
+ registerTool('read_document_structure', 'Return the structural outline of a Google Doc: paragraphs with indices, heading levels, text previews, tables with row/col counts, list metadata. Lightweight map for targeted edits.', ReadDocumentStructureSchema, readDocumentStructure);
128
131
  async function main() {
129
132
  const transport = new StdioServerTransport();
130
133
  await server.connect(transport);
@@ -0,0 +1,24 @@
1
+ import { z } from 'zod';
2
+ export declare const ReadDocumentStructureSchema: z.ZodObject<{
3
+ documentId: z.ZodString;
4
+ }, z.core.$strip>;
5
+ interface StructureElement {
6
+ type: 'paragraph' | 'table' | 'sectionBreak' | 'tableOfContents';
7
+ startIndex: number;
8
+ endIndex: number;
9
+ style?: string;
10
+ text?: string;
11
+ truncated?: boolean;
12
+ rows?: number;
13
+ cols?: number;
14
+ listId?: string;
15
+ nestingLevel?: number;
16
+ }
17
+ export declare function readDocumentStructure(args: z.infer<typeof ReadDocumentStructureSchema>): Promise<{
18
+ documentId: string;
19
+ title: string;
20
+ totalElements: number;
21
+ elements: StructureElement[];
22
+ _apiMs: number;
23
+ }>;
24
+ export {};
@@ -0,0 +1,116 @@
1
+ import { google } from 'googleapis';
2
+ import { z } from 'zod';
3
+ import { getAuthClient } from '../auth/google-auth.js';
4
+ const TEXT_PREVIEW_LENGTH = 80;
5
+ // Field mask: only structural data, no full text styling
6
+ const FIELD_MASK = [
7
+ 'title',
8
+ 'body.content.startIndex',
9
+ 'body.content.endIndex',
10
+ 'body.content.paragraph.paragraphStyle.namedStyleType',
11
+ 'body.content.paragraph.bullet',
12
+ 'body.content.paragraph.elements.startIndex',
13
+ 'body.content.paragraph.elements.endIndex',
14
+ 'body.content.paragraph.elements.textRun.content',
15
+ 'body.content.paragraph.elements.inlineObjectElement',
16
+ 'body.content.table.rows',
17
+ 'body.content.table.columns',
18
+ 'body.content.sectionBreak',
19
+ 'body.content.tableOfContents',
20
+ ].join(',');
21
+ export const ReadDocumentStructureSchema = z.object({
22
+ documentId: z.string().describe('The Google Doc document ID'),
23
+ });
24
+ export async function readDocumentStructure(args) {
25
+ const auth = getAuthClient();
26
+ const docs = google.docs({ version: 'v1', auth });
27
+ const apiStart = performance.now();
28
+ const res = await docs.documents.get({
29
+ documentId: args.documentId,
30
+ fields: FIELD_MASK,
31
+ });
32
+ const apiMs = Math.round(performance.now() - apiStart);
33
+ const content = res.data.body?.content || [];
34
+ const elements = [];
35
+ for (const element of content) {
36
+ const startIndex = element.startIndex ?? 0;
37
+ const endIndex = element.endIndex ?? 0;
38
+ if (element.sectionBreak !== undefined) {
39
+ elements.push({ type: 'sectionBreak', startIndex, endIndex });
40
+ continue;
41
+ }
42
+ if (element.tableOfContents !== undefined) {
43
+ elements.push({ type: 'tableOfContents', startIndex, endIndex });
44
+ continue;
45
+ }
46
+ if (element.table) {
47
+ elements.push({
48
+ type: 'table',
49
+ startIndex,
50
+ endIndex,
51
+ rows: element.table.rows ?? 0,
52
+ cols: element.table.columns ?? 0,
53
+ });
54
+ continue;
55
+ }
56
+ if (element.paragraph) {
57
+ const style = element.paragraph.paragraphStyle?.namedStyleType || 'NORMAL_TEXT';
58
+ // Build text preview from elements
59
+ let hasInlineObject = false;
60
+ let fullText = '';
61
+ for (const el of element.paragraph.elements || []) {
62
+ if (el.inlineObjectElement) {
63
+ hasInlineObject = true;
64
+ }
65
+ if (el.textRun?.content) {
66
+ fullText += el.textRun.content;
67
+ }
68
+ }
69
+ // Clean trailing newline
70
+ fullText = fullText.replace(/\n$/, '');
71
+ // Determine text preview
72
+ let text;
73
+ let truncated;
74
+ if (!fullText && hasInlineObject) {
75
+ text = '[image]';
76
+ truncated = false;
77
+ }
78
+ else if (fullText.length > TEXT_PREVIEW_LENGTH) {
79
+ text = fullText.substring(0, TEXT_PREVIEW_LENGTH) + '...';
80
+ truncated = true;
81
+ }
82
+ else {
83
+ text = fullText;
84
+ truncated = false;
85
+ }
86
+ const entry = {
87
+ type: 'paragraph',
88
+ startIndex,
89
+ endIndex,
90
+ style,
91
+ text,
92
+ truncated,
93
+ };
94
+ // Add list metadata if present
95
+ const bullet = element.paragraph.bullet;
96
+ if (bullet) {
97
+ if (bullet.listId)
98
+ entry.listId = bullet.listId;
99
+ if (bullet.nestingLevel !== undefined && bullet.nestingLevel !== null) {
100
+ entry.nestingLevel = bullet.nestingLevel;
101
+ }
102
+ else if (bullet.listId) {
103
+ entry.nestingLevel = 0;
104
+ }
105
+ }
106
+ elements.push(entry);
107
+ }
108
+ }
109
+ return {
110
+ documentId: args.documentId,
111
+ title: res.data.title || 'Untitled',
112
+ totalElements: elements.length,
113
+ elements,
114
+ _apiMs: apiMs,
115
+ };
116
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdocs-mcp",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
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",