daleel-mcp 1.0.0 → 1.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/db.d.ts CHANGED
@@ -15,6 +15,7 @@ declare class DaleelDiscoveryEngine {
15
15
  constructor();
16
16
  private detectAppDataDir;
17
17
  private initAppDb;
18
+ getAppDb(): any;
18
19
  getAppDataPath(): string | null;
19
20
  getInstalledPackages(): InstalledPackage[];
20
21
  openPackageDb(localFileName: string): any | null;
package/dist/db.js CHANGED
@@ -13,12 +13,10 @@ class DaleelDiscoveryEngine {
13
13
  this.initAppDb();
14
14
  }
15
15
  detectAppDataDir() {
16
- // 1. Explicit environment variable
17
16
  if (process.env.DALEEL_APP_DATA_DIR && fs.existsSync(process.env.DALEEL_APP_DATA_DIR)) {
18
17
  this.appDataDir = process.env.DALEEL_APP_DATA_DIR;
19
18
  return;
20
19
  }
21
- // 2. Platform default paths for Daleel Scholar (Tauri identifier: com.daleel.scholar)
22
20
  const platform = os.platform();
23
21
  let candidate = "";
24
22
  if (platform === "win32") {
@@ -34,13 +32,6 @@ class DaleelDiscoveryEngine {
34
32
  if (fs.existsSync(candidate)) {
35
33
  this.appDataDir = candidate;
36
34
  }
37
- else {
38
- // Fallback check in case running inside the repo
39
- const devPath = path.resolve(process.cwd(), "../../content-source/ur");
40
- if (fs.existsSync(devPath)) {
41
- this.appDataDir = devPath;
42
- }
43
- }
44
35
  }
45
36
  initAppDb() {
46
37
  if (!this.appDataDir)
@@ -48,7 +39,8 @@ class DaleelDiscoveryEngine {
48
39
  const appDbPath = path.join(this.appDataDir, "app.db");
49
40
  if (fs.existsSync(appDbPath)) {
50
41
  try {
51
- this.appDb = new DatabaseSync(appDbPath, { readOnly: true });
42
+ // Open with read-write so MCP can sync timeline markers to Scholar App
43
+ this.appDb = new DatabaseSync(appDbPath);
52
44
  }
53
45
  catch (err) {
54
46
  console.error("Failed to open app.db:", err);
@@ -64,6 +56,9 @@ class DaleelDiscoveryEngine {
64
56
  }
65
57
  }
66
58
  }
59
+ getAppDb() {
60
+ return this.appDb;
61
+ }
67
62
  getAppDataPath() {
68
63
  return this.appDataDir;
69
64
  }
package/dist/index.js CHANGED
@@ -2,11 +2,11 @@
2
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
- import { listInstalledBooks, searchInBook, searchAllInstalledBooks, getBookPage, } from "./services.js";
5
+ import { listInstalledBooks, searchInBook, getBookPage, verifyCitationAgainstDb, syncTimelineToScholarApp, getAppTimelineMarkers, validateScholarVideoAccess, getCurrentScholarProject, } from "./services.js";
6
6
  import { discoveryEngine } from "./db.js";
7
7
  const server = new Server({
8
- name: "daleel-app-mcp",
9
- version: "2.0.0",
8
+ name: "daleel-scholar-mcp",
9
+ version: "2.2.0",
10
10
  }, {
11
11
  capabilities: {
12
12
  tools: {},
@@ -16,68 +16,126 @@ const server = new Server({
16
16
  server.setRequestHandler(ListToolsRequestSchema, async () => {
17
17
  return {
18
18
  tools: [
19
+ {
20
+ name: "get_current_scholar_project",
21
+ description: "Checks which YouTube video is currently active or recently opened in the user's Daleel Scholar Desktop (Tauri) App. Use this first to auto-detect what the scholar is working on.",
22
+ inputSchema: {
23
+ type: "object",
24
+ properties: {},
25
+ },
26
+ },
27
+ {
28
+ name: "validate_scholar_video_access",
29
+ description: "SECURITY GATEKEEPER: Verifies whether a given YouTube video ID exists in the scholar's authorized Daleel workspace. Rejects unknown or unauthorized videos.",
30
+ inputSchema: {
31
+ type: "object",
32
+ properties: {
33
+ youtube_video_id: {
34
+ type: "string",
35
+ description: "YouTube 11-char Video ID (e.g. dQw4w9WgXcQ)",
36
+ },
37
+ },
38
+ required: ["youtube_video_id"],
39
+ },
40
+ },
19
41
  {
20
42
  name: "list_installed_books",
21
- description: "List all Islamic books, Hadith collections, and Quran translations currently downloaded and installed in the user's Daleel Scholar App.",
43
+ description: "List all Islamic books, Hadith collections, and Tafseer works currently downloaded in Daleel Scholar App.",
22
44
  inputSchema: {
23
45
  type: "object",
24
46
  properties: {},
25
47
  },
26
48
  },
27
49
  {
28
- name: "search_in_book",
29
- description: "Search for specific text or keywords inside a specific downloaded book in Daleel App.",
50
+ name: "verify_citation_against_db",
51
+ description: "STRICT AUTHENTICITY: Verifies a religious claim or reference recited in a video by strictly checking Daleel SQLite database. Returns verified verbatim text or reports unverified.",
30
52
  inputSchema: {
31
53
  type: "object",
32
54
  properties: {
33
55
  book_identifier: {
34
56
  type: "string",
35
- description: "Book ID, title, or filename (e.g. 'احکام و مسائل' or 'book-ahkaam-ahkam-wa-masail-jild-1')",
57
+ description: "Book title or ID (e.g. 'احکام و مسائل' or 'fiqh')",
36
58
  },
37
- query: {
59
+ search_phrase: {
38
60
  type: "string",
39
- description: "Text or keyword in Urdu/Arabic to search",
61
+ description: "Specific Arabic or Urdu words mentioned in video",
40
62
  },
41
- limit: {
63
+ timestamp_seconds: {
42
64
  type: "number",
43
- description: "Max pages to return (default: 5)",
65
+ description: "Exact second timestamp in video",
66
+ },
67
+ speaker_claim: {
68
+ type: "string",
69
+ description: "What the speaker claimed in the video",
44
70
  },
45
71
  },
46
- required: ["book_identifier", "query"],
72
+ required: ["book_identifier", "search_phrase", "timestamp_seconds", "speaker_claim"],
47
73
  },
48
74
  },
49
75
  {
50
- name: "search_all_installed_books",
51
- description: "Search across all downloaded books in Daleel App simultaneously for a topic, keyword, or citation.",
76
+ name: "sync_timeline_to_scholar_app",
77
+ description: "Pushes verified citations and timestamps directly into Daleel Scholar Desktop (Tauri) App database so the user sees markers on the video timeline editor visually. Requires video to be registered in Scholar workspace.",
52
78
  inputSchema: {
53
79
  type: "object",
54
80
  properties: {
55
- query: {
81
+ youtube_video_id: {
56
82
  type: "string",
57
- description: "Search keyword or phrase in Urdu/Arabic",
83
+ description: "YouTube 11-char Video ID",
58
84
  },
59
- limit: {
60
- type: "number",
61
- description: "Max results (default: 5)",
85
+ video_title: {
86
+ type: "string",
87
+ description: "Title of the YouTube video",
62
88
  },
89
+ markers: {
90
+ type: "array",
91
+ description: "List of verified markers to place on the timeline",
92
+ items: {
93
+ type: "object",
94
+ properties: {
95
+ timestampSeconds: { type: "number" },
96
+ referenceTitle: { type: "string" },
97
+ exactText: { type: "string", description: "Verbatim text retrieved from Daleel DB" },
98
+ pageOrHadithNo: { type: "string" },
99
+ },
100
+ required: ["timestampSeconds", "referenceTitle", "exactText"],
101
+ },
102
+ },
103
+ },
104
+ required: ["youtube_video_id", "video_title", "markers"],
105
+ },
106
+ },
107
+ {
108
+ name: "get_video_timeline_markers",
109
+ description: "Fetch existing timeline markers saved in Daleel Scholar App for a given video.",
110
+ inputSchema: {
111
+ type: "object",
112
+ properties: {
113
+ youtube_video_id: { type: "string" },
114
+ },
115
+ required: ["youtube_video_id"],
116
+ },
117
+ },
118
+ {
119
+ name: "search_in_book",
120
+ description: "Search for specific text or keywords inside a downloaded book in Daleel App.",
121
+ inputSchema: {
122
+ type: "object",
123
+ properties: {
124
+ book_identifier: { type: "string" },
125
+ query: { type: "string" },
126
+ limit: { type: "number" },
63
127
  },
64
- required: ["query"],
128
+ required: ["book_identifier", "query"],
65
129
  },
66
130
  },
67
131
  {
68
132
  name: "get_book_page",
69
- description: "Fetch the exact page content and footnotes from a specific downloaded book by page number.",
133
+ description: "Fetch the exact page content and footnotes from a book by page number.",
70
134
  inputSchema: {
71
135
  type: "object",
72
136
  properties: {
73
- book_identifier: {
74
- type: "string",
75
- description: "Book ID, title, or filename",
76
- },
77
- page_number: {
78
- type: "number",
79
- description: "Exact page number (ID) to retrieve",
80
- },
137
+ book_identifier: { type: "string" },
138
+ page_number: { type: "number" },
81
139
  },
82
140
  required: ["book_identifier", "page_number"],
83
141
  },
@@ -90,6 +148,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
90
148
  const { name, arguments: args } = request.params;
91
149
  try {
92
150
  switch (name) {
151
+ case "get_current_scholar_project": {
152
+ const project = getCurrentScholarProject();
153
+ return {
154
+ content: [
155
+ {
156
+ type: "text",
157
+ text: project
158
+ ? JSON.stringify(project, null, 2)
159
+ : JSON.stringify({ isRegisteredInScholarApp: false, message: "No active video found in Daleel Scholar App. Please open a video in the desktop app first." }),
160
+ },
161
+ ],
162
+ };
163
+ }
164
+ case "validate_scholar_video_access": {
165
+ const { youtube_video_id } = args;
166
+ const validation = validateScholarVideoAccess(youtube_video_id);
167
+ return {
168
+ content: [{ type: "text", text: JSON.stringify(validation, null, 2) }],
169
+ };
170
+ }
93
171
  case "list_installed_books": {
94
172
  const books = listInstalledBooks();
95
173
  const appPath = discoveryEngine.getAppDataPath();
@@ -110,38 +188,33 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
110
188
  const { book_identifier, query, limit } = args;
111
189
  const results = searchInBook(book_identifier, query, limit || 5);
112
190
  return {
113
- content: [
114
- {
115
- type: "text",
116
- text: results.length
117
- ? JSON.stringify(results, null, 2)
118
- : `No occurrences of "${query}" found in ${book_identifier}.`,
119
- },
120
- ],
191
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
121
192
  };
122
193
  }
123
- case "search_all_installed_books": {
124
- const { query, limit } = args;
125
- const results = searchAllInstalledBooks(query, limit || 5);
194
+ case "verify_citation_against_db": {
195
+ const { book_identifier, search_phrase, timestamp_seconds, speaker_claim } = args;
196
+ const result = verifyCitationAgainstDb(book_identifier, search_phrase, timestamp_seconds, speaker_claim);
126
197
  return {
127
- content: [
128
- {
129
- type: "text",
130
- text: results.length
131
- ? JSON.stringify(results, null, 2)
132
- : `No results found for "${query}" across downloaded books.`,
133
- },
134
- ],
198
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
199
+ };
200
+ }
201
+ case "sync_timeline_to_scholar_app": {
202
+ const { youtube_video_id, video_title, markers } = args;
203
+ const res = syncTimelineToScholarApp(youtube_video_id, video_title, markers);
204
+ return {
205
+ content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
206
+ };
207
+ }
208
+ case "get_video_timeline_markers": {
209
+ const { youtube_video_id } = args;
210
+ const res = getAppTimelineMarkers(youtube_video_id);
211
+ return {
212
+ content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
135
213
  };
136
214
  }
137
215
  case "get_book_page": {
138
216
  const { book_identifier, page_number } = args;
139
217
  const res = getBookPage(book_identifier, page_number);
140
- if (!res) {
141
- return {
142
- content: [{ type: "text", text: `Page ${page_number} not found in ${book_identifier}.` }],
143
- };
144
- }
145
218
  return {
146
219
  content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
147
220
  };
@@ -160,7 +233,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
160
233
  async function main() {
161
234
  const transport = new StdioServerTransport();
162
235
  await server.connect(transport);
163
- console.error("Daleel Zero-Data MCP Server running on stdio");
236
+ console.error("Daleel Scholar Tauri-MCP Sync Server running on stdio");
164
237
  }
165
238
  main().catch((err) => {
166
239
  console.error("Fatal error:", err);
@@ -10,3 +10,37 @@ export declare function listInstalledBooks(): InstalledPackage[];
10
10
  export declare function searchInBook(packageIdOrFile: string, query: string, limit?: number): BookSearchResult[];
11
11
  export declare function searchAllInstalledBooks(query: string, limit?: number): BookSearchResult[];
12
12
  export declare function getBookPage(packageIdOrFile: string, pageNumber: number): any;
13
+ export interface ScholarProjectValidation {
14
+ isRegisteredInScholarApp: boolean;
15
+ youtubeVideoId: string;
16
+ videoTitle?: string;
17
+ durationSeconds?: number;
18
+ existingMarkersCount: number;
19
+ message: string;
20
+ }
21
+ export declare function validateScholarVideoAccess(youtubeVideoId: string): ScholarProjectValidation;
22
+ export declare function getCurrentScholarProject(): ScholarProjectValidation | null;
23
+ export interface VerifiedCitationResult {
24
+ isVerified: boolean;
25
+ bookTitle?: string;
26
+ pageNumber?: number;
27
+ exactDbText?: string;
28
+ footnote?: string;
29
+ timestampSeconds: number;
30
+ speakerClaim: string;
31
+ verificationStatus: "AUTHENTIC_MATCH_FOUND" | "NO_EXACT_MATCH_IN_LOCAL_DB";
32
+ }
33
+ export declare function verifyCitationAgainstDb(bookIdentifier: string, searchPhrase: string, timestampSeconds: number, speakerClaim: string): VerifiedCitationResult;
34
+ export declare function syncTimelineToScholarApp(youtubeVideoId: string, videoTitle: string, markers: Array<{
35
+ timestampSeconds: number;
36
+ referenceTitle: string;
37
+ exactText: string;
38
+ pageOrHadithNo?: string | number;
39
+ }>): {
40
+ success: boolean;
41
+ youtubeVideoId: string;
42
+ videoTitle: string;
43
+ markersAdded: number;
44
+ message: string;
45
+ };
46
+ export declare function getAppTimelineMarkers(youtubeVideoId: string): any;
package/dist/services.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { discoveryEngine } from "./db.js";
2
+ import crypto from "crypto";
2
3
  export function listInstalledBooks() {
3
4
  return discoveryEngine.getInstalledPackages();
4
5
  }
@@ -85,3 +86,139 @@ export function getBookPage(packageIdOrFile, pageNumber) {
85
86
  footnote: row.fnotes,
86
87
  };
87
88
  }
89
+ export function validateScholarVideoAccess(youtubeVideoId) {
90
+ const appDb = discoveryEngine.getAppDb();
91
+ if (!appDb) {
92
+ throw new Error("Daleel Scholar App database (app.db) not accessible. Please ensure Daleel Scholar App is installed.");
93
+ }
94
+ const draftStmt = appDb.prepare(`
95
+ SELECT youtube_video_id, video_title, duration_seconds
96
+ FROM timeline_drafts
97
+ WHERE youtube_video_id = ?
98
+ LIMIT 1
99
+ `);
100
+ const draft = draftStmt.get(youtubeVideoId);
101
+ if (!draft) {
102
+ return {
103
+ isRegisteredInScholarApp: false,
104
+ youtubeVideoId,
105
+ existingMarkersCount: 0,
106
+ message: `Access Denied: Video "${youtubeVideoId}" is not registered in your Daleel Scholar App. Please open or link this video inside Daleel Scholar App first before asking AI to verify citations.`,
107
+ };
108
+ }
109
+ const countStmt = appDb.prepare(`
110
+ SELECT COUNT(*) as count
111
+ FROM timeline_markers
112
+ WHERE youtube_video_id = ?
113
+ `);
114
+ const countRow = countStmt.get(youtubeVideoId);
115
+ return {
116
+ isRegisteredInScholarApp: true,
117
+ youtubeVideoId: draft.youtube_video_id,
118
+ videoTitle: draft.video_title,
119
+ durationSeconds: draft.duration_seconds,
120
+ existingMarkersCount: countRow?.count ?? 0,
121
+ message: `Access Verified: Video "${draft.video_title}" is active in Daleel Scholar workspace.`,
122
+ };
123
+ }
124
+ export function getCurrentScholarProject() {
125
+ const appDb = discoveryEngine.getAppDb();
126
+ if (!appDb)
127
+ return null;
128
+ try {
129
+ const stmt = appDb.prepare(`
130
+ SELECT youtube_video_id, video_title, duration_seconds
131
+ FROM timeline_drafts
132
+ ORDER BY updated_at DESC
133
+ LIMIT 1
134
+ `);
135
+ const row = stmt.get();
136
+ if (!row)
137
+ return null;
138
+ const countStmt = appDb.prepare(`
139
+ SELECT COUNT(*) as count FROM timeline_markers WHERE youtube_video_id = ?
140
+ `);
141
+ const countRow = countStmt.get(row.youtube_video_id);
142
+ return {
143
+ isRegisteredInScholarApp: true,
144
+ youtubeVideoId: row.youtube_video_id,
145
+ videoTitle: row.video_title,
146
+ durationSeconds: row.duration_seconds,
147
+ existingMarkersCount: countRow?.count ?? 0,
148
+ message: `Active Video in Scholar App: "${row.video_title}"`,
149
+ };
150
+ }
151
+ catch {
152
+ return null;
153
+ }
154
+ }
155
+ export function verifyCitationAgainstDb(bookIdentifier, searchPhrase, timestampSeconds, speakerClaim) {
156
+ try {
157
+ const matches = searchInBook(bookIdentifier, searchPhrase, 1);
158
+ if (matches.length > 0) {
159
+ const best = matches[0];
160
+ return {
161
+ isVerified: true,
162
+ bookTitle: best.bookTitle,
163
+ pageNumber: best.pageNumber,
164
+ exactDbText: best.content,
165
+ footnote: best.footnote,
166
+ timestampSeconds,
167
+ speakerClaim,
168
+ verificationStatus: "AUTHENTIC_MATCH_FOUND",
169
+ };
170
+ }
171
+ }
172
+ catch (e) {
173
+ // book not found or search failed
174
+ }
175
+ return {
176
+ isVerified: false,
177
+ timestampSeconds,
178
+ speakerClaim,
179
+ verificationStatus: "NO_EXACT_MATCH_IN_LOCAL_DB",
180
+ };
181
+ }
182
+ export function syncTimelineToScholarApp(youtubeVideoId, videoTitle, markers) {
183
+ // First gatekeeper check: video must be valid
184
+ const validation = validateScholarVideoAccess(youtubeVideoId);
185
+ if (!validation.isRegisteredInScholarApp) {
186
+ throw new Error(validation.message);
187
+ }
188
+ const appDb = discoveryEngine.getAppDb();
189
+ if (!appDb) {
190
+ throw new Error("Daleel Scholar App database (app.db) not accessible.");
191
+ }
192
+ const insertMarker = appDb.prepare(`
193
+ INSERT INTO timeline_markers (id, youtube_video_id, timestamp_seconds, note, created_at, updated_at)
194
+ VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
195
+ `);
196
+ let addedCount = 0;
197
+ for (const m of markers) {
198
+ const markerId = crypto.randomUUID();
199
+ const formattedNote = `[Verified Daleel Citation]\nBook: ${m.referenceTitle}\nRef/Page: ${m.pageOrHadithNo || 'N/A'}\nVerbatim Text:\n"${m.exactText.trim()}"`;
200
+ insertMarker.run(markerId, youtubeVideoId, m.timestampSeconds, formattedNote);
201
+ addedCount++;
202
+ }
203
+ // Update draft timestamp
204
+ appDb.prepare(`UPDATE timeline_drafts SET updated_at = CURRENT_TIMESTAMP WHERE youtube_video_id = ?`).run(youtubeVideoId);
205
+ return {
206
+ success: true,
207
+ youtubeVideoId,
208
+ videoTitle,
209
+ markersAdded: addedCount,
210
+ message: "Timeline markers successfully synchronized with Daleel Scholar Desktop (Tauri).",
211
+ };
212
+ }
213
+ export function getAppTimelineMarkers(youtubeVideoId) {
214
+ const appDb = discoveryEngine.getAppDb();
215
+ if (!appDb)
216
+ return [];
217
+ const stmt = appDb.prepare(`
218
+ SELECT id, youtube_video_id, timestamp_seconds, note, created_at
219
+ FROM timeline_markers
220
+ WHERE youtube_video_id = ?
221
+ ORDER BY timestamp_seconds ASC
222
+ `);
223
+ return stmt.all(youtubeVideoId);
224
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "daleel-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Zero-data Model Context Protocol (MCP) server for Daleel Islamic Scholar App",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",