@roomi-fields/notebooklm-mcp 1.5.9 → 1.6.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.
@@ -0,0 +1,492 @@
1
+ openapi: 3.1.0
2
+ info:
3
+ title: NotebookLM REST API
4
+ version: 1.5.9
5
+ description: |
6
+ Local HTTP REST API for Google NotebookLM. Citation-backed Q&A, Studio
7
+ content generation (audio, video, infographic, report, presentation,
8
+ data table), notebook library management, multi-account auto-reauth.
9
+
10
+ Companion to the MCP server in the same package
11
+ (`@roomi-fields/notebooklm-mcp`). See
12
+ https://roomi-fields.github.io/notebooklm-mcp/ for the full guide.
13
+ license:
14
+ name: MIT
15
+ url: https://opensource.org/licenses/MIT
16
+ contact:
17
+ name: Romain Peyrichou
18
+ url: https://github.com/roomi-fields/notebooklm-mcp
19
+
20
+ servers:
21
+ - url: http://localhost:3000
22
+ description: Local server (default)
23
+ - url: http://{host}:{port}
24
+ description: Custom host
25
+ variables:
26
+ host:
27
+ default: localhost
28
+ port:
29
+ default: '3000'
30
+
31
+ tags:
32
+ - name: Health
33
+ - name: Auth
34
+ - name: Q&A
35
+ - name: Notebooks
36
+ - name: Sources
37
+ - name: Content
38
+ - name: Sessions
39
+
40
+ paths:
41
+ /health:
42
+ get:
43
+ tags: [Health]
44
+ summary: Server health check
45
+ responses:
46
+ '200':
47
+ description: Server status, uptime, active sessions
48
+ content:
49
+ application/json:
50
+ schema: { $ref: '#/components/schemas/Health' }
51
+
52
+ /setup-auth:
53
+ post:
54
+ tags: [Auth]
55
+ summary: First-time Google authentication (opens visible browser)
56
+ requestBody:
57
+ content:
58
+ application/json:
59
+ schema:
60
+ type: object
61
+ properties:
62
+ show_browser: { type: boolean, default: true }
63
+ account: { type: string }
64
+ responses:
65
+ '200':
66
+ description: Auth completed
67
+ content: { application/json: { schema: { $ref: '#/components/schemas/Result' } } }
68
+
69
+ /de-auth:
70
+ post:
71
+ tags: [Auth]
72
+ summary: Logout, clear all credentials
73
+ responses:
74
+ '200':
75
+ description: Logged out
76
+ content: { application/json: { schema: { $ref: '#/components/schemas/Result' } } }
77
+
78
+ /re-auth:
79
+ post:
80
+ tags: [Auth]
81
+ summary: Re-authenticate or switch account
82
+ requestBody:
83
+ content:
84
+ application/json:
85
+ schema:
86
+ type: object
87
+ properties:
88
+ account: { type: string }
89
+ responses:
90
+ '200':
91
+ description: Re-authenticated
92
+ content: { application/json: { schema: { $ref: '#/components/schemas/Result' } } }
93
+
94
+ /ask:
95
+ post:
96
+ tags: [Q&A]
97
+ summary: Ask a question with citation-backed response
98
+ requestBody:
99
+ required: true
100
+ content:
101
+ application/json:
102
+ schema: { $ref: '#/components/schemas/AskRequest' }
103
+ responses:
104
+ '200':
105
+ description: Answer with citations
106
+ content: { application/json: { schema: { $ref: '#/components/schemas/AskResponse' } } }
107
+ '400':
108
+ description: Missing question
109
+ '500':
110
+ description: Server error
111
+
112
+ /notebooks:
113
+ get:
114
+ tags: [Notebooks]
115
+ summary: List all notebooks in the local library
116
+ responses:
117
+ '200': { description: Notebook list }
118
+ post:
119
+ tags: [Notebooks]
120
+ summary: Manually add a notebook to the library
121
+ requestBody:
122
+ content:
123
+ application/json:
124
+ schema:
125
+ type: object
126
+ required: [id, name, url]
127
+ properties:
128
+ id: { type: string }
129
+ name: { type: string }
130
+ url: { type: string }
131
+ description: { type: string }
132
+ topics: { type: array, items: { type: string } }
133
+ responses:
134
+ '200': { description: Notebook added }
135
+
136
+ /notebooks/scrape:
137
+ get:
138
+ tags: [Notebooks]
139
+ summary: Scrape all notebooks from NotebookLM UI
140
+ responses:
141
+ '200': { description: Live notebook list with ids and names }
142
+
143
+ /notebooks/import-from-scrape:
144
+ post:
145
+ tags: [Notebooks]
146
+ summary: Bulk import scraped notebooks into local library
147
+ responses:
148
+ '200': { description: Import summary }
149
+
150
+ /notebooks/auto-discover:
151
+ post:
152
+ tags: [Notebooks]
153
+ summary: Generate notebook metadata autonomously via NotebookLM queries
154
+ requestBody:
155
+ content:
156
+ application/json:
157
+ schema:
158
+ type: object
159
+ properties:
160
+ notebook_id: { type: string }
161
+ notebook_url: { type: string }
162
+ responses:
163
+ '200': { description: Generated metadata }
164
+
165
+ /notebooks/search:
166
+ get:
167
+ tags: [Notebooks]
168
+ summary: Search the local library by keyword
169
+ parameters:
170
+ - in: query
171
+ name: q
172
+ required: true
173
+ schema: { type: string }
174
+ responses:
175
+ '200': { description: Matching notebooks }
176
+
177
+ /notebooks/stats:
178
+ get:
179
+ tags: [Notebooks]
180
+ summary: Library statistics
181
+ responses:
182
+ '200': { description: Counts and aggregates }
183
+
184
+ /notebooks/bulk-delete:
185
+ delete:
186
+ tags: [Notebooks]
187
+ summary: Delete multiple notebooks at once
188
+ requestBody:
189
+ content:
190
+ application/json:
191
+ schema:
192
+ type: object
193
+ properties:
194
+ ids: { type: array, items: { type: string } }
195
+ responses:
196
+ '200': { description: Deletion summary }
197
+
198
+ /notebooks/{id}:
199
+ get:
200
+ tags: [Notebooks]
201
+ summary: Get notebook details
202
+ parameters:
203
+ - in: path
204
+ name: id
205
+ required: true
206
+ schema: { type: string }
207
+ responses:
208
+ '200': { description: Notebook details }
209
+ put:
210
+ tags: [Notebooks]
211
+ summary: Update notebook metadata
212
+ parameters:
213
+ - in: path
214
+ name: id
215
+ required: true
216
+ schema: { type: string }
217
+ responses:
218
+ '200': { description: Updated }
219
+ delete:
220
+ tags: [Notebooks]
221
+ summary: Delete a notebook from the library
222
+ parameters:
223
+ - in: path
224
+ name: id
225
+ required: true
226
+ schema: { type: string }
227
+ responses:
228
+ '200': { description: Deleted }
229
+
230
+ /notebooks/{id}/activate:
231
+ put:
232
+ tags: [Notebooks]
233
+ summary: Set notebook as the default active notebook
234
+ parameters:
235
+ - in: path
236
+ name: id
237
+ required: true
238
+ schema: { type: string }
239
+ responses:
240
+ '200': { description: Activated }
241
+
242
+ /notebooks/create:
243
+ post:
244
+ tags: [Notebooks]
245
+ summary: Create a new notebook in NotebookLM
246
+ requestBody:
247
+ content:
248
+ application/json:
249
+ schema:
250
+ type: object
251
+ properties:
252
+ name: { type: string }
253
+ description: { type: string }
254
+ responses:
255
+ '200': { description: Created notebook }
256
+
257
+ /sessions:
258
+ get:
259
+ tags: [Sessions]
260
+ summary: List active sessions
261
+ responses:
262
+ '200': { description: Active sessions }
263
+
264
+ /sessions/{id}:
265
+ delete:
266
+ tags: [Sessions]
267
+ summary: Close a session
268
+ parameters:
269
+ - in: path
270
+ name: id
271
+ required: true
272
+ schema: { type: string }
273
+ responses:
274
+ '200': { description: Closed }
275
+
276
+ /sessions/{id}/reset:
277
+ post:
278
+ tags: [Sessions]
279
+ summary: Reset a session's history
280
+ parameters:
281
+ - in: path
282
+ name: id
283
+ required: true
284
+ schema: { type: string }
285
+ responses:
286
+ '200': { description: Reset }
287
+
288
+ /content/sources:
289
+ post:
290
+ tags: [Sources]
291
+ summary: Add a source (file, url, text, youtube, drive) to the active notebook
292
+ requestBody:
293
+ required: true
294
+ content:
295
+ application/json:
296
+ schema: { $ref: '#/components/schemas/AddSourceRequest' }
297
+ responses:
298
+ '200': { description: Source added }
299
+ delete:
300
+ tags: [Sources]
301
+ summary: Delete source by name (query parameter)
302
+ parameters:
303
+ - in: query
304
+ name: name
305
+ required: true
306
+ schema: { type: string }
307
+ responses:
308
+ '200': { description: Deleted }
309
+
310
+ /content/sources/{id}:
311
+ delete:
312
+ tags: [Sources]
313
+ summary: Delete source by id
314
+ parameters:
315
+ - in: path
316
+ name: id
317
+ required: true
318
+ schema: { type: string }
319
+ responses:
320
+ '200': { description: Deleted }
321
+
322
+ /content/generate:
323
+ post:
324
+ tags: [Content]
325
+ summary: Generate Studio content (audio, video, infographic, report, presentation, data table)
326
+ requestBody:
327
+ required: true
328
+ content:
329
+ application/json:
330
+ schema: { $ref: '#/components/schemas/GenerateRequest' }
331
+ responses:
332
+ '200': { description: Generated artifact metadata }
333
+
334
+ /content/download:
335
+ get:
336
+ tags: [Content]
337
+ summary: Download a generated artifact (WAV, MP4, PNG)
338
+ parameters:
339
+ - in: query
340
+ name: content_id
341
+ required: true
342
+ schema: { type: string }
343
+ responses:
344
+ '200': { description: Binary file }
345
+
346
+ /content:
347
+ get:
348
+ tags: [Content]
349
+ summary: List sources and generated content for the active notebook
350
+ responses:
351
+ '200': { description: Content list }
352
+
353
+ /content/notes:
354
+ post:
355
+ tags: [Content]
356
+ summary: Create a note in the active notebook
357
+ responses:
358
+ '200': { description: Note created }
359
+
360
+ /content/chat-to-note:
361
+ post:
362
+ tags: [Content]
363
+ summary: Save the current chat discussion as a note
364
+ responses:
365
+ '200': { description: Note saved }
366
+
367
+ /content/notes/{noteTitle}/to-source:
368
+ post:
369
+ tags: [Content]
370
+ summary: Convert a note into a NotebookLM source
371
+ parameters:
372
+ - in: path
373
+ name: noteTitle
374
+ required: true
375
+ schema: { type: string }
376
+ responses:
377
+ '200': { description: Note converted to source }
378
+
379
+ /cleanup-data:
380
+ post:
381
+ tags: [Auth]
382
+ summary: Wipe all local data (requires confirmation)
383
+ requestBody:
384
+ content:
385
+ application/json:
386
+ schema:
387
+ type: object
388
+ properties:
389
+ confirm: { type: boolean }
390
+ responses:
391
+ '200': { description: Cleaned }
392
+
393
+ components:
394
+ schemas:
395
+ Health:
396
+ type: object
397
+ properties:
398
+ status: { type: string, example: ok }
399
+ uptime_s: { type: number }
400
+ active_sessions: { type: integer }
401
+ version: { type: string, example: 1.5.9 }
402
+
403
+ Result:
404
+ type: object
405
+ properties:
406
+ success: { type: boolean }
407
+ message: { type: string }
408
+ error: { type: string }
409
+
410
+ AskRequest:
411
+ type: object
412
+ required: [question]
413
+ properties:
414
+ question:
415
+ type: string
416
+ example: 'Summarize chapter 3'
417
+ notebook_id:
418
+ type: string
419
+ notebook_url:
420
+ type: string
421
+ session_id:
422
+ type: string
423
+ source_format:
424
+ type: string
425
+ enum: [none, inline, footnotes, json, expanded]
426
+ default: json
427
+ show_browser:
428
+ type: boolean
429
+ default: false
430
+
431
+ AskResponse:
432
+ type: object
433
+ properties:
434
+ success: { type: boolean }
435
+ answer: { type: string }
436
+ citations:
437
+ type: array
438
+ items:
439
+ type: object
440
+ properties:
441
+ id: { type: integer }
442
+ source: { type: string }
443
+ excerpt: { type: string }
444
+ session_id: { type: string }
445
+
446
+ AddSourceRequest:
447
+ type: object
448
+ required: [source_type]
449
+ properties:
450
+ source_type:
451
+ type: string
452
+ enum: [file, url, text, youtube, drive]
453
+ file_path: { type: string }
454
+ url: { type: string }
455
+ text: { type: string }
456
+ title: { type: string }
457
+ notebook_url: { type: string }
458
+ session_id: { type: string }
459
+
460
+ GenerateRequest:
461
+ type: object
462
+ required: [content_type]
463
+ properties:
464
+ content_type:
465
+ type: string
466
+ enum:
467
+ - audio_overview
468
+ - video
469
+ - infographic
470
+ - report
471
+ - presentation
472
+ - data_table
473
+ language: { type: string, example: en }
474
+ custom_instructions: { type: string }
475
+ video_style:
476
+ type: string
477
+ enum: [classroom, documentary, animated, corporate, cinematic, minimalist]
478
+ video_format:
479
+ type: string
480
+ enum: [brief, explainer]
481
+ infographic_format:
482
+ type: string
483
+ enum: [horizontal, vertical]
484
+ report_format:
485
+ type: string
486
+ enum: [summary, detailed]
487
+ presentation_style:
488
+ type: string
489
+ enum: [overview, detailed]
490
+ presentation_length:
491
+ type: string
492
+ enum: [short, medium, long]
@@ -1 +1 @@
1
- {"version":3,"file":"http-wrapper.d.ts","sourceRoot":"","sources":["../src/http-wrapper.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAeH,OAAO,CAAC,MAAM,CAAC;IAEb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,SAAS,EAAE,MAAM,CAAC;SACnB;KACF;CACF"}
1
+ {"version":3,"file":"http-wrapper.d.ts","sourceRoot":"","sources":["../src/http-wrapper.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAwBH,OAAO,CAAC,MAAM,CAAC;IAEb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,SAAS,EAAE,MAAM,CAAC;SACnB;KACF;CACF"}
@@ -8,6 +8,8 @@ import express from 'express';
8
8
  import { randomUUID } from 'crypto';
9
9
  import net from 'net';
10
10
  import { execSync } from 'child_process';
11
+ import { promises as fs } from 'fs';
12
+ import path from 'path';
11
13
  import { AuthManager } from './auth/auth-manager.js';
12
14
  import { SessionManager } from './session/session-manager.js';
13
15
  import { NotebookLibrary } from './library/notebook-library.js';
@@ -15,6 +17,7 @@ import { ToolHandlers } from './tools/index.js';
15
17
  import { AutoDiscovery } from './auto-discovery/auto-discovery.js';
16
18
  import { StartupManager } from './startup/startup-manager.js';
17
19
  import { log } from './utils/logger.js';
20
+ import { formatAnswerJson, formatAnswerMarkdown, makeSlug, } from './utils/vault-writer.js';
18
21
  const app = express();
19
22
  app.use(express.json({ limit: '10mb' }));
20
23
  // Request ID middleware for debugging and log correlation
@@ -48,6 +51,7 @@ app.get('/', (_req, res) => {
48
51
  endpoints: {
49
52
  health: 'GET /health',
50
53
  ask: 'POST /ask',
54
+ batch_to_vault: 'POST /batch-to-vault',
51
55
  setup_auth: 'POST /setup-auth',
52
56
  notebooks: 'GET /notebooks',
53
57
  sessions: 'GET /sessions',
@@ -94,6 +98,117 @@ app.post('/ask', async (req, res) => {
94
98
  });
95
99
  }
96
100
  });
101
+ // Batch-to-vault — run a list of questions and persist each answer as
102
+ // markdown + JSON sidecar files, ready for ingestion by RTFM or any
103
+ // markdown vault indexer. Conforms to the nblm-answer-v1 schema.
104
+ app.post('/batch-to-vault', async (req, res) => {
105
+ const reqId = req.requestId.substring(0, 8);
106
+ try {
107
+ const { questions, notebook_id, notebook_url, vault_dir, slug_prefix = '', source_format = 'json', sleep_between_ms = 0, session_id, } = req.body;
108
+ if (!Array.isArray(questions) || questions.length === 0) {
109
+ return res.status(400).json({
110
+ success: false,
111
+ error: 'Missing or empty required field: questions (non-empty string array)',
112
+ });
113
+ }
114
+ if (!vault_dir || typeof vault_dir !== 'string') {
115
+ return res.status(400).json({
116
+ success: false,
117
+ error: 'Missing required field: vault_dir (absolute or relative directory path)',
118
+ });
119
+ }
120
+ const absVaultDir = path.resolve(vault_dir);
121
+ await fs.mkdir(absVaultDir, { recursive: true });
122
+ log.info(`[${reqId}] /batch-to-vault — ${questions.length} questions → ${absVaultDir}`);
123
+ const results = [];
124
+ let currentSession = session_id;
125
+ const notebookMeta = {};
126
+ for (let i = 0; i < questions.length; i++) {
127
+ const q = questions[i];
128
+ log.info(`[${reqId}] [${i + 1}/${questions.length}] ${String(q).substring(0, 80)}`);
129
+ try {
130
+ const askResult = await toolHandlers.handleAskQuestion({
131
+ question: q,
132
+ session_id: currentSession,
133
+ notebook_id,
134
+ notebook_url,
135
+ source_format,
136
+ }, async () => { });
137
+ if (!askResult?.success || !askResult.data || askResult.data.status !== 'success') {
138
+ const errMsg = askResult?.error ||
139
+ (askResult?.data && 'error' in askResult.data ? askResult.data.error : 'Unknown error');
140
+ results.push({
141
+ question: q,
142
+ md_path: '',
143
+ json_path: '',
144
+ success: false,
145
+ citations_count: 0,
146
+ error: errMsg,
147
+ });
148
+ continue;
149
+ }
150
+ const data = askResult.data;
151
+ if (data.session_id)
152
+ currentSession = data.session_id;
153
+ if (!notebookMeta.url && data.notebook_url)
154
+ notebookMeta.url = data.notebook_url;
155
+ if (!notebookMeta.id && notebook_id)
156
+ notebookMeta.id = notebook_id;
157
+ const askedAt = new Date().toISOString();
158
+ const slug = makeSlug(q, slug_prefix, i);
159
+ const mdPath = path.join(absVaultDir, `${slug}.md`);
160
+ const jsonPath = path.join(absVaultDir, `${slug}.json`);
161
+ const markdown = formatAnswerMarkdown(data, notebookMeta, askedAt);
162
+ const jsonPayload = formatAnswerJson(data, notebookMeta, askedAt);
163
+ await fs.writeFile(mdPath, markdown, 'utf-8');
164
+ await fs.writeFile(jsonPath, JSON.stringify(jsonPayload, null, 2), 'utf-8');
165
+ results.push({
166
+ question: q,
167
+ md_path: mdPath,
168
+ json_path: jsonPath,
169
+ success: true,
170
+ citations_count: data.sources?.citations.length ?? 0,
171
+ });
172
+ }
173
+ catch (err) {
174
+ const msg = err instanceof Error ? err.message : String(err);
175
+ log.error(`[${reqId}] [${i + 1}] failed: ${msg}`);
176
+ results.push({
177
+ question: q,
178
+ md_path: '',
179
+ json_path: '',
180
+ success: false,
181
+ citations_count: 0,
182
+ error: msg,
183
+ });
184
+ }
185
+ if (sleep_between_ms > 0 && i < questions.length - 1) {
186
+ await new Promise((r) => setTimeout(r, sleep_between_ms));
187
+ }
188
+ }
189
+ const succeeded = results.filter((r) => r.success).length;
190
+ log.success(`[${reqId}] /batch-to-vault — ${succeeded}/${questions.length} written to ${absVaultDir}`);
191
+ res.json({
192
+ success: true,
193
+ data: {
194
+ vault_dir: absVaultDir,
195
+ total: questions.length,
196
+ succeeded,
197
+ failed: questions.length - succeeded,
198
+ session_id: currentSession,
199
+ notebook: notebookMeta,
200
+ files: results,
201
+ },
202
+ });
203
+ }
204
+ catch (error) {
205
+ log.error(`[${reqId}] /batch-to-vault - Error: ${error instanceof Error ? error.message : String(error)}`);
206
+ res.status(500).json({
207
+ success: false,
208
+ error: error instanceof Error ? error.message : String(error),
209
+ });
210
+ }
211
+ });
97
212
  // Setup auth
98
213
  app.post('/setup-auth', async (req, res) => {
99
214
  try {