sad-mcp 0.1.6 → 0.1.8

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/index.js CHANGED
@@ -7,7 +7,7 @@ import { trackServerStart } from "./tracking.js";
7
7
  async function main() {
8
8
  // Connect to Claude Desktop IMMEDIATELY — no blocking auth here
9
9
  // Auth happens lazily on first Drive API call
10
- const server = new Server({ name: "sad-mcp", version: "0.1.6" }, {
10
+ const server = new Server({ name: "sad-mcp", version: "0.1.7" }, {
11
11
  capabilities: {
12
12
  tools: {},
13
13
  prompts: {},
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.",
@@ -72,6 +86,24 @@ export function registerToolHandlers(server) {
72
86
  },
73
87
  },
74
88
  },
89
+ {
90
+ name: "quiz",
91
+ description: "Generate a practice quiz on a course topic. Searches course materials for relevant content and returns it with instructions for Claude to create quiz questions. Use when a student wants to test their knowledge.",
92
+ inputSchema: {
93
+ type: "object",
94
+ properties: {
95
+ topic: {
96
+ type: "string",
97
+ description: "The topic to quiz on (e.g., 'use case diagrams', 'BPMN', 'normalization', 'state diagrams')",
98
+ },
99
+ num_questions: {
100
+ type: "number",
101
+ description: "Number of questions to generate. Defaults to 5.",
102
+ },
103
+ },
104
+ required: ["topic"],
105
+ },
106
+ },
75
107
  ],
76
108
  }));
77
109
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -91,12 +123,14 @@ export function registerToolHandlers(server) {
91
123
  if (matches.length > 0) {
92
124
  results.push({
93
125
  fileName: file.name,
94
- path: file.path,
95
126
  category: categorizeFile(file),
96
- matches: matches.slice(0, 5), // Top 5 matches per file
127
+ matchCount: matches.length,
128
+ preview: matches[0].line.trim().substring(0, 120),
97
129
  });
98
130
  }
99
131
  }
132
+ // Sort by match count descending (most relevant first)
133
+ results.sort((a, b) => b.matchCount - a.matchCount);
100
134
  if (results.length === 0) {
101
135
  return {
102
136
  content: [
@@ -108,18 +142,53 @@ export function registerToolHandlers(server) {
108
142
  };
109
143
  }
110
144
  const formatted = results
111
- .map((r) => {
112
- const matchLines = r.matches
113
- .map((m) => ` Line ${m.lineNumber}: ${m.line.substring(0, 200)}`)
114
- .join("\n");
115
- return `📄 ${r.fileName} [${r.category}]\n Path: ${r.path}\n${matchLines}`;
116
- })
117
- .join("\n\n");
145
+ .map((r) => `- ${r.fileName} [${r.category}] (${r.matchCount} matches) — "${r.preview}"`)
146
+ .join("\n");
147
+ return {
148
+ content: [
149
+ {
150
+ type: "text",
151
+ text: `Found "${query}" in ${results.length} file(s). Use get_material to read the most relevant one(s):\n\n${formatted}`,
152
+ },
153
+ ],
154
+ };
155
+ }
156
+ if (name === "get_material") {
157
+ const queryName = args.name;
158
+ if (!queryName) {
159
+ return {
160
+ content: [{ type: "text", text: "Error: name parameter is required" }],
161
+ };
162
+ }
163
+ await ensureTextCache();
164
+ const queryLower = queryName.toLowerCase();
165
+ let bestMatch = null;
166
+ for (const [, entry] of textCache) {
167
+ if (entry.file.name.toLowerCase().includes(queryLower)) {
168
+ bestMatch = entry;
169
+ break;
170
+ }
171
+ }
172
+ if (!bestMatch) {
173
+ return {
174
+ content: [
175
+ {
176
+ type: "text",
177
+ text: `No file found matching "${queryName}". Use search_materials or list_materials to find available files.`,
178
+ },
179
+ ],
180
+ };
181
+ }
182
+ // Truncate very large files
183
+ const maxLen = 30000;
184
+ const text = bestMatch.text.length > maxLen
185
+ ? bestMatch.text.substring(0, maxLen) + "\n...[truncated]"
186
+ : bestMatch.text;
118
187
  return {
119
188
  content: [
120
189
  {
121
190
  type: "text",
122
- text: `Found matches in ${results.length} file(s) for "${query}":\n\n${formatted}`,
191
+ text: `📄 ${bestMatch.file.name} [${categorizeFile(bestMatch.file)}]\n\n${text}`,
123
192
  },
124
193
  ],
125
194
  };
@@ -156,6 +225,55 @@ export function registerToolHandlers(server) {
156
225
  ],
157
226
  };
158
227
  }
228
+ if (name === "quiz") {
229
+ const topic = args.topic;
230
+ const numQuestions = args.num_questions || 5;
231
+ if (!topic) {
232
+ return {
233
+ content: [{ type: "text", text: "Error: topic parameter is required" }],
234
+ };
235
+ }
236
+ await ensureTextCache();
237
+ // Gather relevant material for the topic
238
+ const relevantContent = [];
239
+ for (const [, { file, text }] of textCache) {
240
+ const matches = searchInText(text, topic);
241
+ if (matches.length > 0) {
242
+ // Include surrounding context (5 lines around each match)
243
+ const lines = text.split("\n");
244
+ const snippets = [];
245
+ for (const match of matches.slice(0, 10)) {
246
+ const start = Math.max(0, match.lineNumber - 6);
247
+ const end = Math.min(lines.length, match.lineNumber + 4);
248
+ snippets.push(lines.slice(start, end).join("\n"));
249
+ }
250
+ relevantContent.push(`--- ${file.name} ---\n${snippets.join("\n...\n")}`);
251
+ }
252
+ }
253
+ if (relevantContent.length === 0) {
254
+ return {
255
+ content: [
256
+ {
257
+ type: "text",
258
+ text: `No course material found on "${topic}". Try a different topic or check available materials with list_materials.`,
259
+ },
260
+ ],
261
+ };
262
+ }
263
+ const materialText = relevantContent.join("\n\n");
264
+ // Truncate if too large
265
+ const truncated = materialText.length > 15000
266
+ ? materialText.substring(0, 15000) + "\n...[truncated]"
267
+ : materialText;
268
+ return {
269
+ content: [
270
+ {
271
+ type: "text",
272
+ text: `QUIZ REQUEST: Generate ${numQuestions} practice questions on "${topic}" based ONLY on the following course material. Mix question types: multiple choice, true/false, and short answer. For each question, provide the answer and a brief explanation referencing the source material. Write questions and answers in Hebrew.\n\n=== COURSE MATERIAL ===\n${truncated}`,
273
+ },
274
+ ],
275
+ };
276
+ }
159
277
  throw new Error(`Unknown tool: ${name}`);
160
278
  });
161
279
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sad-mcp",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "MCP server for Software Analysis and Design course materials at BGU",
5
5
  "type": "module",
6
6
  "bin": {