@khoinguyen2002/doc-mcp 1.0.0 → 1.0.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.
@@ -1,8 +1,8 @@
1
1
  export declare function initVectorDB(): Promise<void>;
2
2
  export declare function embedText(text: string): Promise<number[]>;
3
- export declare function upsertProjectDocument(projectId: string, text: string, metadata?: Record<string, any>): Promise<void>;
4
- export declare function searchProjectMemory(projectId: string, query: string, topK?: number): Promise<any[]>;
5
- export declare function deleteProjectDocument(projectId: string, fileId: string): Promise<void>;
6
- export declare function checkProjectDocumentExists(projectId: string, fileId: string): Promise<boolean>;
7
- export declare function getProjectDocumentMetadata(projectId: string): Promise<Record<string, string>>;
3
+ export declare function upsertProjectDocument(folderId: string, text: string, metadata?: Record<string, any>): Promise<void>;
4
+ export declare function searchProjectMemory(folderId: string, query: string, topK?: number): Promise<any[]>;
5
+ export declare function deleteProjectDocument(folderId: string, fileId: string): Promise<void>;
6
+ export declare function checkProjectDocumentExists(folderId: string, fileId: string): Promise<boolean>;
7
+ export declare function getProjectDocumentMetadata(folderId: string): Promise<Record<string, string>>;
8
8
  //# sourceMappingURL=vector.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vector.d.ts","sourceRoot":"","sources":["../../src/db/vector.ts"],"names":[],"mappings":"AAOA,wBAAsB,YAAY,kBAqCjC;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAwB/D;AAED,wBAAsB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA0B9H;AAED,wBAAsB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,MAAU,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CA4B5G;AAED,wBAAsB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAa5F;AAED,wBAAsB,0BAA0B,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAapG;AAED,wBAAsB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAwBnG"}
1
+ {"version":3,"file":"vector.d.ts","sourceRoot":"","sources":["../../src/db/vector.ts"],"names":[],"mappings":"AAOA,wBAAsB,YAAY,kBAqCjC;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAwB/D;AAED,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA0B7H;AAED,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,MAAU,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAiC3G;AAED,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAa3F;AAED,wBAAsB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAanG;AAED,wBAAsB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAwBlG"}
package/dist/db/vector.js CHANGED
@@ -24,7 +24,7 @@ export async function initVectorDB() {
24
24
  },
25
25
  });
26
26
  await client.createPayloadIndex(COLLECTION_NAME, {
27
- field_name: "projectId",
27
+ field_name: "folderId",
28
28
  field_schema: "keyword",
29
29
  });
30
30
  await client.createPayloadIndex(COLLECTION_NAME, {
@@ -61,7 +61,7 @@ export async function embedText(text) {
61
61
  }
62
62
  return json.data[0].embedding;
63
63
  }
64
- export async function upsertProjectDocument(projectId, text, metadata = {}) {
64
+ export async function upsertProjectDocument(folderId, text, metadata = {}) {
65
65
  await initVectorDB();
66
66
  if (!client)
67
67
  throw new Error("Qdrant not initialized");
@@ -73,7 +73,7 @@ export async function upsertProjectDocument(projectId, text, metadata = {}) {
73
73
  id: uuidv4(),
74
74
  vector: vector,
75
75
  payload: {
76
- projectId,
76
+ folderId,
77
77
  text,
78
78
  source: metadata.source || "user",
79
79
  file_id: metadata.file_id || null,
@@ -84,71 +84,77 @@ export async function upsertProjectDocument(projectId, text, metadata = {}) {
84
84
  }
85
85
  ]
86
86
  });
87
- console.error(`Upserted document chunk for project ${projectId}`);
87
+ console.error(`Upserted document chunk for folder ${folderId}`);
88
88
  }
89
- export async function searchProjectMemory(projectId, query, topK = 3) {
89
+ export async function searchProjectMemory(folderId, query, topK = 3) {
90
90
  await initVectorDB();
91
91
  if (!client)
92
92
  throw new Error("Qdrant not initialized");
93
- const queryVector = await embedText(query);
94
- const results = await client.search(COLLECTION_NAME, {
95
- vector: queryVector,
96
- limit: topK,
97
- with_payload: true,
98
- filter: {
99
- must: [
100
- {
101
- key: "projectId",
102
- match: {
103
- value: projectId
93
+ try {
94
+ const queryVector = await embedText(query);
95
+ const results = await client.search(COLLECTION_NAME, {
96
+ vector: queryVector,
97
+ limit: topK,
98
+ with_payload: true,
99
+ filter: {
100
+ must: [
101
+ {
102
+ key: "folderId",
103
+ match: {
104
+ value: folderId
105
+ }
104
106
  }
105
- }
106
- ]
107
- }
108
- });
109
- // Map to match LanceDB format expected by other tools
110
- return results.map(r => ({
111
- id: r.id,
112
- vector: r.vector,
113
- ...r.payload
114
- }));
107
+ ]
108
+ }
109
+ });
110
+ // Map to match LanceDB format expected by other tools
111
+ return results.map(r => ({
112
+ id: r.id,
113
+ vector: r.vector,
114
+ ...r.payload
115
+ }));
116
+ }
117
+ catch (err) {
118
+ console.error("Qdrant search error:", err.message);
119
+ return [];
120
+ }
115
121
  }
116
- export async function deleteProjectDocument(projectId, fileId) {
122
+ export async function deleteProjectDocument(folderId, fileId) {
117
123
  await initVectorDB();
118
124
  if (!client)
119
125
  return;
120
126
  await client.delete(COLLECTION_NAME, {
121
127
  filter: {
122
128
  must: [
123
- { key: "projectId", match: { value: projectId } },
129
+ { key: "folderId", match: { value: folderId } },
124
130
  { key: "file_id", match: { value: fileId } }
125
131
  ]
126
132
  }
127
133
  });
128
- console.error(`Deleted old chunks from Qdrant for ${projectId} / ${fileId}`);
134
+ console.error(`Deleted old chunks from Qdrant for ${folderId} / ${fileId}`);
129
135
  }
130
- export async function checkProjectDocumentExists(projectId, fileId) {
136
+ export async function checkProjectDocumentExists(folderId, fileId) {
131
137
  await initVectorDB();
132
138
  if (!client)
133
139
  return false;
134
140
  const res = await client.count(COLLECTION_NAME, {
135
141
  filter: {
136
142
  must: [
137
- { key: "projectId", match: { value: projectId } },
143
+ { key: "folderId", match: { value: folderId } },
138
144
  { key: "file_id", match: { value: fileId } }
139
145
  ]
140
146
  }
141
147
  });
142
148
  return res.count > 0;
143
149
  }
144
- export async function getProjectDocumentMetadata(projectId) {
150
+ export async function getProjectDocumentMetadata(folderId) {
145
151
  await initVectorDB();
146
152
  if (!client)
147
153
  return {};
148
154
  const res = await client.scroll(COLLECTION_NAME, {
149
155
  filter: {
150
156
  must: [
151
- { key: "projectId", match: { value: projectId } },
157
+ { key: "folderId", match: { value: folderId } },
152
158
  { key: "source", match: { value: "google_drive" } }
153
159
  ]
154
160
  },
@@ -16,15 +16,19 @@ const server = new McpServer({
16
16
  });
17
17
  // Register tools
18
18
  server.registerTool("list_drive_files", {
19
- description: "List and search for Google Drive documents in the configured folder.",
19
+ description: "List and search for Google Drive documents and subfolders in a specific folder.",
20
20
  inputSchema: {
21
21
  keyword: z
22
22
  .string()
23
23
  .optional()
24
24
  .describe("Optional keyword to search for in document titles"),
25
+ targetFolderId: z
26
+ .string()
27
+ .optional()
28
+ .describe("Optional Google Drive folder ID to list contents from. Defaults to the root knowledge folder."),
25
29
  },
26
- }, async ({ keyword }) => {
27
- const res = await listDriveFiles(keyword);
30
+ }, async ({ keyword, targetFolderId }) => {
31
+ const res = await listDriveFiles(keyword, targetFolderId);
28
32
  if (!res.success) {
29
33
  return {
30
34
  content: [{ type: "text", text: `Error: ${res.error}` }],
@@ -1,4 +1,4 @@
1
- export declare function listDriveFiles(keyword?: string): Promise<{
1
+ export declare function listDriveFiles(keyword?: string, targetFolderId?: string): Promise<{
2
2
  success: boolean;
3
3
  results: import("googleapis").drive_v3.Schema$File[];
4
4
  error?: undefined;
@@ -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;;;;;;;;GAoCpD;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;;;;;;GAuCrD"}
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"}
@@ -19,8 +19,8 @@ function getDriveClient() {
19
19
  });
20
20
  return google.drive({ version: "v3", auth });
21
21
  }
22
- export async function listDriveFiles(keyword) {
23
- const folderId = process.env.DOC_MCP_DRIVE_FOLDER_ID;
22
+ export async function listDriveFiles(keyword, targetFolderId) {
23
+ const folderId = targetFolderId || config.DOC_MCP_DRIVE_FOLDER_ID;
24
24
  if (!folderId) {
25
25
  return {
26
26
  success: false,
@@ -29,14 +29,14 @@ export async function listDriveFiles(keyword) {
29
29
  }
30
30
  try {
31
31
  const drive = getDriveClient();
32
- let q = "mimeType = 'application/vnd.google-apps.document'";
32
+ let q = "(mimeType = 'application/vnd.google-apps.document' or mimeType = 'application/vnd.google-apps.folder') and trashed = false";
33
33
  q = `'${folderId}' in parents and ${q}`;
34
34
  if (keyword) {
35
35
  q = `name contains '${keyword}' and ${q}`;
36
36
  }
37
37
  const res = await drive.files.list({
38
38
  q,
39
- fields: "files(id, name, description)",
39
+ fields: "files(id, name, description, mimeType)",
40
40
  spaces: "drive",
41
41
  pageSize: 50,
42
42
  supportsAllDrives: true,
@@ -92,7 +92,7 @@ export async function syncSingleDocument(fileId, folderId) {
92
92
  return { synced: false, driveModifiedTime };
93
93
  }
94
94
  export async function readDriveDocument(fileId) {
95
- const folderId = process.env.DOC_MCP_DRIVE_FOLDER_ID;
95
+ const folderId = config.DOC_MCP_DRIVE_FOLDER_ID;
96
96
  if (!folderId) {
97
97
  return {
98
98
  success: false,
@@ -130,17 +130,28 @@ export async function readDriveDocument(fileId) {
130
130
  export async function syncFolderState(folderId) {
131
131
  try {
132
132
  const drive = getDriveClient();
133
- let q = "mimeType = 'application/vnd.google-apps.document'";
134
- q = `'${folderId}' in parents and ${q}`;
135
- const res = await drive.files.list({
136
- q,
137
- fields: "files(id, name, modifiedTime)",
138
- spaces: "drive",
139
- pageSize: 100,
140
- supportsAllDrives: true,
141
- includeItemsFromAllDrives: true,
142
- });
143
- const driveFiles = res.data.files || [];
133
+ async function getAllDocumentsFlat() {
134
+ let allDocs = [];
135
+ let pageToken = undefined;
136
+ do {
137
+ const docsRes = await drive.files.list({
138
+ // Chú ý: Đéo check parentId nữa, gom sạch sành sanh mọi file .doc mà Service Account nhìn thấy
139
+ q: `mimeType = 'application/vnd.google-apps.document' and trashed = false`,
140
+ fields: "nextPageToken, files(id, name, modifiedTime)",
141
+ spaces: "drive",
142
+ pageSize: 100, // Google API limit mỗi page, tự động nhảy trang nếu nhiều hơn
143
+ pageToken,
144
+ supportsAllDrives: true,
145
+ includeItemsFromAllDrives: true,
146
+ });
147
+ if (docsRes.data.files) {
148
+ allDocs = allDocs.concat(docsRes.data.files);
149
+ }
150
+ pageToken = docsRes.data.nextPageToken || undefined;
151
+ } while (pageToken);
152
+ return allDocs;
153
+ }
154
+ const driveFiles = await getAllDocumentsFlat();
144
155
  const dbMetaMap = await getProjectDocumentMetadata(folderId);
145
156
  // Sync updated or new files
146
157
  for (const file of driveFiles) {
@@ -153,7 +164,7 @@ export async function syncFolderState(folderId) {
153
164
  }
154
165
  // Delete removed files from DB
155
166
  for (const dbFileId of Object.keys(dbMetaMap)) {
156
- if (!driveFiles.find(f => f.id === dbFileId)) {
167
+ if (!driveFiles.find((f) => f.id === dbFileId)) {
157
168
  await deleteProjectDocument(folderId, dbFileId);
158
169
  }
159
170
  }
@@ -1 +1 @@
1
- {"version":3,"file":"knowledgeTools.d.ts","sourceRoot":"","sources":["../../src/tools/knowledgeTools.ts"],"names":[],"mappings":"AAGA,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM;;;;;;;;GAelD;AAED,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,MAAU;;;;;;;;GAuBpE"}
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,25 +1,34 @@
1
+ import { config } from "../config.js";
1
2
  import { upsertProjectDocument, searchProjectMemory } from "../db/vector.js";
2
3
  import { syncFolderState } from "./driveTools.js";
3
4
  export async function saveAgentNote(content) {
4
- const folderId = process.env.DOC_MCP_DRIVE_FOLDER_ID;
5
+ const folderId = config.DOC_MCP_DRIVE_FOLDER_ID;
5
6
  if (!folderId) {
6
- return { success: false, error: "DOC_MCP_DRIVE_FOLDER_ID is not configured." };
7
+ return {
8
+ success: false,
9
+ error: "DOC_MCP_DRIVE_FOLDER_ID is not configured.",
10
+ };
7
11
  }
8
12
  try {
9
- // We use folderId as the "projectId" parameter for vector-db namespace
10
13
  await upsertProjectDocument(folderId, content, {
11
14
  source: "agent",
12
15
  });
13
- return { success: true, message: "Successfully stored note in vector memory." };
16
+ return {
17
+ success: true,
18
+ message: "Successfully stored note in vector memory.",
19
+ };
14
20
  }
15
21
  catch (err) {
16
22
  return { success: false, error: `Failed to store note: ${err.message}` };
17
23
  }
18
24
  }
19
25
  export async function searchKnowledge(query, topK = 3) {
20
- const folderId = process.env.DOC_MCP_DRIVE_FOLDER_ID;
26
+ const folderId = config.DOC_MCP_DRIVE_FOLDER_ID;
21
27
  if (!folderId) {
22
- return { success: false, error: "DOC_MCP_DRIVE_FOLDER_ID is not configured." };
28
+ return {
29
+ success: false,
30
+ error: "DOC_MCP_DRIVE_FOLDER_ID is not configured.",
31
+ };
23
32
  }
24
33
  try {
25
34
  // Auto-sync folder state before searching
@@ -30,10 +39,22 @@ export async function searchKnowledge(query, topK = 3) {
30
39
  }
31
40
  return {
32
41
  success: true,
33
- results: results.map((r) => r.text).join("\n\n---\n\n"),
42
+ results: results.map((r) => {
43
+ let title = "Unknown Source";
44
+ if (r.metadata) {
45
+ try {
46
+ const metaObj = JSON.parse(r.metadata);
47
+ if (metaObj.title)
48
+ title = metaObj.title;
49
+ }
50
+ catch (e) { }
51
+ }
52
+ return `[File: ${title} | File ID: ${r.file_id || 'N/A'}]\n${r.text}`;
53
+ }).join("\n\n---\n\n"),
34
54
  };
35
55
  }
36
56
  catch (err) {
57
+ console.error("searchKnowledge outer catch:", err.message);
37
58
  return { success: false, error: `Failed to search: ${err.message}` };
38
59
  }
39
60
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khoinguyen2002/doc-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/db/vector.ts CHANGED
@@ -28,7 +28,7 @@ export async function initVectorDB() {
28
28
  },
29
29
  });
30
30
  await client.createPayloadIndex(COLLECTION_NAME, {
31
- field_name: "projectId",
31
+ field_name: "folderId",
32
32
  field_schema: "keyword",
33
33
  });
34
34
  await client.createPayloadIndex(COLLECTION_NAME, {
@@ -70,7 +70,7 @@ export async function embedText(text: string): Promise<number[]> {
70
70
  return json.data[0].embedding;
71
71
  }
72
72
 
73
- export async function upsertProjectDocument(projectId: string, text: string, metadata: Record<string, any> = {}): Promise<void> {
73
+ export async function upsertProjectDocument(folderId: string, text: string, metadata: Record<string, any> = {}): Promise<void> {
74
74
  await initVectorDB();
75
75
  if (!client) throw new Error("Qdrant not initialized");
76
76
 
@@ -83,7 +83,7 @@ export async function upsertProjectDocument(projectId: string, text: string, met
83
83
  id: uuidv4(),
84
84
  vector: vector,
85
85
  payload: {
86
- projectId,
86
+ folderId,
87
87
  text,
88
88
  source: metadata.source || "user",
89
89
  file_id: metadata.file_id || null,
@@ -95,62 +95,67 @@ export async function upsertProjectDocument(projectId: string, text: string, met
95
95
  ]
96
96
  });
97
97
 
98
- console.error(`Upserted document chunk for project ${projectId}`);
98
+ console.error(`Upserted document chunk for folder ${folderId}`);
99
99
  }
100
100
 
101
- export async function searchProjectMemory(projectId: string, query: string, topK: number = 3): Promise<any[]> {
101
+ export async function searchProjectMemory(folderId: string, query: string, topK: number = 3): Promise<any[]> {
102
102
  await initVectorDB();
103
103
  if (!client) throw new Error("Qdrant not initialized");
104
104
 
105
- const queryVector = await embedText(query);
106
-
107
- const results = await client.search(COLLECTION_NAME, {
108
- vector: queryVector,
109
- limit: topK,
110
- with_payload: true,
111
- filter: {
112
- must: [
113
- {
114
- key: "projectId",
115
- match: {
116
- value: projectId
105
+ try {
106
+ const queryVector = await embedText(query);
107
+
108
+ const results = await client.search(COLLECTION_NAME, {
109
+ vector: queryVector,
110
+ limit: topK,
111
+ with_payload: true,
112
+ filter: {
113
+ must: [
114
+ {
115
+ key: "folderId",
116
+ match: {
117
+ value: folderId
118
+ }
117
119
  }
118
- }
119
- ]
120
- }
121
- });
120
+ ]
121
+ }
122
+ });
122
123
 
123
- // Map to match LanceDB format expected by other tools
124
- return results.map(r => ({
125
- id: r.id,
126
- vector: r.vector,
127
- ...r.payload
128
- }));
124
+ // Map to match LanceDB format expected by other tools
125
+ return results.map(r => ({
126
+ id: r.id,
127
+ vector: r.vector,
128
+ ...r.payload
129
+ }));
130
+ } catch (err: any) {
131
+ console.error("Qdrant search error:", err.message);
132
+ return [];
133
+ }
129
134
  }
130
135
 
131
- export async function deleteProjectDocument(projectId: string, fileId: string): Promise<void> {
136
+ export async function deleteProjectDocument(folderId: string, fileId: string): Promise<void> {
132
137
  await initVectorDB();
133
138
  if (!client) return;
134
139
 
135
140
  await client.delete(COLLECTION_NAME, {
136
141
  filter: {
137
142
  must: [
138
- { key: "projectId", match: { value: projectId } },
143
+ { key: "folderId", match: { value: folderId } },
139
144
  { key: "file_id", match: { value: fileId } }
140
145
  ]
141
146
  }
142
147
  });
143
- console.error(`Deleted old chunks from Qdrant for ${projectId} / ${fileId}`);
148
+ console.error(`Deleted old chunks from Qdrant for ${folderId} / ${fileId}`);
144
149
  }
145
150
 
146
- export async function checkProjectDocumentExists(projectId: string, fileId: string): Promise<boolean> {
151
+ export async function checkProjectDocumentExists(folderId: string, fileId: string): Promise<boolean> {
147
152
  await initVectorDB();
148
153
  if (!client) return false;
149
154
 
150
155
  const res = await client.count(COLLECTION_NAME, {
151
156
  filter: {
152
157
  must: [
153
- { key: "projectId", match: { value: projectId } },
158
+ { key: "folderId", match: { value: folderId } },
154
159
  { key: "file_id", match: { value: fileId } }
155
160
  ]
156
161
  }
@@ -158,14 +163,14 @@ export async function checkProjectDocumentExists(projectId: string, fileId: stri
158
163
  return res.count > 0;
159
164
  }
160
165
 
161
- export async function getProjectDocumentMetadata(projectId: string): Promise<Record<string, string>> {
166
+ export async function getProjectDocumentMetadata(folderId: string): Promise<Record<string, string>> {
162
167
  await initVectorDB();
163
168
  if (!client) return {};
164
169
 
165
170
  const res = await client.scroll(COLLECTION_NAME, {
166
171
  filter: {
167
172
  must: [
168
- { key: "projectId", match: { value: projectId } },
173
+ { key: "folderId", match: { value: folderId } },
169
174
  { key: "source", match: { value: "google_drive" } }
170
175
  ]
171
176
  },
package/src/mcp-server.ts CHANGED
@@ -27,16 +27,20 @@ const server = new McpServer({
27
27
  server.registerTool(
28
28
  "list_drive_files",
29
29
  {
30
- description: "List and search for Google Drive documents in the configured folder.",
30
+ description: "List and search for Google Drive documents and subfolders in a specific folder.",
31
31
  inputSchema: {
32
32
  keyword: z
33
33
  .string()
34
34
  .optional()
35
35
  .describe("Optional keyword to search for in document titles"),
36
+ targetFolderId: z
37
+ .string()
38
+ .optional()
39
+ .describe("Optional Google Drive folder ID to list contents from. Defaults to the root knowledge folder."),
36
40
  },
37
41
  },
38
- async ({ keyword }) => {
39
- const res = await listDriveFiles(keyword);
42
+ async ({ keyword, targetFolderId }) => {
43
+ const res = await listDriveFiles(keyword, targetFolderId);
40
44
  if (!res.success) {
41
45
  return {
42
46
  content: [{ type: "text", text: `Error: ${res.error}` }],
@@ -31,8 +31,8 @@ function getDriveClient() {
31
31
  return google.drive({ version: "v3", auth });
32
32
  }
33
33
 
34
- export async function listDriveFiles(keyword?: string) {
35
- const folderId = process.env.DOC_MCP_DRIVE_FOLDER_ID;
34
+ export async function listDriveFiles(keyword?: string, targetFolderId?: string) {
35
+ const folderId = targetFolderId || config.DOC_MCP_DRIVE_FOLDER_ID;
36
36
  if (!folderId) {
37
37
  return {
38
38
  success: false,
@@ -42,7 +42,7 @@ export async function listDriveFiles(keyword?: string) {
42
42
 
43
43
  try {
44
44
  const drive = getDriveClient();
45
- let q = "mimeType = 'application/vnd.google-apps.document'";
45
+ let q = "(mimeType = 'application/vnd.google-apps.document' or mimeType = 'application/vnd.google-apps.folder') and trashed = false";
46
46
  q = `'${folderId}' in parents and ${q}`;
47
47
 
48
48
  if (keyword) {
@@ -51,7 +51,7 @@ export async function listDriveFiles(keyword?: string) {
51
51
 
52
52
  const res = await drive.files.list({
53
53
  q,
54
- fields: "files(id, name, description)",
54
+ fields: "files(id, name, description, mimeType)",
55
55
  spaces: "drive",
56
56
  pageSize: 50,
57
57
  supportsAllDrives: true,
@@ -112,12 +112,12 @@ export async function syncSingleDocument(fileId: string, folderId: string) {
112
112
  }
113
113
  return { synced: true, content, driveModifiedTime };
114
114
  }
115
-
115
+
116
116
  return { synced: false, driveModifiedTime };
117
117
  }
118
118
 
119
119
  export async function readDriveDocument(fileId: string) {
120
- const folderId = process.env.DOC_MCP_DRIVE_FOLDER_ID;
120
+ const folderId = config.DOC_MCP_DRIVE_FOLDER_ID;
121
121
  if (!folderId) {
122
122
  return {
123
123
  success: false,
@@ -127,7 +127,7 @@ export async function readDriveDocument(fileId: string) {
127
127
 
128
128
  try {
129
129
  const result = await syncSingleDocument(fileId, folderId);
130
-
130
+
131
131
  // If not synced just now, we need to fetch content to return to the user
132
132
  let content = result.content;
133
133
  if (!content) {
@@ -159,19 +159,33 @@ export async function readDriveDocument(fileId: string) {
159
159
  export async function syncFolderState(folderId: string) {
160
160
  try {
161
161
  const drive = getDriveClient();
162
- let q = "mimeType = 'application/vnd.google-apps.document'";
163
- q = `'${folderId}' in parents and ${q}`;
164
162
 
165
- const res = await drive.files.list({
166
- q,
167
- fields: "files(id, name, modifiedTime)",
168
- spaces: "drive",
169
- pageSize: 100,
170
- supportsAllDrives: true,
171
- includeItemsFromAllDrives: true,
172
- });
163
+ async function getAllDocumentsFlat(): Promise<any[]> {
164
+ let allDocs: any[] = [];
165
+ let pageToken: string | undefined = undefined;
166
+
167
+ do {
168
+ const docsRes: any = await drive.files.list({
169
+ // Chú ý: Đéo check parentId nữa, gom sạch sành sanh mọi file .doc mà Service Account nhìn thấy
170
+ q: `mimeType = 'application/vnd.google-apps.document' and trashed = false`,
171
+ fields: "nextPageToken, files(id, name, modifiedTime)",
172
+ spaces: "drive",
173
+ pageSize: 100, // Google API limit mỗi page, tự động nhảy trang nếu nhiều hơn
174
+ pageToken,
175
+ supportsAllDrives: true,
176
+ includeItemsFromAllDrives: true,
177
+ });
178
+
179
+ if (docsRes.data.files) {
180
+ allDocs = allDocs.concat(docsRes.data.files);
181
+ }
182
+ pageToken = docsRes.data.nextPageToken || undefined;
183
+ } while (pageToken);
184
+
185
+ return allDocs;
186
+ }
173
187
 
174
- const driveFiles = res.data.files || [];
188
+ const driveFiles = await getAllDocumentsFlat();
175
189
  const dbMetaMap = await getProjectDocumentMetadata(folderId);
176
190
 
177
191
  // Sync updated or new files
@@ -185,7 +199,7 @@ export async function syncFolderState(folderId: string) {
185
199
 
186
200
  // Delete removed files from DB
187
201
  for (const dbFileId of Object.keys(dbMetaMap)) {
188
- if (!driveFiles.find(f => f.id === dbFileId)) {
202
+ if (!driveFiles.find((f) => f.id === dbFileId)) {
189
203
  await deleteProjectDocument(folderId, dbFileId);
190
204
  }
191
205
  }
@@ -1,27 +1,35 @@
1
+ import { config } from "../config.js";
1
2
  import { upsertProjectDocument, searchProjectMemory } from "../db/vector.js";
2
3
  import { syncFolderState } from "./driveTools.js";
3
4
 
4
5
  export async function saveAgentNote(content: string) {
5
- const folderId = process.env.DOC_MCP_DRIVE_FOLDER_ID;
6
+ const folderId = config.DOC_MCP_DRIVE_FOLDER_ID;
6
7
  if (!folderId) {
7
- return { success: false, error: "DOC_MCP_DRIVE_FOLDER_ID is not configured." };
8
+ return {
9
+ success: false,
10
+ error: "DOC_MCP_DRIVE_FOLDER_ID is not configured.",
11
+ };
8
12
  }
9
-
10
13
  try {
11
- // We use folderId as the "projectId" parameter for vector-db namespace
12
14
  await upsertProjectDocument(folderId, content, {
13
15
  source: "agent",
14
16
  });
15
- return { success: true, message: "Successfully stored note in vector memory." };
17
+ return {
18
+ success: true,
19
+ message: "Successfully stored note in vector memory.",
20
+ };
16
21
  } catch (err: any) {
17
22
  return { success: false, error: `Failed to store note: ${err.message}` };
18
23
  }
19
24
  }
20
25
 
21
26
  export async function searchKnowledge(query: string, topK: number = 3) {
22
- const folderId = process.env.DOC_MCP_DRIVE_FOLDER_ID;
27
+ const folderId = config.DOC_MCP_DRIVE_FOLDER_ID;
23
28
  if (!folderId) {
24
- return { success: false, error: "DOC_MCP_DRIVE_FOLDER_ID is not configured." };
29
+ return {
30
+ success: false,
31
+ error: "DOC_MCP_DRIVE_FOLDER_ID is not configured.",
32
+ };
25
33
  }
26
34
 
27
35
  try {
@@ -36,7 +44,18 @@ export async function searchKnowledge(query: string, topK: number = 3) {
36
44
 
37
45
  return {
38
46
  success: true,
39
- results: results.map((r: any) => r.text).join("\n\n---\n\n"),
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"),
40
59
  };
41
60
  } catch (err: any) {
42
61
  return { success: false, error: `Failed to search: ${err.message}` };
@@ -1,108 +0,0 @@
1
- import {
2
- getProjectDocumentMetadata,
3
- deleteProjectDocument,
4
- upsertProjectDocument,
5
- } from "../db/vector.js";
6
- import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
7
- // Remove childLogger dependency to fully decouple from core
8
- // import { childLogger } from "@workspace/core";
9
- import { config } from "../config.js";
10
-
11
- const log = {
12
- info: (obj: any, msg: string) => console.log(`[driveSync] ${msg}`, obj),
13
- error: (obj: any, msg: string) => console.error(`[driveSync] ${msg}`, obj)
14
- };
15
-
16
- export async function syncProjectDriveFiles(
17
- projectId: string,
18
- onSyncMessage?: (msg: string) => void,
19
- ): Promise<void> {
20
- const dbMeta = await getProjectDocumentMetadata(projectId);
21
- const fileIds = Object.keys(dbMeta);
22
-
23
- if (fileIds.length === 0) return;
24
-
25
- log.info({ projectId, fileCount: fileIds.length }, "Checking Drive files for updates...");
26
-
27
- const { google } = await import("googleapis");
28
-
29
- const clientEmail = config.DOC_MCP_GOOGLE_CLIENT_EMAIL;
30
- let privateKey = config.DOC_MCP_GOOGLE_PRIVATE_KEY;
31
- if (!clientEmail || !privateKey) return;
32
- if (privateKey.startsWith('"') && privateKey.endsWith('"')) {
33
- privateKey = privateKey.slice(1, -1);
34
- }
35
- privateKey = privateKey.replace(/\\n/g, "\n");
36
-
37
- const auth = new google.auth.JWT({
38
- email: clientEmail,
39
- key: privateKey,
40
- scopes: ["https://www.googleapis.com/auth/drive.readonly"],
41
- });
42
-
43
- const drive = google.drive({ version: "v3", auth });
44
-
45
- let updatedCount = 0;
46
- for (const fileId of fileIds) {
47
- try {
48
- const fileInfo = await drive.files.get({
49
- fileId: fileId,
50
- fields: "id, name, modifiedTime, trashed",
51
- supportsAllDrives: true,
52
- });
53
-
54
- if (fileInfo.data.trashed) {
55
- log.info({ fileId, projectId }, "File trashed on Drive, deleting from VectorDB...");
56
- await deleteProjectDocument(projectId, fileId);
57
- continue;
58
- }
59
-
60
- const driveModifiedTime = fileInfo.data.modifiedTime || "";
61
- const dbModifiedTime = dbMeta[fileId];
62
-
63
- if (driveModifiedTime !== dbModifiedTime) {
64
- if (onSyncMessage) {
65
- onSyncMessage(`🔄 Syncing updated file: ${fileInfo.data.name}...`);
66
- }
67
-
68
- log.info({ fileId, projectId }, "File updated on Drive, syncing...");
69
- await deleteProjectDocument(projectId, fileId);
70
-
71
- const res = await drive.files.export({
72
- fileId: fileId,
73
- mimeType: "text/plain",
74
- });
75
-
76
- const content = res.data;
77
- if (typeof content === "string" && content.trim() !== "") {
78
- const splitter = new RecursiveCharacterTextSplitter({
79
- chunkSize: config.CHUNK_SIZE,
80
- chunkOverlap: config.CHUNK_OVERLAP,
81
- });
82
- const chunks = await splitter.splitText(content);
83
-
84
- for (const chunk of chunks) {
85
- await upsertProjectDocument(projectId, chunk, {
86
- title: fileInfo.data.name,
87
- file_id: fileId,
88
- source: "google_drive",
89
- modified_time: driveModifiedTime,
90
- });
91
- }
92
- updatedCount++;
93
- }
94
- }
95
- } catch (err: any) {
96
- if (err.code === 404) {
97
- log.info({ fileId, projectId }, "File not found on Drive, deleting from VectorDB...");
98
- await deleteProjectDocument(projectId, fileId);
99
- } else {
100
- log.error({ fileId, err: err.message }, "Error syncing drive file");
101
- }
102
- }
103
- }
104
-
105
- if (updatedCount > 0 && onSyncMessage) {
106
- onSyncMessage(`✅ Synced ${updatedCount} files from Google Drive.`);
107
- }
108
- }