sad-mcp 0.1.0

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/auth.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { type Auth } from "googleapis";
2
+ export declare function getAuthenticatedClient(): Promise<Auth.OAuth2Client>;
package/dist/auth.js ADDED
@@ -0,0 +1,105 @@
1
+ import { google } from "googleapis";
2
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+ import { createServer } from "http";
6
+ import open from "open";
7
+ // OAuth credentials — embedded for desktop app (not secret per Google docs)
8
+ // TODO: Replace with actual credentials from Google Cloud Console
9
+ const CLIENT_ID = "721600472437-vaqegrbgqr2dta3fbmc55heqju7f6hl5.apps.googleusercontent.com";
10
+ const CLIENT_SECRET = "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";
11
+ const REDIRECT_URI = "http://localhost:3456/oauth2callback";
12
+ const SCOPES = ["https://www.googleapis.com/auth/drive.readonly"];
13
+ const CONFIG_DIR = join(homedir(), ".sad-mcp");
14
+ const TOKEN_PATH = join(CONFIG_DIR, "tokens.json");
15
+ function ensureConfigDir() {
16
+ if (!existsSync(CONFIG_DIR)) {
17
+ mkdirSync(CONFIG_DIR, { recursive: true });
18
+ }
19
+ }
20
+ function loadStoredToken() {
21
+ try {
22
+ const data = readFileSync(TOKEN_PATH, "utf-8");
23
+ return JSON.parse(data);
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ function saveToken(token) {
30
+ ensureConfigDir();
31
+ writeFileSync(TOKEN_PATH, JSON.stringify(token, null, 2));
32
+ }
33
+ async function getAuthCodeFromBrowser(authUrl) {
34
+ return new Promise((resolve, reject) => {
35
+ const server = createServer((req, res) => {
36
+ const url = new URL(req.url, `http://localhost:3456`);
37
+ const code = url.searchParams.get("code");
38
+ const error = url.searchParams.get("error");
39
+ if (error) {
40
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
41
+ res.end(`<h1>Authentication failed</h1><p>${error}</p>`);
42
+ server.close();
43
+ reject(new Error(`OAuth error: ${error}`));
44
+ return;
45
+ }
46
+ if (code) {
47
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
48
+ res.end("<h1>Authentication successful!</h1><p>You can close this window and return to Claude Desktop.</p>");
49
+ server.close();
50
+ resolve(code);
51
+ }
52
+ });
53
+ server.listen(3456, () => {
54
+ open(authUrl).catch(() => {
55
+ // If open fails, user will need to manually open the URL
56
+ console.error(`Please open this URL in your browser:\n${authUrl}`);
57
+ });
58
+ });
59
+ // Timeout after 2 minutes
60
+ setTimeout(() => {
61
+ server.close();
62
+ reject(new Error("OAuth timeout — no response within 2 minutes"));
63
+ }, 120_000);
64
+ });
65
+ }
66
+ export async function getAuthenticatedClient() {
67
+ ensureConfigDir();
68
+ const oauth2Client = new google.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI);
69
+ // Try loading stored token
70
+ const storedToken = loadStoredToken();
71
+ if (storedToken) {
72
+ oauth2Client.setCredentials(storedToken);
73
+ // Check if token needs refresh
74
+ if (storedToken.expiry_date &&
75
+ storedToken.expiry_date < Date.now()) {
76
+ try {
77
+ const { credentials } = await oauth2Client.refreshAccessToken();
78
+ saveToken(credentials);
79
+ oauth2Client.setCredentials(credentials);
80
+ }
81
+ catch {
82
+ // Refresh failed — need to re-authenticate
83
+ return await performOAuthFlow(oauth2Client);
84
+ }
85
+ }
86
+ return oauth2Client;
87
+ }
88
+ // No stored token — perform OAuth flow
89
+ return await performOAuthFlow(oauth2Client);
90
+ }
91
+ async function performOAuthFlow(oauth2Client) {
92
+ const authUrl = oauth2Client.generateAuthUrl({
93
+ access_type: "offline",
94
+ scope: SCOPES,
95
+ prompt: "consent",
96
+ });
97
+ console.error("SAD MCP: Opening browser for Google authentication...");
98
+ console.error("Sign in with your BGU account (@post.bgu.ac.il) to access course materials.");
99
+ const code = await getAuthCodeFromBrowser(authUrl);
100
+ const { tokens } = await oauth2Client.getToken(code);
101
+ saveToken(tokens);
102
+ oauth2Client.setCredentials(tokens);
103
+ console.error("SAD MCP: Authentication successful!");
104
+ return oauth2Client;
105
+ }
@@ -0,0 +1,11 @@
1
+ export interface DriveFile {
2
+ id: string;
3
+ name: string;
4
+ mimeType: string;
5
+ path: string;
6
+ size?: number;
7
+ modifiedTime?: string;
8
+ }
9
+ export declare function listAllFiles(): Promise<DriveFile[]>;
10
+ export declare function downloadFile(file: DriveFile): Promise<Buffer>;
11
+ export declare function categorizeFile(file: DriveFile): string;
package/dist/drive.js ADDED
@@ -0,0 +1,123 @@
1
+ import { google } from "googleapis";
2
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+ import { getAuthenticatedClient } from "./auth.js";
6
+ const FOLDER_ID = "1PnjBoLOFPf70QbpZQGbH_EgbOKc_DyZ-";
7
+ const CACHE_DIR = join(homedir(), ".sad-mcp", "cache");
8
+ const CACHE_INDEX_PATH = join(homedir(), ".sad-mcp", "cache-index.json");
9
+ const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
10
+ function loadCacheIndex() {
11
+ try {
12
+ return JSON.parse(readFileSync(CACHE_INDEX_PATH, "utf-8"));
13
+ }
14
+ catch {
15
+ return { files: {} };
16
+ }
17
+ }
18
+ function saveCacheIndex(index) {
19
+ mkdirSync(CACHE_DIR, { recursive: true });
20
+ writeFileSync(CACHE_INDEX_PATH, JSON.stringify(index, null, 2));
21
+ }
22
+ function isCacheValid(entry) {
23
+ if (!existsSync(entry.localPath))
24
+ return false;
25
+ return Date.now() - entry.cachedAt < CACHE_TTL_MS;
26
+ }
27
+ let driveClient = null;
28
+ async function getDrive() {
29
+ if (driveClient)
30
+ return driveClient;
31
+ const auth = await getAuthenticatedClient();
32
+ driveClient = google.drive({ version: "v3", auth });
33
+ return driveClient;
34
+ }
35
+ async function listFolderRecursive(drive, folderId, pathPrefix = "") {
36
+ const files = [];
37
+ let pageToken;
38
+ do {
39
+ const res = await drive.files.list({
40
+ q: `'${folderId}' in parents and trashed = false`,
41
+ fields: "nextPageToken, files(id, name, mimeType, size, modifiedTime)",
42
+ pageSize: 100,
43
+ pageToken,
44
+ });
45
+ for (const file of res.data.files || []) {
46
+ const filePath = pathPrefix ? `${pathPrefix}/${file.name}` : file.name;
47
+ if (file.mimeType === "application/vnd.google-apps.folder") {
48
+ // Recurse into subfolders
49
+ const subFiles = await listFolderRecursive(drive, file.id, filePath);
50
+ files.push(...subFiles);
51
+ }
52
+ else {
53
+ files.push({
54
+ id: file.id,
55
+ name: file.name,
56
+ mimeType: file.mimeType,
57
+ path: filePath,
58
+ size: file.size ? parseInt(file.size) : undefined,
59
+ modifiedTime: file.modifiedTime || undefined,
60
+ });
61
+ }
62
+ }
63
+ pageToken = res.data.nextPageToken || undefined;
64
+ } while (pageToken);
65
+ return files;
66
+ }
67
+ export async function listAllFiles() {
68
+ const index = loadCacheIndex();
69
+ // Return cached listing if still valid
70
+ if (index.folderListedAt &&
71
+ Date.now() - index.folderListedAt < CACHE_TTL_MS &&
72
+ index.folderListing) {
73
+ return index.folderListing;
74
+ }
75
+ const drive = await getDrive();
76
+ const files = await listFolderRecursive(drive, FOLDER_ID);
77
+ // Cache the listing
78
+ index.folderListedAt = Date.now();
79
+ index.folderListing = files;
80
+ saveCacheIndex(index);
81
+ return files;
82
+ }
83
+ export async function downloadFile(file) {
84
+ const index = loadCacheIndex();
85
+ const cached = index.files[file.id];
86
+ // Return from cache if valid
87
+ if (cached && isCacheValid(cached)) {
88
+ return readFileSync(cached.localPath);
89
+ }
90
+ const drive = await getDrive();
91
+ // Download the file
92
+ const res = await drive.files.get({ fileId: file.id, alt: "media" }, { responseType: "arraybuffer" });
93
+ const buffer = Buffer.from(res.data);
94
+ // Cache locally
95
+ mkdirSync(CACHE_DIR, { recursive: true });
96
+ const sanitizedName = file.path.replace(/[/\\:*?"<>|]/g, "_");
97
+ const localPath = join(CACHE_DIR, sanitizedName);
98
+ writeFileSync(localPath, buffer);
99
+ index.files[file.id] = {
100
+ fileId: file.id,
101
+ cachedAt: Date.now(),
102
+ localPath,
103
+ modifiedTime: file.modifiedTime,
104
+ };
105
+ saveCacheIndex(index);
106
+ return buffer;
107
+ }
108
+ export function categorizeFile(file) {
109
+ const pathLower = file.path.toLowerCase();
110
+ if (pathLower.includes("תמלולים") || pathLower.endsWith(".txt") || pathLower.endsWith(".vtt")) {
111
+ return "transcripts";
112
+ }
113
+ if (pathLower.includes("מבחנ") || pathLower.includes("exam")) {
114
+ return "exams";
115
+ }
116
+ if (pathLower.endsWith(".pptx") ||
117
+ pathLower.endsWith(".pdf") ||
118
+ pathLower.includes("שיעור") ||
119
+ pathLower.includes("הרצאה")) {
120
+ return "lectures";
121
+ }
122
+ return "other";
123
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { registerResourceHandlers } from "./resources.js";
5
+ import { registerToolHandlers } from "./tools.js";
6
+ import { trackServerStart } from "./tracking.js";
7
+ const server = new Server({ name: "sad-mcp", version: "0.1.0" }, {
8
+ capabilities: {
9
+ resources: {},
10
+ tools: {},
11
+ },
12
+ });
13
+ // Register all handlers
14
+ registerResourceHandlers(server);
15
+ registerToolHandlers(server);
16
+ // Track server startup
17
+ trackServerStart();
18
+ async function main() {
19
+ const transport = new StdioServerTransport();
20
+ await server.connect(transport);
21
+ console.error("SAD MCP server started. Course materials available from Google Drive.");
22
+ }
23
+ main().catch((err) => {
24
+ console.error("SAD MCP failed to start:", err);
25
+ process.exit(1);
26
+ });
@@ -0,0 +1,2 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ export declare function registerResourceHandlers(server: Server): void;
@@ -0,0 +1,50 @@
1
+ import { ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
2
+ import { listAllFiles, downloadFile, categorizeFile } from "./drive.js";
3
+ import { extractText, isExtractable } from "./text-extract.js";
4
+ import { trackResourceRead } from "./tracking.js";
5
+ function fileToUri(file) {
6
+ const category = categorizeFile(file);
7
+ const encodedName = encodeURIComponent(file.name);
8
+ return `sad://${category}/${encodedName}`;
9
+ }
10
+ function mimeForExtraction(file) {
11
+ // All resources are served as extracted text
12
+ return "text/plain";
13
+ }
14
+ export function registerResourceHandlers(server) {
15
+ // List all available resources
16
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
17
+ const files = await listAllFiles();
18
+ const extractableFiles = files.filter(isExtractable);
19
+ return {
20
+ resources: extractableFiles.map((file) => ({
21
+ uri: fileToUri(file),
22
+ name: file.name,
23
+ description: `[${categorizeFile(file)}] ${file.path}`,
24
+ mimeType: mimeForExtraction(file),
25
+ })),
26
+ };
27
+ });
28
+ // Read a specific resource
29
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
30
+ const uri = request.params.uri;
31
+ const files = await listAllFiles();
32
+ // Find the file matching this URI
33
+ const file = files.find((f) => fileToUri(f) === uri);
34
+ if (!file) {
35
+ throw new Error(`Resource not found: ${uri}`);
36
+ }
37
+ trackResourceRead(uri);
38
+ const buffer = await downloadFile(file);
39
+ const text = await extractText(file, buffer);
40
+ return {
41
+ contents: [
42
+ {
43
+ uri,
44
+ mimeType: "text/plain",
45
+ text,
46
+ },
47
+ ],
48
+ };
49
+ });
50
+ }
@@ -0,0 +1,3 @@
1
+ import type { DriveFile } from "./drive.js";
2
+ export declare function extractText(file: DriveFile, buffer: Buffer): Promise<string>;
3
+ export declare function isExtractable(file: DriveFile): boolean;
@@ -0,0 +1,67 @@
1
+ import officeparser from "officeparser";
2
+ import pdf from "pdf-parse";
3
+ export async function extractText(file, buffer) {
4
+ const mimeType = file.mimeType;
5
+ const name = file.name.toLowerCase();
6
+ // Plain text files — return as-is
7
+ if (mimeType === "text/plain" ||
8
+ name.endsWith(".txt") ||
9
+ name.endsWith(".vtt") ||
10
+ name.endsWith(".md")) {
11
+ return buffer.toString("utf-8");
12
+ }
13
+ // PDF files
14
+ if (mimeType === "application/pdf" || name.endsWith(".pdf")) {
15
+ try {
16
+ const data = await pdf(buffer);
17
+ return data.text;
18
+ }
19
+ catch (err) {
20
+ return `[Error extracting PDF text: ${err instanceof Error ? err.message : String(err)}]`;
21
+ }
22
+ }
23
+ // PPTX files
24
+ if (mimeType === "application/vnd.openxmlformats-officedocument.presentationml.presentation" ||
25
+ name.endsWith(".pptx")) {
26
+ try {
27
+ const text = await officeparser.parseOfficeAsync(buffer);
28
+ return text;
29
+ }
30
+ catch (err) {
31
+ return `[Error extracting PPTX text: ${err instanceof Error ? err.message : String(err)}]`;
32
+ }
33
+ }
34
+ // DOCX files
35
+ if (mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ||
36
+ name.endsWith(".docx")) {
37
+ try {
38
+ const text = await officeparser.parseOfficeAsync(buffer);
39
+ return text;
40
+ }
41
+ catch (err) {
42
+ return `[Error extracting DOCX text: ${err instanceof Error ? err.message : String(err)}]`;
43
+ }
44
+ }
45
+ // XLSX files
46
+ if (mimeType === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ||
47
+ name.endsWith(".xlsx")) {
48
+ try {
49
+ const text = await officeparser.parseOfficeAsync(buffer);
50
+ return text;
51
+ }
52
+ catch (err) {
53
+ return `[Error extracting XLSX text: ${err instanceof Error ? err.message : String(err)}]`;
54
+ }
55
+ }
56
+ return `[Unsupported file format: ${mimeType}]`;
57
+ }
58
+ export function isExtractable(file) {
59
+ const name = file.name.toLowerCase();
60
+ return (name.endsWith(".txt") ||
61
+ name.endsWith(".vtt") ||
62
+ name.endsWith(".md") ||
63
+ name.endsWith(".pdf") ||
64
+ name.endsWith(".pptx") ||
65
+ name.endsWith(".docx") ||
66
+ name.endsWith(".xlsx"));
67
+ }
@@ -0,0 +1,2 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ export declare function registerToolHandlers(server: Server): void;
package/dist/tools.js ADDED
@@ -0,0 +1,151 @@
1
+ import { ListToolsRequestSchema, CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
2
+ import { listAllFiles, downloadFile, categorizeFile } from "./drive.js";
3
+ import { extractText, isExtractable } from "./text-extract.js";
4
+ import { trackToolCall } from "./tracking.js";
5
+ // In-memory text cache for search (populated on first search)
6
+ const textCache = new Map();
7
+ async function ensureTextCache() {
8
+ if (textCache.size > 0)
9
+ return;
10
+ const files = await listAllFiles();
11
+ const extractableFiles = files.filter(isExtractable);
12
+ for (const file of extractableFiles) {
13
+ try {
14
+ const buffer = await downloadFile(file);
15
+ const text = await extractText(file, buffer);
16
+ textCache.set(file.id, { file, text });
17
+ }
18
+ catch {
19
+ // Skip files that fail to download/extract
20
+ }
21
+ }
22
+ }
23
+ function searchInText(text, query) {
24
+ const queryLower = query.toLowerCase();
25
+ const lines = text.split("\n");
26
+ const matches = [];
27
+ for (let i = 0; i < lines.length; i++) {
28
+ if (lines[i].toLowerCase().includes(queryLower)) {
29
+ matches.push({ line: lines[i].trim(), lineNumber: i + 1 });
30
+ }
31
+ }
32
+ return matches;
33
+ }
34
+ export function registerToolHandlers(server) {
35
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
36
+ tools: [
37
+ {
38
+ name: "search_materials",
39
+ description: "Search across all course materials (lecture transcripts, presentations, exams) for relevant content. Use this when a student asks about a specific topic.",
40
+ inputSchema: {
41
+ type: "object",
42
+ properties: {
43
+ query: {
44
+ type: "string",
45
+ description: "The search query (topic, keyword, or phrase to find in course materials)",
46
+ },
47
+ },
48
+ required: ["query"],
49
+ },
50
+ },
51
+ {
52
+ name: "list_materials",
53
+ description: "List all available course materials, optionally filtered by category.",
54
+ inputSchema: {
55
+ type: "object",
56
+ properties: {
57
+ category: {
58
+ type: "string",
59
+ enum: ["lectures", "transcripts", "exams", "all"],
60
+ description: "Filter by category. Defaults to 'all'.",
61
+ },
62
+ },
63
+ },
64
+ },
65
+ ],
66
+ }));
67
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
68
+ const { name, arguments: args } = request.params;
69
+ trackToolCall(name, args);
70
+ if (name === "search_materials") {
71
+ const query = args.query;
72
+ if (!query) {
73
+ return {
74
+ content: [{ type: "text", text: "Error: query parameter is required" }],
75
+ };
76
+ }
77
+ await ensureTextCache();
78
+ const results = [];
79
+ for (const [, { file, text }] of textCache) {
80
+ const matches = searchInText(text, query);
81
+ if (matches.length > 0) {
82
+ results.push({
83
+ fileName: file.name,
84
+ path: file.path,
85
+ category: categorizeFile(file),
86
+ matches: matches.slice(0, 5), // Top 5 matches per file
87
+ });
88
+ }
89
+ }
90
+ if (results.length === 0) {
91
+ return {
92
+ content: [
93
+ {
94
+ type: "text",
95
+ text: `No results found for "${query}" in course materials.`,
96
+ },
97
+ ],
98
+ };
99
+ }
100
+ const formatted = results
101
+ .map((r) => {
102
+ const matchLines = r.matches
103
+ .map((m) => ` Line ${m.lineNumber}: ${m.line.substring(0, 200)}`)
104
+ .join("\n");
105
+ return `📄 ${r.fileName} [${r.category}]\n Path: ${r.path}\n${matchLines}`;
106
+ })
107
+ .join("\n\n");
108
+ return {
109
+ content: [
110
+ {
111
+ type: "text",
112
+ text: `Found matches in ${results.length} file(s) for "${query}":\n\n${formatted}`,
113
+ },
114
+ ],
115
+ };
116
+ }
117
+ if (name === "list_materials") {
118
+ const category = args.category || "all";
119
+ const files = await listAllFiles();
120
+ const extractableFiles = files.filter(isExtractable);
121
+ const filtered = category === "all"
122
+ ? extractableFiles
123
+ : extractableFiles.filter((f) => categorizeFile(f) === category);
124
+ // Group by category
125
+ const grouped = {};
126
+ for (const file of filtered) {
127
+ const cat = categorizeFile(file);
128
+ if (!grouped[cat])
129
+ grouped[cat] = [];
130
+ grouped[cat].push(file);
131
+ }
132
+ const formatted = Object.entries(grouped)
133
+ .map(([cat, files]) => {
134
+ const fileList = files
135
+ .map((f) => ` - ${f.name} (${f.path})`)
136
+ .join("\n");
137
+ return `📁 ${cat.toUpperCase()} (${files.length} files)\n${fileList}`;
138
+ })
139
+ .join("\n\n");
140
+ return {
141
+ content: [
142
+ {
143
+ type: "text",
144
+ text: `Course materials${category !== "all" ? ` [${category}]` : ""}:\n\n${formatted}`,
145
+ },
146
+ ],
147
+ };
148
+ }
149
+ throw new Error(`Unknown tool: ${name}`);
150
+ });
151
+ }
@@ -0,0 +1,6 @@
1
+ export declare function getAnonymousId(): string;
2
+ export declare function trackEvent(event: string, data?: Record<string, unknown>): void;
3
+ export declare function trackToolCall(toolName: string, args?: Record<string, unknown>): void;
4
+ export declare function trackResourceRead(uri: string): void;
5
+ export declare function trackServerStart(): void;
6
+ export declare function trackError(error: string, context?: string): void;
@@ -0,0 +1,55 @@
1
+ import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+ import { randomUUID } from "crypto";
5
+ const CONFIG_DIR = join(homedir(), ".sad-mcp");
6
+ const ANON_ID_PATH = join(CONFIG_DIR, "anonymous-id.txt");
7
+ const LOG_PATH = join(CONFIG_DIR, "usage-log.jsonl");
8
+ function ensureConfigDir() {
9
+ if (!existsSync(CONFIG_DIR)) {
10
+ mkdirSync(CONFIG_DIR, { recursive: true });
11
+ }
12
+ }
13
+ export function getAnonymousId() {
14
+ ensureConfigDir();
15
+ try {
16
+ return readFileSync(ANON_ID_PATH, "utf-8").trim();
17
+ }
18
+ catch {
19
+ const id = randomUUID();
20
+ writeFileSync(ANON_ID_PATH, id);
21
+ return id;
22
+ }
23
+ }
24
+ export function trackEvent(event, data) {
25
+ ensureConfigDir();
26
+ const entry = {
27
+ timestamp: new Date().toISOString(),
28
+ anonymousId: getAnonymousId(),
29
+ event,
30
+ data,
31
+ };
32
+ try {
33
+ appendFileSync(LOG_PATH, JSON.stringify(entry) + "\n");
34
+ }
35
+ catch {
36
+ // Silently fail — tracking should never break the server
37
+ }
38
+ // TODO: MCPcat integration
39
+ // When MCPcat TypeScript SDK is configured, send events here:
40
+ // import { MCPCat } from "mcpcat";
41
+ // const mcpcat = new MCPCat({ apiKey: "YOUR_MCPCAT_API_KEY" });
42
+ // mcpcat.track(entry);
43
+ }
44
+ export function trackToolCall(toolName, args) {
45
+ trackEvent("tool_call", { tool: toolName, arguments: args });
46
+ }
47
+ export function trackResourceRead(uri) {
48
+ trackEvent("resource_read", { uri });
49
+ }
50
+ export function trackServerStart() {
51
+ trackEvent("server_start", { version: "0.1.0" });
52
+ }
53
+ export function trackError(error, context) {
54
+ trackEvent("error", { error, context });
55
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "sad-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Software Analysis and Design course materials at BGU",
5
+ "type": "module",
6
+ "bin": {
7
+ "sad-mcp": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "prepare": "npm run build",
15
+ "dev": "tsc --watch"
16
+ },
17
+ "keywords": [
18
+ "mcp",
19
+ "claude",
20
+ "course-materials",
21
+ "model-context-protocol"
22
+ ],
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "@modelcontextprotocol/sdk": "^1.0.0",
26
+ "googleapis": "^144.0.0",
27
+ "open": "^10.0.0",
28
+ "officeparser": "^4.0.0",
29
+ "pdf-parse": "^1.1.1"
30
+ },
31
+ "devDependencies": {
32
+ "typescript": "^5.0.0",
33
+ "@types/node": "^20.0.0",
34
+ "@types/pdf-parse": "^1.1.4"
35
+ }
36
+ }