sad-mcp 0.1.5 → 0.1.7
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 +1 -1
- package/dist/text-cache.d.ts +6 -0
- package/dist/text-cache.js +44 -0
- package/dist/tools.js +78 -1
- package/package.json +1 -1
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.
|
|
10
|
+
const server = new Server({ name: "sad-mcp", version: "0.1.7" }, {
|
|
11
11
|
capabilities: {
|
|
12
12
|
tools: {},
|
|
13
13
|
prompts: {},
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
const TEXT_CACHE_DIR = join(homedir(), ".sad-mcp", "text-cache");
|
|
5
|
+
const TEXT_CACHE_INDEX = join(TEXT_CACHE_DIR, "index.json");
|
|
6
|
+
function ensureDir() {
|
|
7
|
+
if (!existsSync(TEXT_CACHE_DIR)) {
|
|
8
|
+
mkdirSync(TEXT_CACHE_DIR, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function loadIndex() {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(readFileSync(TEXT_CACHE_INDEX, "utf-8"));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function saveIndex(index) {
|
|
20
|
+
ensureDir();
|
|
21
|
+
writeFileSync(TEXT_CACHE_INDEX, JSON.stringify(index, null, 2));
|
|
22
|
+
}
|
|
23
|
+
export function loadTextCache() {
|
|
24
|
+
const index = loadIndex();
|
|
25
|
+
const result = {};
|
|
26
|
+
for (const [fileId, entry] of Object.entries(index)) {
|
|
27
|
+
try {
|
|
28
|
+
const text = readFileSync(join(TEXT_CACHE_DIR, entry.textFile), "utf-8");
|
|
29
|
+
result[fileId] = { modifiedTime: entry.modifiedTime, text };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Skip missing text files
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
export function saveTextEntry(fileId, entry) {
|
|
38
|
+
ensureDir();
|
|
39
|
+
const textFile = `${fileId}.txt`;
|
|
40
|
+
writeFileSync(join(TEXT_CACHE_DIR, textFile), entry.text, "utf-8");
|
|
41
|
+
const index = loadIndex();
|
|
42
|
+
index[fileId] = { modifiedTime: entry.modifiedTime, textFile };
|
|
43
|
+
saveIndex(index);
|
|
44
|
+
}
|
package/dist/tools.js
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
import { ListToolsRequestSchema, CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
2
2
|
import { listAllFiles, downloadFile, categorizeFile } from "./drive.js";
|
|
3
3
|
import { extractText, isExtractable } from "./text-extract.js";
|
|
4
|
+
import { loadTextCache, saveTextEntry } from "./text-cache.js";
|
|
4
5
|
import { trackToolCall } from "./tracking.js";
|
|
5
|
-
// In-memory text cache for search (populated
|
|
6
|
+
// In-memory text cache for search (populated from disk cache + fresh extractions)
|
|
6
7
|
const textCache = new Map();
|
|
7
8
|
async function ensureTextCache() {
|
|
8
9
|
if (textCache.size > 0)
|
|
9
10
|
return;
|
|
10
11
|
const files = await listAllFiles();
|
|
11
12
|
const extractableFiles = files.filter(isExtractable);
|
|
13
|
+
const diskCache = loadTextCache();
|
|
12
14
|
for (const file of extractableFiles) {
|
|
13
15
|
try {
|
|
16
|
+
// Check disk cache — hit if file hasn't changed
|
|
17
|
+
const cached = diskCache[file.id];
|
|
18
|
+
if (cached && cached.modifiedTime === file.modifiedTime) {
|
|
19
|
+
textCache.set(file.id, { file, text: cached.text });
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
// Cache miss — download and extract
|
|
14
23
|
const buffer = await downloadFile(file);
|
|
15
24
|
const text = await extractText(file, buffer);
|
|
16
25
|
textCache.set(file.id, { file, text });
|
|
26
|
+
saveTextEntry(file.id, { modifiedTime: file.modifiedTime, text });
|
|
17
27
|
}
|
|
18
28
|
catch {
|
|
19
29
|
// Skip files that fail to download/extract
|
|
@@ -62,6 +72,24 @@ export function registerToolHandlers(server) {
|
|
|
62
72
|
},
|
|
63
73
|
},
|
|
64
74
|
},
|
|
75
|
+
{
|
|
76
|
+
name: "quiz",
|
|
77
|
+
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.",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: {
|
|
81
|
+
topic: {
|
|
82
|
+
type: "string",
|
|
83
|
+
description: "The topic to quiz on (e.g., 'use case diagrams', 'BPMN', 'normalization', 'state diagrams')",
|
|
84
|
+
},
|
|
85
|
+
num_questions: {
|
|
86
|
+
type: "number",
|
|
87
|
+
description: "Number of questions to generate. Defaults to 5.",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
required: ["topic"],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
65
93
|
],
|
|
66
94
|
}));
|
|
67
95
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -146,6 +174,55 @@ export function registerToolHandlers(server) {
|
|
|
146
174
|
],
|
|
147
175
|
};
|
|
148
176
|
}
|
|
177
|
+
if (name === "quiz") {
|
|
178
|
+
const topic = args.topic;
|
|
179
|
+
const numQuestions = args.num_questions || 5;
|
|
180
|
+
if (!topic) {
|
|
181
|
+
return {
|
|
182
|
+
content: [{ type: "text", text: "Error: topic parameter is required" }],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
await ensureTextCache();
|
|
186
|
+
// Gather relevant material for the topic
|
|
187
|
+
const relevantContent = [];
|
|
188
|
+
for (const [, { file, text }] of textCache) {
|
|
189
|
+
const matches = searchInText(text, topic);
|
|
190
|
+
if (matches.length > 0) {
|
|
191
|
+
// Include surrounding context (5 lines around each match)
|
|
192
|
+
const lines = text.split("\n");
|
|
193
|
+
const snippets = [];
|
|
194
|
+
for (const match of matches.slice(0, 10)) {
|
|
195
|
+
const start = Math.max(0, match.lineNumber - 6);
|
|
196
|
+
const end = Math.min(lines.length, match.lineNumber + 4);
|
|
197
|
+
snippets.push(lines.slice(start, end).join("\n"));
|
|
198
|
+
}
|
|
199
|
+
relevantContent.push(`--- ${file.name} ---\n${snippets.join("\n...\n")}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (relevantContent.length === 0) {
|
|
203
|
+
return {
|
|
204
|
+
content: [
|
|
205
|
+
{
|
|
206
|
+
type: "text",
|
|
207
|
+
text: `No course material found on "${topic}". Try a different topic or check available materials with list_materials.`,
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const materialText = relevantContent.join("\n\n");
|
|
213
|
+
// Truncate if too large
|
|
214
|
+
const truncated = materialText.length > 15000
|
|
215
|
+
? materialText.substring(0, 15000) + "\n...[truncated]"
|
|
216
|
+
: materialText;
|
|
217
|
+
return {
|
|
218
|
+
content: [
|
|
219
|
+
{
|
|
220
|
+
type: "text",
|
|
221
|
+
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}`,
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
149
226
|
throw new Error(`Unknown tool: ${name}`);
|
|
150
227
|
});
|
|
151
228
|
}
|