@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.
- package/dist/mcp-server.js +16 -25
- package/dist/tools/driveTools.d.ts +12 -3
- package/dist/tools/driveTools.d.ts.map +1 -1
- package/dist/tools/driveTools.js +26 -7
- package/dist/tools/knowledgeTools.d.ts +9 -0
- package/dist/tools/knowledgeTools.d.ts.map +1 -1
- package/dist/tools/knowledgeTools.js +10 -3
- package/package.json +1 -1
- package/src/mcp-server.ts +21 -33
- package/src/tools/driveTools.ts +30 -7
- package/src/tools/knowledgeTools.ts +17 -12
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
|
6
|
-
import {
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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;;;;;;;;
|
|
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"}
|
package/dist/tools/driveTools.js
CHANGED
|
@@ -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
|
|
116
|
-
if (finalContent
|
|
117
|
-
finalContent =
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
53
|
-
|
|
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
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.
|
|
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:
|
|
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(
|
|
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.
|
|
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.
|
|
82
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
75
83
|
};
|
|
76
84
|
},
|
|
77
85
|
);
|
|
78
86
|
|
|
79
|
-
|
|
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
|
package/src/tools/driveTools.ts
CHANGED
|
@@ -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
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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}` };
|