@khoinguyen2002/doc-mcp 1.0.2 → 1.0.4

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.
@@ -2,8 +2,8 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
- import { listDriveFiles, readDriveDocument, } from "./tools/driveTools.js";
6
- import { saveAgentNote, searchKnowledge } from "./tools/knowledgeTools.js";
5
+ import { listDriveFiles, readDriveDocument } from "./tools/driveTools.js";
6
+ import { searchKnowledge } from "./tools/knowledgeTools.js";
7
7
  import { config } from "./config.js";
8
8
  const DRIVE_FOLDER_ID = config.DOC_MCP_DRIVE_FOLDER_ID;
9
9
  if (!DRIVE_FOLDER_ID) {
@@ -12,7 +12,7 @@ if (!DRIVE_FOLDER_ID) {
12
12
  }
13
13
  const server = new McpServer({
14
14
  name: "doc-agent",
15
- version: "1.0.0",
15
+ version: "1.0.4",
16
16
  });
17
17
  // Register tools
18
18
  server.registerTool("list_drive_files", {
@@ -40,29 +40,20 @@ server.registerTool("list_drive_files", {
40
40
  };
41
41
  });
42
42
  server.registerTool("read_drive_document", {
43
- description: "Read the content of a specific Google Drive document. The document will also be automatically ingested into vector memory for future semantic search.",
43
+ description: "Read the content of a specific Google Drive document. You can use the 'offset' parameter (obtained from search_knowledge) to read a specific chunk of text.",
44
44
  inputSchema: {
45
45
  fileId: z.string().describe("The Google Drive file ID to read"),
46
+ offset: z
47
+ .number()
48
+ .optional()
49
+ .describe("Starting character index (default: 0)"),
50
+ limit: z
51
+ .number()
52
+ .optional()
53
+ .describe("Maximum number of characters to return (default: 10000)"),
46
54
  },
47
- }, async ({ fileId }) => {
48
- const res = await readDriveDocument(fileId);
49
- if (!res.success) {
50
- return {
51
- content: [{ type: "text", text: `Error: ${res.error}` }],
52
- isError: true,
53
- };
54
- }
55
- return {
56
- content: [{ type: "text", text: res.content || "No content found." }],
57
- };
58
- });
59
- server.registerTool("save_agent_note", {
60
- description: "Save an agent note, thought, or summary directly into the vector memory.",
61
- inputSchema: {
62
- content: z.string().describe("The note or knowledge content to store"),
63
- },
64
- }, async ({ content }) => {
65
- const res = await saveAgentNote(content);
55
+ }, async ({ fileId, offset, limit }) => {
56
+ const res = await readDriveDocument(fileId, offset, limit);
66
57
  if (!res.success) {
67
58
  return {
68
59
  content: [{ type: "text", text: `Error: ${res.error}` }],
@@ -70,11 +61,11 @@ server.registerTool("save_agent_note", {
70
61
  };
71
62
  }
72
63
  return {
73
- content: [{ type: "text", text: res.message || "Saved successfully" }],
64
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
74
65
  };
75
66
  });
76
67
  server.registerTool("search_knowledge", {
77
- description: "Search the folder's vector memory for relevant context or knowledge.",
68
+ description: "Search the folder's vector memory for relevant context or knowledge. Returns structured JSON array of matching chunks.",
78
69
  inputSchema: {
79
70
  query: z.string().describe("The search query"),
80
71
  topK: z
@@ -16,14 +16,23 @@ export declare function syncSingleDocument(fileId: string, folderId: string): Pr
16
16
  driveModifiedTime: string;
17
17
  content?: undefined;
18
18
  }>;
19
- export declare function readDriveDocument(fileId: string): Promise<{
19
+ export declare function readDriveDocument(fileId: string, offset?: number, limit?: number): Promise<{
20
20
  success: boolean;
21
- content: string;
21
+ data: {
22
+ content: string;
23
+ metadata: {
24
+ totalSize: number;
25
+ offset: number;
26
+ limit: number;
27
+ isTruncated: boolean;
28
+ warning: string | undefined;
29
+ };
30
+ };
22
31
  error?: undefined;
23
32
  } | {
24
33
  success: boolean;
25
34
  error: any;
26
- content?: undefined;
35
+ data?: undefined;
27
36
  }>;
28
37
  export declare function syncFolderState(folderId: string): Promise<{
29
38
  success: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"driveTools.d.ts","sourceRoot":"","sources":["../../src/tools/driveTools.ts"],"names":[],"mappings":"AAiCA,wBAAsB,cAAc,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM;;;;;;;;GAoC7E;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;;;;;;;;GA6CxE;AAED,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM;;;;;;;;GAsCrD;AAED,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM;;;;;;GAqDrD"}
1
+ {"version":3,"file":"driveTools.d.ts","sourceRoot":"","sources":["../../src/tools/driveTools.ts"],"names":[],"mappings":"AAiCA,wBAAsB,cAAc,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM;;;;;;;;GAoC7E;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;;;;;;;;GAoDxE;AAED,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,GAAE,MAAU,EAAE,KAAK,GAAE,MAAc;;;;;;;;;;;;;;;;;GAsDhG;AAED,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM;;;;;;GAqDrD"}
@@ -79,19 +79,25 @@ export async function syncSingleDocument(fileId, folderId) {
79
79
  chunkOverlap: config.CHUNK_OVERLAP,
80
80
  });
81
81
  const chunks = await splitter.splitText(content);
82
+ let currentOffset = 0;
82
83
  for (const chunk of chunks) {
84
+ const offset = content.indexOf(chunk, currentOffset);
85
+ if (offset !== -1) {
86
+ currentOffset = offset;
87
+ }
83
88
  await upsertProjectDocument(folderId, chunk, {
84
89
  title: fileInfo.data.name || "Untitled Google Doc",
85
90
  source: "google_drive",
86
91
  file_id: fileId,
87
92
  modified_time: driveModifiedTime,
93
+ offset: offset !== -1 ? offset : 0,
88
94
  });
89
95
  }
90
96
  return { synced: true, content, driveModifiedTime };
91
97
  }
92
98
  return { synced: false, driveModifiedTime };
93
99
  }
94
- export async function readDriveDocument(fileId) {
100
+ export async function readDriveDocument(fileId, offset = 0, limit = 10000) {
95
101
  const folderId = config.DOC_MCP_DRIVE_FOLDER_ID;
96
102
  if (!folderId) {
97
103
  return {
@@ -112,15 +118,28 @@ export async function readDriveDocument(fileId) {
112
118
  content = typeof res.data === "string" ? res.data : "";
113
119
  }
114
120
  let finalContent = content;
115
- const MAX_CHARS = 10000;
116
- if (finalContent && finalContent.length > MAX_CHARS) {
117
- finalContent =
118
- finalContent.substring(0, MAX_CHARS) +
119
- "\n\n... [Content truncated due to length. The full document has been automatically ingested into Vector Memory. Use search_knowledge to query specific details.]";
121
+ const totalSize = finalContent ? finalContent.length : 0;
122
+ if (finalContent) {
123
+ finalContent = finalContent.substring(offset, offset + limit);
124
+ }
125
+ const isTruncated = offset + (finalContent?.length || 0) < totalSize;
126
+ let warning = undefined;
127
+ if (isTruncated) {
128
+ warning = `[WARNING]: This is not the entire document. Content has been truncated from character ${offset} to ${offset + finalContent.length} out of ${totalSize} total characters. Please use 'offset' and 'limit' parameters to read the rest of the document, or use search_knowledge to query specific details.`;
129
+ finalContent += `\n\n${warning}`;
120
130
  }
121
131
  return {
122
132
  success: true,
123
- content: finalContent || "Empty file",
133
+ data: {
134
+ content: finalContent || "Empty file",
135
+ metadata: {
136
+ totalSize,
137
+ offset,
138
+ limit,
139
+ isTruncated,
140
+ warning,
141
+ },
142
+ },
124
143
  };
125
144
  }
126
145
  catch (err) {
@@ -15,5 +15,14 @@ export declare function searchKnowledge(query: string, topK?: number): Promise<{
15
15
  success: boolean;
16
16
  results: string;
17
17
  error?: undefined;
18
+ } | {
19
+ success: boolean;
20
+ results: {
21
+ title: string;
22
+ fileId: any;
23
+ offset: any;
24
+ text: any;
25
+ }[];
26
+ error?: undefined;
18
27
  }>;
19
28
  //# sourceMappingURL=knowledgeTools.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"knowledgeTools.d.ts","sourceRoot":"","sources":["../../src/tools/knowledgeTools.ts"],"names":[],"mappings":"AAIA,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM;;;;;;;;GAmBlD;AAED,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,MAAU;;;;;;;;GAoCpE"}
1
+ {"version":3,"file":"knowledgeTools.d.ts","sourceRoot":"","sources":["../../src/tools/knowledgeTools.ts"],"names":[],"mappings":"AAIA,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM;;;;;;;;GAmBlD;AAED,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,MAAU;;;;;;;;;;;;;;;;;GA0CpE"}
@@ -41,20 +41,27 @@ export async function searchKnowledge(query, topK = 3) {
41
41
  success: true,
42
42
  results: results.map((r) => {
43
43
  let title = "Unknown Source";
44
+ let offset = undefined;
44
45
  if (r.metadata) {
45
46
  try {
46
47
  const metaObj = JSON.parse(r.metadata);
47
48
  if (metaObj.title)
48
49
  title = metaObj.title;
50
+ if (metaObj.offset !== undefined)
51
+ offset = metaObj.offset;
49
52
  }
50
53
  catch (e) { }
51
54
  }
52
- return `[File: ${title} | File ID: ${r.file_id || 'N/A'}]\n${r.text}`;
53
- }).join("\n\n---\n\n"),
55
+ return {
56
+ title,
57
+ fileId: r.file_id || "N/A",
58
+ offset,
59
+ text: r.text,
60
+ };
61
+ }),
54
62
  };
55
63
  }
56
64
  catch (err) {
57
- console.error("searchKnowledge outer catch:", err.message);
58
65
  return { success: false, error: `Failed to search: ${err.message}` };
59
66
  }
60
67
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khoinguyen2002/doc-mcp",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/mcp-server.ts CHANGED
@@ -2,10 +2,7 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
- import {
6
- listDriveFiles,
7
- readDriveDocument,
8
- } from "./tools/driveTools.js";
5
+ import { listDriveFiles, readDriveDocument } from "./tools/driveTools.js";
9
6
  import { saveAgentNote, searchKnowledge } from "./tools/knowledgeTools.js";
10
7
  import { config } from "./config.js";
11
8
 
@@ -20,14 +17,15 @@ if (!DRIVE_FOLDER_ID) {
20
17
 
21
18
  const server = new McpServer({
22
19
  name: "doc-agent",
23
- version: "1.0.0",
20
+ version: "1.0.4",
24
21
  });
25
22
 
26
23
  // Register tools
27
24
  server.registerTool(
28
25
  "list_drive_files",
29
26
  {
30
- description: "List and search for Google Drive documents and subfolders in a specific folder.",
27
+ description:
28
+ "List and search for Google Drive documents and subfolders in a specific folder.",
31
29
  inputSchema: {
32
30
  keyword: z
33
31
  .string()
@@ -36,7 +34,9 @@ server.registerTool(
36
34
  targetFolderId: z
37
35
  .string()
38
36
  .optional()
39
- .describe("Optional Google Drive folder ID to list contents from. Defaults to the root knowledge folder."),
37
+ .describe(
38
+ "Optional Google Drive folder ID to list contents from. Defaults to the root knowledge folder.",
39
+ ),
40
40
  },
41
41
  },
42
42
  async ({ keyword, targetFolderId }) => {
@@ -57,13 +57,21 @@ server.registerTool(
57
57
  "read_drive_document",
58
58
  {
59
59
  description:
60
- "Read the content of a specific Google Drive document. The document will also be automatically ingested into vector memory for future semantic search.",
60
+ "Read the content of a specific Google Drive document. You can use the 'offset' parameter (obtained from search_knowledge) to read a specific chunk of text.",
61
61
  inputSchema: {
62
62
  fileId: z.string().describe("The Google Drive file ID to read"),
63
+ offset: z
64
+ .number()
65
+ .optional()
66
+ .describe("Starting character index (default: 0)"),
67
+ limit: z
68
+ .number()
69
+ .optional()
70
+ .describe("Maximum number of characters to return (default: 10000)"),
63
71
  },
64
72
  },
65
- async ({ fileId }) => {
66
- const res = await readDriveDocument(fileId);
73
+ async ({ fileId, offset, limit }) => {
74
+ const res = await readDriveDocument(fileId, offset, limit);
67
75
  if (!res.success) {
68
76
  return {
69
77
  content: [{ type: "text", text: `Error: ${res.error}` }],
@@ -71,38 +79,18 @@ server.registerTool(
71
79
  };
72
80
  }
73
81
  return {
74
- content: [{ type: "text", text: res.content || "No content found." }],
82
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
75
83
  };
76
84
  },
77
85
  );
78
86
 
79
- server.registerTool(
80
- "save_agent_note",
81
- {
82
- description: "Save an agent note, thought, or summary directly into the vector memory.",
83
- inputSchema: {
84
- content: z.string().describe("The note or knowledge content to store"),
85
- },
86
- },
87
- async ({ content }) => {
88
- const res = await saveAgentNote(content);
89
- if (!res.success) {
90
- return {
91
- content: [{ type: "text", text: `Error: ${res.error}` }],
92
- isError: true,
93
- };
94
- }
95
- return {
96
- content: [{ type: "text", text: res.message || "Saved successfully" }],
97
- };
98
- },
99
- );
87
+
100
88
 
101
89
  server.registerTool(
102
90
  "search_knowledge",
103
91
  {
104
92
  description:
105
- "Search the folder's vector memory for relevant context or knowledge.",
93
+ "Search the folder's vector memory for relevant context or knowledge. Returns structured JSON array of matching chunks.",
106
94
  inputSchema: {
107
95
  query: z.string().describe("The search query"),
108
96
  topK: z
@@ -102,12 +102,19 @@ export async function syncSingleDocument(fileId: string, folderId: string) {
102
102
  });
103
103
  const chunks = await splitter.splitText(content);
104
104
 
105
+ let currentOffset = 0;
105
106
  for (const chunk of chunks) {
107
+ const offset = content.indexOf(chunk, currentOffset);
108
+ if (offset !== -1) {
109
+ currentOffset = offset;
110
+ }
111
+
106
112
  await upsertProjectDocument(folderId, chunk, {
107
113
  title: fileInfo.data.name || "Untitled Google Doc",
108
114
  source: "google_drive",
109
115
  file_id: fileId,
110
116
  modified_time: driveModifiedTime,
117
+ offset: offset !== -1 ? offset : 0,
111
118
  });
112
119
  }
113
120
  return { synced: true, content, driveModifiedTime };
@@ -116,7 +123,7 @@ export async function syncSingleDocument(fileId: string, folderId: string) {
116
123
  return { synced: false, driveModifiedTime };
117
124
  }
118
125
 
119
- export async function readDriveDocument(fileId: string) {
126
+ export async function readDriveDocument(fileId: string, offset: number = 0, limit: number = 10000) {
120
127
  const folderId = config.DOC_MCP_DRIVE_FOLDER_ID;
121
128
  if (!folderId) {
122
129
  return {
@@ -140,16 +147,32 @@ export async function readDriveDocument(fileId: string) {
140
147
  }
141
148
 
142
149
  let finalContent = content;
143
- const MAX_CHARS = 10000;
144
- if (finalContent && finalContent.length > MAX_CHARS) {
145
- finalContent =
146
- finalContent.substring(0, MAX_CHARS) +
147
- "\n\n... [Content truncated due to length. The full document has been automatically ingested into Vector Memory. Use search_knowledge to query specific details.]";
150
+ const totalSize = finalContent ? finalContent.length : 0;
151
+
152
+ if (finalContent) {
153
+ finalContent = finalContent.substring(offset, offset + limit);
154
+ }
155
+
156
+ const isTruncated = offset + (finalContent?.length || 0) < totalSize;
157
+ let warning = undefined;
158
+
159
+ if (isTruncated) {
160
+ warning = `[WARNING]: This is not the entire document. Content has been truncated from character ${offset} to ${offset + finalContent!.length} out of ${totalSize} total characters. Please use 'offset' and 'limit' parameters to read the rest of the document, or use search_knowledge to query specific details.`;
161
+ finalContent += `\n\n${warning}`;
148
162
  }
149
163
 
150
164
  return {
151
165
  success: true,
152
- content: finalContent || "Empty file",
166
+ data: {
167
+ content: finalContent || "Empty file",
168
+ metadata: {
169
+ totalSize,
170
+ offset,
171
+ limit,
172
+ isTruncated,
173
+ warning,
174
+ },
175
+ },
153
176
  };
154
177
  } catch (err: any) {
155
178
  return { success: false, error: err.message };
@@ -44,18 +44,23 @@ export async function searchKnowledge(query: string, topK: number = 3) {
44
44
 
45
45
  return {
46
46
  success: true,
47
- results: results
48
- .map((r: any) => {
49
- let title = "Unknown Source";
50
- if (r.metadata) {
51
- try {
52
- const metaObj = JSON.parse(r.metadata);
53
- if (metaObj.title) title = metaObj.title;
54
- } catch (e) {}
55
- }
56
- return `[File: ${title} | File ID: ${r.file_id || "N/A"}]\n${r.text}`;
57
- })
58
- .join("\n\n---\n\n"),
47
+ results: results.map((r: any) => {
48
+ let title = "Unknown Source";
49
+ let offset = undefined;
50
+ if (r.metadata) {
51
+ try {
52
+ const metaObj = JSON.parse(r.metadata);
53
+ if (metaObj.title) title = metaObj.title;
54
+ if (metaObj.offset !== undefined) offset = metaObj.offset;
55
+ } catch (e) {}
56
+ }
57
+ return {
58
+ title,
59
+ fileId: r.file_id || "N/A",
60
+ offset,
61
+ text: r.text,
62
+ };
63
+ }),
59
64
  };
60
65
  } catch (err: any) {
61
66
  return { success: false, error: `Failed to search: ${err.message}` };