sad-mcp 1.0.3 → 1.1.1

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/README.md CHANGED
@@ -1,84 +1,91 @@
1
1
  # sad-mcp
2
2
 
3
- MCP server that gives students access to **Software Analysis and Design** course materials through Claude Desktop. Materials are served from a shared Google Drive folder.
3
+ MCP server that gives students access to **Software Analysis and Design** course materials and diagram tools through Claude Desktop.
4
4
 
5
- ## What it does
5
+ ## Student Setup
6
6
 
7
- - Exposes course materials (lectures, transcripts, exams) as MCP **resources**
8
- - Provides `search_materials` and `list_materials` **tools** for Claude to query course content
9
- - Extracts text from PPTX, PDF, DOCX, XLSX, and plain text files
10
- - Caches files locally to avoid repeated downloads
11
- - Tracks anonymous usage for research purposes
7
+ ### Step 1 Find your config file
12
8
 
13
- ## Student setup
9
+ | How you installed Claude Desktop | Config file location |
10
+ |---|---|
11
+ | **Microsoft Store** (Windows) | `%LOCALAPPDATA%\Packages\Claude_pzs8sxrjxfjjc\LocalCache\Roaming\Claude\claude_desktop_config.json` |
12
+ | **Standard installer** (Windows) | `%APPDATA%\Roaming\Claude\claude_desktop_config.json` |
13
+ | **Mac** | `~/Library/Application Support/Claude/claude_desktop_config.json` |
14
14
 
15
- Add to your Claude Desktop configuration (`claude_desktop_config.json`):
15
+ Not sure which? Search your user profile for `claude_desktop_config.json`.
16
16
 
17
- ```json
18
- {
19
- "mcpServers": {
20
- "sad-course": {
21
- "command": "npx",
22
- "args": ["-y", "sad-mcp@latest"]
23
- }
24
- }
25
- }
26
- ```
17
+ ### Step 2 — Edit the config file
27
18
 
28
- On Windows, use the full path to `npx.cmd`:
19
+ Open the file in a text editor and add the `sad-mcp` entry. If the file already has other MCP servers, add alongside them:
29
20
 
30
21
  ```json
31
22
  {
32
23
  "mcpServers": {
33
- "sad-course": {
34
- "command": "C:\\Program Files\\nodejs\\npx.cmd",
24
+ "sad-mcp": {
25
+ "command": "npx",
35
26
  "args": ["-y", "sad-mcp@latest"]
36
27
  }
37
28
  }
38
29
  }
39
30
  ```
40
31
 
41
- On first run, a browser window will open for Google authentication. Sign in with your **@post.bgu.ac.il** account. After that, the server connects automatically.
42
-
43
- ## Tools
44
-
45
- | Tool | Description |
46
- |------|-------------|
47
- | `search_materials(query)` | Full-text search across all course materials |
48
- | `list_materials(category?)` | List available materials, optionally filtered by `lectures`, `transcripts`, `exams`, or `all` |
32
+ > **Windows note:** If Claude Desktop doesn't find `npx`, use the full path:
33
+ > ```json
34
+ > {
35
+ > "mcpServers": {
36
+ > "sad-mcp": {
37
+ > "command": "C:\\Program Files\\nodejs\\npx.cmd",
38
+ > "args": ["-y", "sad-mcp@latest"]
39
+ > }
40
+ > }
41
+ > }
42
+ > ```
49
43
 
50
- ## Resources
44
+ ### Step 3 — Restart Claude Desktop
51
45
 
52
- All extractable files from the course Google Drive folder are exposed as MCP resources with URIs like `sad://lectures/filename.pptx` or `sad://transcripts/filename.txt`. Claude can read these directly.
46
+ Fully quit Claude Desktop (system tray Quit not just close the window), then reopen it.
53
47
 
54
- ## Local data
48
+ ### Step 4 — Authenticate with Google
55
49
 
56
- The server stores data in `~/.sad-mcp/`:
50
+ On the first tool call, a browser window will open for Google authentication. Sign in with your **@post.bgu.ac.il** account. After that, the server connects automatically on every restart.
57
51
 
58
- | File | Purpose |
59
- |------|---------|
60
- | `tokens.json` | Google OAuth refresh token |
61
- | `anonymous-id.txt` | Random UUID for usage tracking |
62
- | `usage-log.jsonl` | Local usage event log |
63
- | `cache/` | Downloaded file cache (1-hour TTL) |
64
- | `cache-index.json` | Cache metadata |
52
+ ---
65
53
 
66
- ## Development
67
-
68
- ```bash
69
- npm install
70
- npm run build # compile TypeScript
71
- npm run dev # watch mode
72
- ```
73
-
74
- ## Publishing
75
-
76
- ```bash
77
- # bump version in package.json
78
- npm publish
79
- ```
54
+ ## What it does
80
55
 
81
- Students get the latest version automatically via `npx -y sad-mcp@latest`.
56
+ ### Course materials
57
+ - Search and browse lectures, transcripts, and past exams from the course Google Drive
58
+ - Extracts text from PPTX, PDF, DOCX files for full-text search
59
+
60
+ ### Diagram tools
61
+ | Tool | What it does |
62
+ |---|---|
63
+ | `bpmn_analysis` | Analyze a business process and produce a BPMN 1.0 model |
64
+ | `bpmn_validate_model` | Validate the JSON model before rendering |
65
+ | `bpmn_render` | Render the validated model as an HTML file saved to your Desktop |
66
+ | `uml_use_case` | Create a UML use case diagram |
67
+ | `uml_class_diagram` | Create a UML class diagram |
68
+ | `uml_state_diagram` | Create a UML state diagram |
69
+ | `uml_write_file` | Save a generated UML diagram to your Desktop |
70
+
71
+ ### Course material tools
72
+ | Tool | What it does |
73
+ |---|---|
74
+ | `search_materials` | Full-text search across all course materials |
75
+ | `list_materials` | List available materials by category |
76
+ | `get_material` | Read the full text of a specific file |
77
+ | `list_exams` | List available past exams |
78
+ | `practice_exam` | Get a past exam to practice with |
79
+
80
+ ---
81
+
82
+ ## Requirements
83
+
84
+ - [Claude Desktop](https://claude.ai/download) installed
85
+ - [Node.js](https://nodejs.org/) v18 or later (verify: `node --version` in a terminal)
86
+ - A `@post.bgu.ac.il` Google account with access to the course Drive folder
87
+
88
+ ---
82
89
 
83
90
  ## License
84
91
 
@@ -0,0 +1,8 @@
1
+ import type { LaidOutModel } from "./types.js";
2
+ export declare function buildBPMNXml(laid: LaidOutModel): string;
3
+ export declare function safeFilename(m: {
4
+ title: {
5
+ he: string;
6
+ en?: string;
7
+ };
8
+ }): string;
@@ -0,0 +1,167 @@
1
+ // Generates BPMN 2.0 XML (with DI section) server-side, using the
2
+ // coordinates already computed by the layout pass. The VP export button
3
+ // embedded in the HTML just triggers a download of this precomputed
4
+ // string — no DOM walking, no getBBox() calls.
5
+ //
6
+ // Reference namespaces from OMG BPMN 2.0:
7
+ // bpmn http://www.omg.org/spec/BPMN/20100524/MODEL
8
+ // bpmndi http://www.omg.org/spec/BPMN/20100524/DI
9
+ // dc http://www.omg.org/spec/DD/20100524/DC
10
+ // di http://www.omg.org/spec/DD/20100524/DI
11
+ import { escapeXml } from "./svg.js";
12
+ export function buildBPMNXml(laid) {
13
+ const m = laid.model;
14
+ const title = escapeXml(m.title.en || m.title.he || "process");
15
+ // Map pool id → process id (each pool gets its own process element)
16
+ const processIds = new Map();
17
+ m.pools.forEach((p, i) => processIds.set(p.id, `process_${i + 1}`));
18
+ const processLines = [];
19
+ const collabLines = [];
20
+ // Participants reference pool processes
21
+ for (const p of m.pools) {
22
+ const procId = processIds.get(p.id);
23
+ collabLines.push(` <bpmn:participant id="${escapeXml(p.id)}" name="${escapeXml(p.name)}" processRef="${procId}"/>`);
24
+ }
25
+ for (const mf of m.messageFlows) {
26
+ const src = mf.fromElement ?? mf.fromPool;
27
+ const tgt = mf.toElement ?? mf.toPool;
28
+ const name = mf.label ? ` name="${escapeXml(mf.label)}"` : "";
29
+ collabLines.push(` <bpmn:messageFlow id="${escapeXml(mf.id)}" sourceRef="${escapeXml(src)}" targetRef="${escapeXml(tgt)}"${name}/>`);
30
+ }
31
+ // One bpmn:process block per pool
32
+ const processBlocks = [];
33
+ for (const p of m.pools) {
34
+ const procId = processIds.get(p.id);
35
+ const poolElements = m.elements.filter((el) => {
36
+ if (el.kind === "task" || el.kind === "gateway" || el.kind === "event" || el.kind === "collapsedSubProcess") {
37
+ return el.pool === p.id;
38
+ }
39
+ return false;
40
+ });
41
+ const poolFlows = m.sequenceFlows.filter((f) => {
42
+ const src = m.elements.find((e) => e.id === f.from);
43
+ const tgt = m.elements.find((e) => e.id === f.to);
44
+ if (!src || !tgt)
45
+ return false;
46
+ const srcPool = src.pool;
47
+ const tgtPool = tgt.pool;
48
+ return srcPool === p.id && tgtPool === p.id;
49
+ });
50
+ // Lanes (if organization pool with lanes)
51
+ const laneSetLines = [];
52
+ if (p.lanes.length > 0) {
53
+ laneSetLines.push(` <bpmn:laneSet id="${procId}_laneset">`);
54
+ for (const lane of p.lanes) {
55
+ const laneElements = poolElements.filter((e) => e.lane === lane.id);
56
+ laneSetLines.push(` <bpmn:lane id="${escapeXml(lane.id)}" name="${escapeXml(lane.name)}">`);
57
+ for (const le of laneElements) {
58
+ laneSetLines.push(` <bpmn:flowNodeRef>${escapeXml(le.id)}</bpmn:flowNodeRef>`);
59
+ }
60
+ laneSetLines.push(` </bpmn:lane>`);
61
+ }
62
+ laneSetLines.push(` </bpmn:laneSet>`);
63
+ }
64
+ // Element lines
65
+ const elLines = [];
66
+ for (const el of poolElements) {
67
+ const id = escapeXml(el.id);
68
+ const name = escapeXml(el.name ?? "");
69
+ if (el.kind === "task") {
70
+ elLines.push(` <bpmn:task id="${id}" name="${name}"/>`);
71
+ }
72
+ else if (el.kind === "collapsedSubProcess") {
73
+ elLines.push(` <bpmn:subProcess id="${id}" name="${name}"/>`);
74
+ }
75
+ else if (el.kind === "gateway") {
76
+ const tag = el.gatewayType === "AND" ? "parallelGateway"
77
+ : el.gatewayType === "OR" ? "inclusiveGateway"
78
+ : "exclusiveGateway";
79
+ elLines.push(` <bpmn:${tag} id="${id}" name="${name}"/>`);
80
+ }
81
+ else if (el.kind === "event") {
82
+ const tag = el.eventType === "start" ? "startEvent"
83
+ : el.eventType === "end" ? "endEvent"
84
+ : "intermediateCatchEvent";
85
+ elLines.push(` <bpmn:${tag} id="${id}" name="${name}"/>`);
86
+ }
87
+ }
88
+ // Data stores and objects are declared at process level
89
+ for (const el of m.elements) {
90
+ if (el.kind === "dataStore") {
91
+ elLines.push(` <bpmn:dataStoreReference id="${escapeXml(el.id)}" name="${escapeXml(el.name)}"/>`);
92
+ }
93
+ else if (el.kind === "dataObject") {
94
+ elLines.push(` <bpmn:dataObjectReference id="${escapeXml(el.id)}" name="${escapeXml(el.name)}"/>`);
95
+ }
96
+ }
97
+ for (const f of poolFlows) {
98
+ const name = f.label ? ` name="${escapeXml(f.label)}"` : "";
99
+ elLines.push(` <bpmn:sequenceFlow id="${escapeXml(f.id)}" sourceRef="${escapeXml(f.from)}" targetRef="${escapeXml(f.to)}"${name}/>`);
100
+ }
101
+ processBlocks.push(` <bpmn:process id="${procId}" name="${escapeXml(p.name)}" isExecutable="false">
102
+ ${laneSetLines.join("\n")}
103
+ ${elLines.join("\n")}
104
+ </bpmn:process>`);
105
+ }
106
+ // DI section — harvest coords from layout
107
+ const diLines = [];
108
+ // Pool shapes
109
+ for (const p of laid.pools) {
110
+ diLines.push(` <bpmndi:BPMNShape id="${escapeXml(p.poolId)}_di" bpmnElement="${escapeXml(p.poolId)}" isHorizontal="true">
111
+ <dc:Bounds x="${round(p.x)}" y="${round(p.y)}" width="${round(p.w)}" height="${round(p.h)}"/>
112
+ </bpmndi:BPMNShape>`);
113
+ for (const ln of p.lanes) {
114
+ if (!ln.laneId)
115
+ continue;
116
+ diLines.push(` <bpmndi:BPMNShape id="${escapeXml(ln.laneId)}_di" bpmnElement="${escapeXml(ln.laneId)}" isHorizontal="true">
117
+ <dc:Bounds x="${round(ln.x)}" y="${round(ln.y)}" width="${round(ln.w)}" height="${round(ln.h)}"/>
118
+ </bpmndi:BPMNShape>`);
119
+ }
120
+ }
121
+ // Element shapes
122
+ for (const [id, pl] of laid.placements) {
123
+ diLines.push(` <bpmndi:BPMNShape id="${escapeXml(id)}_di" bpmnElement="${escapeXml(id)}">
124
+ <dc:Bounds x="${round(pl.box.x)}" y="${round(pl.box.y)}" width="${round(pl.box.w)}" height="${round(pl.box.h)}"/>
125
+ </bpmndi:BPMNShape>`);
126
+ }
127
+ // Sequence flow edges
128
+ for (const r of laid.sequenceRoutes) {
129
+ const waypoints = r.points.map((p) => ` <di:waypoint x="${round(p.x)}" y="${round(p.y)}"/>`).join("\n");
130
+ diLines.push(` <bpmndi:BPMNEdge id="${escapeXml(r.id)}_di" bpmnElement="${escapeXml(r.id)}">
131
+ ${waypoints}
132
+ </bpmndi:BPMNEdge>`);
133
+ }
134
+ for (const r of laid.messageRoutes) {
135
+ const waypoints = r.points.map((p) => ` <di:waypoint x="${round(p.x)}" y="${round(p.y)}"/>`).join("\n");
136
+ diLines.push(` <bpmndi:BPMNEdge id="${escapeXml(r.id)}_di" bpmnElement="${escapeXml(r.id)}">
137
+ ${waypoints}
138
+ </bpmndi:BPMNEdge>`);
139
+ }
140
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
141
+ <bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
142
+ xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
143
+ xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
144
+ xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
145
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
146
+ id="Definitions_1" targetNamespace="http://bpmn.io/schema/bpmn">
147
+ ${processBlocks.join("\n")}
148
+ <bpmn:collaboration id="collab_1">
149
+ ${collabLines.join("\n")}
150
+ </bpmn:collaboration>
151
+ <bpmndi:BPMNDiagram id="BPMNDiagram_1">
152
+ <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="collab_1">
153
+ ${diLines.join("\n")}
154
+ </bpmndi:BPMNPlane>
155
+ </bpmndi:BPMNDiagram>
156
+ </bpmn:definitions>`;
157
+ return xml;
158
+ }
159
+ function round(n) { return Math.round(n); }
160
+ // Returned for completeness — a caller may want the stable safe title
161
+ export function safeFilename(m) {
162
+ const base = (m.title.en || m.title.he || "process")
163
+ .replace(/[^\p{L}\p{N}\s_-]/gu, "")
164
+ .replace(/\s+/g, "_")
165
+ .slice(0, 60);
166
+ return base || "process";
167
+ }
@@ -0,0 +1,18 @@
1
+ import { layoutModel } from "./layout.js";
2
+ import { routeAll } from "./routing.js";
3
+ import { computeStats, renderHTML, type RenderStats } from "./template.js";
4
+ import { parseAndValidate, validateModel, formatIssues } from "./validate.js";
5
+ import type { DiagramModel, ValidationIssue } from "./types.js";
6
+ import { safeFilename } from "./export-bpmn.js";
7
+ export type RenderResult = {
8
+ ok: true;
9
+ html: string;
10
+ stats: RenderStats;
11
+ filenameBase: string;
12
+ } | {
13
+ ok: false;
14
+ issues: ValidationIssue[];
15
+ };
16
+ export declare function renderBPMN(rawJson: string | object): RenderResult;
17
+ export { validateModel, parseAndValidate, formatIssues, layoutModel, routeAll, renderHTML, computeStats, safeFilename, };
18
+ export type { DiagramModel, ValidationIssue };
@@ -0,0 +1,23 @@
1
+ // Public entry point for the BPMN rendering pipeline.
2
+ // Pipeline: raw JSON → validate → layout → route → render HTML.
3
+ import { layoutModel } from "./layout.js";
4
+ import { routeAll } from "./routing.js";
5
+ import { computeStats, renderHTML } from "./template.js";
6
+ import { parseAndValidate, validateModel, formatIssues } from "./validate.js";
7
+ import { safeFilename } from "./export-bpmn.js";
8
+ export function renderBPMN(rawJson) {
9
+ const vr = typeof rawJson === "string"
10
+ ? parseAndValidate(rawJson)
11
+ : validateModel(rawJson);
12
+ if (!vr.ok)
13
+ return { ok: false, issues: vr.issues };
14
+ const laid = routeAll(layoutModel(vr.model));
15
+ const html = renderHTML(laid);
16
+ return {
17
+ ok: true,
18
+ html,
19
+ stats: computeStats(laid),
20
+ filenameBase: safeFilename(vr.model),
21
+ };
22
+ }
23
+ export { validateModel, parseAndValidate, formatIssues, layoutModel, routeAll, renderHTML, computeStats, safeFilename, };
@@ -0,0 +1,22 @@
1
+ import type { DiagramModel, LaidOutModel } from "./types.js";
2
+ export declare const L: {
3
+ readonly TASK_W: 140;
4
+ readonly TASK_H: 64;
5
+ readonly GW: 48;
6
+ readonly EVT_R: 18;
7
+ readonly COL_W: 186;
8
+ readonly GAP_X: 46;
9
+ readonly GAP_Y: 20;
10
+ readonly POOL_HEADER_W: 30;
11
+ readonly LANE_HEADER_W: 24;
12
+ readonly POOL_VGAP: 16;
13
+ readonly LANE_MIN_H: 140;
14
+ readonly LANE_CORRIDOR_H: 56;
15
+ readonly EMPTY_POOL_H: 60;
16
+ readonly CANVAS_TOP: 72;
17
+ readonly CANVAS_LEFT_PAD: 20;
18
+ readonly CANVAS_RIGHT_PAD: 40;
19
+ readonly CANVAS_BOTTOM_PAD: 20;
20
+ readonly MIN_CANVAS_W: 1400;
21
+ };
22
+ export declare function layoutModel(m: DiagramModel): LaidOutModel;