Notion MCP Example β
This example demonstrates how to connect to Notion's Model Context Protocol (MCP) server using OAuth Callback's browserAuth() provider with Dynamic Client Registration (DCR). Unlike traditional OAuth flows that require pre-registering an OAuth application, this example shows how to automatically register and authenticate with Notion's authorization server.
Overview β
The Notion MCP integration showcases several advanced features:
- Dynamic Client Registration (RFC 7591) - Automatic OAuth client registration
- Model Context Protocol Integration - Seamless MCP SDK authentication
- Browser-Based Authorization - Automatic browser opening for user consent
- Token Management - Automatic token storage and retrieval
- Zero Configuration - No client ID or secret required
Prerequisites β
Before running this example, ensure you have:
- Bun, Node.js 18+, or Deno installed
- Port 3000 available for the OAuth callback server
- Default browser configured for opening authorization URLs
- Internet connection to reach Notion's servers
Installation β
Install the required dependencies:
bun add oauth-callback @modelcontextprotocol/sdknpm install oauth-callback @modelcontextprotocol/sdkpnpm add oauth-callback @modelcontextprotocol/sdkQuick Start β
Running the Example β
The simplest way to run the Notion MCP example:
# Clone the repository
git clone https://github.com/kriasoft/oauth-callback.git
cd oauth-callback
# Install dependencies
bun install
# Run the Notion example
bun run example:notionThe example will:
- Start a local OAuth callback server on port 3000
- Open your browser to Notion's authorization page
- Capture the authorization code after you approve
- Exchange the code for access tokens
- Connect to Notion's MCP server
- Display available tools and resources
Complete Example Code β
Here's the full implementation demonstrating Notion MCP integration:
#!/usr/bin/env bun
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { browserAuth, inMemoryStore } from "oauth-callback/mcp";
async function connectToNotion() {
  console.log("π Starting OAuth flow with Notion MCP Server\n");
  const serverUrl = new URL("https://mcp.notion.com/mcp");
  // Create OAuth provider - no client_id or client_secret needed!
  const authProvider = browserAuth({
    port: 3000,
    scope: "read write",
    store: inMemoryStore(), // Use fileStore() for persistence
    onRequest(req) {
      const url = new URL(req.url);
      console.log(`π¨ OAuth: ${req.method} ${url.pathname}`);
    },
  });
  try {
    // Create MCP transport with OAuth provider
    const transport = new StreamableHTTPClientTransport(serverUrl, {
      authProvider,
    });
    // Create MCP client
    const client = new Client(
      { name: "notion-example", version: "1.0.0" },
      { capabilities: {} },
    );
    // Connect triggers OAuth flow if needed
    await client.connect(transport);
    console.log("β
 Connected to Notion MCP server!");
    // List available tools
    const tools = await client.listTools();
    console.log("\nπ Available tools:");
    for (const tool of tools.tools || []) {
      console.log(`   - ${tool.name}: ${tool.description}`);
    }
    // List available resources
    const resources = await client.listResources();
    console.log("\nπ Available resources:");
    for (const resource of resources.resources || []) {
      console.log(`   - ${resource.uri}: ${resource.name}`);
    }
    await client.close();
  } catch (error) {
    console.error("β Connection failed:", error);
  }
}
connectToNotion();How It Works β
OAuth Flow Sequence β
Dynamic Client Registration β
Unlike traditional OAuth, Notion's MCP server supports Dynamic Client Registration (RFC 7591):
- No Pre-Registration - You don't need to manually register an OAuth app
- Automatic Registration - The client registers itself on first use
- Credential Persistence - Client credentials are stored for reuse
- Simplified Distribution - Ship apps without OAuth setup instructions
Key Features β
Browser-Based Authorization β
The browserAuth() provider handles the complete OAuth flow:
const authProvider = browserAuth({
  port: 3000, // Callback server port
  scope: "read write", // Requested permissions
  store: inMemoryStore(), // Token storage
  onRequest(req) {
    // Request logging
    console.log(`OAuth: ${req.url}`);
  },
});Token Storage Options β
Choose between ephemeral and persistent storage:
// Tokens lost on restart (more secure)
const authProvider = browserAuth({
  store: inMemoryStore(),
});// Tokens saved to disk (convenient)
import { fileStore } from "oauth-callback/mcp";
const authProvider = browserAuth({
  store: fileStore(), // Default: ~/.mcp/tokens.json
});// Specify custom file path
const authProvider = browserAuth({
  store: fileStore("~/my-app/notion-tokens.json"),
});Error Handling β
Properly handle authentication failures:
try {
  await client.connect(transport);
} catch (error) {
  if (error.message.includes("Unauthorized")) {
    console.log("Authorization required - check browser");
  } else if (error.message.includes("access_denied")) {
    console.log("User cancelled authorization");
  } else {
    console.error("Connection failed:", error);
  }
}Working with Notion MCP β
Available Tools β
Once connected, you can use Notion's MCP tools:
// Search for content
const searchResults = await client.callTool("search_objects", {
  query: "meeting notes",
  limit: 10,
});
// Create a new page
const newPage = await client.callTool("create_page", {
  title: "My New Page",
  content: "Page content here",
});
// Update existing content
const updated = await client.callTool("update_page", {
  page_id: "page-123",
  content: "Updated content",
});Available Resources β
Access Notion resources through MCP:
// List all resources
const resources = await client.listResources();
// Read a specific resource
const pageContent = await client.readResource({
  uri: "notion://page/page-123",
});
// Subscribe to changes
await client.subscribeToResource({
  uri: "notion://database/db-456",
});Advanced Configuration β
Custom Success Pages β
Provide branded callback pages:
const authProvider = browserAuth({
  successHtml: `
    <!DOCTYPE html>
    <html>
      <head>
        <title>Notion Connected!</title>
        <style>
          body {
            font-family: -apple-system, system-ui, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            background: linear-gradient(135deg, #000000 0%, #434343 100%);
            color: white;
          }
        </style>
      </head>
      <body>
        <div>
          <h1>β¨ Connected to Notion!</h1>
          <p>You can close this window and return to your app.</p>
        </div>
      </body>
    </html>
  `,
});Request Logging β
Debug OAuth flow with detailed logging:
const authProvider = browserAuth({
  onRequest(req) {
    const url = new URL(req.url);
    const timestamp = new Date().toISOString();
    console.log(`[${timestamp}] OAuth Request`);
    console.log(`  Method: ${req.method}`);
    console.log(`  Path: ${url.pathname}`);
    if (url.pathname === "/callback") {
      console.log(`  Code: ${url.searchParams.get("code")}`);
      console.log(`  State: ${url.searchParams.get("state")}`);
    }
  },
});Multi-Account Support β
Support multiple Notion accounts:
function createNotionAuth(accountName: string) {
  return browserAuth({
    store: fileStore(`~/.mcp/notion-${accountName}.json`),
    storeKey: `notion-${accountName}`,
    port: 3000 + Math.floor(Math.random() * 1000), // Random port
  });
}
// Use different accounts
const personalAuth = createNotionAuth("personal");
const workAuth = createNotionAuth("work");Troubleshooting β
Common Issues and Solutions β
Browser doesn't open automatically
If the browser doesn't open automatically:
const authProvider = browserAuth({
  openBrowser: false, // Disable auto-open
});
// Manually instruct user
console.log("Please open this URL in your browser:");
console.log(authorizationUrl);Port 3000 is already in use
Use a different port for the callback server:
const authProvider = browserAuth({
  port: 8080, // Use alternative port
});Tokens not persisting
Ensure you're using file storage, not in-memory:
import { fileStore } from "oauth-callback/mcp";
const authProvider = browserAuth({
  store: fileStore(), // β
 Persistent storage
  // store: inMemoryStore() // β Lost on restart
});Authorization fails repeatedly
Clear stored credentials and try again:
// Clear all stored data
await authProvider.invalidateCredentials("all");
// Or clear specific data
await authProvider.invalidateCredentials("tokens");
await authProvider.invalidateCredentials("client");Security Considerations β
Best Practices β
- Use Ephemeral Storage for Sensitive Data typescript- // Tokens are never written to disk const authProvider = browserAuth({ store: inMemoryStore(), });
- Validate State Parameter - The library automatically generates and validates state parameters
- Prevents CSRF attacks during authorization
 
- PKCE Protection - Enabled by default for enhanced security
- Prevents authorization code interception
 
- Secure File Permissions - File storage uses mode 0600 (owner read/write only)
- Tokens are protected from other users on the system
 
Token Security β
WARNING
Never commit tokens to version control:
# Add to .gitignore
~/.mcp/
*.json
tokens.jsonComplete Working Example β
For a production-ready implementation with full error handling:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { browserAuth, fileStore } from "oauth-callback/mcp";
class NotionMCPClient {
  private client?: Client;
  private authProvider: any;
  constructor() {
    this.authProvider = browserAuth({
      port: 3000,
      scope: "read write",
      store: fileStore("~/.mcp/notion.json"),
      authTimeout: 300000, // 5 minutes
      onRequest: this.logRequest.bind(this),
    });
  }
  private logRequest(req: Request) {
    const url = new URL(req.url);
    console.log(`[OAuth] ${req.method} ${url.pathname}`);
  }
  async connect(): Promise<void> {
    const serverUrl = new URL("https://mcp.notion.com/mcp");
    const transport = new StreamableHTTPClientTransport(serverUrl, {
      authProvider: this.authProvider,
    });
    this.client = new Client(
      { name: "notion-client", version: "1.0.0" },
      { capabilities: {} },
    );
    try {
      await this.client.connect(transport);
      console.log("β
 Connected to Notion MCP");
    } catch (error: any) {
      if (error.message.includes("Unauthorized")) {
        throw new Error("Authorization required. Please check your browser.");
      }
      throw error;
    }
  }
  async search(query: string): Promise<any> {
    if (!this.client) throw new Error("Not connected");
    return await this.client.callTool("search_objects", {
      query,
      limit: 10,
    });
  }
  async disconnect(): Promise<void> {
    if (this.client) {
      await this.client.close();
      this.client = undefined;
    }
  }
}
// Usage
async function main() {
  const notion = new NotionMCPClient();
  try {
    await notion.connect();
    const results = await notion.search("project roadmap");
    console.log("Search results:", results);
    await notion.disconnect();
  } catch (error) {
    console.error("Error:", error);
  }
}
main();Related Resources β
- browserAuth API Documentation - Complete API reference
- Storage Providers - Token storage options
- Core Concepts - OAuth and MCP architecture
- GitHub Example - Source code