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.
Files changed (2) hide show
  1. package/dist/tools.js +107 -12
  2. 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 (lecture transcripts, presentations, exams) for relevant content. Use this when a student asks about a specific topic.",
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.slice(0, 5), // Top 5 matches per file
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
- const matchLines = r.matches
131
- .map((m) => ` Line ${m.lineNumber}: ${m.line.substring(0, 200)}`)
132
- .join("\n");
133
- return `📄 ${r.fileName} [${r.category}]\n Path: ${r.path}\n${matchLines}`;
134
- })
135
- .join("\n\n");
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: `Found matches in ${results.length} file(s) for "${query}":\n\n${formatted}`,
235
+ text: `📄 ${bestMatch.file.name} [${categorizeFile(bestMatch.file)}]\n\n${text}`,
141
236
  },
142
237
  ],
143
238
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sad-mcp",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "MCP server for Software Analysis and Design course materials at BGU",
5
5
  "type": "module",
6
6
  "bin": {