Setup and context
Every developer working with large codebases faces a fundamental constraint: context window limitations. You can't fit millions of lines of code into a single prompt, and even if you could, models trained on historical data struggle to understand your project's current architectural decisions and design philosophy.
NotebookLM—Google's sophisticated AI knowledge management platform—solves this problem by serving as a specialized knowledge layer. With its 8x expanded context window, NotebookLM can simultaneously analyze multiple design documents, architecture specifications, and code references. When connected to Antigravity agents through Model Context Protocol (MCP), this becomes a powerhouse for context-aware code generation, intelligent refactoring, and architectural decision support.
This article walks you through building a production-ready system where NotebookLM acts as a persistent knowledge repository and Antigravity's manager surface orchestrates multiple specialized agents that query this knowledge base to make informed decisions about your codebase.
Understanding NotebookLM's 8x Context Window
How the Extended Context Works
Claude's standard API provides approximately 100K tokens of context. NotebookLM extends this to roughly 800K token equivalents through intelligent mechanisms:
1. Document-Level Indexing & Dynamic Retrieval
Rather than treating your codebase as a monolithic blob, NotebookLM:
- Segments your documentation into logical units (files, modules, architectural layers)
- Creates semantic indexes for each segment
- Dynamically retrieves only the most relevant portions for each query
- Maintains reasoning chains across multiple document boundaries
2. Hybrid Search Architecture
NotebookLM combines:
- Lexical search: Fast keyword matching for file names, function signatures
- Semantic search: Vector similarity to find "calls to getUserProfile()" or "components using Suspense pattern"
- Graph-based reasoning: Understanding that FileA.ts imports FileB.ts, which exports a type that impacts FileC.ts
3. Incremental Context Assembly
Queries are processed in layers:
- Query analysis → identify relevant concepts
- Retrieve primary documents (high relevance)
- Fetch secondary context (related patterns, similar implementations)
- Build final context window only from assembled pieces
- Return answer within the expanded window
This is dramatically more efficient than pre-loading everything.
Optimal Document Structure for Codebases
To maximize NotebookLM's effectiveness, structure your knowledge base as:
NotebookLM Codebase Knowledge Base
├─ Architecture & Design
│ ├─ ARCHITECTURE.md (system design, module relationships)
│ ├─ DATA_MODEL.md (schemas, types, entities)
│ ├─ MODULE_DEPENDENCY_GRAPH.md (visual/text format)
│ └─ DESIGN_PATTERNS.md (adopted patterns & their rationales)
├─ Codebase Reference
│ ├─ packages/*/README.md (package purposes & exports)
│ ├─ src/*/PUBLIC_API.md (public interfaces only)
│ └─ [Optional] key entry points (.tsx, .ts)
├─ Operational Knowledge
│ ├─ DEPLOYMENT.md
│ ├─ TESTING_STRATEGY.md
│ ├─ PERFORMANCE_TUNING.md
│ └─ TROUBLESHOOTING.md
└─ Decision Records
├─ ADR/ (Architecture Decision Records)
└─ RFC/ (implemented proposals)
Key principle: Prioritize why over what. Include design rationales, trade-offs documented in ADRs, and public interfaces—but not every internal implementation detail. NotebookLM excels at understanding intent and reasoning, not memorizing 10,000 lines of implementation.
The MCP Bridge: Connecting NotebookLM to Antigravity Agents
NotebookLM as an MCP Server
Model Context Protocol (MCP) standardizes how LLM applications access external tools, databases, and knowledge systems. By implementing NotebookLM as an MCP server, Antigravity agents can query the knowledge base as if it's part of their extended memory—instantly and reliably.
Here's a production-grade implementation:
// mcp/notebooklm-server.ts
import Anthropic from "@anthropic-ai/sdk";
interface NotebookQuery {
notebook_id: string;
query: string;
max_results?: number;
context_depth?: "shallow" | "deep"; // shallow: direct matches, deep: multi-hop relationships
focus_area?: string; // e.g., "performance", "security", "migration"
}
interface ContextResult {
source: string; // filename or section
relevance: number; // 0.0 to 1.0
excerpt: string; // relevant text, ~300-500 chars
related_items: string[]; // other sections this connects to
confidence: number; // how certain we are this answers the query
}
class NotebookLMMCPServer {
private client: Anthropic;
private notebookCache: Map<string, string> = new Map();
private queryCache: Map<string, ContextResult[]> = new Map();
async queryNotebook(req: NotebookQuery): Promise<ContextResult[]> {
// Check cache first (queries are expensive)
const cacheKey = `${req.notebook_id}:${req.query}:${req.context_depth}`;
if (this.queryCache.has(cacheKey)) {
return this.queryCache.get(cacheKey)!;
}
// Fetch the notebook content (pre-exported or via API)
const notebookContent = await this.fetchNotebook(req.notebook_id);
// Use Claude to search within the notebook
const message = await this.client.messages.create({
model: "claude-3-5-sonnet-20241022",
max_tokens: 2000,
system: `You are a specialized knowledge retrieval system for a codebase.
Given a notebook of codebase documentation, extract the most relevant information.
For each result, provide:
- source: exact section name or filename
- relevance: confidence 0.0-1.0
- excerpt: the key relevant text
- related_items: ["Section A", "Section B"] - what else is connected
- confidence: how certain this directly answers the user's question
Return results as a valid JSON array.`,
messages: [
{
role: "user",
content: `Documentation:\n\n${notebookContent}\n\nQuestion: ${req.query}\n\nFind the ${req.max_results || 5} most relevant results.${req.focus_area ? ` Focus on ${req.focus_area}.` : ""}`,
},
],
});
const results = this.parseResults(message.content[0]);
// Cache for future requests
this.queryCache.set(cacheKey, results);
if (this.queryCache.size > 1000) {
// LRU eviction if cache grows too large
const firstKey = this.queryCache.keys().next().value;
this.queryCache.delete(firstKey);
}
return results;
}
private async fetchNotebook(notebookId: string): Promise<string> {
if (this.notebookCache.has(notebookId)) {
return this.notebookCache.get(notebookId)!;
}
// TODO: Integrate with NotebookLM API or load from storage
// For now, assume pre-exported markdown
const content = await this.loadFromFileSystem(notebookId);
this.notebookCache.set(notebookId, content);
return content;
}
private parseResults(content: any): ContextResult[] {
try {
const parsed = JSON.parse(content.text);
return Array.isArray(parsed) ? parsed : [];
} catch {
console.error("Failed to parse NotebookLM response");
return [];
}
}
}
export default NotebookLMMCPServer;Agent Query Workflow
Here's how an Antigravity agent uses this MCP server in practice:
// In Antigravity IDE
agent.registerMCPTool("notebook_query", {
description: "Query the NotebookLM codebase knowledge base",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Natural language question about codebase architecture, patterns, or decisions",
},
context_depth: {
type: "string",
enum: ["shallow", "deep"],
default: "shallow",
},
focus_area: {
type: "string",
enum: ["performance", "security", "reliability", "maintainability"],
},
},
required: ["query"],
},
});
// Agent uses it autonomously
agent.think(
"I need to refactor getUserData(). Let me check what the current design expects."
);
// Internally executes:
const context = await agent.callMCPTool("notebook_query", {
query: "getUserData function: responsibilities, dependencies, error handling patterns",
context_depth: "deep",
focus_area: "reliability",
});
// Returns with specific knowledge about this function's role
console.log(context);
// [
// {
// source: "ARCHITECTURE.md",
// relevance: 0.95,
// excerpt: "Data retrieval layer handles async operations with Suspense...",
// related_items: ["ERROR_HANDLING.md", "TESTING_STRATEGY.md"]
// },
// ...
// ]Multi-Agent Orchestration with Manager Surface
Specialized Agents, Unified Knowledge Base
NotebookLM enables agents to specialize by domain while maintaining architectural consistency:
| Agent | Knowledge Focus | Specialization |
|---|---|---|
| Architecture Agent | ARCHITECTURE.md, ADRs, MODULE_DEPENDENCY_GRAPH.md | System-wide impact analysis, migration strategy, refactoring scope |
| Code Quality Agent | CODE_PATTERNS.md, TESTING_STRATEGY.md, STYLE_GUIDE.md | Code generation, refactoring, test coverage, patterns |
| Performance Agent | PERFORMANCE_TUNING.md, METRICS.md, DEPENDENCY_GRAPH.md | Bottleneck identification, optimization suggestions, impact measurement |
| Security Agent | SECURITY.md, COMPLIANCE.md, THREAT_MODEL.md | Vulnerability detection, secure pattern enforcement |
| Documentation Agent | All documentation | Auto-generating guides, examples, keeping docs in sync |
Antigravity's Manager Surface orchestrates these agents:
// Antigravity Manager Surface
class CodebaseCoordinationManager {
async refactorComponentWithFullAnalysis(
componentPath: string,
refactoringGoal: string
): Promise<ComprehensiveRefactoringPlan> {
// 1. Architecture Agent: What are the blast radius & dependencies?
const architectureAnalysis = await this.architectureAgent.analyze({
query: `What components depend on ${componentPath}?
What would break if we change this?
Which systems would this affect?`,
focus_area: "reliability",
});
// 2. Code Quality Agent: What's the best approach?
const implementationStrategy = await this.codeQualityAgent.generate({
query: `${refactoringGoal} for ${componentPath}.
Follow our project patterns.
Include tests.`,
context: architectureAnalysis,
});
// 3. Performance Agent: What's the impact?
const performanceReview = await this.performanceAgent.evaluate({
proposed_code: implementationStrategy,
baseline_metrics: await this.getMetrics(componentPath),
query: "Will this improve or degrade performance? Bundle size?",
});
// 4. Security Agent: Any risks?
const securityReview = await this.securityAgent.review({
changes: implementationStrategy,
affected_systems: architectureAnalysis.dependencies,
});
// 5. Documentation Agent: Update guides
const docUpdates = await this.documentationAgent.generate({
changes: implementationStrategy,
reason: refactoringGoal,
});
// Synthesize into a comprehensive plan
return {
architecture: architectureAnalysis,
implementation: implementationStrategy,
performance: performanceReview,
security: securityReview,
documentation: docUpdates,
executionOrder: this.determineExecutionOrder([
architectureAnalysis,
implementationStrategy,
performanceReview,
securityReview,
]),
rollbackStrategy: await this.planRollback(
architectureAnalysis.dependencies
),
approvalCheckpoints: this.identifyHumanCheckpoints(
performanceReview,
securityReview
),
};
}
}Knowledge Consistency Across Agents
Because all agents query the same NotebookLM knowledge base:
- They work from consistent premises
- Contradictions are rare
- The "source of truth" is automatically maintained
- When documentation updates, all agents see the new information immediately
Production Scenario: Large-Scale Refactoring
The Challenge
You have a 500+ file TypeScript/React codebase using Redux for state management. Your goal: migrate to Context API + Zustand with zero breaking changes and measurable performance improvements.
This is complex because:
- Redux selectors are used across 200+ components
- Actions have 50+ test files depending on them
- Removing Redux means changes across the entire application
- Performance predictions must be accurate (false promises break trust)
Stage 1: Preparing the Knowledge Base
Feed NotebookLM documents covering:
# State Management Architecture
## Current System (Redux)
- Redux store location: `src/store/redux/`
- Selectors: `src/selectors/*` (48 files, 3000+ lines)
- Actions: `src/actions/*` (32 files, 2500+ lines)
- Test coverage: 89%
**Pain points:**
- Boilerplate: 1 action = 5-8 files (action, type, reducer, selector)
- DevTools coupling: Redux DevTools middleware required
- Bundle size: Redux + middleware = 45KB (gzipped)
## Target System (Zustand)
- Store files: `src/stores/zustand/*.ts`
- Philosophy: Minimal boilerplate, functional API
- Bundle size estimate: 12KB (gzipped)
## Migration Strategy
### Phase 1: Global State (Week 1-2)
- User authentication store
- Theme/preferences store
- Global UI state (modals, notifications)
### Phase 2: Domain Stores (Week 3-4)
- Product management (currently Redux ProductSlice)
- Cart management (Redux CartSlice)
- Orders (Redux OrdersSlice)
### Phase 3: Local State Conversion (Week 5)
- Complex local Redux stores → Zustand
- Cleanup: Remove Redux library
## Pattern Equivalency Guide
### Redux Selector → Zustand Pattern
```typescript
// Redux: const user = useSelector(selectUser)
// Zustand: const user = useUserStore(s => s.user)Redux Action Dispatch → Zustand Action
// Redux: dispatch(setUser(data))
// Zustand: useUserStore.setState({user: data})Risk Assessment
- Estimated effort: 160 developer-hours
- Risk level: Medium (breaking changes in 5% of test suite estimated)
- Rollback window: 2 weeks
- Success metrics: bundle -60%, performance +15%, test coverage maintained
NotebookLM now understands:
- The current Redux structure in detail
- The target Zustand design
- Exact equivalencies between patterns
- Risk and effort estimates
- Success metrics
### Stage 2: Agent Analysis & Planning
```typescript
// User triggers refactoring in Antigravity IDE
> Refactor: Redux → Zustand (Full Migration)
// Manager Surface coordinates:
const refactoringPlan = await manager.planRefactoring({
currentArchitecture: "Redux + middleware",
targetArchitecture: "Zustand stores",
scope: "application-wide",
});
// Agent 1: Architecture
const impactAnalysis = await archAgent.query({
query: "Show Redux store tree, identify which components use Redux, dependencies between stores",
});
// Returns: 48 selectors, 72 actions, 200 connected components
// Agent 2: Code Quality
const patterns = await codeQualityAgent.query({
query: "Generate Zustand equivalents for these Redux patterns",
provided: impactAnalysis,
});
// Returns: Zustand store templates, hook equivalencies
// Agent 3: Performance
const perfPredictions = await performanceAgent.analyze({
proposed_stores: patterns,
baseline: { bundleSize: 45, ttfb: 2100, lcpScore: 0.74 },
query: "Predict performance impact of Zustand migration",
});
// Returns: -60% bundle, +200ms faster TTI, +15% LCP improvement
// Agent 4: Testing
const testStrategy = await testAgent.plan({
current_tests: await getTestCoverage("Redux"),
target_architecture: "Zustand",
query: "Update test suite for Zustand. Maintain 89% coverage",
});
// Returns: 50+ new test patterns, deprecation scripts
// Agent 5: Documentation
const devGuide = await docAgent.generate({
query: "Write migration guide for developers",
context: [impactAnalysis, patterns, perfPredictions],
});
Stage 3: Incremental Execution with Learning
// Execute migration in manageable chunks with continuous learning
for (const phase of refactoringPlan.phases) {
console.log(`Executing: ${phase.name}`);
// Generate and review code
const generatedCode = await agent.generate(phase);
const review = await human_review(generatedCode); // Developer reviews in IDE
// Run tests
const testResults = await runTests(phase.affected_files);
if (!testResults.allPassed) {
// **Learn from failures**: Update NotebookLM with lessons
await notebookLM.addNote({
title: `Migration Issue: ${testResults.failedTest}`,
root_cause: await agent.analyze(testResults.failure),
resolution: "...",
lesson_for_next_phase: "When handling store subscriptions, remember...",
});
// **Adjust next phase** based on what we learned
const nextPhaseAdjustments = await archAgent.replan({
lessons_learned: notebookLM.getRecentNotes(),
original_plan: refactoringPlan.phases[i + 1],
});
refactoringPlan.phases[i + 1] = nextPhaseAdjustments;
}
// Measure actual vs predicted metrics
const actualMetrics = await measure({
bundleSize: "after compilation",
performance: "run lighthouse",
coverage: "run coverage report",
});
const prediction = perfPredictions[phase];
const accuracy = calculateAccuracy(prediction, actualMetrics);
// Store metrics for next refactoring's accuracy improvement
await notebookLM.recordMetrics({
phase: phase.name,
predicted: prediction,
actual: actualMetrics,
accuracy: accuracy,
});
}This creates a learning loop: each phase refines the knowledge base, improving future agent decisions.
Keeping NotebookLM Synchronized
Continuous Document Updates
NotebookLM is only as valuable as its documentation is current. Implement automated synchronization:
// Daily sync pipeline
class NotebookSyncManager {
async runDailySync() {
// 1. Detect what changed in the codebase
const gitDiff = await git.getDiff("HEAD~1", "HEAD", [
"src/",
"docs/",
".adr-dir/",
]);
// 2. Classify changes (some require doc updates, others don't)
for (const change of gitDiff) {
const classification = await classifyImpact(change);
if (classification === "ARCHITECTURE_CHANGE") {
// e.g., new module, major refactor
const updatedArchDoc = await autoGenerate.architecture(change);
await updateFile("docs/ARCHITECTURE.md", updatedArchDoc);
} else if (classification === "API_CHANGE") {
// e.g., public interface modified
const updatedAPI = await autoGenerate.publicAPI(change);
await updateFile("docs/PUBLIC_API.md", updatedAPI);
} else if (classification === "PATTERN_CHANGE") {
// e.g., new pattern adopted
const updatedPatterns = await autoGenerate.patterns(change);
await updateFile("docs/CODE_PATTERNS.md", updatedPatterns);
}
}
// 3. Push updated docs to NotebookLM
await notebookLM.sync({
updateSections: Object.keys(changedDocs),
retainHistory: true, // Keep version history
});
console.log(`Synced ${Object.keys(changedDocs).length} doc sections`);
}
}
// Run daily (e.g., via GitHub Actions)
schedule.daily("03:00 UTC", () => new NotebookSyncManager().runDailySync());Leveraging NotebookLM Enterprise API
For deeper integration, Google's NotebookLM Enterprise API offers:
import { NotebookLMClient } from "@google-cloud/notebooklm";
const nlm = new NotebookLMClient({ apiKey: process.env.NOTEBOOKLM_API_KEY });
// 1. Generate Podcast: Turn architecture docs into audio guides
const podcast = await nlm.generatePodcast({
notebookId: "antigravity-codebase",
sourceDocuments: ["ARCHITECTURE.md", "DESIGN_PATTERNS.md"],
audienceLevel: "advanced_engineer",
style: "discussion", // 2 speakers discussing the topic
duration: 30, // minutes
});
// Use for onboarding new team members
await onboarding.addResource({
type: "audio",
url: podcast.publicUrl,
title: "Architecture Overview (30 min)",
assignedTo: newEngineer,
});
// 2. Manage Notebook Collections: Connect related notebooks
await nlm.createCollection({
name: "Antigravity Codebase",
description: "All documentation and knowledge for the Antigravity IDE codebase",
notebooks: [
{ id: "arch-notebook", role: "source_of_truth" },
{ id: "performance-notebook", role: "derived_analysis" },
{ id: "migration-notebook", role: "execution_plan" },
{ id: "decision-records", role: "historical" },
],
enableCrossReferences: true,
});
// Now queries can reference across notebooks
const query = await nlm.query({
collectionId: "Antigravity Codebase",
question: "How does the migration to Zustand impact performance according to ARCHITECTURE.md and PERFORMANCE_TUNING.md?",
});Conclusion
Connecting NotebookLM to Antigravity agents via MCP creates a knowledge-driven development system:
- 8x expanded context: Understand projects with unprecedented depth
- Knowledge consistency: All agents work from the same source of truth
- Safe complexity: Large refactorings become systematic, testable, reversible
- Accumulated wisdom: ADRs, migration learnings, and performance data automatically enrich the knowledge base
Your codebase becomes a living system that learns from each refactoring, each decision, each migration. This is the foundation for truly intelligent AI-assisted development.
Ready to build this? Start with Antigravity agent memory patterns and the MCP server implementation guide.