notebooklm-mcp-server 1.1.9 → 2.0.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/client.d.ts CHANGED
@@ -10,14 +10,24 @@ export declare class NotebookLMClient {
10
10
  private client;
11
11
  private csrfToken;
12
12
  private sessionId;
13
+ private initialized;
13
14
  constructor(cookies: string);
14
15
  /**
15
- * Internal RPC executor with the original, working legacy format.
16
+ * Initialize CSRF token and session ID from the main page.
17
+ * Must be called before any RPC call.
18
+ */
19
+ init(): Promise<void>;
20
+ /**
21
+ * Internal RPC executor using the standard Google batchexecute format.
16
22
  */
17
23
  private callRpc;
18
24
  /**
19
25
  * Parses the weird batchexecute envelope format.
20
26
  */
27
+ /**
28
+ * Parses the Google batchexecute chunked response format.
29
+ * Response format: )]}'\n<bytecount>\n[["wrb.fr","rpcId","<json>",null,...]]\n...
30
+ */
21
31
  private parseBatchResponse;
22
32
  listNotebooks(): Promise<Notebook[]>;
23
33
  createNotebook(title: string): Promise<string>;
@@ -48,7 +58,7 @@ export declare class NotebookLMClient {
48
58
  private parseQueryResponse;
49
59
  private extractFromChunk;
50
60
  /**
51
- * Fetches the CSRF token (at) from the main page if not present.
61
+ * Force re-fetch of CSRF token and session ID.
52
62
  */
53
63
  refreshTokens(): Promise<void>;
54
64
  }
package/dist/client.js CHANGED
@@ -16,6 +16,7 @@ export class NotebookLMClient {
16
16
  client;
17
17
  csrfToken = null;
18
18
  sessionId = null;
19
+ initialized = false;
19
20
  constructor(cookies) {
20
21
  this.client = axios.create({
21
22
  baseURL: BASE_URL,
@@ -27,37 +28,70 @@ export class NotebookLMClient {
27
28
  });
28
29
  }
29
30
  /**
30
- * Internal RPC executor with the original, working legacy format.
31
+ * Initialize CSRF token and session ID from the main page.
32
+ * Must be called before any RPC call.
33
+ */
34
+ async init() {
35
+ if (this.initialized)
36
+ return;
37
+ try {
38
+ const response = await this.client.get('/');
39
+ const csrfMatch = response.data.match(/"SNlM0e"\s*:\s*"([^"]+)"/);
40
+ if (csrfMatch) {
41
+ this.csrfToken = csrfMatch[1];
42
+ }
43
+ const sidMatch = response.data.match(/"FdrFJe"\s*:\s*"([^"]+)"/);
44
+ if (sidMatch) {
45
+ this.sessionId = sidMatch[1];
46
+ }
47
+ this.initialized = true;
48
+ if (!this.csrfToken) {
49
+ console.error('[NotebookLM] Warning: Could not extract CSRF token. Authentication may be expired.');
50
+ }
51
+ }
52
+ catch (e) {
53
+ console.error('[NotebookLM] Failed to initialize session:', e.message);
54
+ }
55
+ }
56
+ /**
57
+ * Internal RPC executor using the standard Google batchexecute format.
31
58
  */
32
59
  async callRpc(rpcId, params, _retryCount = 0) {
33
- // Reverting to the simpler format that proved stable in v1.1.2
34
- const fReq = JSON.stringify([null, JSON.stringify(params)]);
60
+ // Ensure we have CSRF token before making any call
61
+ await this.init();
62
+ // Standard Google batchexecute envelope: [[[rpcId, paramsJson, null, "generic"]]]
63
+ const paramsJson = JSON.stringify(params);
64
+ const fReq = JSON.stringify([[[rpcId, paramsJson, null, "generic"]]]);
35
65
  const body = new URLSearchParams();
36
66
  body.append('f.req', fReq);
37
67
  if (this.csrfToken) {
38
68
  body.append('at', this.csrfToken);
39
69
  }
70
+ const queryParams = {
71
+ 'rpcids': rpcId,
72
+ 'source-path': '/',
73
+ 'bl': 'boq_labs-tailwind-frontend_20260108.06_p0',
74
+ 'hl': 'en',
75
+ '_reqid': Math.floor(Math.random() * 900000 + 100000).toString(),
76
+ 'rt': 'c'
77
+ };
78
+ if (this.sessionId) {
79
+ queryParams['f.sid'] = this.sessionId;
80
+ }
40
81
  try {
41
82
  const response = await this.client.post(BATCH_EXECUTE_PATH, body.toString(), {
42
- params: {
43
- 'rpcids': rpcId,
44
- 'source-path': '/',
45
- 'f.sid': this.sessionId,
46
- 'bl': 'boq_labs-tailwind-frontend_20260108.06_p0',
47
- 'hl': 'en',
48
- '_reqid': Math.floor(Math.random() * 1000000).toString(),
49
- 'rt': 'c'
50
- }
83
+ params: queryParams
51
84
  });
52
- // Special case: Google might return success but the body indicates an internal auth failure
53
- // (usually represented by specific error codes in the response array)
54
- if (typeof response.data === 'string' && response.data.includes('session expired')) {
55
- throw new AuthenticationError('Session expired in response body');
56
- }
57
85
  const rpcResult = this.parseBatchResponse(response.data, rpcId);
58
- if (rpcResult === null && typeof response.data === 'string' && response.data.length > 0 && !response.data.includes(rpcId)) {
59
- // Response received but doesn't contain the expected RPC data - likely an auth/session issue
60
- throw new AuthenticationError('Invalid session or session expired (RPC data not found)');
86
+ if (rpcResult === null) {
87
+ const dataStr = typeof response.data === 'string' ? response.data : '';
88
+ // Check for error envelope from Google
89
+ if (dataStr.includes('"er"')) {
90
+ throw new AuthenticationError('Google returned an error. Session may be expired.');
91
+ }
92
+ if (!dataStr.includes(rpcId)) {
93
+ throw new AuthenticationError('Invalid session or session expired (RPC ID not in response)');
94
+ }
61
95
  }
62
96
  return rpcResult;
63
97
  }
@@ -66,12 +100,13 @@ export class NotebookLMClient {
66
100
  error.response?.status === 401 ||
67
101
  error.response?.status === 403;
68
102
  if (isAuthError && _retryCount < 2) {
69
- console.error(`Auth failure detected. Attempting token recovery (Attempt ${_retryCount + 1})...`);
70
- await this.refreshTokens();
103
+ console.error(`[NotebookLM] Auth failure. Refreshing tokens (attempt ${_retryCount + 1})...`);
104
+ this.initialized = false;
105
+ await this.init();
71
106
  return this.callRpc(rpcId, params, _retryCount + 1);
72
107
  }
73
108
  if (isAuthError) {
74
- throw new AuthenticationError('Authentication failed after retries. Please run notebook-mcp-auth.');
109
+ throw new AuthenticationError('Authentication failed. Please run: notebooklm-mcp-server auth');
75
110
  }
76
111
  throw error;
77
112
  }
@@ -79,24 +114,75 @@ export class NotebookLMClient {
79
114
  /**
80
115
  * Parses the weird batchexecute envelope format.
81
116
  */
117
+ /**
118
+ * Parses the Google batchexecute chunked response format.
119
+ * Response format: )]}'\n<bytecount>\n[["wrb.fr","rpcId","<json>",null,...]]\n...
120
+ */
82
121
  parseBatchResponse(data, rpcId) {
83
- // Google's format is basically a set of chunked JSON arrays
84
- // We need to extract the payload for the given rpcId
85
122
  try {
86
- const dataStr = typeof data === 'string' ? data : JSON.stringify(data);
123
+ let dataStr = typeof data === 'string' ? data : JSON.stringify(data);
124
+ // Strip anti-XSSI prefix
125
+ if (dataStr.startsWith(")]}'\n")) {
126
+ dataStr = dataStr.substring(5);
127
+ }
128
+ else if (dataStr.startsWith(")]}'\r\n")) {
129
+ dataStr = dataStr.substring(6);
130
+ }
131
+ // Parse chunked format: alternating byte_count + json_payload lines
87
132
  const lines = dataStr.split('\n');
88
- for (const line of lines) {
89
- if (line.includes(rpcId)) {
90
- const match = line.match(/\["wobti",\s*"(.*?)",\s*"(.*?)"\]/);
91
- if (match) {
92
- const innerJson = match[2].replace(/\\"/g, '"').replace(/\\\\/g, '\\');
93
- return JSON.parse(innerJson);
133
+ let i = 0;
134
+ while (i < lines.length) {
135
+ const line = lines[i].trim();
136
+ if (!line) {
137
+ i++;
138
+ continue;
139
+ }
140
+ let jsonLine = null;
141
+ // Check if this is a byte count (next line is the JSON)
142
+ if (/^\d+$/.test(line)) {
143
+ i++;
144
+ if (i < lines.length) {
145
+ jsonLine = lines[i].trim();
94
146
  }
95
147
  }
148
+ else {
149
+ jsonLine = line;
150
+ }
151
+ if (jsonLine) {
152
+ try {
153
+ const chunk = JSON.parse(jsonLine);
154
+ // chunk is an array of items like ["wrb.fr", rpcId, data, ...]
155
+ const items = Array.isArray(chunk) && Array.isArray(chunk[0]) ? chunk : [chunk];
156
+ for (const item of items) {
157
+ if (!Array.isArray(item) || item.length < 3)
158
+ continue;
159
+ // Error response
160
+ if (item[0] === 'er' && item[1] === rpcId) {
161
+ throw new Error(`Google RPC error for ${rpcId}: ${JSON.stringify(item[2])}`);
162
+ }
163
+ // Success response
164
+ if (item[0] === 'wrb.fr' && item[1] === rpcId) {
165
+ const resultData = item[2];
166
+ if (typeof resultData === 'string') {
167
+ return JSON.parse(resultData);
168
+ }
169
+ return resultData;
170
+ }
171
+ }
172
+ }
173
+ catch (parseErr) {
174
+ if (parseErr.message?.startsWith('Google RPC error'))
175
+ throw parseErr;
176
+ // Not valid JSON, skip
177
+ }
178
+ }
179
+ i++;
96
180
  }
97
181
  }
98
182
  catch (e) {
99
- console.error('Failed to parse RPC response', e);
183
+ if (e.message?.startsWith('Google RPC error'))
184
+ throw e;
185
+ console.error('[NotebookLM] Failed to parse RPC response:', e.message);
100
186
  }
101
187
  return null;
102
188
  }
@@ -308,6 +394,8 @@ export class NotebookLMClient {
308
394
  * Complex query method with streaming support.
309
395
  */
310
396
  async query(notebookId, queryText, sourceIds, conversationId) {
397
+ // Ensure tokens are available
398
+ await this.init();
311
399
  const cid = conversationId || uuidv4();
312
400
  const sources = sourceIds ? sourceIds.map(id => [[id]]) : [];
313
401
  // Structure matching Python: [sources_array, query_text, history, [2, null, [1]], conversation_id]
@@ -416,13 +504,10 @@ export class NotebookLMClient {
416
504
  return { text: null, isAnswer: false };
417
505
  }
418
506
  /**
419
- * Fetches the CSRF token (at) from the main page if not present.
507
+ * Force re-fetch of CSRF token and session ID.
420
508
  */
421
509
  async refreshTokens() {
422
- const response = await this.client.get('/');
423
- const match = response.data.match(/"SNlM0e":"(.*?)"/);
424
- if (match) {
425
- this.csrfToken = match[1];
426
- }
510
+ this.initialized = false;
511
+ await this.init();
427
512
  }
428
513
  }
@@ -27,6 +27,6 @@ export declare const RPC_IDS: {
27
27
  DELETE_MIND_MAP: string;
28
28
  };
29
29
  export declare const BASE_URL = "https://notebooklm.google.com";
30
- export declare const BATCH_EXECUTE_PATH = "/_/NotebookLMApp/wob/v1/batchexecute";
31
- export declare const QUERY_PATH = "/_/NotebookLMApp/rt/c";
30
+ export declare const BATCH_EXECUTE_PATH = "/_/LabsTailwindUi/data/batchexecute";
31
+ export declare const QUERY_PATH = "/_/LabsTailwindUi/data/batchexecute";
32
32
  export declare const DEFAULT_QUERY_TIMEOUT = 120000;
package/dist/constants.js CHANGED
@@ -27,6 +27,6 @@ export const RPC_IDS = {
27
27
  DELETE_MIND_MAP: "AH0mwd",
28
28
  };
29
29
  export const BASE_URL = "https://notebooklm.google.com";
30
- export const BATCH_EXECUTE_PATH = "/_/NotebookLMApp/wob/v1/batchexecute";
31
- export const QUERY_PATH = "/_/NotebookLMApp/rt/c";
30
+ export const BATCH_EXECUTE_PATH = "/_/LabsTailwindUi/data/batchexecute";
31
+ export const QUERY_PATH = "/_/LabsTailwindUi/data/batchexecute";
32
32
  export const DEFAULT_QUERY_TIMEOUT = 120000; // 120s
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ const program = new Command();
5
5
  program
6
6
  .name('notebooklm-mcp-server')
7
7
  .description('NotebookLM MCP Server (Node.js)')
8
- .version('1.1.9');
8
+ .version('2.0.0');
9
9
  program
10
10
  .command('server')
11
11
  .description('Start the MCP server (default)')
package/dist/server.js CHANGED
@@ -6,7 +6,7 @@ import { AuthManager } from "./auth.js";
6
6
  import chalk from "chalk";
7
7
  const server = new Server({
8
8
  name: "notebooklm-mcp-server",
9
- version: "1.1.9",
9
+ version: "2.0.0",
10
10
  }, {
11
11
  capabilities: {
12
12
  tools: {},
@@ -364,7 +364,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
364
364
  async function main() {
365
365
  const transport = new StdioServerTransport();
366
366
  await server.connect(transport);
367
- console.error("NotebookLM MCP Server v1.1.9 running on stdio");
367
+ console.error("NotebookLM MCP Server v2.0.0 running on stdio");
368
368
  }
369
369
  main().catch((error) => {
370
370
  console.error("Fatal error:", error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "notebooklm-mcp-server",
3
- "version": "1.1.9",
3
+ "version": "2.0.0",
4
4
  "description": "Node.js Model Context Protocol server for Google NotebookLM",
5
5
  "type": "module",
6
6
  "repository": {