sad-mcp 0.1.7 → 0.1.9
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/tools.js +107 -12
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -46,7 +46,7 @@ export function registerToolHandlers(server) {
|
|
|
46
46
|
tools: [
|
|
47
47
|
{
|
|
48
48
|
name: "search_materials",
|
|
49
|
-
description: "Search across all course materials (
|
|
49
|
+
description: "Search across all course materials for a topic. Returns a SHORT summary list of matching files (name, category, match count). To read the actual content, use get_material on the most relevant file(s) from the results.",
|
|
50
50
|
inputSchema: {
|
|
51
51
|
type: "object",
|
|
52
52
|
properties: {
|
|
@@ -58,6 +58,20 @@ export function registerToolHandlers(server) {
|
|
|
58
58
|
required: ["query"],
|
|
59
59
|
},
|
|
60
60
|
},
|
|
61
|
+
{
|
|
62
|
+
name: "get_material",
|
|
63
|
+
description: "Get the full text content of a specific course material file. Use this AFTER search_materials to read the content of a relevant file.",
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
name: {
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "The file name (or partial name) to retrieve. Matched against file names from search_materials or list_materials results.",
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
required: ["name"],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
61
75
|
{
|
|
62
76
|
name: "list_materials",
|
|
63
77
|
description: "List all available course materials, optionally filtered by category.",
|
|
@@ -103,18 +117,38 @@ export function registerToolHandlers(server) {
|
|
|
103
117
|
};
|
|
104
118
|
}
|
|
105
119
|
await ensureTextCache();
|
|
120
|
+
const queryLower = query.toLowerCase();
|
|
106
121
|
const results = [];
|
|
107
122
|
for (const [, { file, text }] of textCache) {
|
|
123
|
+
const nameMatch = file.name.toLowerCase().includes(queryLower) || file.path.toLowerCase().includes(queryLower);
|
|
108
124
|
const matches = searchInText(text, query);
|
|
109
|
-
if (matches.length > 0) {
|
|
125
|
+
if (matches.length > 0 || nameMatch) {
|
|
110
126
|
results.push({
|
|
111
127
|
fileName: file.name,
|
|
112
|
-
path: file.path,
|
|
113
128
|
category: categorizeFile(file),
|
|
114
|
-
matches: matches.
|
|
129
|
+
matchCount: nameMatch ? matches.length + 100 : matches.length, // Boost file-name matches
|
|
130
|
+
preview: matches.length > 0
|
|
131
|
+
? matches[0].line.trim().substring(0, 120)
|
|
132
|
+
: `(file name matches "${query}")`,
|
|
115
133
|
});
|
|
116
134
|
}
|
|
117
135
|
}
|
|
136
|
+
// Also search files NOT in textCache (failed extraction) by name
|
|
137
|
+
const allFiles = await listAllFiles();
|
|
138
|
+
for (const file of allFiles) {
|
|
139
|
+
if (textCache.has(file.id))
|
|
140
|
+
continue; // Already checked
|
|
141
|
+
if (file.name.toLowerCase().includes(queryLower) || file.path.toLowerCase().includes(queryLower)) {
|
|
142
|
+
results.push({
|
|
143
|
+
fileName: file.name,
|
|
144
|
+
category: categorizeFile(file),
|
|
145
|
+
matchCount: 100,
|
|
146
|
+
preview: `(file name matches "${query}" — use get_material to read)`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Sort by match count descending (most relevant first)
|
|
151
|
+
results.sort((a, b) => b.matchCount - a.matchCount);
|
|
118
152
|
if (results.length === 0) {
|
|
119
153
|
return {
|
|
120
154
|
content: [
|
|
@@ -126,18 +160,79 @@ export function registerToolHandlers(server) {
|
|
|
126
160
|
};
|
|
127
161
|
}
|
|
128
162
|
const formatted = results
|
|
129
|
-
.map((r) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
163
|
+
.map((r) => `- ${r.fileName} [${r.category}] (${r.matchCount} matches) — "${r.preview}"`)
|
|
164
|
+
.join("\n");
|
|
165
|
+
return {
|
|
166
|
+
content: [
|
|
167
|
+
{
|
|
168
|
+
type: "text",
|
|
169
|
+
text: `Found "${query}" in ${results.length} file(s). Use get_material to read the most relevant one(s):\n\n${formatted}`,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (name === "get_material") {
|
|
175
|
+
const queryName = args.name;
|
|
176
|
+
if (!queryName) {
|
|
177
|
+
return {
|
|
178
|
+
content: [{ type: "text", text: "Error: name parameter is required" }],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
await ensureTextCache();
|
|
182
|
+
const queryLower = queryName.toLowerCase();
|
|
183
|
+
// First: check text cache
|
|
184
|
+
let bestMatch = null;
|
|
185
|
+
for (const [, entry] of textCache) {
|
|
186
|
+
if (entry.file.name.toLowerCase().includes(queryLower)) {
|
|
187
|
+
bestMatch = entry;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Fallback: search all files by name and attempt fresh extraction
|
|
192
|
+
if (!bestMatch) {
|
|
193
|
+
const allFiles = await listAllFiles();
|
|
194
|
+
const matchedFile = allFiles.find(f => f.name.toLowerCase().includes(queryLower));
|
|
195
|
+
if (matchedFile && isExtractable(matchedFile)) {
|
|
196
|
+
try {
|
|
197
|
+
const buffer = await downloadFile(matchedFile);
|
|
198
|
+
const text = await extractText(matchedFile, buffer);
|
|
199
|
+
textCache.set(matchedFile.id, { file: matchedFile, text });
|
|
200
|
+
saveTextEntry(matchedFile.id, { modifiedTime: matchedFile.modifiedTime, text });
|
|
201
|
+
bestMatch = { file: matchedFile, text };
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// Extraction failed — return what we know
|
|
205
|
+
return {
|
|
206
|
+
content: [
|
|
207
|
+
{
|
|
208
|
+
type: "text",
|
|
209
|
+
text: `Found file "${matchedFile.name}" but could not extract its text content. It may be an image-heavy presentation. Try searching for a transcript of the same lecture instead (e.g., search for the lecture name in transcripts).`,
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (!bestMatch) {
|
|
217
|
+
return {
|
|
218
|
+
content: [
|
|
219
|
+
{
|
|
220
|
+
type: "text",
|
|
221
|
+
text: `No file found matching "${queryName}". Use search_materials or list_materials to find available files.`,
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
// Truncate very large files
|
|
227
|
+
const maxLen = 30000;
|
|
228
|
+
const text = bestMatch.text.length > maxLen
|
|
229
|
+
? bestMatch.text.substring(0, maxLen) + "\n...[truncated]"
|
|
230
|
+
: bestMatch.text;
|
|
136
231
|
return {
|
|
137
232
|
content: [
|
|
138
233
|
{
|
|
139
234
|
type: "text",
|
|
140
|
-
text:
|
|
235
|
+
text: `📄 ${bestMatch.file.name} [${categorizeFile(bestMatch.file)}]\n\n${text}`,
|
|
141
236
|
},
|
|
142
237
|
],
|
|
143
238
|
};
|