@khoinguyen2002/doc-mcp 1.0.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/dist/config.d.ts +13 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +24 -0
- package/dist/db/vector.d.ts +8 -0
- package/dist/db/vector.d.ts.map +1 -0
- package/dist/db/vector.js +166 -0
- package/dist/hooks/driveSync.d.ts +2 -0
- package/dist/hooks/driveSync.d.ts.map +1 -0
- package/dist/hooks/driveSync.js +88 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/mcp-server.d.ts +3 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +109 -0
- package/dist/tools/driveTools.d.ts +35 -0
- package/dist/tools/driveTools.d.ts.map +1 -0
- package/dist/tools/driveTools.js +166 -0
- package/dist/tools/knowledgeTools.d.ts +19 -0
- package/dist/tools/knowledgeTools.d.ts.map +1 -0
- package/dist/tools/knowledgeTools.js +39 -0
- package/package.json +28 -0
- package/src/config.ts +29 -0
- package/src/db/vector.ts +185 -0
- package/src/hooks/driveSync.ts +108 -0
- package/src/index.ts +3 -0
- package/src/mcp-server.ts +142 -0
- package/src/tools/driveTools.ts +198 -0
- package/src/tools/knowledgeTools.ts +44 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { google } from "googleapis";
|
|
2
|
+
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
|
|
3
|
+
import { config } from "../config.js";
|
|
4
|
+
import {
|
|
5
|
+
upsertProjectDocument,
|
|
6
|
+
getProjectDocumentMetadata,
|
|
7
|
+
deleteProjectDocument,
|
|
8
|
+
} from "../db/vector.js";
|
|
9
|
+
|
|
10
|
+
function getDriveClient() {
|
|
11
|
+
const clientEmail = config.DOC_MCP_GOOGLE_CLIENT_EMAIL;
|
|
12
|
+
let privateKey = config.DOC_MCP_GOOGLE_PRIVATE_KEY;
|
|
13
|
+
|
|
14
|
+
if (!clientEmail || !privateKey) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
"Google Drive credentials not configured. Please set DOC_MCP_GOOGLE_CLIENT_EMAIL and DOC_MCP_GOOGLE_PRIVATE_KEY in .env",
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (privateKey.startsWith('"') && privateKey.endsWith('"')) {
|
|
21
|
+
privateKey = privateKey.slice(1, -1);
|
|
22
|
+
}
|
|
23
|
+
privateKey = privateKey.replace(/\\n/g, "\n");
|
|
24
|
+
|
|
25
|
+
const auth = new google.auth.JWT({
|
|
26
|
+
email: clientEmail,
|
|
27
|
+
key: privateKey,
|
|
28
|
+
scopes: ["https://www.googleapis.com/auth/drive.readonly"],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return google.drive({ version: "v3", auth });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function listDriveFiles(keyword?: string) {
|
|
35
|
+
const folderId = process.env.DOC_MCP_DRIVE_FOLDER_ID;
|
|
36
|
+
if (!folderId) {
|
|
37
|
+
return {
|
|
38
|
+
success: false,
|
|
39
|
+
error: "DOC_MCP_DRIVE_FOLDER_ID is not configured for this agent.",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const drive = getDriveClient();
|
|
45
|
+
let q = "mimeType = 'application/vnd.google-apps.document'";
|
|
46
|
+
q = `'${folderId}' in parents and ${q}`;
|
|
47
|
+
|
|
48
|
+
if (keyword) {
|
|
49
|
+
q = `name contains '${keyword}' and ${q}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const res = await drive.files.list({
|
|
53
|
+
q,
|
|
54
|
+
fields: "files(id, name, description)",
|
|
55
|
+
spaces: "drive",
|
|
56
|
+
pageSize: 50,
|
|
57
|
+
supportsAllDrives: true,
|
|
58
|
+
includeItemsFromAllDrives: true,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const files = res.data.files;
|
|
62
|
+
if (!files || files.length === 0) {
|
|
63
|
+
return { success: true, results: [] };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { success: true, results: files };
|
|
67
|
+
} catch (err: any) {
|
|
68
|
+
return { success: false, error: err.message };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function syncSingleDocument(fileId: string, folderId: string) {
|
|
73
|
+
const drive = getDriveClient();
|
|
74
|
+
const fileInfo = await drive.files.get({
|
|
75
|
+
fileId,
|
|
76
|
+
fields: "id, name, modifiedTime",
|
|
77
|
+
supportsAllDrives: true,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const driveModifiedTime = fileInfo.data.modifiedTime || "";
|
|
81
|
+
const dbMetaMap = await getProjectDocumentMetadata(folderId);
|
|
82
|
+
const dbModifiedTime = dbMetaMap[fileId];
|
|
83
|
+
|
|
84
|
+
if (!dbModifiedTime || dbModifiedTime !== driveModifiedTime) {
|
|
85
|
+
if (dbModifiedTime) {
|
|
86
|
+
await deleteProjectDocument(folderId, fileId);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const res = await drive.files.export({
|
|
90
|
+
fileId: fileId,
|
|
91
|
+
mimeType: "text/plain",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const content = res.data;
|
|
95
|
+
if (typeof content !== "string" || content.trim() === "") {
|
|
96
|
+
throw new Error("Empty or invalid file content");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const splitter = new RecursiveCharacterTextSplitter({
|
|
100
|
+
chunkSize: config.CHUNK_SIZE,
|
|
101
|
+
chunkOverlap: config.CHUNK_OVERLAP,
|
|
102
|
+
});
|
|
103
|
+
const chunks = await splitter.splitText(content);
|
|
104
|
+
|
|
105
|
+
for (const chunk of chunks) {
|
|
106
|
+
await upsertProjectDocument(folderId, chunk, {
|
|
107
|
+
title: fileInfo.data.name || "Untitled Google Doc",
|
|
108
|
+
source: "google_drive",
|
|
109
|
+
file_id: fileId,
|
|
110
|
+
modified_time: driveModifiedTime,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return { synced: true, content, driveModifiedTime };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { synced: false, driveModifiedTime };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function readDriveDocument(fileId: string) {
|
|
120
|
+
const folderId = process.env.DOC_MCP_DRIVE_FOLDER_ID;
|
|
121
|
+
if (!folderId) {
|
|
122
|
+
return {
|
|
123
|
+
success: false,
|
|
124
|
+
error: "DOC_MCP_DRIVE_FOLDER_ID is not configured for this agent.",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const result = await syncSingleDocument(fileId, folderId);
|
|
130
|
+
|
|
131
|
+
// If not synced just now, we need to fetch content to return to the user
|
|
132
|
+
let content = result.content;
|
|
133
|
+
if (!content) {
|
|
134
|
+
const drive = getDriveClient();
|
|
135
|
+
const res = await drive.files.export({
|
|
136
|
+
fileId: fileId,
|
|
137
|
+
mimeType: "text/plain",
|
|
138
|
+
});
|
|
139
|
+
content = typeof res.data === "string" ? res.data : "";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
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.]";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
success: true,
|
|
152
|
+
content: finalContent || "Empty file",
|
|
153
|
+
};
|
|
154
|
+
} catch (err: any) {
|
|
155
|
+
return { success: false, error: err.message };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function syncFolderState(folderId: string) {
|
|
160
|
+
try {
|
|
161
|
+
const drive = getDriveClient();
|
|
162
|
+
let q = "mimeType = 'application/vnd.google-apps.document'";
|
|
163
|
+
q = `'${folderId}' in parents and ${q}`;
|
|
164
|
+
|
|
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
|
+
});
|
|
173
|
+
|
|
174
|
+
const driveFiles = res.data.files || [];
|
|
175
|
+
const dbMetaMap = await getProjectDocumentMetadata(folderId);
|
|
176
|
+
|
|
177
|
+
// Sync updated or new files
|
|
178
|
+
for (const file of driveFiles) {
|
|
179
|
+
if (!file.id) continue;
|
|
180
|
+
const dbModTime = dbMetaMap[file.id];
|
|
181
|
+
if (!dbModTime || dbModTime !== file.modifiedTime) {
|
|
182
|
+
await syncSingleDocument(file.id, folderId);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Delete removed files from DB
|
|
187
|
+
for (const dbFileId of Object.keys(dbMetaMap)) {
|
|
188
|
+
if (!driveFiles.find(f => f.id === dbFileId)) {
|
|
189
|
+
await deleteProjectDocument(folderId, dbFileId);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { success: true };
|
|
194
|
+
} catch (err: any) {
|
|
195
|
+
console.error("Auto-sync failed:", err.message);
|
|
196
|
+
return { success: false, error: err.message };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { upsertProjectDocument, searchProjectMemory } from "../db/vector.js";
|
|
2
|
+
import { syncFolderState } from "./driveTools.js";
|
|
3
|
+
|
|
4
|
+
export async function saveAgentNote(content: string) {
|
|
5
|
+
const folderId = process.env.DOC_MCP_DRIVE_FOLDER_ID;
|
|
6
|
+
if (!folderId) {
|
|
7
|
+
return { success: false, error: "DOC_MCP_DRIVE_FOLDER_ID is not configured." };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
// We use folderId as the "projectId" parameter for vector-db namespace
|
|
12
|
+
await upsertProjectDocument(folderId, content, {
|
|
13
|
+
source: "agent",
|
|
14
|
+
});
|
|
15
|
+
return { success: true, message: "Successfully stored note in vector memory." };
|
|
16
|
+
} catch (err: any) {
|
|
17
|
+
return { success: false, error: `Failed to store note: ${err.message}` };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function searchKnowledge(query: string, topK: number = 3) {
|
|
22
|
+
const folderId = process.env.DOC_MCP_DRIVE_FOLDER_ID;
|
|
23
|
+
if (!folderId) {
|
|
24
|
+
return { success: false, error: "DOC_MCP_DRIVE_FOLDER_ID is not configured." };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Auto-sync folder state before searching
|
|
29
|
+
await syncFolderState(folderId);
|
|
30
|
+
|
|
31
|
+
const results = await searchProjectMemory(folderId, query, topK);
|
|
32
|
+
|
|
33
|
+
if (!results || results.length === 0) {
|
|
34
|
+
return { success: true, results: "NOT_FOUND" };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
success: true,
|
|
39
|
+
results: results.map((r: any) => r.text).join("\n\n---\n\n"),
|
|
40
|
+
};
|
|
41
|
+
} catch (err: any) {
|
|
42
|
+
return { success: false, error: `Failed to search: ${err.message}` };
|
|
43
|
+
}
|
|
44
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true
|
|
13
|
+
},
|
|
14
|
+
"include": [
|
|
15
|
+
"src/**/*"
|
|
16
|
+
]
|
|
17
|
+
}
|