#
tokens: 46866/50000 90/93 files (page 1/2)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 of 2. Use http://codebase.md/neondatabase-labs/mcp-server-neon?page={x} to view the full context.

# Directory Structure

```
├── .bun-version
├── .dockerignore
├── .env.example
├── .github
│   └── workflows
│       ├── koyeb-preview.yml
│       ├── koyeb-prod.yml
│       └── pr.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .prettierignore
├── .prettierrc
├── bun.lock
├── CHANGELOG.md
├── Dockerfile
├── eslint.config.js
├── landing
│   ├── .gitignore
│   ├── app
│   │   ├── globals.css
│   │   ├── layout.tsx
│   │   └── page.tsx
│   ├── components
│   │   ├── CodeSnippet.tsx
│   │   ├── CopyableUrl.tsx
│   │   ├── DescriptionItem.tsx
│   │   ├── ExternalIcon.tsx
│   │   ├── ExternalLink.tsx
│   │   ├── Header.tsx
│   │   ├── Introduction.tsx
│   │   ├── ThemeProvider.tsx
│   │   └── ui
│   │       ├── accordion.tsx
│   │       ├── alert.tsx
│   │       └── button.tsx
│   ├── components.json
│   ├── eslint.config.mjs
│   ├── icons
│   │   ├── github.svg
│   │   └── neon.svg
│   ├── lib
│   │   ├── description.ts
│   │   └── utils.ts
│   ├── next.config.ts
│   ├── package-lock.json
│   ├── package.json
│   ├── postcss.config.mjs
│   ├── public
│   │   └── favicon.ico
│   ├── README.md
│   └── tsconfig.json
├── LICENSE
├── mcp-client
│   ├── .env.example
│   ├── package-lock.json
│   ├── package.json
│   ├── README.md
│   ├── src
│   │   ├── bin.ts
│   │   ├── cli-client.ts
│   │   ├── index.ts
│   │   ├── logger.ts
│   │   └── neon-cli-client.ts
│   └── tsconfig.json
├── package-lock.json
├── package.json
├── public
│   └── logo.png
├── PUBLISH.md
├── README.md
├── remote.Dockerfile
├── scripts
│   └── before-publish.ts
├── src
│   ├── analytics
│   │   └── analytics.ts
│   ├── constants.ts
│   ├── describeUtils.ts
│   ├── index.ts
│   ├── initConfig.ts
│   ├── oauth
│   │   ├── client.ts
│   │   ├── cookies.ts
│   │   ├── kv-store.ts
│   │   ├── model.ts
│   │   ├── server.ts
│   │   └── utils.ts
│   ├── resources.ts
│   ├── sentry
│   │   ├── instrument.ts
│   │   └── utils.ts
│   ├── server
│   │   ├── api.ts
│   │   ├── errors.ts
│   │   └── index.ts
│   ├── tools
│   │   ├── definitions.ts
│   │   ├── handlers
│   │   │   └── neon-auth.ts
│   │   ├── index.ts
│   │   ├── state.ts
│   │   ├── tools.ts
│   │   ├── toolsSchema.ts
│   │   ├── types.ts
│   │   └── utils.ts
│   ├── tools-evaluations
│   │   ├── evalUtils.ts
│   │   └── prepare-database-migration.eval.ts
│   ├── transports
│   │   ├── sse-express.ts
│   │   ├── stdio.ts
│   │   └── stream.ts
│   ├── types
│   │   ├── auth.ts
│   │   ├── context.ts
│   │   └── express.d.ts
│   ├── utils
│   │   ├── logger.ts
│   │   └── polyfills.ts
│   └── views
│       ├── approval-dialog.pug
│       └── styles.css
├── tsconfig.json
└── tsconfig.test.json
```

# Files

--------------------------------------------------------------------------------
/.bun-version:
--------------------------------------------------------------------------------

```
1.2.13

```

--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------

```
v22.15.1

```

--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------

```
save-exact=true
```

--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------

```
build/
landing/.next
landing/out

```

--------------------------------------------------------------------------------
/mcp-client/.env.example:
--------------------------------------------------------------------------------

```
ANTHROPIC_API_KEY=
NEON_API_KEY=

```

--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------

```
{
  "printWidth": 80,
  "singleQuote": true,
  "semi": true
}

```

--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------

```
**/node_modules
**/*.log
**/.env
**/dist
**/build
# VS Code history extension
**/.history 

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
node_modules/
*.log
.env
dist/
build/

# landing page generated files are placed in root /public directory,
# so ignoring it in order to do not commit something unintentionally.
# if something should be added in /public it can be done by force add:
#   git add -f public/somefile
/public/

# IDE stuff
.history
.idea

```

--------------------------------------------------------------------------------
/landing/.gitignore:
--------------------------------------------------------------------------------

```
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

# part of building process
tools.json

```

--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------

```
## BrainTrust
BRAINTRUST_API_KEY=

## Neon BaaS team org api key
NEON_API_KEY=

## Anthropic api key to run the evals
ANTHROPIC_API_KEY=

## Neon API
NEON_API_HOST=https://api.neon.tech/api/v2
## OAuth upstream oauth host
UPSTREAM_OAUTH_HOST='https://oauth2.neon.tech';

## OAuth client id
CLIENT_ID=
## OAuth client secret
CLIENT_SECRET=

## Redirect URI for OIDC callback
REDIRECT_URI=http://localhost:3001/callback

## A connection string to postgres database for client and token persistence
## Optional while running in MCP in stdio
OAUTH_DATABASE_URL=

## A secret key to sign and verify the cookies
## Optional while running MCP in stdio 
COOKIE_SECRET=

## Optional Analytics
ANALYTICS_WRITE_KEY=

## Optional Sentry 
SENTRY_DSN=
```

--------------------------------------------------------------------------------
/mcp-client/README.md:
--------------------------------------------------------------------------------

```markdown
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://neon.com/brand/neon-logo-dark-color.svg">
  <source media="(prefers-color-scheme: light)" srcset="https://neon.com/brand/neon-logo-light-color.svg">
  <img width="250px" alt="Neon Logo fallback" src="https://neon.com/brand/neon-logo-dark-color.svg">
</picture>

## MCP Client CLI

This is a CLI client that can be used to interact with any MCP server and its tools. For more, see [Building a CLI Client For Model Context Protocol Servers](https://neon.tech/blog/building-a-cli-client-for-model-context-protocol-servers).

## Requirements

- ANTHROPIC_API_KEY - Get one from [Anthropic](https://console.anthropic.com/)
- Node.js >= v18.0.0

## How to use

```bash
export ANTHROPIC_API_KEY=your_key_here
npx @neondatabase/mcp-client-cli --server-command="npx" --server-args="-y @neondatabase/mcp-server-neon start <neon-api-key>"
```

## How to develop

1. Clone the repository
2. Setup a `.env` file based on the `.env.example` file
3. Run `npm install`
4. Run `npm run start:mcp-server-neon`

```

--------------------------------------------------------------------------------
/landing/README.md:
--------------------------------------------------------------------------------

```markdown
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://neon.com/brand/neon-logo-dark-color.svg">
  <source media="(prefers-color-scheme: light)" srcset="https://neon.com/brand/neon-logo-light-color.svg">
  <img width="250px" alt="Neon Logo fallback" src="https://neon.com/brand/neon-logo-dark-color.svg">
</picture>

# Neon MCP Server

[![Install MCP Server in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=Neon&config=eyJ1cmwiOiJodHRwczovL21jcC5uZW9uLnRlY2gvbWNwIn0%3D)

**Neon MCP Server** is an open-source tool that lets you interact with your Neon Postgres databases in **natural language**.

[![npm version](https://img.shields.io/npm/v/@neondatabase/mcp-server-neon)](https://www.npmjs.com/package/@neondatabase/mcp-server-neon)
[![npm downloads](https://img.shields.io/npm/dt/@neondatabase/mcp-server-neon)](https://www.npmjs.com/package/@neondatabase/mcp-server-neon)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

The Model Context Protocol (MCP) is a [new, standardized protocol](https://modelcontextprotocol.io/introduction) designed to manage context between large language models (LLMs) and external systems. This repository offers an installer and an MCP Server for [Neon](https://neon.tech).

Neon's MCP server acts as a bridge between natural language requests and the [Neon API](https://api-docs.neon.tech/reference/getting-started-with-neon-api). Built upon MCP, it translates your requests into the necessary API calls, enabling you to manage tasks such as creating projects and branches, running queries, and performing database migrations seamlessly.

Some of the key features of the Neon MCP server include:

- **Natural language interaction:** Manage Neon databases using intuitive, conversational commands.
- **Simplified database management:** Perform complex actions without writing SQL or directly using the Neon API.
- **Accessibility for non-developers:** Empower users with varying technical backgrounds to interact with Neon databases.
- **Database migration support:** Leverage Neon's branching capabilities for database schema changes initiated via natural language.

For example, in Claude Desktop, or any MCP Client, you can use natural language to accomplish things with Neon, such as:

- `Let's create a new Postgres database, and call it "my-database". Let's then create a table called users with the following columns: id, name, email, and password.`
- `I want to run a migration on my project called "my-project" that alters the users table to add a new column called "created_at".`
- `Can you give me a summary of all of my Neon projects and what data is in each one?`

> [!WARNING]  
> **Neon MCP Server Security Considerations**  
> The Neon MCP Server grants powerful database management capabilities through natural language requests. **Always review and authorize actions requested by the LLM before execution.** Ensure that only authorized users and applications have access to the Neon MCP Server.
>
> The Neon MCP Server is intended for local development and IDE integrations only. **We do not recommend using the Neon MCP Server in production environments.** It can execute powerful operations that may lead to accidental or unauthorized changes.
>
> For more information, see [MCP security guidance →](https://neon.tech/docs/ai/neon-mcp-server#mcp-security-guidance).

## Setting up Neon MCP Server

You have two options for connecting your MCP client to Neon:

1. **Remote MCP Server (Preview):** Connect to Neon's managed MCP server using OAuth for authentication. This method is more convenient as it eliminates the need to manage API keys. Additionally, you will automatically receive the latest features and improvements as soon as they are released.

2. **Local MCP Server:** Run the Neon MCP server locally on your machine, authenticating with a Neon API key.

## Prerequisites

- An MCP Client application.
- A [Neon account](https://console.neon.tech/signup).
- **Node.js (>= v18.0.0) and npm:** Download from [nodejs.org](https://nodejs.org).

For Local MCP Server setup, you also need a Neon API key. See [Neon API Keys documentation](https://neon.tech/docs/manage/api-keys) for instructions on generating one.

### Option 1. Remote Hosted MCP Server (Preview)

Connect to Neon's managed MCP server using OAuth for authentication. This is the easiest setup, requires no local installation of this server, and doesn't need a Neon API key configured in the client.

- Add the following "Neon" entry to your client's MCP server configuration file (e.g., `mcp.json`, `mcp_config.json`):

  ```json
  {
    "mcpServers": {
      "Neon": {
        "command": "npx",
        "args": ["-y", "mcp-remote", "https://mcp.neon.tech/mcp"]
      }
    }
  }
  ```

- Save the configuration file.
- Restart or refresh your MCP client.
- An OAuth window will open in your browser. Follow the prompts to authorize your MCP client to access your Neon account.

> With OAuth base authentication, the MCP server will, by default operate on projects under your personal Neon account. To access or manage projects under organization, you must explicitly provide either the `org_id` or the `project_id` in your prompt to MCP client.

Remote MCP Server also supports authentication using API key in the `Authorization` header if your client supports it

```json
{
  "mcpServers": {
    "Neon": {
      "url": "https://mcp.neon.tech/mcp",
      "headers": {
        "Authorization": "Bearer <$NEON_API_KEY>"
      }
    }
  }
}
```

> Provider organization's API key to limit access to projects under the organization only.

MCP supports two remote server transports: the deprecated Server-Sent Events (SSE) and the newer, recommended Streamable HTTP. If your LLM client doesn't support Streamable HTTP yet, you can switch the endpoint from `https://mcp.neon.tech/mcp` to `https://mcp.neon.tech/sse` to use SSE instead.

### Option 2. Local MCP Server

Run the Neon MCP server on your local machine with your Neon API key. This method allows you to manage your Neon projects and databases without relying on a remote MCP server.

Add the following JSON configuration within the `mcpServers` section of your client's `mcp_config` file, replacing `<YOUR_NEON_API_KEY>` with your actual Neon API key:

```json
{
  "mcpServers": {
    "neon": {
      "command": "npx",
      "args": [
        "-y",
        "@neondatabase/mcp-server-neon",
        "start",
        "<YOUR_NEON_API_KEY>"
      ]
    }
  }
}
```

### Troubleshooting

If your client does not use `JSON` for configuration of MCP servers (such as older versions of Cursor), you can use the following command when prompted:

```bash
npx -y @neondatabase/mcp-server-neon start <YOUR_NEON_API_KEY>
```

#### Troubleshooting on Windows

If you are using Windows and encounter issues while adding the MCP server, you might need to use the Command Prompt (`cmd`) or Windows Subsystem for Linux (`wsl`) to run the necessary commands. Your configuration setup may resemble the following:

```json
{
  "mcpServers": {
    "neon": {
      "command": "cmd",
      "args": [
        "/c",
        "npx",
        "-y",
        "@neondatabase/mcp-server-neon",
        "start",
        "<YOUR_NEON_API_KEY>"
      ]
    }
  }
}
```

```json
{
  "mcpServers": {
    "neon": {
      "command": "wsl",
      "args": [
        "npx",
        "-y",
        "@neondatabase/mcp-server-neon",
        "start",
        "<YOUR_NEON_API_KEY>"
      ]
    }
  }
}
```

## Guides

- [Neon MCP Server Guide](https://neon.tech/docs/ai/neon-mcp-server)
- [Connect MCP Clients to Neon](https://neon.tech/docs/ai/connect-mcp-clients-to-neon)
- [Cursor with Neon MCP Server](https://neon.tech/guides/cursor-mcp-neon)
- [Claude Desktop with Neon MCP Server](https://neon.tech/guides/neon-mcp-server)
- [Cline with Neon MCP Server](https://neon.tech/guides/cline-mcp-neon)
- [Windsurf with Neon MCP Server](https://neon.tech/guides/windsurf-mcp-neon)
- [Zed with Neon MCP Server](https://neon.tech/guides/zed-mcp-neon)

# Features

## Supported Tools

The Neon MCP Server provides the following actions, which are exposed as "tools" to MCP Clients. You can use these tools to interact with your Neon projects and databases using natural language commands.

**Project Management:**

- **`list_projects`**: Lists the first 10 Neon projects in your account, providing a summary of each project. If you can't find a specific project, increase the limit by passing a higher value to the `limit` parameter.
- **`list_shared_projects`**: Lists Neon projects shared with the current user. Supports a search parameter and limiting the number of projects returned (default: 10).
- **`describe_project`**: Fetches detailed information about a specific Neon project, including its ID, name, and associated branches and databases.
- **`create_project`**: Creates a new Neon project in your Neon account. A project acts as a container for branches, databases, roles, and computes.
- **`delete_project`**: Deletes an existing Neon project and all its associated resources.

**Branch Management:**

- **`create_branch`**: Creates a new branch within a specified Neon project. Leverages [Neon's branching](/docs/introduction/branching) feature for development, testing, or migrations.
- **`delete_branch`**: Deletes an existing branch from a Neon project.
- **`describe_branch`**: Retrieves details about a specific branch, such as its name, ID, and parent branch.
- **`list_branch_computes`**: Lists compute endpoints for a project or specific branch, including compute ID, type, size, and autoscaling information.
- **`list_organizations`**: Lists all organizations that the current user has access to. Optionally filter by organization name or ID using the search parameter.
- **`reset_from_parent`**: Resets the current branch to its parent's state, discarding local changes. Automatically preserves to backup if branch has children, or optionally preserve on request with a custom name.
- **`compare_database_schema`**: Shows the schema diff between the child branch and its parent

**SQL Query Execution:**

- **`get_connection_string`**: Returns your database connection string.
- **`run_sql`**: Executes a single SQL query against a specified Neon database. Supports both read and write operations.
- **`run_sql_transaction`**: Executes a series of SQL queries within a single transaction against a Neon database.
- **`get_database_tables`**: Lists all tables within a specified Neon database.
- **`describe_table_schema`**: Retrieves the schema definition of a specific table, detailing columns, data types, and constraints.
- **`list_slow_queries`**: Identifies performance bottlenecks by finding the slowest queries in a database. Requires the pg_stat_statements extension.

**Database Migrations (Schema Changes):**

- **`prepare_database_migration`**: Initiates a database migration process. Critically, it creates a temporary branch to apply and test the migration safely before affecting the main branch.
- **`complete_database_migration`**: Finalizes and applies a prepared database migration to the main branch. This action merges changes from the temporary migration branch and cleans up temporary resources.

**Query Performance Optimization:**

- **`explain_sql_statement`**: Provides detailed execution plans for SQL queries to help identify performance bottlenecks.
- **`prepare_query_tuning`**: Analyzes query performance and suggests optimizations like index creation. Creates a temporary branch for safely testing these optimizations.
- **`complete_query_tuning`**: Applies or discards query optimizations after testing. Can merge changes from the temporary branch to the main branch.
- **`list_slow_queries`**: Identifies and analyzes slow-performing queries in your database. Requires the `pg_stat_statements` extension.

**Compute Management:**

- **`list_branch_computes`**: Lists compute endpoints for a project or specific branch, showing details like compute ID, type, size, and last active time.

**Neon Auth:**

- **`provision_neon_auth`**: Provisions Neon Auth for a Neon project. It allows developers to easily set up authentication infrastructure by creating an integration with Stack Auth (`@stackframe/stack`).

**Query Performance Tuning:**

- **`explain_sql_statement`**: Analyzes a SQL query and returns detailed execution plan information to help understand query performance.
- **`prepare_query_tuning`**: Identifies potential performance issues in a SQL query and suggests optimizations. Creates a temporary branch for testing improvements.
- **`complete_query_tuning`**: Finalizes and applies query optimizations after testing. Merges changes from the temporary tuning branch to the main branch.

## Migrations

Migrations are a way to manage changes to your database schema over time. With the Neon MCP server, LLMs are empowered to do migrations safely with separate "Start" (`prepare_database_migration`) and "Commit" (`complete_database_migration`) commands.

The "Start" command accepts a migration and runs it in a new temporary branch. Upon returning, this command hints to the LLM that it should test the migration on this branch. The LLM can then run the "Commit" command to apply the migration to the original branch.

# Development

## Development with MCP CLI Client

The easiest way to iterate on the MCP Server is using the `mcp-client/`. Learn more in `mcp-client/README.md`.

```bash
npm install
npm run build
npm run watch # You can keep this open.
cd mcp-client/ && NEON_API_KEY=... npm run start:mcp-server-neon
```

## Development with Claude Desktop (Local MCP Server)

```bash
npm install
npm run build
npm run watch # You can keep this open.
node dist/index.js init $NEON_API_KEY
```

Then, **restart Claude** each time you want to test changes.

# Testing

To run the tests you need to setup the `.env` file according to the `.env.example` file.

```bash
npm run test
```

```

--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------

```json
{
  "extends": "./tsconfig.json",
  "include": ["src/tools-evaluations/**/*"]
}

```

--------------------------------------------------------------------------------
/landing/postcss.config.mjs:
--------------------------------------------------------------------------------

```
const config = {
  plugins: ['@tailwindcss/postcss'],
};

export default config;

```

--------------------------------------------------------------------------------
/landing/next.config.ts:
--------------------------------------------------------------------------------

```typescript
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  output: 'export',
};

export default nextConfig;

```

--------------------------------------------------------------------------------
/src/tools/index.ts:
--------------------------------------------------------------------------------

```typescript
export { NEON_TOOLS } from './definitions.js';
export { NEON_HANDLERS } from './tools.js';
export { ToolHandlers, ToolHandlerExtended } from './types.js';

```

--------------------------------------------------------------------------------
/landing/lib/utils.ts:
--------------------------------------------------------------------------------

```typescript
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

```

--------------------------------------------------------------------------------
/PUBLISH.md:
--------------------------------------------------------------------------------

```markdown
## Publish

### New release

```bash
npm run build
npm version patch|minor|major
npm publish
```

### New Beta Release

```bash
npm run build
npm version prerelease --preid=beta
npm publish --tag beta
```

### Promote beta to release

```bash
npm version patch
npm publish
```

```

--------------------------------------------------------------------------------
/mcp-client/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

```

--------------------------------------------------------------------------------
/mcp-client/src/neon-cli-client.ts:
--------------------------------------------------------------------------------

```typescript
import { MCPClientCLI } from './cli-client.js';
import path from 'path';
import dotenv from 'dotenv';

dotenv.config({
  path: path.resolve(__dirname, '../.env'),
});
const cli = new MCPClientCLI({
  command: path.resolve(__dirname, '../../dist/index.js'), // Use __dirname for relative path
  args: ['start', process.env.NEON_API_KEY!],
});

cli.start();

```

--------------------------------------------------------------------------------
/src/types/auth.ts:
--------------------------------------------------------------------------------

```typescript
import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';

export type AuthContext = {
  extra: {
    account: {
      id: string;
      name: string;
      email?: string;
      isOrg?: boolean; // For STDIO mode with org API key
    };
    client?: {
      id: string;
      name: string;
    };
    [key: string]: unknown;
  };
} & AuthInfo;

```

--------------------------------------------------------------------------------
/src/types/express.d.ts:
--------------------------------------------------------------------------------

```typescript
import { AuthContext } from './auth.js';

// to make the file a module and avoid the TypeScript error
export {};

// Extends the Express Request interface to add the auth context
declare global {
  namespace Express {
    // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
    export interface Request {
      auth?: AuthContext;
    }
  }
}

```

--------------------------------------------------------------------------------
/src/types/context.ts:
--------------------------------------------------------------------------------

```typescript
import { Environment } from '../constants.js';
import { AuthContext } from './auth.js';

export type AppContext = {
  name: string;
  transport: 'sse' | 'stdio' | 'stream';
  environment: Environment;
  version: string;
};

export type ServerContext = {
  apiKey: string;
  client?: AuthContext['extra']['client'];
  account: AuthContext['extra']['account'];
  app: AppContext;
};

```

--------------------------------------------------------------------------------
/src/sentry/instrument.ts:
--------------------------------------------------------------------------------

```typescript
import { init } from '@sentry/node';
import { SENTRY_DSN } from '../constants.js';
import { getPackageJson } from '../server/api.js';

init({
  dsn: SENTRY_DSN,
  environment: process.env.NODE_ENV,
  release: getPackageJson().version,
  tracesSampleRate: 1.0,

  // Setting this option to true will send default PII data to Sentry.
  // For example, automatic IP address collection on events
  sendDefaultPii: true,
});

```

--------------------------------------------------------------------------------
/landing/components/ExternalLink.tsx:
--------------------------------------------------------------------------------

```typescript
import { ReactNode } from 'react';

import { ExternalIcon } from '@/components/ExternalIcon';

type ExternalLinkProps = { href: string; children?: ReactNode };

export const ExternalLink = ({ href, children }: ExternalLinkProps) => (
  <a
    className="inline-flex items-center gap-1 w-fit external-link"
    href={href}
    target="_blank"
    rel="noopener noreferrer"
  >
    {children}
    <ExternalIcon />
  </a>
);

```

--------------------------------------------------------------------------------
/landing/components.json:
--------------------------------------------------------------------------------

```json
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "",
    "css": "app/globals.css",
    "baseColor": "slate",
    "cssVariables": true,
    "prefix": ""
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  },
  "iconLibrary": "lucide"
}

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "typeRoots": ["./node_modules/@types", "./src/types"]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "src/tools-evaluations/**/*"]
}

```

--------------------------------------------------------------------------------
/src/transports/stdio.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
/**
 * Start the server using stdio transport.
 * This allows the server to communicate via standard input/output streams.
 */
export const startStdio = async (server: McpServer) => {
  const transport = new StdioServerTransport();
  await server.connect(transport);
};

```

--------------------------------------------------------------------------------
/src/utils/polyfills.ts:
--------------------------------------------------------------------------------

```typescript
import nodeFetch, {
  Headers as NodeHeaders,
  Request as NodeRequest,
  Response as NodeResponse,
} from 'node-fetch';

// Use different names to avoid conflicts
declare global {
  function fetch(
    url: string | Request | URL,
    init?: RequestInit,
  ): Promise<Response>;
}

if (!global.fetch) {
  global.fetch = nodeFetch as any;
  global.Headers = NodeHeaders as any;
  global.Request = NodeRequest as any;
  global.Response = NodeResponse as any;
}

```

--------------------------------------------------------------------------------
/landing/eslint.config.mjs:
--------------------------------------------------------------------------------

```
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import { FlatCompat } from '@eslint/eslintrc';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const compat = new FlatCompat({
  baseDirectory: __dirname,
});

const eslintConfig = [
  ...compat.extends('next/core-web-vitals', 'next/typescript'),
  {
    rules: {
      'react/no-unescaped-entities': 'off',
    },
  },
];

export default eslintConfig;

```

--------------------------------------------------------------------------------
/landing/components/ExternalIcon.tsx:
--------------------------------------------------------------------------------

```typescript
export const ExternalIcon = () => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width="12"
    height="12"
    fill="none"
    viewBox="0 0 12 12"
    className="-mb-px shrink-0"
  >
    <rect
      width="12"
      height="12"
      fill="currentColor"
      opacity="0.2"
      rx="2"
    ></rect>
    <path
      stroke="currentColor"
      strokeLinecap="round"
      strokeLinejoin="round"
      d="M8.499 7.616v-4.12h-4.12M8.25 3.75 3.5 8.5"
    ></path>
  </svg>
);

```

--------------------------------------------------------------------------------
/src/sentry/utils.ts:
--------------------------------------------------------------------------------

```typescript
import { setTags, setUser } from '@sentry/node';
import { ServerContext } from '../types/context.js';

export const setSentryTags = (context: ServerContext) => {
  setUser({
    id: context.account.id,
  });
  setTags({
    'app.name': context.app.name,
    'app.version': context.app.version,
    'app.transport': context.app.transport,
    'app.environment': context.app.environment,
  });
  if (context.client) {
    setTags({
      'client.id': context.client.id,
      'client.name': context.client.name,
    });
  }
};

```

--------------------------------------------------------------------------------
/src/server/api.ts:
--------------------------------------------------------------------------------

```typescript
import { createApiClient } from '@neondatabase/api-client';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import fs from 'node:fs';
import { NEON_API_HOST } from '../constants.js';

export const getPackageJson = () => {
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
  return JSON.parse(
    fs.readFileSync(path.join(__dirname, '../..', 'package.json'), 'utf8'),
  );
};

export const createNeonClient = (apiKey: string) =>
  createApiClient({
    apiKey,
    baseURL: NEON_API_HOST,
    headers: {
      'User-Agent': `mcp-server-neon/${getPackageJson().version}`,
    },
  });

```

--------------------------------------------------------------------------------
/landing/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2018",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": [
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts",
    "next-env.d.ts",
    "../public/types/**/*.ts"
  ],
  "exclude": ["node_modules"]
}

```

--------------------------------------------------------------------------------
/.github/workflows/pr.yml:
--------------------------------------------------------------------------------

```yaml
name: Lint and Build

on:
  pull_request:
    branches:
      - main

jobs:
  lint-and-build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version-file: .nvmrc
      - name: Setup Bun
        uses: oven-sh/setup-bun@v2
        with:
          bun-version-file: .bun-version
      - name: Install root dependencies
        run: bun install --frozen-lockfile
      - name: Install landing dependencies
        working-directory: landing
        run: bun install --frozen-lockfile
      - name: Lint
        run: bun run lint
      - name: Build
        run: bun run build

```

--------------------------------------------------------------------------------
/landing/icons/github.svg:
--------------------------------------------------------------------------------

```
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
```

--------------------------------------------------------------------------------
/mcp-client/src/logger.ts:
--------------------------------------------------------------------------------

```typescript
import chalk from 'chalk';

type LoggingMode = 'verbose' | 'error' | 'none';

export type LoggerOptions = {
  mode: LoggingMode;
};

export const consoleStyles = {
  prompt: chalk.green('You: '),
  assistant: chalk.blue('Claude: '),
  tool: {
    name: chalk.cyan.bold,
    args: chalk.yellow,
    bracket: chalk.dim,
  },
  error: chalk.red,
  info: chalk.blue,
  success: chalk.green,
  warning: chalk.yellow,
  separator: chalk.gray('─'.repeat(50)),
  default: chalk,
};

export class Logger {
  private mode: LoggingMode = 'verbose';

  constructor({ mode }: LoggerOptions) {
    this.mode = mode;
  }

  log(
    message: string,
    options?: { type?: 'info' | 'error' | 'success' | 'warning' },
  ) {
    if (this.mode === 'none') return;
    if (this.mode === 'error' && options?.type !== 'error') return;

    process.stdout.write(consoleStyles[options?.type ?? 'default'](message));
  }
}

```

--------------------------------------------------------------------------------
/landing/app/layout.tsx:
--------------------------------------------------------------------------------

```typescript
import type { ReactNode } from 'react';
import type { Metadata } from 'next';
import Head from 'next/head';
import { Geist, Geist_Mono } from 'next/font/google';

import { ThemeProvider } from '@/components/ThemeProvider';

import './globals.css';

const geistSans = Geist({
  variable: '--font-geist-sans',
  subsets: ['latin'],
});

const geistMono = Geist_Mono({
  variable: '--font-geist-mono',
  subsets: ['latin'],
});

export const metadata: Metadata = {
  title: 'Neon MCP',
  description: 'Learn how to use Neon MCP',
};

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <Head>
        <link rel="icon" href="/favicon.ico" type="image/x-icon" />
      </Head>
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}

```

--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------

```typescript
import winston from 'winston';
import morgan from 'morgan';
import { Request, Response, NextFunction } from 'express';

const loggerFormat = winston.format.combine(
  winston.format.timestamp(),
  winston.format.simple(),
  winston.format.errors({ stack: true }),
  winston.format.align(),
  winston.format.colorize(),
);
// Configure Winston logger
export const logger = winston.createLogger({
  level: 'info',
  format: loggerFormat,
  transports: [
    new winston.transports.Console({
      format: loggerFormat,
    }),
  ],
});

// Configure Morgan for HTTP request logging
export const morganConfig = morgan('combined', {
  stream: {
    write: (message: string) => logger.info(message.trim()),
  },
});

// Configure error handling middleware
export const errorHandler = (
  err: Error,
  req: Request,
  res: Response,
  next: NextFunction,
) => {
  logger.error('Error:', { error: err.message, stack: err.stack });
  next(err);
};

```

--------------------------------------------------------------------------------
/landing/components/CopyableUrl.tsx:
--------------------------------------------------------------------------------

```typescript
'use client';

import { useState } from 'react';

export const CopyableUrl = ({ url }: { url: string }) => {
  const [copied, setCopied] = useState(false);

  const handleCopy = async () => {
    try {
      await navigator.clipboard.writeText(url);
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    } catch (err) {
      console.error('Failed to copy:', err);
    }
  };

  return (
    <div className="my-2 relative">
      <div className="monospaced whitespace-pre-wrap bg-secondary px-3 py-2 border-l-4 border-primary/20 rounded-r-md flex items-center justify-between group">
        <span className="text-sm">{url}</span>
        <button
          onClick={handleCopy}
          className="ml-3 px-2 py-1 text-xs bg-primary/10 hover:bg-primary/20 rounded transition-colors opacity-0 group-hover:opacity-100"
          title="Copy to clipboard"
        >
          {copied ? 'Copied!' : 'Copy'}
        </button>
      </div>
    </div>
  );
};

```

--------------------------------------------------------------------------------
/landing/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "neon-mcp-landing",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@radix-ui/react-accordion": "^1.2.10",
    "@radix-ui/react-slot": "^1.2.2",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "lodash": "^4.17.21",
    "lucide-react": "^0.511.0",
    "next": "15.3.2",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-syntax-highlighter": "^15.6.1",
    "tailwind-merge": "^3.3.0"
  },
  "devDependencies": {
    "@eslint/eslintrc": "^3",
    "@tailwindcss/postcss": "^4",
    "@types/lodash": "^4.17.16",
    "@types/node": "^20",
    "@types/react": "19.1.8",
    "@types/react-dom": "^19",
    "@types/react-syntax-highlighter": "^15.5.13",
    "eslint": "^9",
    "eslint-config-next": "15.3.2",
    "tailwindcss": "^4",
    "tw-animate-css": "^1.3.0",
    "typescript": "^5"
  }
}

```

--------------------------------------------------------------------------------
/landing/components/CodeSnippet.tsx:
--------------------------------------------------------------------------------

```typescript
'use client';

import { Suspense } from 'react';
import dynamic from 'next/dynamic';
import {
  docco,
  stackoverflowDark,
} from 'react-syntax-highlighter/dist/esm/styles/hljs';
import { useTheme } from '@/components/ThemeProvider';

const SyntaxHighlighter = dynamic(
  () => import('react-syntax-highlighter').then((module) => module.default),
  {
    ssr: false,
  },
);

type Props = {
  type?: string;
  children: string;
};

export const CodeSnippet = ({ type, children }: Props) => {
  const theme = useTheme();

  return (
    <div className="my-2">
      <Suspense
        fallback={
          <div className="monospaced whitespace-pre-wrap bg-secondary px-2 py-[0.5em] border-l-4">
            {children}
          </div>
        }
      >
        <SyntaxHighlighter
          language={type}
          wrapLongLines
          style={theme === 'light' ? docco : stackoverflowDark}
        >
          {children}
        </SyntaxHighlighter>
      </Suspense>
    </div>
  );
};

```

--------------------------------------------------------------------------------
/src/tools/types.ts:
--------------------------------------------------------------------------------

```typescript
import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Api } from '@neondatabase/api-client';

import { NEON_TOOLS } from './definitions.js';
import { AuthContext } from '../types/auth.js';

// Extract the tool names as a union type
type NeonToolName = (typeof NEON_TOOLS)[number]['name'];
export type ToolParams<T extends NeonToolName> = Extract<
  (typeof NEON_TOOLS)[number],
  { name: T }
>['inputSchema'];

export type ToolHandler<T extends NeonToolName> = ToolCallback<{
  params: ToolParams<T>;
}>;

export type ToolHandlerExtraParams = Parameters<
  ToolHandler<NeonToolName>
>['1'] & { account: AuthContext['extra']['account'] };

export type ToolHandlerExtended<T extends NeonToolName> = (
  ...args: [
    args: Parameters<ToolHandler<T>>['0'],
    neonClient: Api<unknown>,
    extra: ToolHandlerExtraParams,
  ]
) => ReturnType<ToolHandler<T>>;

// Create a type for the tool handlers that directly maps each tool to its appropriate input schema
export type ToolHandlers = {
  [K in NeonToolName]: ToolHandlerExtended<K>;
};

```

--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------

```typescript
import { config } from 'dotenv';

config();

export type Environment = 'development' | 'production' | 'preview';
export const NEON_DEFAULT_DATABASE_NAME = 'neondb';

export const NODE_ENV = (process.env.NODE_ENV ?? 'production') as Environment;
export const IS_DEV = NODE_ENV === 'development';
export const SERVER_PORT = 3001;
export const SERVER_HOST =
  process.env.SERVER_HOST ?? `http://localhost:${SERVER_PORT}`;
export const CLIENT_ID = process.env.CLIENT_ID ?? '';
export const CLIENT_SECRET = process.env.CLIENT_SECRET ?? '';
export const UPSTREAM_OAUTH_HOST =
  process.env.UPSTREAM_OAUTH_HOST ?? 'https://oauth2.neon.tech';
export const REDIRECT_URI = `${SERVER_HOST}/callback`;
export const NEON_API_HOST =
  process.env.NEON_API_HOST ?? 'https://console.neon.tech/api/v2';
export const COOKIE_SECRET = process.env.COOKIE_SECRET ?? '';
export const ANALYTICS_WRITE_KEY =
  process.env.ANALYTICS_WRITE_KEY ?? 'gFVzt8ozOp6AZRXoD0g0Lv6UQ6aaoS7O';
export const SENTRY_DSN =
  process.env.SENTRY_DSN ??
  'https://[email protected]/4509328350380033';

```

--------------------------------------------------------------------------------
/mcp-client/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "@neondatabase/mcp-client-cli",
  "version": "0.1.1",
  "description": "MCP client CLI for interacting with a MCP server",
  "license": "MIT",
  "author": "Neon, Inc. (https://neon.tech/)",
  "homepage": "https://github.com/neondatabase/mcp-server-neon/",
  "bugs": "https://github.com/neondatabase/mcp-server-neon/issues",
  "type": "module",
  "access": "public",
  "bin": {
    "mcp-client": "./dist/bin.js"
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "start:mcp-server-neon": "cd .. && bun run build && cd - && bun ./src/neon-cli-client.ts",
    "build": "tsc && node -e \"require('fs').chmodSync('dist/index.js', '755')\"",
    "prepare": "npm run build",
    "watch": "tsc-watch --onSuccess \"chmod 755 dist/index.js\"",
    "format": "prettier --write ."
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.32.1",
    "@modelcontextprotocol/sdk": "^1.0.3",
    "chalk": "^5.3.0",
    "dotenv": "16.4.7",
    "zod": "^3.24.1"
  },
  "devDependencies": {
    "@types/node": "^22.10.2",
    "bun": "^1.1.38",
    "prettier": "^3.4.1",
    "tsc-watch": "^6.2.1",
    "typescript": "^5.7.2"
  }
}

```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
# Use the imbios/bun-node image as the base image with Node and Bun
# Keep bun and node version in sync with package.json
FROM imbios/bun-node:1.1.38-18-alpine AS builder

# Set the working directory in the container
WORKDIR /app

# Copy package.json and package-lock.json
COPY package.json package-lock.json ./

# Copy the entire project to the working directory
COPY . .

# Install the dependencies and devDependencies
RUN npm install

# Build the project
RUN npm run build

# Use a smaller base image for the final image
FROM node:18-alpine AS release

# Set the working directory
WORKDIR /app

# Copy only the necessary files from the builder stage
COPY --from=builder /app/dist /app/dist
COPY --from=builder /app/package.json /app/package.json
COPY --from=builder /app/package-lock.json /app/package-lock.json

# Install only production dependencies
RUN npm ci --omit=dev

# Define environment variables
ENV NODE_ENV=production

# Specify the command to run the MCP server
ENTRYPOINT ["node", "dist/index.js", "start", "$NEON_API_KEY"]

```

--------------------------------------------------------------------------------
/src/tools-evaluations/evalUtils.ts:
--------------------------------------------------------------------------------

```typescript
import { createApiClient } from '@neondatabase/api-client';
import path from 'path';
import { MCPClient } from '../../mcp-client/src/index.js';

export async function deleteNonDefaultBranches(projectId: string) {
  const neonClient = createApiClient({
    apiKey: process.env.NEON_API_KEY!,
  });

  try {
    const allBranches = await neonClient.listProjectBranches({
      projectId: projectId,
    });

    const branchesToDelete = allBranches.data.branches.filter(
      (b) => !b.default,
    );

    await Promise.all(
      branchesToDelete.map((b) =>
        neonClient.deleteProjectBranch(b.project_id, b.id),
      ),
    );
  } catch (e) {
    console.error(e);
  }
}

export async function evaluateTask(input: string) {
  const client = new MCPClient({
    command: path.resolve(__dirname, '../../dist/index.js'),
    args: ['start', process.env.NEON_API_KEY!],
    loggerOptions: {
      mode: 'error',
    },
  });

  await client.start();
  const response = await client.processQuery(input);
  await client.stop();

  if (!response) {
    throw new Error('No response from MCP Client');
  }

  return response;
}

```

--------------------------------------------------------------------------------
/mcp-client/src/bin.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

import { parseArgs } from 'node:util';
import { MCPClientCLI } from './cli-client.js';

function checkRequiredEnvVars() {
  if (!process.env.ANTHROPIC_API_KEY) {
    console.error(
      '\x1b[31mError: ANTHROPIC_API_KEY environment variable is required\x1b[0m',
    );
    console.error('Please set it before running the CLI:');
    console.error('  export ANTHROPIC_API_KEY=your_key_here');
    process.exit(1);
  }
}

async function main() {
  try {
    checkRequiredEnvVars();

    const args = parseArgs({
      options: {
        'server-command': { type: 'string' },
        'server-args': { type: 'string' },
      },
      allowPositionals: true,
    });

    const serverCommand = args.values['server-command'];
    const serverArgs = args.values['server-args']?.split(' ') || [];

    if (!serverCommand) {
      console.error('Error: --server-command is required');
      process.exit(1);
    }

    const cli = new MCPClientCLI({
      command: serverCommand,
      args: serverArgs,
    });

    await cli.start();
  } catch (error) {
    console.error('Failed to start CLI:', error);
    process.exit(1);
  }
}

main();

```

--------------------------------------------------------------------------------
/landing/components/ThemeProvider.tsx:
--------------------------------------------------------------------------------

```typescript
'use client';

import {
  createContext,
  ReactNode,
  useContext,
  useLayoutEffect,
  useState,
} from 'react';

export type Theme = 'light' | 'dark';

type ThemeProviderState = {
  theme: Theme;
};

const ThemeContext = createContext<ThemeProviderState>({
  theme: 'light',
});

export const ThemeProvider = ({ children }: { children?: ReactNode }) => {
  const [themeState, setThemeState] = useState<ThemeProviderState>({
    theme: 'light',
  });

  useLayoutEffect(() => {
    const match = window.matchMedia('(prefers-color-scheme:dark)');

    function onChange(event: { matches: boolean }) {
      setThemeState((themeState) => {
        const targetTheme = event.matches ? 'dark' : 'light';

        if (themeState.theme === targetTheme) {
          return themeState;
        }

        return {
          ...themeState,
          theme: targetTheme,
        };
      });
    }

    onChange(match);

    match.addEventListener('change', onChange);
    return () => {
      match.removeEventListener('change', onChange);
    };
  }, []);

  return <ThemeContext value={themeState}>{children}</ThemeContext>;
};

export function useTheme(): Theme {
  return useContext(ThemeContext).theme;
}

```

--------------------------------------------------------------------------------
/src/tools/state.ts:
--------------------------------------------------------------------------------

```typescript
import { Branch } from '@neondatabase/api-client';

type MigrationId = string;
export type MigrationDetails = {
  migrationSql: string;
  databaseName: string;
  appliedBranch: Branch;
  roleName?: string;
};

type TuningId = string;
export type TuningDetails = {
  sql: string;
  databaseName: string;
  tuningBranch: Branch;
  roleName?: string;
  originalPlan?: any;
  suggestedChanges?: string[];
  improvedPlan?: any;
};

const migrationsState = new Map<MigrationId, MigrationDetails>();
const tuningState = new Map<TuningId, TuningDetails>();

export function getMigrationFromMemory(migrationId: string) {
  return migrationsState.get(migrationId);
}

export function persistMigrationToMemory(
  migrationId: string,
  migrationDetails: MigrationDetails,
) {
  migrationsState.set(migrationId, migrationDetails);
}

export function getTuningFromMemory(tuningId: string) {
  return tuningState.get(tuningId);
}

export function persistTuningToMemory(
  tuningId: string,
  tuningDetails: TuningDetails,
) {
  tuningState.set(tuningId, tuningDetails);
}

export function updateTuningInMemory(
  tuningId: string,
  updates: Partial<TuningDetails>,
) {
  const existing = tuningState.get(tuningId);
  if (existing) {
    tuningState.set(tuningId, { ...existing, ...updates });
  }
}

```

--------------------------------------------------------------------------------
/.github/workflows/koyeb-prod.yml:
--------------------------------------------------------------------------------

```yaml
name: Build and deploy backend to production

on:
  workflow_dispatch:
  push:
    branches:
      - 'main'

jobs:
  deploy:
    concurrency:
      group: '${{ github.ref_name }}'
      cancel-in-progress: true
    runs-on: ubuntu-latest
    # Only main branch is allowed to deploy to production
    if: github.ref == 'refs/heads/main'
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Install and configure the Koyeb CLI
        uses: koyeb-community/koyeb-actions@v2
        with:
          api_token: '${{ secrets.KOYEB_PROD_TOKEN }}'
      - name: Build and deploy to Koyeb production
        run: |
          koyeb deploy . platform-${{ github.ref_name }}/main \
            --instance-type medium \
            --region was \
            --archive-builder docker \
            --archive-docker-dockerfile remote.Dockerfile \
            --privileged \
            --type web \
            --port 3001:http \
            --route /:3001 \
            --wait \
            --env CLIENT_ID=${{secrets.PROD_CLIENT_ID}} \
            --env CLIENT_SECRET=${{secrets.PROD_CLIENT_SECRET}} \
            --env OAUTH_DATABASE_URL=${{secrets.PROD_OAUTH_DATABASE_URL}} \
            --env SERVER_HOST=${{vars.PROD_SERVER_HOST}} \
            --env NEON_API_HOST=${{vars.PROD_NEON_API_HOST}} \
            --env UPSTREAM_OAUTH_HOST=${{vars.PROD_OAUTH_HOST}} \
            --env COOKIE_SECRET=${{secrets.COOKIE_SECRET}} \

```

--------------------------------------------------------------------------------
/scripts/before-publish.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

import fs from 'fs';
import path from 'path';
import { execSync } from 'child_process';

function checkMainBranch(version) {
  // Skip main branch check for beta versions
  if (version.includes('beta')) {
    return;
  }

  try {
    const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
      encoding: 'utf8',
    }).trim();

    if (currentBranch !== 'main') {
      console.error(
        '\x1b[31mError: Publishing stable versions is only allowed from the main branch\x1b[0m',
      );
      console.error(`Current branch: ${currentBranch}`);
      process.exit(1);
    }
  } catch (error) {
    console.error('Error: Git repository not found');
    process.exit(1);
  }
}

function checkChangelog() {
  const changelogPath = path.join(__dirname, '../CHANGELOG.md');
  const packagePath = path.join(__dirname, '../package.json');

  const { version } = require(packagePath);

  try {
    const changelog = fs.readFileSync(changelogPath, 'utf8');
    if (!changelog.includes(version)) {
      console.error(
        `\x1b[31mError: Version ${version} not found in CHANGELOG.md\x1b[0m`,
      );
      console.error('Please update the changelog before publishing');
      process.exit(1);
    }
    return version;
  } catch (err) {
    console.error('\x1b[31mError: CHANGELOG.md not found\x1b[0m');
    process.exit(1);
  }
}

function beforePublish() {
  const version = checkChangelog();
  checkMainBranch(version);
}

beforePublish();

```

--------------------------------------------------------------------------------
/landing/components/Introduction.tsx:
--------------------------------------------------------------------------------

```typescript
import { cn } from '@/lib/utils';
import { ExternalLink } from '@/components/ExternalLink';
import { CopyableUrl } from '@/components/CopyableUrl';

export const Introduction = ({ className }: { className?: string }) => (
  <div className={cn('flex flex-col gap-2', className)}>
    <desc className="text-xl mb-2">
      Manage your Neon Postgres databases with natural language.
    </desc>

    <CopyableUrl url="https://mcp.neon.tech/mcp" />

    <div>
      The <strong className="font-semibold">Neon MCP Server</strong> lets AI
      agents and dev tools like Cursor interact with Neon by translating plain
      English into{' '}
      <ExternalLink href="https://api-docs.neon.tech/reference/getting-started-with-neon-api">
        Neon API
      </ExternalLink>{' '}
      calls—no code required. You can create databases, run queries, and make
      schema changes just by typing commands like "Create a database named
      'my-new-database'" or "List all my Neon projects".
    </div>
    <div>
      Built on the{' '}
      <ExternalLink href="https://modelcontextprotocol.org/">
        Model Context Protocol (MCP)
      </ExternalLink>
      , the server bridges natural language and the Neon API to support actions
      like creating projects, managing branches, running queries, and handling
      migrations.
      <br />
      <ExternalLink href="https://neon.tech/docs/ai/neon-mcp-server">
        Learn more in the docs
      </ExternalLink>
    </div>
  </div>
);

```

--------------------------------------------------------------------------------
/remote.Dockerfile:
--------------------------------------------------------------------------------

```dockerfile

# Use the imbios/bun-node image as the base image with Node and Bun
# Keep bun and node version in sync with package.json
ARG NODE_VERSION=22.0.0
ARG BUN_VERSION=1.2.13
FROM imbios/bun-node:1.2.13-22-slim AS base

# Set the working directory in the container
WORKDIR /app

# Set production environment
ENV NODE_ENV="production"

# Throw-away build stage to reduce size of final image
FROM base As builder

# Install packages needed to build node modules
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y build-essential node-gyp pkg-config python-is-python3

# Copy package.json and package-lock.json
COPY package.json package-lock.json ./

# Install the root dependencies and devDependencies
RUN npm ci --include=dev

# Copy landing's package.json and package-lock.json
COPY landing/package.json landing/package-lock.json ./landing/

# Install the landing dependencies and devDependencies
RUN cd landing/ && npm ci --include=dev

# Copy the entire project to the working directory
COPY . .

# Build the project
RUN npm run build

# Remove development dependencies
RUN npm prune --omit=dev

# We don't need Next.js dependencies since landing is statically exported during build step.
RUN rm -rf landing/node_modules

# Final stage for app image
FROM base

# Copy built application
COPY --from=builder /app /app


# Define environment variables
ENV NODE_ENV=production

EXPOSE 3001
# Specify the command to run the MCP server
CMD ["node", "dist/index.js", "start:sse"]

```

--------------------------------------------------------------------------------
/landing/components/Header.tsx:
--------------------------------------------------------------------------------

```typescript
import Image from 'next/image';

import { Button } from '@/components/ui/button';
import githubSvg from '@/icons/github.svg';
import neonSvg from '@/icons/neon.svg';

type HeaderProps = {
  packageVersion: number;
};

export const Header = ({ packageVersion }: HeaderProps) => (
  <header className="flex items-center justify-between gap-2">
    <div className="flex items-center gap-3">
      <Image src={neonSvg} width={30} height={30} alt="Neon logo" />
      <div className="flex items-baseline gap-2">
        <h1 className="text-3xl font-bold whitespace-nowrap">Neon MCP</h1>{' '}
        version: {packageVersion}
      </div>
    </div>
    <div className="flex items-center gap-2">
      <a
        href="https://cursor.com/install-mcp?name=Neon&config=eyJ1cmwiOiJodHRwczovL21jcC5uZW9uLnRlY2gvbWNwIn0%3D"
        target="_blank"
        rel="noopener noreferrer"
      >
        <Image
          alt="Add to Cursor"
          src="https://cursor.com/deeplink/mcp-install-light.svg"
          className="invert dark:invert-0"
          width={126}
          height={32}
        />
      </a>

      <Button asChild size="xs">
        <a
          href="https://github.com/neondatabase-labs/mcp-server-neon?tab=readme-ov-file"
          target="_blank"
          rel="noopener noreferrer"
        >
          <Image
            alt=""
            src={githubSvg}
            className="invert dark:invert-0"
            width={16}
            height={16}
          />{' '}
          Github
        </a>
      </Button>
    </div>
  </header>
);

```

--------------------------------------------------------------------------------
/landing/components/ui/alert.tsx:
--------------------------------------------------------------------------------

```typescript
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';

import { cn } from '@/lib/utils';

const alertVariants = cva(
  'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-3 [&>svg]:text-foreground [&>svg~*]:pl-7',
  {
    variants: {
      variant: {
        default: 'bg-background text-foreground',
        important:
          'border-important-notes/50 text-important-notes dark:border-important-notes [&>svg]:text-important-notes',
        destructive:
          'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
      },
    },
    defaultVariants: {
      variant: 'default',
    },
  },
);

export type AlertVariant = VariantProps<typeof alertVariants>['variant'];

export function Alert({
  className,
  variant,
  ...props
}: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
  return (
    <div
      role="alert"
      className={cn(alertVariants({ variant }), className)}
      {...props}
    />
  );
}

export function AlertTitle({
  className,
  ...props
}: React.ComponentProps<'div'>) {
  return (
    <div
      className={cn(
        'py-1 mb-1 font-medium leading-none tracking-tight',
        className,
      )}
      {...props}
    />
  );
}

export function AlertDescription({
  className,
  ...props
}: React.ComponentProps<'div'>) {
  return (
    <div
      className={cn('text-sm [&_p]:leading-relaxed', className)}
      {...props}
    />
  );
}

```

--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------

```javascript
import js from '@eslint/js';
import ts from 'typescript-eslint';
import prettierConfig from 'eslint-config-prettier';

// @ts-check
export default ts.config(
  {
    files: ['**/*.ts', '**/*.cts', '**.*.mts'],
    ignores: [
      '**/*.js',
      '**/*.gen.ts',
      // see https://eslint.org/docs/latest/use/configure/configuration-files#globally-ignoring-files-with-ignores
      'src/tools-evaluations/**/*',
      'landing/**/*',
    ],
    rules: {
      'no-console': 'off',
      '@typescript-eslint/consistent-type-definitions': ['error', 'type'],
      '@typescript-eslint/no-explicit-any': 'off',
      '@typescript-eslint/no-unsafe-member-access': 'off',
      '@typescript-eslint/no-unsafe-argument': 'off',
      '@typescript-eslint/no-unsafe-assignment': 'off',
      '@typescript-eslint/no-unsafe-return': 'off',
      '@typescript-eslint/no-unsafe-call': 'off',
      '@typescript-eslint/non-nullable-type-assertion-style': 'off',
      '@typescript-eslint/prefer-nullish-coalescing': 'off',
      '@typescript-eslint/no-unnecessary-condition': 'off',
      '@typescript-eslint/restrict-template-expressions': [
        'error',
        {
          allowAny: true,
          allowBoolean: true,
          allowNullish: true,
          allowNumber: true,
          allowRegExp: true,
        },
      ],
    },
    languageOptions: {
      parserOptions: {
        project: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
    extends: [
      js.configs.recommended,
      ...ts.configs.strictTypeChecked,
      ...ts.configs.stylisticTypeChecked,
    ],
  },
  prettierConfig,
);

```

--------------------------------------------------------------------------------
/mcp-client/src/cli-client.ts:
--------------------------------------------------------------------------------

```typescript
import { StdioServerParameters } from '@modelcontextprotocol/sdk/client/stdio.js';
import readline from 'readline/promises';
import { MCPClient } from './index.js';
import { consoleStyles, Logger } from './logger.js';

const EXIT_COMMAND = 'exit';

export class MCPClientCLI {
  private rl: readline.Interface;
  private client: MCPClient;
  private logger: Logger;

  constructor(serverConfig: StdioServerParameters) {
    this.client = new MCPClient(serverConfig);
    this.logger = new Logger({ mode: 'verbose' });

    this.rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout,
    });
  }

  async start() {
    try {
      this.logger.log(consoleStyles.separator + '\n', { type: 'info' });
      this.logger.log('🤖 Interactive Claude CLI\n', { type: 'info' });
      this.logger.log(`Type your queries or "${EXIT_COMMAND}" to exit\n`, {
        type: 'info',
      });
      this.logger.log(consoleStyles.separator + '\n', { type: 'info' });
      this.client.start();

      await this.chat_loop();
    } catch (error) {
      this.logger.log('Failed to initialize tools: ' + error + '\n', {
        type: 'error',
      });
      process.exit(1);
    } finally {
      this.rl.close();
      process.exit(0);
    }
  }

  private async chat_loop() {
    while (true) {
      try {
        const query = (await this.rl.question(consoleStyles.prompt)).trim();
        if (query.toLowerCase() === EXIT_COMMAND) {
          this.logger.log('\nGoodbye! 👋\n', { type: 'warning' });
          break;
        }

        await this.client.processQuery(query);
        this.logger.log('\n' + consoleStyles.separator + '\n');
      } catch (error) {
        this.logger.log('\nError: ' + error + '\n', { type: 'error' });
      }
    }
  }
}

```

--------------------------------------------------------------------------------
/src/server/errors.ts:
--------------------------------------------------------------------------------

```typescript
import { isAxiosError } from 'axios';
import { NeonDbError } from '@neondatabase/serverless';
import { logger } from '../utils/logger.js';
import { captureException } from '@sentry/node';

export class InvalidArgumentError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'InvalidArgumentError';
  }
}

export class NotFoundError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'NotFoundError';
  }
}

export function isClientError(
  error: unknown,
): error is InvalidArgumentError | NotFoundError {
  return (
    error instanceof InvalidArgumentError || error instanceof NotFoundError
  );
}

export function errorResponse(error: unknown) {
  return {
    isError: true,
    content: [
      {
        type: 'text' as const,
        text:
          error instanceof Error
            ? `${error.name}: ${error.message}`
            : 'Unknown error',
      },
    ],
  };
}

export function handleToolError(
  error: unknown,
  properties: Record<string, string>,
) {
  if (error instanceof NeonDbError || isClientError(error)) {
    return errorResponse(error);
  } else if (
    isAxiosError(error) &&
    error.response?.status &&
    error.response?.status < 500
  ) {
    return {
      isError: true,
      content: [
        {
          type: 'text' as const,
          text: error.response.data.message,
        },
        {
          type: 'text' as const,
          text: `[${error.response.statusText}] ${error.message}`,
        },
      ],
    };
  } else {
    logger.error('Tool call error:', {
      error:
        error instanceof Error
          ? `${error.name}: ${error.message}`
          : 'Unknown error',
      properties,
    });
    captureException(error, { extra: properties });
    return errorResponse(error);
  }
}

```

--------------------------------------------------------------------------------
/src/analytics/analytics.ts:
--------------------------------------------------------------------------------

```typescript
import { Analytics } from '@segment/analytics-node';
import { ANALYTICS_WRITE_KEY } from '../constants.js';
import { Api, AuthDetailsResponse } from '@neondatabase/api-client';
import { AuthContext } from '../types/auth.js';

let analytics: Analytics | undefined;
type Account = AuthContext['extra']['account'];
export const initAnalytics = () => {
  if (ANALYTICS_WRITE_KEY) {
    analytics = new Analytics({
      writeKey: ANALYTICS_WRITE_KEY,
      host: 'https://track.neon.tech',
    });
  }
};

export const identify = (
  account: Account | null,
  params: Omit<Parameters<Analytics['identify']>[0], 'userId' | 'anonymousId'>,
) => {
  if (account) {
    analytics?.identify({
      ...params,
      userId: account.id,
      traits: {
        name: account.name,
        email: account.email,
        isOrg: account.isOrg,
      },
    });
  } else {
    analytics?.identify({
      ...params,
      anonymousId: 'anonymous',
    });
  }
};

export const track = (params: Parameters<Analytics['track']>[0]) => {
  analytics?.track(params);
};

/**
 * Util for identifying the user based on the auth method. If the api key belongs to an organization, identify the organization instead of user details.
 */
export const identifyApiKey = async (
  auth: AuthDetailsResponse,
  neonClient: Api<unknown>,
  params: Omit<Parameters<Analytics['identify']>[0], 'userId' | 'anonymousId'>,
) => {
  if (auth.auth_method === 'api_key_org') {
    const { data: org } = await neonClient.getOrganization(auth.account_id);
    const account = {
      id: auth.account_id,
      name: org.name,
      isOrg: true,
    };
    identify(account, params);
    return account;
  }
  const { data: user } = await neonClient.getCurrentUserInfo();
  const account = {
    id: user.id,
    name: user.name,
    email: user.email,
    isOrg: false,
  };
  identify(account, params);
  return account;
};

```

--------------------------------------------------------------------------------
/src/oauth/kv-store.ts:
--------------------------------------------------------------------------------

```typescript
import { KeyvPostgres, KeyvPostgresOptions } from '@keyv/postgres';
import { logger } from '../utils/logger.js';
import { AuthorizationCode, Client, Token } from 'oauth2-server';
import Keyv from 'keyv';
import { AuthContext } from '../types/auth.js';
import { AuthDetailsResponse } from '@neondatabase/api-client';

const SCHEMA = 'mcpauth';

const createKeyv = <T>(options: KeyvPostgresOptions) =>
  new Keyv<T>({ store: new KeyvPostgres(options) });

export const clients = createKeyv<Client>({
  connectionString: process.env.OAUTH_DATABASE_URL,
  schema: SCHEMA,
  table: 'clients',
});

clients.on('error', (err) => {
  logger.error('Clients keyv error:', { err });
});

export const tokens = createKeyv<Token>({
  connectionString: process.env.OAUTH_DATABASE_URL,
  schema: SCHEMA,
  table: 'tokens',
});

tokens.on('error', (err) => {
  logger.error('Tokens keyv error:', { err });
});

export type RefreshToken = {
  refreshToken: string;
  refreshTokenExpiresAt?: Date | undefined;
  accessToken: string;
};

export const refreshTokens = createKeyv<RefreshToken>({
  connectionString: process.env.OAUTH_DATABASE_URL,
  schema: SCHEMA,
  table: 'refresh_tokens',
});

refreshTokens.on('error', (err) => {
  logger.error('Refresh tokens keyv error:', { err });
});

export const authorizationCodes = createKeyv<AuthorizationCode>({
  connectionString: process.env.OAUTH_DATABASE_URL,
  schema: SCHEMA,
  table: 'authorization_codes',
});

authorizationCodes.on('error', (err) => {
  logger.error('Authorization codes keyv error:', { err });
});

export type ApiKeyRecord = {
  apiKey: string;
  authMethod: AuthDetailsResponse['auth_method'];
  account: AuthContext['extra']['account'];
};

export const apiKeys = createKeyv<ApiKeyRecord>({
  connectionString: process.env.OAUTH_DATABASE_URL,
  schema: SCHEMA,
  table: 'api_keys',
});

apiKeys.on('error', (err) => {
  logger.error('API keys keyv error:', { err });
});

```

--------------------------------------------------------------------------------
/src/views/approval-dialog.pug:
--------------------------------------------------------------------------------

```
- var clientName = client.client_name || 'A new MCP Client'
- var logo = client.logo || client.logo_url || 'https://placehold.co/100x100/EEE/31343C?font=montserrat&text=MCP Client'
- var website = client.client_uri || client.website 
- var redirectUris = client.redirect_uris
- var serverName = 'Neon MCP Server'

html(lang='en')
  head
    meta(charset='utf-8')
    meta(name='viewport', content='width=device-width, initial-scale=1')
    style
      include styles.css
    title #{clientName} | Authorization Request
  body
    div(class='container')
      div(class='precard')
        a(class="header", href='/', target='_blank')
          img(src='/logo.png', alt="Neon MCP", class="logo")
      div(class="card")
        h2(class="alert") 
          strong MCP Client Authorization Request
        div(class="client-info")
          div(class='client-detail')
            div(class='detail-label') Name:
            div(class='detail-value') #{clientName}
          if website
            div(class='client-detail')
              div(class='detail-label') Website:
              div(class='detail-value small')
                a(href=website, target='_blank' rel='noopener noreferrer') #{website}
          if redirectUris
            div(class='client-detail')
              div(class='detail-label') Redirect URIs:
              div(class='detail-value small')
                each uri in redirectUris
                  div #{uri}
        p(class="description") This MCP client is requesting to be authorized 
          | on #{serverName}. If you approve, you will be redirected to complete the authentication. 

        form(method='POST', action='/authorize')
          input(type='hidden', name='state', value=state)
          
          div(class='actions')
            button(type='button', class='button button-secondary' onclick='window.history.back()') Cancel
            button(type='submit', class='button button-primary') Approve

```

--------------------------------------------------------------------------------
/src/oauth/client.ts:
--------------------------------------------------------------------------------

```typescript
import { Request } from 'express';
import {
  discovery,
  buildAuthorizationUrl,
  authorizationCodeGrant,
  ClientSecretPost,
  refreshTokenGrant,
} from 'openid-client';
import {
  CLIENT_ID,
  CLIENT_SECRET,
  UPSTREAM_OAUTH_HOST,
  REDIRECT_URI,
  SERVER_HOST,
} from '../constants.js';
import { logger } from '../utils/logger.js';

const NEON_MCP_SCOPES = [
  'openid',
  'offline',
  'offline_access',
  'urn:neoncloud:projects:create',
  'urn:neoncloud:projects:read',
  'urn:neoncloud:projects:update',
  'urn:neoncloud:projects:delete',
  'urn:neoncloud:orgs:create',
  'urn:neoncloud:orgs:read',
  'urn:neoncloud:orgs:update',
  'urn:neoncloud:orgs:delete',
  'urn:neoncloud:orgs:permission',
] as const;

const getUpstreamConfig = async () => {
  const url = new URL(UPSTREAM_OAUTH_HOST);
  const config = await discovery(
    url,
    CLIENT_ID,
    {
      client_secret: CLIENT_SECRET,
    },
    ClientSecretPost(CLIENT_SECRET),
    {},
  );

  return config;
};

export const upstreamAuth = async (state: string) => {
  const config = await getUpstreamConfig();
  return buildAuthorizationUrl(config, {
    redirect_uri: REDIRECT_URI,
    token_endpoint_auth_method: 'client_secret_post',
    scope: NEON_MCP_SCOPES.join(' '),
    response_type: 'code',
    state,
  });
};

export const exchangeCode = async (req: Request) => {
  try {
    const config = await getUpstreamConfig();
    const currentUrl = new URL(req.originalUrl, SERVER_HOST);
    return await authorizationCodeGrant(config, currentUrl, {
      expectedState: req.query.state as string,
      idTokenExpected: true,
    });
  } catch (error: unknown) {
    logger.error('failed to exchange code:', {
      message: error instanceof Error ? error.message : 'Unknown error',
      error,
    });
    throw error;
  }
};

export const exchangeRefreshToken = async (token: string) => {
  const config = await getUpstreamConfig();
  return refreshTokenGrant(config, token);
};

```

--------------------------------------------------------------------------------
/src/resources.ts:
--------------------------------------------------------------------------------

```typescript
import { ReadResourceCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Resource } from '@modelcontextprotocol/sdk/types.js';

async function fetchRawGithubContent(rawPath: string) {
  const path = rawPath.replace('/blob', '');

  return fetch(`https://raw.githubusercontent.com${path}`).then((res) =>
    res.text(),
  );
}

export const NEON_RESOURCES = [
  {
    name: 'neon-auth',
    uri: 'https://github.com/neondatabase-labs/ai-rules/blob/main/neon-auth.mdc',
    mimeType: 'text/plain',
    description: 'Neon Auth usage instructions',
    handler: async (url) => {
      const uri = url.host;
      const rawPath = url.pathname;
      const content = await fetchRawGithubContent(rawPath);
      return {
        contents: [
          {
            uri: uri,
            mimeType: 'text/plain',
            text: content,
          },
        ],
      };
    },
  },
  {
    name: 'neon-serverless',
    uri: 'https://github.com/neondatabase-labs/ai-rules/blob/main/neon-serverless.mdc',
    mimeType: 'text/plain',
    description: 'Neon Serverless usage instructions',
    handler: async (url) => {
      const uri = url.host;
      const rawPath = url.pathname;
      const content = await fetchRawGithubContent(rawPath);
      return {
        contents: [
          {
            uri,
            mimeType: 'text/plain',
            text: content,
          },
        ],
      };
    },
  },
  {
    name: 'neon-drizzle',
    uri: 'https://github.com/neondatabase-labs/ai-rules/blob/main/neon-drizzle.mdc',
    mimeType: 'text/plain',
    description: 'Neon Drizzle usage instructions',
    handler: async (url) => {
      const uri = url.host;
      const rawPath = url.pathname;
      const content = await fetchRawGithubContent(rawPath);
      return {
        contents: [
          {
            uri,
            mimeType: 'text/plain',
            text: content,
          },
        ],
      };
    },
  },
] satisfies (Resource & { handler: ReadResourceCallback })[];

```

--------------------------------------------------------------------------------
/landing/components/ui/accordion.tsx:
--------------------------------------------------------------------------------

```typescript
'use client';

import * as React from 'react';
import * as AccordionPrimitive from '@radix-ui/react-accordion';
import { ChevronDownIcon } from 'lucide-react';

import { cn } from '@/lib/utils';

function Accordion({
  ...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
  return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
}

function AccordionItem({
  className,
  ...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
  return (
    <AccordionPrimitive.Item
      data-slot="accordion-item"
      className={cn('border-b last:border-b-0', className)}
      {...props}
    />
  );
}

function AccordionTrigger({
  className,
  children,
  ...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
  return (
    <AccordionPrimitive.Header className="flex">
      <AccordionPrimitive.Trigger
        data-slot="accordion-trigger"
        className={cn(
          'focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',
          className,
        )}
        {...props}
      >
        {children}
        <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
      </AccordionPrimitive.Trigger>
    </AccordionPrimitive.Header>
  );
}

function AccordionContent({
  className,
  children,
  ...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
  return (
    <AccordionPrimitive.Content
      data-slot="accordion-content"
      className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
      {...props}
    >
      <div className={cn('pt-0 pb-4', className)}>{children}</div>
    </AccordionPrimitive.Content>
  );
}

export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

```

--------------------------------------------------------------------------------
/landing/components/ui/button.tsx:
--------------------------------------------------------------------------------

```typescript
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';

import { cn } from '@/lib/utils';

const buttonVariants = cva(
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
  {
    variants: {
      variant: {
        default:
          'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
        destructive:
          'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
        outline:
          'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
        secondary:
          'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
        ghost:
          'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
        link: 'text-primary underline-offset-4 hover:underline',
      },
      size: {
        default: 'h-9 px-4 py-2 has-[>svg]:px-3',
        xs: 'h-7 px-2.5 has-[>svg]:px-2',
        sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
        lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
        icon: 'size-9',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  },
);

function Button({
  className,
  variant,
  size,
  asChild = false,
  ...props
}: React.ComponentProps<'button'> &
  VariantProps<typeof buttonVariants> & {
    asChild?: boolean;
  }) {
  const Comp = asChild ? Slot : 'button';

  return (
    <Comp
      data-slot="button"
      className={cn(buttonVariants({ variant, size, className }))}
      {...props}
    />
  );
}

export { Button, buttonVariants };

```

--------------------------------------------------------------------------------
/src/oauth/model.ts:
--------------------------------------------------------------------------------

```typescript
import {
  AuthorizationCode,
  AuthorizationCodeModel,
  Client,
  Token,
  User,
} from 'oauth2-server';
import {
  clients,
  tokens,
  refreshTokens,
  authorizationCodes,
  RefreshToken,
} from './kv-store.js';

class Model implements AuthorizationCodeModel {
  getClient: (
    clientId: string,
    clientSecret: string,
  ) => Promise<Client | undefined> = async (clientId) => {
    return clients.get(clientId);
  };
  saveClient: (client: Client) => Promise<Client> = async (client) => {
    await clients.set(client.id, client);
    return client;
  };
  saveToken: (token: Token) => Promise<Token> = async (token) => {
    await tokens.set(token.accessToken, token);
    return token;
  };
  deleteToken: (token: Token) => Promise<boolean> = async (token) => {
    return tokens.delete(token.accessToken);
  };
  saveRefreshToken: (token: RefreshToken) => Promise<RefreshToken> = async (
    token,
  ) => {
    await refreshTokens.set(token.refreshToken, token);
    return token;
  };
  deleteRefreshToken: (token: RefreshToken) => Promise<boolean> = async (
    token,
  ) => {
    return refreshTokens.delete(token.refreshToken);
  };

  validateScope: (
    user: User,
    client: Client,
    scope: string,
  ) => Promise<string> = (user, client, scope) => {
    // For demo purposes, accept all scopes
    return Promise.resolve(scope);
  };
  verifyScope: (token: Token, scope: string) => Promise<boolean> = () => {
    // For demo purposes, accept all scopes
    return Promise.resolve(true);
  };
  getAccessToken: (accessToken: string) => Promise<Token | undefined> = async (
    accessToken,
  ) => {
    const token = await tokens.get(accessToken);
    return token;
  };
  getRefreshToken: (refreshToken: string) => Promise<RefreshToken | undefined> =
    async (refreshToken) => {
      return refreshTokens.get(refreshToken);
    };
  saveAuthorizationCode: (
    code: AuthorizationCode,
  ) => Promise<AuthorizationCode> = async (code) => {
    await authorizationCodes.set(code.authorizationCode, code);
    return code;
  };
  getAuthorizationCode: (
    code: string,
  ) => Promise<AuthorizationCode | undefined> = async (code) => {
    return authorizationCodes.get(code);
  };
  revokeAuthorizationCode: (code: AuthorizationCode) => Promise<boolean> =
    async (code) => {
      return authorizationCodes.delete(code.authorizationCode);
    };
}

export const model = new Model();

```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

import { identifyApiKey, initAnalytics, track } from './analytics/analytics.js';
import { NODE_ENV } from './constants.js';
import { handleInit, parseArgs } from './initConfig.js';
import { createNeonClient, getPackageJson } from './server/api.js';
import { createMcpServer } from './server/index.js';
import { createSseTransport } from './transports/sse-express.js';
import { startStdio } from './transports/stdio.js';
import { logger } from './utils/logger.js';
import { AppContext } from './types/context.js';
import { NEON_TOOLS } from './tools/index.js';
import './utils/polyfills.js';

const args = parseArgs();
const appVersion = getPackageJson().version;
const appName = getPackageJson().name;

if (args.command === 'export-tools') {
  console.log(
    JSON.stringify(
      NEON_TOOLS.map((item) => ({ ...item, inputSchema: undefined })),
      null,
      2,
    ),
  );
  process.exit(0);
}

const appContext: AppContext = {
  environment: NODE_ENV,
  name: appName,
  version: appVersion,
  transport: 'stdio',
};

if (args.analytics) {
  initAnalytics();
}

if (args.command === 'start:sse') {
  createSseTransport({
    ...appContext,
    transport: 'sse',
  });
} else {
  // Turn off logger in stdio mode to avoid capturing stderr in wrong format by host application (Claude Desktop)
  logger.silent = true;

  try {
    const neonClient = createNeonClient(args.neonApiKey);
    const { data } = await neonClient.getAuthDetails();
    const accountId = data.account_id;

    const account = await identifyApiKey(data, neonClient, {
      context: appContext,
    });

    if (args.command === 'init') {
      track({
        userId: accountId,
        event: 'init_stdio',
        context: appContext,
      });
      handleInit({
        executablePath: args.executablePath,
        neonApiKey: args.neonApiKey,
        analytics: args.analytics,
      });
      process.exit(0);
    }

    if (args.command === 'start') {
      track({
        userId: accountId,
        event: 'start_stdio',
        context: appContext,
      });
      const server = createMcpServer({
        apiKey: args.neonApiKey,
        account,
        app: appContext,
      });
      await startStdio(server);
    }
  } catch (error) {
    console.error('Server error:', error);
    track({
      anonymousId: 'anonymous',
      event: 'server_error',
      properties: { error },
      context: appContext,
    });
    process.exit(1);
  }
}

```

--------------------------------------------------------------------------------
/src/transports/stream.ts:
--------------------------------------------------------------------------------

```typescript
import { Request, Response, Router } from 'express';
import { AppContext } from '../types/context.js';
import { createMcpServer } from '../server/index.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { logger } from '../utils/logger.js';
import { track } from '../analytics/analytics.js';
import { requiresAuth } from '../oauth/utils.js';

export const createStreamTransport = (appContext: AppContext) => {
  const router = Router();

  router.post('/', requiresAuth(), async (req: Request, res: Response) => {
    const auth = req.auth;
    if (!auth) {
      res.status(401).send('Unauthorized');
      return;
    }

    try {
      const server = createMcpServer({
        apiKey: auth.token,
        client: auth.extra.client,
        account: auth.extra.account,
        app: appContext,
      });

      const transport = new StreamableHTTPServerTransport({
        sessionIdGenerator: undefined,
      });
      res.on('close', () => {
        void transport.close();
        void server.close();
      });
      await server.connect(transport);
      await transport.handleRequest(req, res, req.body);
    } catch (error: unknown) {
      logger.error('Failed to connect to MCP server:', {
        message: error instanceof Error ? error.message : 'Unknown error',
        error,
      });
      track({
        userId: auth.extra.account.id,
        event: 'stream_connection_errored',
        properties: { error },
        context: {
          app: appContext,
          client: auth.extra.client,
        },
      });
      if (!res.headersSent) {
        res.status(500).json({
          jsonrpc: '2.0',
          error: {
            code: -32603,
            message: 'Internal server error',
          },
          id: null,
        });
      }
    }
  });

  router.get('/', requiresAuth(), (req: Request, res: Response) => {
    logger.info('Received GET MCP request');
    res.writeHead(405).end(
      JSON.stringify({
        jsonrpc: '2.0',
        error: {
          code: -32000,
          message: 'Method not allowed.',
        },
        id: null,
      }),
    );
  });

  router.delete('/', requiresAuth(), (req: Request, res: Response) => {
    logger.info('Received DELETE MCP request');
    res.writeHead(405).end(
      JSON.stringify({
        jsonrpc: '2.0',
        error: {
          code: -32000,
          message: 'Method not allowed.',
        },
        id: null,
      }),
    );
  });

  return router;
};

```

--------------------------------------------------------------------------------
/landing/icons/neon.svg:
--------------------------------------------------------------------------------

```
<svg width="26px" height="26px" viewBox="0 0 58 58" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-hidden="true"><g clip-path="url(#clip0_8136_183)"><path fill-rule="evenodd" clip-rule="evenodd" d="M0 10C0 4.47715 4.47705 0 9.99976 0H47.9989C53.5216 0 57.9986 4.47715 57.9986 10V42.3189C57.9986 48.0326 50.7684 50.5124 47.2618 46.0014L36.2991 31.8988V49C36.2991 53.9706 32.2698 58 27.2993 58H9.99976C4.47705 58 0 53.5228 0 48V10ZM9.99976 8C8.89522 8 7.99981 8.89543 7.99981 10V48C7.99981 49.1046 8.89522 50 9.99976 50H27.5993C28.1516 50 28.2993 49.5523 28.2993 49V26.0673C28.2993 20.3536 35.5295 17.8738 39.0361 22.3848L49.9988 36.4874V10C49.9988 8.89543 50.1034 8 48.9988 8H9.99976Z" fill="#32C0ED"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M0 10C0 4.47715 4.47705 0 9.99976 0H47.9989C53.5216 0 57.9986 4.47715 57.9986 10V42.3189C57.9986 48.0326 50.7684 50.5124 47.2618 46.0014L36.2991 31.8988V49C36.2991 53.9706 32.2698 58 27.2993 58H9.99976C4.47705 58 0 53.5228 0 48V10ZM9.99976 8C8.89522 8 7.99981 8.89543 7.99981 10V48C7.99981 49.1046 8.89522 50 9.99976 50H27.5993C28.1516 50 28.2993 49.5523 28.2993 49V26.0673C28.2993 20.3536 35.5295 17.8738 39.0361 22.3848L49.9988 36.4874V10C49.9988 8.89543 50.1034 8 48.9988 8H9.99976Z" fill="url(#paint0_linear_8136_183)"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M0 10C0 4.47715 4.47705 0 9.99976 0H47.9989C53.5216 0 57.9986 4.47715 57.9986 10V42.3189C57.9986 48.0326 50.7684 50.5124 47.2618 46.0014L36.2991 31.8988V49C36.2991 53.9706 32.2698 58 27.2993 58H9.99976C4.47705 58 0 53.5228 0 48V10ZM9.99976 8C8.89522 8 7.99981 8.89543 7.99981 10V48C7.99981 49.1046 8.89522 50 9.99976 50H27.5993C28.1516 50 28.2993 49.5523 28.2993 49V26.0673C28.2993 20.3536 35.5295 17.8738 39.0361 22.3848L49.9988 36.4874V10C49.9988 8.89543 50.1034 8 48.9988 8H9.99976Z" fill="url(#paint1_linear_8136_183)" fill-opacity="0.35"></path><path d="M48.0003 0C53.523 0 58 4.47715 58 10V42.3189C58 48.0326 50.7699 50.5124 47.2633 46.0014L36.3006 31.8988V49C36.3006 53.9706 32.2712 58 27.3008 58C27.8531 58 28.3008 57.5523 28.3008 57V26.0673C28.3008 20.3536 35.5309 17.8738 39.0375 22.3848L50.0002 36.4874V2C50.0002 0.89543 49.1048 0 48.0003 0Z" fill="#63F655"></path></g><defs><linearGradient id="paint0_linear_8136_183" x1="57.9986" y1="58" x2="6.99848" y2="0.0012267" gradientUnits="userSpaceOnUse"><stop stop-color="#2EF51C"></stop><stop offset="1" stop-color="#2EF51C" stop-opacity="0"></stop></linearGradient><linearGradient id="paint1_linear_8136_183" x1="57.9986" y1="58" x2="23.5492" y2="44.6006" gradientUnits="userSpaceOnUse"><stop stop-opacity="0.9"></stop><stop offset="1" stop-color="#1A1A1A" stop-opacity="0"></stop></linearGradient><clipPath id="clip0_8136_183"><rect width="58" height="58" fill="white"></rect></clipPath></defs></svg>
```

--------------------------------------------------------------------------------
/src/views/styles.css:
--------------------------------------------------------------------------------

```css
/* Modern, responsive styling with system fonts */
:root {
  --primary-color: #0070f3;
  --error-color: #f44336;
  --border-color: #e5e7eb;
  --text-color: #dedede;
  --text-color-secondary: #949494;
  --background-color: #1c1c1c;
  --border-color: #2a2929;
  --card-shadow: 0 0px 12px 0px rgb(0 230 153 / 0.3);
  --link-color: rgb(0 230 153 / 1);
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
    Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
  line-height: 1.6;
  color: var(--text-color);
  background-color: var(--background-color);
  margin: 0;
  padding: 0;
}

.container {
  max-width: 600px;

  margin: 2rem auto;
  padding: 1rem;
}

.precard {
  padding: 2rem;
  text-align: center;
}

.card {
  background-color: #0a0c09e6;
  border-radius: 8px;
  box-shadow: var(--card-shadow);
  padding: 2rem 2rem 0.5rem;
}

.header {
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 1.5rem;
  color: var(--text-color);
  text-decoration: none;
}

.logo {
  width: 48px;
  height: 48px;
  margin-right: 1rem;
  border-radius: 8px;
  object-fit: contain;
}

.title {
  margin: 0;
  font-size: 1.3rem;
  font-weight: 400;
}

.alert {
  margin: 0;
  font-size: 1.5rem;
  font-weight: 400;
  margin: 1rem 0;
  text-align: center;
}

.description {
  color: var(--text-color-secondary);
}

.client-info {
  border: 1px solid var(--border-color);
  border-radius: 6px;
  padding: 1rem 1rem 0.5rem;
  margin-bottom: 1.5rem;
}

.client-name {
  font-weight: 600;
  font-size: 1.2rem;
  margin: 0 0 0.5rem 0;
}

.client-detail {
  display: flex;
  margin-bottom: 0.5rem;
  align-items: baseline;
}

.detail-label {
  font-weight: 500;
  min-width: 120px;
}

.detail-value {
  font-family: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
    'Courier New', monospace;
  word-break: break-all;
}

.detail-value a {
  color: inherit;
  text-decoration: underline;
}

.detail-value.small {
  font-size: 0.8em;
}

.external-link-icon {
  font-size: 0.75em;
  margin-left: 0.25rem;
  vertical-align: super;
}

.actions {
  display: flex;
  justify-content: flex-end;
  gap: 1rem;
  margin-top: 2rem;
}

.button {
  padding: 0.65rem 1rem;
  border-radius: 6px;
  font-weight: 500;
  cursor: pointer;
  border: none;
  font-size: 1rem;
}

.button-primary {
  background-color: rgb(0 229 153 / 1);
  color: rgb(26 26 26 / 1);
}

.button-secondary {
  background-color: transparent;
  border: 1px solid rgb(73 75 80 / 1);
  color: var(--text-color);
}

/* Responsive adjustments */
@media (max-width: 640px) {
  .container {
    margin: 1rem auto;
    padding: 0.5rem;
  }

  .card {
    padding: 1.5rem;
  }

  .client-detail {
    flex-direction: column;
  }

  .detail-label {
    min-width: unset;
    margin-bottom: 0.25rem;
  }

  .actions {
    flex-direction: column;
  }

  .button {
    width: 100%;
  }
}

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "@neondatabase/mcp-server-neon",
  "version": "0.6.4",
  "description": "MCP server for interacting with Neon Management API and databases",
  "license": "MIT",
  "author": "Neon, Inc. (https://neon.tech/)",
  "homepage": "https://github.com/neondatabase/mcp-server-neon/",
  "bugs": "https://github.com/neondatabase/mcp-server-neon/issues",
  "type": "module",
  "access": "public",
  "bin": {
    "mcp-server-neon": "./dist/index.js"
  },
  "files": [
    "dist",
    "CHANGELOG.md"
  ],
  "scripts": {
    "typecheck": "tsc --noEmit",
    "build": "tsc && npm run build:chmod && npm run export-tools && npm run build:landing",
    "build:chmod": "node -e \"require('fs').chmodSync('dist/index.js', '755')\"",
    "build:landing": "cd landing/ && npm run build && cp -r out/* ../public",
    "watch": "tsc-watch --onSuccess \"chmod 755 dist/index.js\"",
    "inspector": "npx @modelcontextprotocol/inspector dist/index.js",
    "format": "prettier --write .",
    "lint": "npm run typecheck && eslint src && prettier --check .",
    "lint:fix": "npm run typecheck && eslint src --fix && prettier --w .",
    "prerelease": "npm run build",
    "prepublishOnly": "bun scripts/before-publish.ts",
    "test": "npx braintrust eval src/tools-evaluations",
    "start": "node dist/index.js start",
    "start:sse": "node dist/index.js start:sse",
    "export-tools": "node dist/index.js export-tools > landing/tools.json"
  },
  "dependencies": {
    "@keyv/postgres": "2.1.2",
    "@modelcontextprotocol/sdk": "1.11.2",
    "@neondatabase/api-client": "2.0.0",
    "@neondatabase/serverless": "1.0.0",
    "@radix-ui/react-accordion": "1.2.11",
    "@segment/analytics-node": "2.2.1",
    "@sentry/node": "9.19.0",
    "@tailwindcss/postcss": "4.1.10",
    "axios": "1.11.0",
    "body-parser": "2.2.0",
    "chalk": "5.3.0",
    "class-variance-authority": "0.7.1",
    "cookie-parser": "1.4.7",
    "cors": "2.8.5",
    "dotenv": "16.4.7",
    "express": "5.0.1",
    "keyv": "5.3.2",
    "lucide-react": "0.515.0",
    "morgan": "1.10.0",
    "next": "15.3.3",
    "node-fetch": "2.7.0",
    "oauth2-server": "3.1.1",
    "openid-client": "6.3.4",
    "pug": "3.0.3",
    "react-syntax-highlighter": "15.6.1",
    "tailwind-merge": "3.3.1",
    "winston": "3.17.0",
    "zod": "3.24.1"
  },
  "devDependencies": {
    "@eslint/js": "9.21.0",
    "@types/cookie-parser": "1.4.8",
    "@types/cors": "2.8.17",
    "@types/express": "5.0.1",
    "@types/morgan": "1.9.9",
    "@types/node": "20.17.9",
    "@types/node-fetch": "2.6.12",
    "@types/oauth2-server": "3.0.18",
    "@types/react": "19.1.8",
    "autoevals": "0.0.111",
    "braintrust": "0.0.177",
    "bun": "1.1.40",
    "eslint": "9.21.0",
    "eslint-config-prettier": "10.0.2",
    "prettier": "3.4.1",
    "tsc-watch": "6.2.1",
    "typescript": "5.7.2",
    "typescript-eslint": "v8.25.0"
  },
  "engines": {
    "node": ">=22.0.0"
  }
}

```

--------------------------------------------------------------------------------
/.github/workflows/koyeb-preview.yml:
--------------------------------------------------------------------------------

```yaml
name: Build and deploy backend to preview

on:
  workflow_dispatch:
  pull_request:
    types: [synchronize, labeled]

jobs:
  deploy:
    concurrency:
      group: '${{ github.ref_name }}'
      cancel-in-progress: true
    runs-on: ubuntu-latest
    if: contains(github.event.pull_request.labels.*.name, 'deploy-preview')
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Install and configure the Koyeb CLI
        uses: koyeb-community/koyeb-actions@v2
        with:
          api_token: '${{ secrets.KOYEB_PREVIEW_TOKEN }}'
      - name: Build and deploy to Koyeb preview
        run: |
          koyeb deploy . platform-koyeb-preview/main \
            --instance-type nano \
            --region was \
            --archive-builder docker \
            --archive-docker-dockerfile remote.Dockerfile \
            --privileged \
            --type web \
            --port 3001:http \
            --route /:3001 \
            --wait \
            --env CLIENT_ID=${{secrets.CLIENT_ID}} \
            --env CLIENT_SECRET=${{secrets.CLIENT_SECRET}} \
            --env OAUTH_DATABASE_URL=${{secrets.PREVIEW_OAUTH_DATABASE_URL}} \
            --env SERVER_HOST=${{vars.KOYEB_PREVIEW_SERVER_HOST}} \
            --env NEON_API_HOST=${{vars.NEON_API_HOST_STAGING}} \
            --env UPSTREAM_OAUTH_HOST=${{vars.OAUTH_HOST_STAGING}} \
            --env COOKIE_SECRET=${{secrets.COOKIE_SECRET}} \

      - name: Comment on PR with deployment URL
        if: ${{ github.event_name == 'pull_request' && success() }}
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            // GitHub bot id taken from (https://api.github.com/users/github-actions[bot])
            const githubActionsBotId = 41898282

            const ownerRepoParams = {
              owner: context.repo.owner,
              repo: context.repo.repo,
            }

            // Hidden start marker for the comment
            const startMarker = '<!-- Preview Deployment Comment-->';
            const body = `${startMarker}
            🚀 Preview deployment ready: [https://preview-mcp.neon.tech](https://preview-mcp.neon.tech)`;

            const comments = await github.paginate(github.rest.issues.listComments, {
              ...ownerRepoParams,
              issue_number: context.issue.number,
            });

             // Delete previous comments regarding preview deployments.
            for (comment of comments.filter(comment => comment.user.id === githubActionsBotId && comment.body.startsWith(startMarker))) {
              await github.rest.issues.deleteComment({
                comment_id: comment.id,
                ...ownerRepoParams,
              })
            }

            await github.rest.issues.createComment({
              ...ownerRepoParams,
              issue_number: context.issue.number,
              body
            });

```

--------------------------------------------------------------------------------
/src/tools/handlers/neon-auth.ts:
--------------------------------------------------------------------------------

```typescript
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';

import { Api, NeonAuthSupportedAuthProvider } from '@neondatabase/api-client';
import { provisionNeonAuthInputSchema } from '../toolsSchema.js';
import { z } from 'zod';
import { getDefaultDatabase } from '../utils.js';

type Props = z.infer<typeof provisionNeonAuthInputSchema>;
export async function handleProvisionNeonAuth(
  { projectId, database }: Props,
  neonClient: Api<unknown>,
): Promise<CallToolResult> {
  const {
    data: { branches },
  } = await neonClient.listProjectBranches({
    projectId,
  });
  const defaultBranch =
    branches.find((branch) => branch.default) ?? branches[0];
  if (!defaultBranch) {
    return {
      isError: true,
      content: [
        {
          type: 'text',
          text: 'The project has no default branch. Neon Auth can only be provisioned with a default branch.',
        },
      ],
    };
  }
  const defaultDatabase = await getDefaultDatabase(
    {
      projectId,
      branchId: defaultBranch.id,
      databaseName: database,
    },
    neonClient,
  );

  if (!defaultDatabase) {
    return {
      isError: true,
      content: [
        {
          type: 'text',
          text: `The project has no database named '${database}'.`,
        },
      ],
    };
  }

  const response = await neonClient.createNeonAuthIntegration({
    auth_provider: NeonAuthSupportedAuthProvider.Stack,
    project_id: projectId,
    branch_id: defaultBranch.id,
    database_name: defaultDatabase.name,
    role_name: defaultDatabase.owner_name,
  });

  // In case of 409, it means that the integration already exists
  // We should not return an error, but a message that the integration already exists and fetch the existing integration
  if (response.status === 409) {
    return {
      content: [
        {
          type: 'text',
          text: 'Neon Auth already provisioned.',
        },
      ],
    };
  }

  if (response.status !== 201) {
    return {
      isError: true,
      content: [
        {
          type: 'text',
          text: `Failed to provision Neon Auth. Error: ${response.statusText}`,
        },
      ],
    };
  }

  return {
    content: [
      {
        type: 'text',
        text: `Authentication has been successfully provisioned for your Neon project. Following are the environment variables you need to set in your project:
        \`\`\`
          NEXT_PUBLIC_STACK_PROJECT_ID='${response.data.auth_provider_project_id}'
          NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY='${response.data.pub_client_key}'
          STACK_SECRET_SERVER_KEY='${response.data.secret_server_key}'
        \`\`\`

        Copy the above environment variables and place them in  your \`.env.local\` file for Next.js project. Note that variables with \`NEXT_PUBLIC_\` prefix will be available in the client side.
        `,
      },
      {
        type: 'text',
        text: `
        Use Following JWKS URL to retrieve the public key to verify the JSON Web Tokens (JWT) issued by authentication provider:
        \`\`\`
        ${response.data.jwks_url}
        \`\`\`
        `,
      },
    ],
  };
}

```

--------------------------------------------------------------------------------
/landing/components/DescriptionItem.tsx:
--------------------------------------------------------------------------------

```typescript
import {
  DescriptionItem,
  DescriptionItemType,
  TextBlock,
} from '@/lib/description';
import { CodeSnippet } from '@/components/CodeSnippet';
import {
  Alert,
  AlertDescription,
  AlertTitle,
  AlertVariant,
} from '@/components/ui/alert';
import {
  Terminal,
  CircleAlert,
  Lightbulb,
  BadgeInfo,
  Workflow,
  SquareArrowRight,
  Component,
  BookOpenCheck,
} from 'lucide-react';

const ALERT_VARIANT_PER_DESCRIPTION_TYPE: Record<
  DescriptionItemType,
  {
    variant: AlertVariant;
    icon: typeof Component;
  }
> = {
  use_case: { variant: 'default', icon: BookOpenCheck },
  next_steps: { variant: 'default', icon: SquareArrowRight },
  important_notes: { variant: 'important', icon: CircleAlert },
  workflow: { variant: 'default', icon: Workflow },
  hints: { variant: 'default', icon: BadgeInfo },
  hint: { variant: 'default', icon: Lightbulb },
  instructions: { variant: 'default', icon: Terminal },
  response_instructions: { variant: 'default', icon: Terminal },
  example: { variant: 'default', icon: Terminal },
  do_not_include: { variant: 'destructive', icon: CircleAlert },
  error_handling: { variant: 'destructive', icon: CircleAlert },
};

export const TextBlockUi = (block: TextBlock) => {
  if (block.type === 'text') {
    return (
      <div className="text-sm/[24px]">
        {block.content.map((item, index) =>
          item.type === 'text' ? (
            item.content
          ) : (
            <span key={index} className="monospaced bg-secondary p-1 py-0.25">
              {item.content}
            </span>
          ),
        )}
      </div>
    );
  }

  return <CodeSnippet type={block.syntax}>{block.content}</CodeSnippet>;
};

export const DescriptionItemUi = (item: DescriptionItem) => {
  if (item.type === 'text') {
    return (
      <div className="whitespace-pre-line">
        {item.content.map((childItem, index) => (
          <TextBlockUi key={index} {...childItem} />
        ))}
      </div>
    );
  }

  // If an example section contains only code snippet then render snippet
  // without a section wrapper. An extra wrapper makes the code less readable.
  if (
    item.type === 'example' &&
    item.content.length === 1 &&
    item.content[0].type === 'text' &&
    item.content[0].content.length === 1 &&
    item.content[0].content[0].type === 'code'
  ) {
    const snippet = item.content[0].content[0];

    return <CodeSnippet type={snippet.syntax}>{snippet.content}</CodeSnippet>;
  }

  const { variant, icon: IconComp } =
    ALERT_VARIANT_PER_DESCRIPTION_TYPE[item.type];

  return (
    <Alert variant={variant} className="my-2">
      <IconComp className="w-4 h-4" />
      <AlertTitle className="first-letter:capitalize font-semibold">
        {item.type.replaceAll('_', ' ')}
      </AlertTitle>
      <AlertDescription className="whitespace-pre-line">
        <DescriptionItemsUi description={item.content} />
      </AlertDescription>
    </Alert>
  );
};

export const DescriptionItemsUi = ({
  description,
}: {
  description: DescriptionItem[];
}) => (
  <div className="flex flex-col">
    {description.map((item, index) => (
      <DescriptionItemUi key={index} {...item} />
    ))}
  </div>
);

```

--------------------------------------------------------------------------------
/landing/app/page.tsx:
--------------------------------------------------------------------------------

```typescript
import fs from 'fs/promises';

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from '@/components/ui/accordion';
import { parseDescription } from '@/lib/description';
import { DescriptionItemsUi } from '@/components/DescriptionItem';
import { Introduction } from '@/components/Introduction';
import { Header } from '@/components/Header';

type ToolDescription = {
  name: string;
  description: string;
};

export default async function Home() {
  const packageJson = await fs.readFile('../package.json', 'utf-8');
  const packageVersion = JSON.parse(packageJson).version as number;

  const toolsJson = await fs.readFile('./tools.json', 'utf-8');
  const rawTools = JSON.parse(toolsJson) as ToolDescription[];

  const tools = rawTools.map(({ description, ...data }) => ({
    ...data,
    description: parseDescription(description),
  }));

  return (
    <div className="flex flex-col items-center min-h-screen p-4 pb-0 sm:p-8 sm:pb-0">
      <main className="w-full max-w-3xl">
        <article className="flex flex-col gap-10">
          <Header packageVersion={packageVersion} />
          <Introduction />
          <div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-6">
            <div className="flex">
              <div className="ml-3">
                <p className="text-sm text-yellow-700">
                  <strong>Security Notice:</strong> The Neon MCP Server grants
                  powerful database management capabilities through natural
                  language requests. Please review our{' '}
                  <a
                    href="https://neon.tech/docs/ai/neon-mcp-server#mcp-security-guidance"
                    className="underline hover:text-yellow-800"
                    target="_blank"
                    rel="noopener noreferrer"
                  >
                    MCP security guidance
                  </a>{' '}
                  before using.
                </p>
              </div>
            </div>
          </div>
          <section id="tools">
            <h2 className="text-2xl font-bold mb-2 border-b-3 border-b-emerald-600">
              Available Tools
            </h2>
            {tools === undefined ? (
              <div>tools.json is not found</div>
            ) : (
              <Accordion type="multiple" asChild>
                <ul>
                  {tools.map(({ name, description }) => (
                    <AccordionItem key={name} value={name} asChild>
                      <li key={name}>
                        <AccordionTrigger className="flex items-center">
                          <h3 className="monospaced text-xl font-semibold">
                            {name}
                          </h3>
                        </AccordionTrigger>
                        <AccordionContent>
                          <DescriptionItemsUi description={description} />
                        </AccordionContent>
                      </li>
                    </AccordionItem>
                  ))}
                </ul>
              </Accordion>
            )}
          </section>
        </article>
      </main>
      <footer className="text-center w-full p-4 mt-10">Neon Inc. 2025</footer>
    </div>
  );
}

```

--------------------------------------------------------------------------------
/landing/lib/description.ts:
--------------------------------------------------------------------------------

```typescript
import { min } from 'lodash';

const POSSIBLE_TYPES = [
  'use_case',
  'workflow',
  'important_notes',
  'next_steps',
  'response_instructions',
  'instructions',
  'example',
  'do_not_include',
  'error_handling',
  'hint',
  'hints',
] as const;

export type DescriptionItemType = (typeof POSSIBLE_TYPES)[number];

export type DescriptionItem =
  | {
      type: 'text';
      content: TextBlock[];
    }
  | {
      type: DescriptionItemType;
      content: DescriptionItem[];
    };

export type TextBlock =
  | {
      type: 'text';
      content: TextSpan[];
    }
  | {
      type: 'code';
      syntax?: string;
      content: string;
    };

export type TextSpan =
  | {
      type: 'text';
      content: string;
    }
  | {
      type: 'code';
      content: string;
    };

function isValidType(string: string): string is DescriptionItemType {
  return POSSIBLE_TYPES.includes(string as DescriptionItemType);
}

function highlightCodeSpans(text: string): TextSpan[] {
  const items: TextSpan[] = [];
  let rest = text.trim();

  while (rest.length > 0) {
    const match = rest.match(/`([^`]*)?`/);

    if (!match) {
      items.push({
        type: 'text',
        content: rest,
      });
      break;
    }

    if ((match.index ?? 0) !== 0) {
      items.push({
        type: 'text',
        content: rest.substring(0, match.index),
      });
    }

    items.push({
      type: 'code',
      content: match[1].trim(),
    });

    rest = rest.substring((match.index ?? 0) + match[0].length);
  }

  return items;
}

function removeRedundantIndentation(text: string): string {
  const lines = text.split('\n');
  const minIndent = min(
    lines.map((line) => line.match(/^\s+/)?.[0].length ?? 0),
  );
  if (!minIndent) {
    return text;
  }

  return lines.map((line) => line.substring(minIndent)).join('\n');
}

function highlightCodeBlocks(description: string): TextBlock[] {
  const parts: TextBlock[] = [];
  let rest = description.trim();

  while (rest.length > 0) {
    const match = rest.match(/```([^\n]*?)\n(.*?)\n\s*?```/s);

    if (!match) {
      parts.push({
        type: 'text',
        content: highlightCodeSpans(rest),
      });
      break;
    }

    if ((match.index ?? 0) > 0) {
      parts.push({
        type: 'text',
        content: highlightCodeSpans(rest.slice(0, match.index).trim()),
      });
    }

    parts.push({
      type: 'code',
      syntax: match[1].trim() || undefined,
      content: removeRedundantIndentation(match[2]),
    });

    rest = rest.substring((match.index ?? 0) + match[0].length).trim();
  }

  return parts;
}

export function parseDescription(description: string): DescriptionItem[] {
  const parts: DescriptionItem[] = [];
  let rest = description.trim();

  while (rest.length > 0) {
    const match = rest.match(
      /<(use_case|workflow|important_notes|next_steps|response_instructions|instructions|example|do_not_include|error_handling|hints?)>(.*?)<\/\1>/s,
    );

    if (!match) {
      parts.push({
        type: 'text',
        content: highlightCodeBlocks(rest),
      });
      break;
    }

    const type = match[1];

    if (!isValidType(type)) {
      throw new Error('Invalid type');
    }

    if ((match.index ?? 0) > 0) {
      parts.push({
        type: 'text',
        content: highlightCodeBlocks(rest.slice(0, match.index).trim()),
      });
    }

    parts.push({
      type,
      content: parseDescription(match[2].trim()),
    });

    rest = rest.substring((match.index ?? 0) + match[0].length).trim();
  }

  return parts;
}

```

--------------------------------------------------------------------------------
/src/server/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { NEON_RESOURCES } from '../resources.js';
import {
  NEON_HANDLERS,
  NEON_TOOLS,
  ToolHandlerExtended,
} from '../tools/index.js';
import { logger } from '../utils/logger.js';
import { createNeonClient, getPackageJson } from './api.js';
import { track } from '../analytics/analytics.js';
import { captureException, startSpan } from '@sentry/node';
import { ServerContext } from '../types/context.js';
import { setSentryTags } from '../sentry/utils.js';
import { ToolHandlerExtraParams } from '../tools/types.js';
import { handleToolError } from './errors.js';

export const createMcpServer = (context: ServerContext) => {
  const server = new McpServer(
    {
      name: 'mcp-server-neon',
      version: getPackageJson().version,
    },
    {
      capabilities: {
        tools: {},
        resources: {},
      },
    },
  );

  const neonClient = createNeonClient(context.apiKey);

  // Register tools
  NEON_TOOLS.forEach((tool) => {
    const handler = NEON_HANDLERS[tool.name];
    if (!handler) {
      throw new Error(`Handler for tool ${tool.name} not found`);
    }

    const toolHandler = handler as ToolHandlerExtended<typeof tool.name>;

    server.tool(
      tool.name,
      tool.description,
      { params: tool.inputSchema },
      async (args, extra) => {
        return await startSpan(
          {
            name: 'tool_call',
            attributes: {
              tool_name: tool.name,
            },
          },
          async (span) => {
            const properties = { tool_name: tool.name };
            logger.info('tool call:', properties);
            setSentryTags(context);
            track({
              userId: context.account.id,
              event: 'tool_call',
              properties,
              context: { client: context.client, app: context.app },
            });
            const extraArgs: ToolHandlerExtraParams = {
              ...extra,
              account: context.account,
            };
            try {
              return await toolHandler(args, neonClient, extraArgs);
            } catch (error) {
              span.setStatus({
                code: 2,
              });
              return handleToolError(error, properties);
            }
          },
        );
      },
    );
  });

  // Register resources
  NEON_RESOURCES.forEach((resource) => {
    server.resource(
      resource.name,
      resource.uri,
      {
        description: resource.description,
        mimeType: resource.mimeType,
      },
      async (url) => {
        const properties = { resource_name: resource.name };
        logger.info('resource call:', properties);
        setSentryTags(context);
        track({
          userId: context.account.id,
          event: 'resource_call',
          properties,
          context: { client: context.client, app: context.app },
        });
        try {
          return await resource.handler(url);
        } catch (error) {
          captureException(error, {
            extra: properties,
          });
          throw error;
        }
      },
    );
  });

  server.server.onerror = (error: unknown) => {
    const message = error instanceof Error ? error.message : 'Unknown error';
    logger.error('Server error:', {
      message,
      error,
    });
    const contexts = { app: context.app, client: context.client };
    const eventId = captureException(error, {
      user: { id: context.account.id },
      contexts: contexts,
    });
    track({
      userId: context.account.id,
      event: 'server_error',
      properties: { message, error, eventId },
      context: contexts,
    });
  };

  return server;
};

```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
# Changelog

# [Unreleased]

- Feat: `list_shared_projects` tool to fetch projects that user has permissions to collaborate on
- Feat: `reset_from_parent` tool to reset a branch from its parent's current state
- Feat: `compare_database_schema` tool to compare the schema from the child branch and its parent

# [0.6.4] 2025-08-22

- Fix: Do not log user sensitive information on errors
- Fix: Return non-500 errors as valid response with `isError=true` without logging
- Improvement: Custom error handling user generated erorrs
- Improvement: Extend org-only users search to support orgs not managed by console.

# [0.6.3] 2025-08-04

- Feat: A new tool to list authenitcated user's organizations - `list_organizations`
- Docs: Switch configs to use streamable HTTP by default
- Impr: While searching for project in `list_projects` tool, extend the search to all organizations.

## [0.6.2] 2025-07-17

- Add warnings on security risks involved in MCP tools in production environments
- Migrate the deployment to Koyeb
- Mark `param` as required argument for all tools

## [0.6.1] 2025-06-19

- Documentation: Updated README with new tools and features
- Support API key authentication for remote server

## [0.6.0] 2025-06-16

- Fix: Issue with ORG API keys in local mode
- Refc: Tools into smaller manageable modules
- Feat: New landing page with details of supported tools
- Feat: Streamable HTTP support

## [0.5.0] 2025-05-28

- Tracking tool calls and errors with Segment
- Capture exections with Sentry
- Add tracing with sentry
- Support new org-only accounts

## [0.4.1] - 2025-05-08

- fix the `npx start` command to start server in stdio transport mode
- fix issue with unexpected tokens in stdio transport mode

## [0.4.0] - 2025-05-08

- Feature: Support for remote MCP with OAuth flow.
- Remove `__node_version` tool
- Feature: Add `list_slow_queries` tool for monitoring database performance
- Add `list_branch_computes` tool to list compute endpoints for a project or specific branch

## [0.3.7] - 2025-04-23

- Fixes Neon Auth instructions to install latest version of the SDK

## [0.3.6] - 2025-04-20

- Bumps the Neon serverless driver to 1.0.0

## [0.3.5] - 2025-04-19

- Fix default database name or role name assumptions.
- Adds better error message for project creations.

## [0.3.4] - 2025-03-26

- Add `neon-auth`, `neon-serverless`, and `neon-drizzle` resources
- Fix initialization on Windows by implementing correct platform-specific paths for Claude configuration

## [0.3.3] - 2025-03-19

- Fix the API Host

## [0.3.2] - 2025-03-19

- Add User-Agent to api calls from mcp server

## [0.3.1] - 2025-03-19

- Add User-Agent to api calls from mcp server

## [0.3.0] - 2025-03-14

- Add `provision_neon_auth` tool

## [0.2.3] - 2025-03-06

- Adds `get_connection_string` tool
- Hints the LLM to call the `create_project` tool to create new databases

## [0.2.2] - 2025-02-26

- Fixed a bug in the `list_projects` tool when passing no params
- Added a `params` property to all the tools input schemas

## [0.2.1] - 2025-02-25

- Fixes a bug in the `list_projects` tool
- Update the `@modelcontextprotocol/sdk` to the latest version
- Use `zod` to validate tool input schemas

## [0.2.0] - 2025-02-24

- Add [Smithery](https://smithery.ai/server/neon) deployment config

## [0.1.9] - 2025-01-06

- Setups tests to the `prepare_database_migration` tool
- Updates the `prepare_database_migration` tool to be more deterministic
- Removes logging from the MCP server, following the [docs](https://modelcontextprotocol.io/docs/tools/debugging#implementing-logging)

## [0.1.8] - 2024-12-25

- Added `beforePublish` script so make sure the changelog is updated before publishing
- Makes the descriptions/prompts for the prepare_database_migration and complete_database_migration tools much better

## [0.1.7-beta.1] - 2024-12-19

- Added support for `prepare_database_migration` and `complete_database_migration` tools

```

--------------------------------------------------------------------------------
/src/initConfig.ts:
--------------------------------------------------------------------------------

```typescript
import path from 'node:path';
import os from 'node:os';
import fs from 'node:fs';
import chalk from 'chalk';
import { fileURLToPath } from 'url';
import { logger } from './utils/logger.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const packageJson = JSON.parse(
  fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'),
);
// Determine Claude config path based on OS platform
let claudeConfigPath: string;
const platform = os.platform();

if (platform === 'win32') {
  // Windows path - using %APPDATA%
  // For Node.js, we access %APPDATA% via process.env.APPDATA
  claudeConfigPath = path.join(
    process.env.APPDATA || '',
    'Claude',
    'claude_desktop_config.json',
  );
} else {
  // macOS and Linux path (according to official docs)
  claudeConfigPath = path.join(
    os.homedir(),
    'Library',
    'Application Support',
    'Claude',
    'claude_desktop_config.json',
  );
}

const MCP_NEON_SERVER = 'neon';

type Args =
  | {
      command: 'start:sse';
      analytics: boolean;
    }
  | {
      command: 'start';
      neonApiKey: string;
      analytics: boolean;
    }
  | {
      command: 'init';
      executablePath: string;
      neonApiKey: string;
      analytics: boolean;
    }
  | {
      command: 'export-tools';
    };

const commands = ['init', 'start', 'start:sse', 'export-tools'] as const;

export const parseArgs = (): Args => {
  const args = process.argv;

  if (args.length < 3) {
    logger.error('Invalid number of arguments');
    process.exit(1);
  }

  if (args.length === 3 && args[2] === 'start:sse') {
    return {
      command: 'start:sse',
      analytics: true,
    };
  }

  if (args.length === 3 && args[2] === 'export-tools') {
    return {
      command: 'export-tools',
    };
  }

  const command = args[2];

  if (!commands.includes(command as (typeof commands)[number])) {
    logger.error(`Invalid command: ${command}`);
    process.exit(1);
  }

  if (command === 'export-tools') {
    return {
      command: 'export-tools',
    };
  }

  if (args.length < 4) {
    logger.error(
      'Please provide a NEON_API_KEY as a command-line argument - you can get one through the Neon console: https://neon.tech/docs/manage/api-keys',
    );
    process.exit(1);
  }

  return {
    executablePath: args[1],
    command: args[2] as 'start' | 'init',
    neonApiKey: args[3],
    analytics: !args[4]?.includes('no-analytics'),
  };
};

export function handleInit({
  executablePath,
  neonApiKey,
  analytics,
}: {
  executablePath: string;
  neonApiKey: string;
  analytics: boolean;
}) {
  // If the executable path is a local path to the dist/index.js file, use it directly
  // Otherwise, use the name of the package to always load the latest version from remote
  const serverPath = executablePath.includes('dist/index.js')
    ? executablePath
    : packageJson.name;

  const neonConfig = {
    command: 'npx',
    args: [
      '-y',
      serverPath,
      'start',
      neonApiKey,
      analytics ? '' : '--no-analytics',
    ],
  };

  const configDir = path.dirname(claudeConfigPath);
  if (!fs.existsSync(configDir)) {
    console.log(chalk.blue('Creating Claude config directory...'));
    fs.mkdirSync(configDir, { recursive: true });
  }

  const existingConfig = fs.existsSync(claudeConfigPath)
    ? JSON.parse(fs.readFileSync(claudeConfigPath, 'utf8'))
    : { mcpServers: {} };

  if (MCP_NEON_SERVER in (existingConfig?.mcpServers || {})) {
    console.log(chalk.yellow('Replacing existing Neon MCP config...'));
  }

  const newConfig = {
    ...existingConfig,
    mcpServers: {
      ...existingConfig.mcpServers,
      [MCP_NEON_SERVER]: neonConfig,
    },
  };

  fs.writeFileSync(claudeConfigPath, JSON.stringify(newConfig, null, 2));
  console.log(chalk.green(`Config written to: ${claudeConfigPath}`));
  console.log(
    chalk.blue(
      'The Neon MCP server will start automatically the next time you open Claude.',
    ),
  );
}

```

--------------------------------------------------------------------------------
/src/transports/sse-express.ts:
--------------------------------------------------------------------------------

```typescript
import '../sentry/instrument.js';
import { setupExpressErrorHandler } from '@sentry/node';
import express, { Request, Response, RequestHandler } from 'express';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { createMcpServer } from '../server/index.js';
import { logger, morganConfig, errorHandler } from '../utils/logger.js';
import { authRouter } from '../oauth/server.js';
import { SERVER_PORT, SERVER_HOST } from '../constants.js';
import { ensureCorsHeaders, requiresAuth } from '../oauth/utils.js';
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import { track } from '../analytics/analytics.js';
import { AppContext } from '../types/context.js';
import { createStreamTransport } from './stream.js';

export const createSseTransport = (appContext: AppContext) => {
  const app = express();

  app.use(morganConfig);
  app.use(errorHandler);
  app.use(cookieParser());
  app.use(ensureCorsHeaders());
  app.use(express.static('public'));
  app.set('view engine', 'pug');
  app.set('views', 'src/views');
  const streamHandler = createStreamTransport({
    ...appContext,
    transport: 'stream',
  });
  app.use('/mcp', streamHandler);
  app.use('/', authRouter);

  // to support multiple simultaneous connections we have a lookup object from
  // sessionId to transport
  const transports = new Map<string, SSEServerTransport>();

  app.get(
    '/sse',
    bodyParser.raw(),
    requiresAuth(),
    async (req: Request, res: Response) => {
      const auth = req.auth;
      if (!auth) {
        res.status(401).send('Unauthorized');
        return;
      }
      const transport = new SSEServerTransport('/messages', res);
      transports.set(transport.sessionId, transport);
      logger.info('new sse connection', {
        sessionId: transport.sessionId,
      });

      res.on('close', () => {
        logger.info('SSE connection closed', {
          sessionId: transport.sessionId,
        });
        transports.delete(transport.sessionId);
      });

      try {
        const server = createMcpServer({
          apiKey: auth.token,
          client: auth.extra.client,
          account: auth.extra.account,
          app: appContext,
        });
        await server.connect(transport);
      } catch (error: unknown) {
        logger.error('Failed to connect to MCP server:', {
          message: error instanceof Error ? error.message : 'Unknown error',
          error,
        });
        track({
          userId: auth.extra.account.id,
          event: 'sse_connection_errored',
          properties: { error },
          context: {
            app: appContext,
            client: auth.extra.client,
          },
        });
      }
    },
  );

  app.post('/messages', bodyParser.raw(), requiresAuth(), (async (
    request: Request,
    response: Response,
  ) => {
    const auth = request.auth;
    if (!auth) {
      response.status(401).send('Unauthorized');
      return;
    }
    const sessionId = request.query.sessionId as string;
    const transport = transports.get(sessionId);
    logger.info('transport message received', {
      sessionId,
      hasTransport: Boolean(transport),
    });

    try {
      if (transport) {
        await transport.handlePostMessage(request, response);
      } else {
        logger.warn('No transport found for sessionId', { sessionId });
        response.status(400).send('No transport found for sessionId');
      }
    } catch (error: unknown) {
      logger.error('Failed to handle post message:', {
        message: error instanceof Error ? error.message : 'Unknown error',
        error,
      });
      track({
        userId: auth.extra.account.id,
        event: 'transport_message_errored',
        properties: { error },
        context: { app: appContext, client: auth.extra.client },
      });
    }
  }) as RequestHandler);

  setupExpressErrorHandler(app);

  try {
    app.listen({ port: SERVER_PORT });
    logger.info(`Server started on ${SERVER_HOST}`);
  } catch (err: unknown) {
    logger.error('Failed to start server:', {
      error: err instanceof Error ? err.message : 'Unknown error',
    });
    process.exit(1);
  }
};

```

--------------------------------------------------------------------------------
/src/describeUtils.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * This module is derived from @neondatabase/psql-describe
 * Original source: https://github.com/neondatabase/psql-describe
 */

import { neon } from '@neondatabase/serverless';

export type TableDescription = {
  columns: ColumnDescription[];
  indexes: IndexDescription[];
  constraints: ConstraintDescription[];
  tableSize: string;
  indexSize: string;
  totalSize: string;
};

export type ColumnDescription = {
  name: string;
  type: string;
  nullable: boolean;
  default: string | null;
  description: string | null;
};

export type IndexDescription = {
  name: string;
  definition: string;
  size: string;
};

export type ConstraintDescription = {
  name: string;
  type: string;
  definition: string;
};

export const DESCRIBE_TABLE_STATEMENTS = [
  // Get column information
  `
  SELECT 
    c.column_name as name,
    c.data_type as type,
    c.is_nullable = 'YES' as nullable,
    c.column_default as default,
    pd.description
  FROM information_schema.columns c
  LEFT JOIN pg_catalog.pg_statio_all_tables st ON c.table_schema = st.schemaname AND c.table_name = st.relname
  LEFT JOIN pg_catalog.pg_description pd ON pd.objoid = st.relid AND pd.objsubid = c.ordinal_position
  WHERE c.table_schema = 'public' AND c.table_name = $1
  ORDER BY c.ordinal_position;
  `,

  // Get index information
  `
  SELECT
    i.relname as name,
    pg_get_indexdef(i.oid) as definition,
    pg_size_pretty(pg_relation_size(i.oid)) as size
  FROM pg_class t
  JOIN pg_index ix ON t.oid = ix.indrelid
  JOIN pg_class i ON i.oid = ix.indexrelid
  WHERE t.relname = $1 AND t.relkind = 'r';
  `,

  // Get constraint information
  `
  SELECT
    tc.constraint_name as name,
    tc.constraint_type as type,
    pg_get_constraintdef(cc.oid) as definition
  FROM information_schema.table_constraints tc
  JOIN pg_catalog.pg_constraint cc ON tc.constraint_name = cc.conname
  WHERE tc.table_schema = 'public' AND tc.table_name = $1;
  `,

  // Get table size information
  `
  SELECT
    pg_size_pretty(pg_total_relation_size($1)) as total_size,
    pg_size_pretty(pg_relation_size($1)) as table_size,
    pg_size_pretty(pg_total_relation_size($1) - pg_relation_size($1)) as index_size;
  `,
];

export async function describeTable(
  connectionString: string,
  tableName: string,
): Promise<TableDescription> {
  const sql = neon(connectionString);

  // Execute all queries in parallel
  const [columns, indexes, constraints, sizes] = await Promise.all([
    sql.query(DESCRIBE_TABLE_STATEMENTS[0], [tableName]),
    sql.query(DESCRIBE_TABLE_STATEMENTS[1], [tableName]),
    sql.query(DESCRIBE_TABLE_STATEMENTS[2], [tableName]),
    sql.query(DESCRIBE_TABLE_STATEMENTS[3], [tableName]),
  ]);

  return {
    columns: columns.map((col) => ({
      name: col.name,
      type: col.type,
      nullable: col.nullable,
      default: col.default,
      description: col.description,
    })),
    indexes: indexes.map((idx) => ({
      name: idx.name,
      definition: idx.definition,
      size: idx.size,
    })),
    constraints: constraints.map((con) => ({
      name: con.name,
      type: con.type,
      definition: con.definition,
    })),
    tableSize: sizes[0].table_size,
    indexSize: sizes[0].index_size,
    totalSize: sizes[0].total_size,
  };
}

export function formatTableDescription(desc: TableDescription): string {
  const lines: string[] = [];

  // Add table size information
  lines.push(`Table size: ${desc.tableSize}`);
  lines.push(`Index size: ${desc.indexSize}`);
  lines.push(`Total size: ${desc.totalSize}`);
  lines.push('');

  // Add columns
  lines.push('Columns:');
  desc.columns.forEach((col) => {
    const nullable = col.nullable ? 'NULL' : 'NOT NULL';
    const defaultStr = col.default ? ` DEFAULT ${col.default}` : '';
    const descStr = col.description ? `\n    ${col.description}` : '';
    lines.push(`  ${col.name} ${col.type} ${nullable}${defaultStr}${descStr}`);
  });
  lines.push('');

  // Add indexes
  if (desc.indexes.length > 0) {
    lines.push('Indexes:');
    desc.indexes.forEach((idx) => {
      lines.push(`  ${idx.name} (${idx.size})`);
      lines.push(`    ${idx.definition}`);
    });
    lines.push('');
  }

  // Add constraints
  if (desc.constraints.length > 0) {
    lines.push('Constraints:');
    desc.constraints.forEach((con) => {
      lines.push(`  ${con.name} (${con.type})`);
      lines.push(`    ${con.definition}`);
    });
  }

  return lines.join('\n');
}

```

--------------------------------------------------------------------------------
/src/oauth/cookies.ts:
--------------------------------------------------------------------------------

```typescript
import {
  Request as ExpressRequest,
  Response as ExpressResponse,
} from 'express';

const COOKIE_NAME = 'approved-mcp-clients';
const ONE_YEAR_IN_SECONDS = 365 * 24 * 60 * 60 * 1000; // 365 days

/**
 * Imports a secret key string for HMAC-SHA256 signing.
 * @param secret - The raw secret key string.
 * @returns A promise resolving to the CryptoKey object.
 */
const importKey = async (secret: string): Promise<CryptoKey> => {
  const enc = new TextEncoder();
  return crypto.subtle.importKey(
    'raw',
    enc.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign', 'verify'],
  );
};

/**
 * Signs data using HMAC-SHA256.
 * @param key - The CryptoKey for signing.
 * @param data - The string data to sign.
 * @returns A promise resolving to the signature as a hex string.
 */
const signData = async (key: CryptoKey, data: string): Promise<string> => {
  const enc = new TextEncoder();
  const signatureBuffer = await crypto.subtle.sign(
    'HMAC',
    key,
    enc.encode(data),
  );
  // Convert ArrayBuffer to hex string
  return Array.from(new Uint8Array(signatureBuffer))
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('');
};

/**
 * Verifies an HMAC-SHA256 signature.
 * @param key - The CryptoKey for verification.
 * @param signatureHex - The signature to verify (hex string).
 * @param data - The original data that was signed.
 * @returns A promise resolving to true if the signature is valid, false otherwise.
 */
const verifySignature = async (
  key: CryptoKey,
  signatureHex: string,
  data: string,
): Promise<boolean> => {
  try {
    // Convert hex signature back to ArrayBuffer
    const enc = new TextEncoder();
    const signatureBytes = new Uint8Array(
      signatureHex.match(/.{1,2}/g)?.map((byte) => parseInt(byte, 16)) ?? [],
    );

    return await crypto.subtle.verify(
      'HMAC',
      key,
      signatureBytes.buffer,
      enc.encode(data),
    );
  } catch (e) {
    // Handle errors during hex parsing or verification
    console.error('Error verifying signature:', e);
    return false;
  }
};

/**
 * Parses the signed cookie and verifies its integrity.
 * @param cookieHeader - The value of the Cookie header from the request.
 * @param secret - The secret key used for signing.
 * @returns A promise resolving to the list of approved client IDs if the cookie is valid, otherwise null.
 */
const getApprovedClientsFromCookie = async (
  cookie: string,
  secret: string,
): Promise<string[]> => {
  if (!cookie) return [];

  try {
    const [signatureHex, base64Payload] = cookie.split('.');
    if (!signatureHex || !base64Payload) return [];

    const payload = atob(base64Payload);
    const key = await importKey(secret);
    const isValid = await verifySignature(key, signatureHex, payload);
    if (!isValid) return [];

    const clients = JSON.parse(payload);
    return Array.isArray(clients) ? clients : [];
  } catch {
    return [];
  }
};

/**
 * Checks if a given client has already been approved by the user,
 * based on a signed cookie.
 *
 * @param request - The incoming Request object to read cookies from.
 * @param clientId - The OAuth client ID to check approval for.
 * @param cookieSecret - The secret key used to sign/verify the approval cookie.
 * @returns A promise resolving to true if the client ID is in the list of approved clients in a valid cookie, false otherwise.
 */
export const isClientAlreadyApproved = async (
  req: ExpressRequest,
  clientId: string,
  cookieSecret: string,
) => {
  const approvedClients = await getApprovedClientsFromCookie(
    req.cookies[COOKIE_NAME] ?? '',
    cookieSecret,
  );
  return approvedClients.includes(clientId);
};

/**
 * Updates the approved clients cookie with a new client ID.
 * The cookie is signed using HMAC-SHA256 for integrity.
 *
 * @param request - Express request containing existing cookie
 * @param clientId - Client ID to add to approved list
 * @param cookieSecret - Secret key for signing cookie
 * @returns Cookie string with updated approved clients list
 */
export const updateApprovedClientsCookie = async (
  req: ExpressRequest,
  res: ExpressResponse,
  clientId: string,
  cookieSecret: string,
) => {
  const approvedClients = await getApprovedClientsFromCookie(
    req.cookies[COOKIE_NAME] ?? '',
    cookieSecret,
  );
  const newApprovedClients = JSON.stringify(
    Array.from(new Set([...approvedClients, clientId])),
  );
  const key = await importKey(cookieSecret);
  const signature = await signData(key, newApprovedClients);
  res.cookie(COOKIE_NAME, `${signature}.${btoa(newApprovedClients)}`, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: ONE_YEAR_IN_SECONDS,
    path: '/',
  });
};

```

--------------------------------------------------------------------------------
/landing/app/globals.css:
--------------------------------------------------------------------------------

```css
@import 'tailwindcss';
@import 'tw-animate-css';

/* @custom-variant dark (&:is(.dark *)); */

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-sidebar-ring: var(--sidebar-ring);
  --color-sidebar-border: var(--sidebar-border);
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
  --color-sidebar-accent: var(--sidebar-accent);
  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
  --color-sidebar-primary: var(--sidebar-primary);
  --color-sidebar-foreground: var(--sidebar-foreground);
  --color-sidebar: var(--sidebar);
  --color-chart-5: var(--chart-5);
  --color-chart-4: var(--chart-4);
  --color-chart-3: var(--chart-3);
  --color-chart-2: var(--chart-2);
  --color-chart-1: var(--chart-1);
  --color-ring: var(--ring);
  --color-input: var(--input);
  --color-border: var(--border);
  --color-destructive: var(--destructive);
  --color-accent-foreground: var(--accent-foreground);
  --color-accent: var(--accent);
  --color-muted-foreground: var(--muted-foreground);
  --color-muted: var(--muted);
  --color-secondary-foreground: var(--secondary-foreground);
  --color-secondary: var(--secondary);
  --color-primary-foreground: var(--primary-foreground);
  --color-primary: var(--primary);
  --color-popover-foreground: var(--popover-foreground);
  --color-popover: var(--popover);
  --color-card-foreground: var(--card-foreground);
  --color-card: var(--card);
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);
  --font-sans: var(--font-geist-sans);
  --font-mono: var(--font-geist-mono);

  /* user defined */
  --color-important-notes: var(--important-notes);
  --color-link: var(--link);
}

:root {
  --radius: 0.625rem;
  --background: oklch(1 0 0);
  --foreground: oklch(0.129 0.042 264.695);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.129 0.042 264.695);
  --popover: oklch(1 0 0);
  --popover-foreground: oklch(0.129 0.042 264.695);
  --primary: oklch(0.208 0.042 265.755);
  --primary-foreground: oklch(0.984 0.003 247.858);
  --secondary: oklch(0.968 0.007 247.896);
  --secondary-foreground: oklch(0.208 0.042 265.755);
  --muted: oklch(0.968 0.007 247.896);
  --muted-foreground: oklch(0.554 0.046 257.417);
  --accent: oklch(0.968 0.007 247.896);
  --accent-foreground: oklch(0.208 0.042 265.755);
  --destructive: oklch(0.577 0.245 27.325);
  --border: oklch(0.929 0.013 255.508);
  --input: oklch(0.929 0.013 255.508);
  --ring: oklch(0.704 0.04 256.788);
  --chart-1: oklch(0.646 0.222 41.116);
  --chart-2: oklch(0.6 0.118 184.704);
  --chart-3: oklch(0.398 0.07 227.392);
  --chart-4: oklch(0.828 0.189 84.429);
  --chart-5: oklch(0.769 0.188 70.08);
  --sidebar: oklch(0.984 0.003 247.858);
  --sidebar-foreground: oklch(0.129 0.042 264.695);
  --sidebar-primary: oklch(0.208 0.042 265.755);
  --sidebar-primary-foreground: oklch(0.984 0.003 247.858);
  --sidebar-accent: oklch(0.968 0.007 247.896);
  --sidebar-accent-foreground: oklch(0.208 0.042 265.755);
  --sidebar-border: oklch(0.929 0.013 255.508);
  --sidebar-ring: oklch(0.704 0.04 256.788);

  /* user defined */
  --important-notes: var(--color-orange-800);
  --link: oklch(0.64 0.14 160.38);
}

@variant dark {
  :root {
    --background: oklch(0.129 0.042 264.695);
    --foreground: oklch(0.984 0.003 247.858);
    --card: oklch(0.208 0.042 265.755);
    --card-foreground: oklch(0.984 0.003 247.858);
    --popover: oklch(0.208 0.042 265.755);
    --popover-foreground: oklch(0.984 0.003 247.858);
    --primary: oklch(0.929 0.013 255.508);
    --primary-foreground: oklch(0.208 0.042 265.755);
    --secondary: oklch(0.279 0.041 260.031);
    --secondary-foreground: oklch(0.984 0.003 247.858);
    --muted: oklch(0.279 0.041 260.031);
    --muted-foreground: oklch(0.704 0.04 256.788);
    --accent: oklch(0.279 0.041 260.031);
    --accent-foreground: oklch(0.984 0.003 247.858);
    --destructive: oklch(0.704 0.191 22.216);
    --border: oklch(1 0 0 / 10%);
    --input: oklch(1 0 0 / 15%);
    --ring: oklch(0.551 0.027 264.364);
    --chart-1: oklch(0.488 0.243 264.376);
    --chart-2: oklch(0.696 0.17 162.48);
    --chart-3: oklch(0.769 0.188 70.08);
    --chart-4: oklch(0.627 0.265 303.9);
    --chart-5: oklch(0.645 0.246 16.439);
    --sidebar: oklch(0.208 0.042 265.755);
    --sidebar-foreground: oklch(0.984 0.003 247.858);
    --sidebar-primary: oklch(0.488 0.243 264.376);
    --sidebar-primary-foreground: oklch(0.984 0.003 247.858);
    --sidebar-accent: oklch(0.279 0.041 260.031);
    --sidebar-accent-foreground: oklch(0.984 0.003 247.858);
    --sidebar-border: oklch(1 0 0 / 10%);
    --sidebar-ring: oklch(0.551 0.027 264.364);

    /* user defined */
    --important-notes: var(--color-orange-100);
    --link: oklch(0.81 0.18 160.37);
  }
}

@utility monospaced {
  @apply font-[family-name:var(--font-geist-mono)];
}

@layer base {
  * {
    @apply border-border outline-ring/50;
  }

  body {
    @apply bg-background text-foreground;
    /*font-family: Arial, Helvetica, sans-serif;*/
  }

  button:not(:disabled),
  [role='button']:not(:disabled) {
    cursor: pointer;
  }
}

@layer page {
  .external-link {
    @apply text-link;
    @apply font-semibold;
    @apply border-b;
    @apply border-transparent;
    @apply hover:border-current;
  }
}

```

--------------------------------------------------------------------------------
/mcp-client/src/index.ts:
--------------------------------------------------------------------------------

```typescript
import { Anthropic } from '@anthropic-ai/sdk';

import {
  StdioClientTransport,
  StdioServerParameters,
} from '@modelcontextprotocol/sdk/client/stdio.js';
import {
  ListToolsResultSchema,
  CallToolResultSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import chalk from 'chalk';
import { Tool } from '@anthropic-ai/sdk/resources/index.mjs';
import { Stream } from '@anthropic-ai/sdk/streaming.mjs';
import { consoleStyles, Logger, LoggerOptions } from './logger.js';

interface Message {
  role: 'user' | 'assistant';
  content: string;
}

type MCPClientOptions = StdioServerParameters & {
  loggerOptions?: LoggerOptions;
};

export class MCPClient {
  private anthropicClient: Anthropic;
  private messages: Message[] = [];
  private mcpClient: Client;
  private transport: StdioClientTransport;
  private tools: Tool[] = [];
  private logger: Logger;

  constructor({ loggerOptions, ...serverConfig }: MCPClientOptions) {
    this.anthropicClient = new Anthropic({
      apiKey: process.env.ANTHROPIC_API_KEY,
    });

    this.mcpClient = new Client(
      { name: 'cli-client', version: '1.0.0' },
      { capabilities: {} },
    );

    this.transport = new StdioClientTransport(serverConfig);
    this.logger = new Logger(loggerOptions ?? { mode: 'verbose' });
  }

  async start() {
    try {
      await this.mcpClient.connect(this.transport);
      await this.initMCPTools();
    } catch (error) {
      this.logger.log('Failed to initialize MCP Client: ' + error + '\n', {
        type: 'error',
      });
      process.exit(1);
    }
  }

  async stop() {
    await this.mcpClient.close();
  }

  private async initMCPTools() {
    const toolsResults = await this.mcpClient.request(
      { method: 'tools/list' },
      ListToolsResultSchema,
    );
    this.tools = toolsResults.tools.map(({ inputSchema, ...tool }) => ({
      ...tool,
      input_schema: inputSchema,
    }));
  }

  private formatToolCall(toolName: string, args: any): string {
    return (
      '\n' +
      consoleStyles.tool.bracket('[') +
      consoleStyles.tool.name(toolName) +
      consoleStyles.tool.bracket('] ') +
      consoleStyles.tool.args(JSON.stringify(args, null, 2)) +
      '\n'
    );
  }

  private formatJSON(json: string): string {
    return json
      .replace(/"([^"]+)":/g, chalk.blue('"$1":'))
      .replace(/: "([^"]+)"/g, ': ' + chalk.green('"$1"'));
  }

  private async processStream(
    stream: Stream<Anthropic.Messages.RawMessageStreamEvent>,
  ): Promise<void> {
    let currentMessage = '';
    let currentToolName = '';
    let currentToolInputString = '';

    this.logger.log(consoleStyles.assistant);
    for await (const chunk of stream) {
      switch (chunk.type) {
        case 'message_start':
        case 'content_block_stop':
          continue;

        case 'content_block_start':
          if (chunk.content_block?.type === 'tool_use') {
            currentToolName = chunk.content_block.name;
          }
          break;

        case 'content_block_delta':
          if (chunk.delta.type === 'text_delta') {
            this.logger.log(chunk.delta.text);
            currentMessage += chunk.delta.text;
          } else if (chunk.delta.type === 'input_json_delta') {
            if (currentToolName && chunk.delta.partial_json) {
              currentToolInputString += chunk.delta.partial_json;
            }
          }
          break;

        case 'message_delta':
          if (currentMessage) {
            this.messages.push({
              role: 'assistant',
              content: currentMessage,
            });
          }

          if (chunk.delta.stop_reason === 'tool_use') {
            const toolArgs = currentToolInputString
              ? JSON.parse(currentToolInputString)
              : {};

            this.logger.log(
              this.formatToolCall(currentToolName, toolArgs) + '\n',
            );
            const toolResult = await this.mcpClient.request(
              {
                method: 'tools/call',
                params: {
                  name: currentToolName,
                  arguments: toolArgs,
                },
              },
              CallToolResultSchema,
            );

            const formattedResult = this.formatJSON(
              JSON.stringify(toolResult.content.flatMap((c) => c.text)),
            );

            this.messages.push({
              role: 'user',
              content: formattedResult,
            });

            const nextStream = await this.anthropicClient.messages.create({
              messages: this.messages,
              model: 'claude-3-5-sonnet-20241022',
              max_tokens: 8192,
              tools: this.tools,
              stream: true,
            });
            await this.processStream(nextStream);
          }
          break;

        case 'message_stop':
          break;

        default:
          this.logger.log(`Unknown event type: ${JSON.stringify(chunk)}\n`, {
            type: 'warning',
          });
      }
    }
  }

  async processQuery(query: string) {
    try {
      this.messages.push({ role: 'user', content: query });

      const stream = await this.anthropicClient.messages.create({
        messages: this.messages,
        model: 'claude-3-5-sonnet-20241022',
        max_tokens: 8192,
        tools: this.tools,
        stream: true,
      });
      await this.processStream(stream);

      return this.messages;
    } catch (error) {
      this.logger.log('\nError during query processing: ' + error + '\n', {
        type: 'error',
      });
      if (error instanceof Error) {
        this.logger.log(
          consoleStyles.assistant +
            'I apologize, but I encountered an error: ' +
            error.message +
            '\n',
        );
      }
    }
  }
}

```

--------------------------------------------------------------------------------
/src/oauth/utils.ts:
--------------------------------------------------------------------------------

```typescript
import { NextFunction, Request, Response } from 'express';
import cors from 'cors';
import crypto from 'crypto';
import { model } from './model.js';
import { ApiKeyRecord, apiKeys } from './kv-store.js';
import { createNeonClient } from '../server/api.js';
import { identify } from '../analytics/analytics.js';

export const ensureCorsHeaders = () =>
  cors({
    origin: true,
    methods: '*',
    allowedHeaders: 'Authorization, Origin, Content-Type, Accept, *',
  });

const fetchAccountDetails = async (
  accessToken: string,
): Promise<ApiKeyRecord | null> => {
  const apiKeyRecord = await apiKeys.get(accessToken);
  if (apiKeyRecord) {
    return apiKeyRecord;
  }

  try {
    const neonClient = createNeonClient(accessToken);
    const { data: auth } = await neonClient.getAuthDetails();
    if (auth.auth_method === 'api_key_org') {
      const { data: org } = await neonClient.getOrganization(auth.account_id);
      const record = {
        apiKey: accessToken,
        authMethod: auth.auth_method,
        account: {
          id: auth.account_id,
          name: org.name,
          isOrg: true,
        },
      };
      identify(record.account, { context: { authMethod: record.authMethod } });
      await apiKeys.set(accessToken, record);
      return record;
    }
    const { data: user } = await neonClient.getCurrentUserInfo();
    const record = {
      apiKey: accessToken,
      authMethod: auth.auth_method,
      account: {
        id: user.id,
        name: user.name,
        email: user.email,
        isOrg: false,
      },
    };
    identify(record.account, { context: { authMethod: record.authMethod } });
    await apiKeys.set(accessToken, record);
    return record;
  } catch {
    return null;
  }
};

export const requiresAuth =
  () => async (request: Request, response: Response, next: NextFunction) => {
    const authorization = request.headers.authorization;
    if (!authorization) {
      response.status(401).json({ error: 'Unauthorized' });
      return;
    }

    const accessToken = extractBearerToken(authorization);
    const token = await model.getAccessToken(accessToken);
    if (token) {
      if (!token.expires_at || token.expires_at < Date.now()) {
        response.status(401).json({ error: 'Access token expired' });
        return;
      }

      request.auth = {
        token: token.accessToken,
        clientId: token.client.id,
        scopes: Array.isArray(token.scope)
          ? token.scope
          : (token.scope?.split(' ') ?? []),
        extra: {
          account: {
            id: token.user.id,
            name: token.user.name,
            email: token.user.email,
            isOrg: false,
          },
          client: {
            id: token.client.id,
            name: token.client.client_name,
          },
        },
      };

      next();
      return;
    }

    // If the token is not found, try to resolve the auth headers with Neon for other means of authentication.
    const apiKeyRecord = await fetchAccountDetails(accessToken);
    if (!apiKeyRecord) {
      response.status(401).json({ error: 'Invalid access token' });
      return;
    }
    request.auth = {
      token: accessToken,
      clientId: 'api-key',
      scopes: ['*'],
      extra: {
        account: apiKeyRecord.account,
      },
    };
    next();
    return;
  };

export type DownstreamAuthRequest = {
  responseType: string;
  clientId: string;
  redirectUri: string;
  scope: string[];
  state: string;
  codeChallenge?: string;
  codeChallengeMethod?: string;
};

export const parseAuthRequest = (request: Request): DownstreamAuthRequest => {
  const responseType = (request.query.response_type || '') as string;
  const clientId = (request.query.client_id || '') as string;
  const redirectUri = (request.query.redirect_uri || '') as string;
  const scope = (request.query.scope || '') as string;
  const state = (request.query.state || '') as string;
  const codeChallenge = (request.query.code_challenge as string) || undefined;
  const codeChallengeMethod = (request.query.code_challenge_method ||
    'plain') as string;

  return {
    responseType,
    clientId,
    redirectUri,
    scope: scope.split(' ').filter(Boolean),
    state,
    codeChallenge,
    codeChallengeMethod,
  };
};

export const decodeAuthParams = (state: string): DownstreamAuthRequest => {
  const decoded = atob(state);
  return JSON.parse(decoded);
};

export const generateRandomString = (length: number): string => {
  const charset =
    'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  const array = new Uint8Array(length);
  crypto.getRandomValues(array);
  return Array.from(array, (byte) => charset[byte % charset.length]).join('');
};

export const extractBearerToken = (authorizationHeader: string): string => {
  if (!authorizationHeader) return '';
  return authorizationHeader.replace(/^Bearer\s+/i, '');
};

export const extractClientCredentials = (request: Request) => {
  const authorization = request.headers.authorization;
  if (authorization?.startsWith('Basic ')) {
    const credentials = atob(authorization.replace(/^Basic\s+/i, ''));
    const [clientId, clientSecret] = credentials.split(':');
    return { clientId, clientSecret };
  }

  return {
    clientId: request.body.client_id,
    clientSecret: request.body.client_secret,
  };
};

export const toSeconds = (ms: number): number => {
  return Math.floor(ms / 1000);
};

export const toMilliseconds = (seconds: number): number => {
  return seconds * 1000;
};

export const verifyPKCE = (
  codeChallenge: string,
  codeChallengeMethod: string,
  codeVerifier: string,
): boolean => {
  if (!codeChallenge || !codeChallengeMethod || !codeVerifier) {
    return false;
  }

  if (codeChallengeMethod === 'S256') {
    const hash = crypto
      .createHash('sha256')
      .update(codeVerifier)
      .digest('base64url');
    return codeChallenge === hash;
  }

  if (codeChallengeMethod === 'plain') {
    return codeChallenge === codeVerifier;
  }

  return false;
};

```

--------------------------------------------------------------------------------
/src/tools/utils.ts:
--------------------------------------------------------------------------------

```typescript
import { NEON_DEFAULT_DATABASE_NAME } from '../constants.js';
import { Api, Organization, Branch } from '@neondatabase/api-client';
import { ToolHandlerExtraParams } from './types.js';
import { NotFoundError } from '../server/errors.js';

export const splitSqlStatements = (sql: string) => {
  return sql.split(';').filter(Boolean);
};

export const DESCRIBE_DATABASE_STATEMENTS = [
  `
CREATE OR REPLACE FUNCTION public.show_db_tree()
RETURNS TABLE (tree_structure text) AS
$$
BEGIN
    -- First show all databases
    RETURN QUERY
    SELECT ':file_folder: ' || datname || ' (DATABASE)'
    FROM pg_database 
    WHERE datistemplate = false;

    -- Then show current database structure
    RETURN QUERY
    WITH RECURSIVE 
    -- Get schemas
    schemas AS (
        SELECT 
            n.nspname AS object_name,
            1 AS level,
            n.nspname AS path,
            'SCHEMA' AS object_type
        FROM pg_namespace n
        WHERE n.nspname NOT LIKE 'pg_%' 
        AND n.nspname != 'information_schema'
    ),

    -- Get all objects (tables, views, functions, etc.)
    objects AS (
        SELECT 
            c.relname AS object_name,
            2 AS level,
            s.path || ' → ' || c.relname AS path,
            CASE c.relkind
                WHEN 'r' THEN 'TABLE'
                WHEN 'v' THEN 'VIEW'
                WHEN 'm' THEN 'MATERIALIZED VIEW'
                WHEN 'i' THEN 'INDEX'
                WHEN 'S' THEN 'SEQUENCE'
                WHEN 'f' THEN 'FOREIGN TABLE'
            END AS object_type
        FROM pg_class c
        JOIN pg_namespace n ON n.oid = c.relnamespace
        JOIN schemas s ON n.nspname = s.object_name
        WHERE c.relkind IN ('r','v','m','i','S','f')

        UNION ALL

        SELECT 
            p.proname AS object_name,
            2 AS level,
            s.path || ' → ' || p.proname AS path,
            'FUNCTION' AS object_type
        FROM pg_proc p
        JOIN pg_namespace n ON n.oid = p.pronamespace
        JOIN schemas s ON n.nspname = s.object_name
    ),

    -- Combine schemas and objects
    combined AS (
        SELECT * FROM schemas
        UNION ALL
        SELECT * FROM objects
    )

    -- Final output with tree-like formatting
    SELECT 
        REPEAT('    ', level) || 
        CASE 
            WHEN level = 1 THEN '└── :open_file_folder: '
            ELSE '    └── ' || 
                CASE object_type
                    WHEN 'TABLE' THEN ':bar_chart: '
                    WHEN 'VIEW' THEN ':eye: '
                    WHEN 'MATERIALIZED VIEW' THEN ':newspaper: '
                    WHEN 'FUNCTION' THEN ':zap: '
                    WHEN 'INDEX' THEN ':mag: '
                    WHEN 'SEQUENCE' THEN ':1234: '
                    WHEN 'FOREIGN TABLE' THEN ':globe_with_meridians: '
                    ELSE ''
                END
        END || object_name || ' (' || object_type || ')'
    FROM combined
    ORDER BY path;
END;
$$ LANGUAGE plpgsql;
`,
  `     
-- To use the function:
SELECT * FROM show_db_tree();
`,
];

/**
 * Returns the default database for a project branch
 * If a database name is provided, it fetches and returns that database
 * Otherwise, it looks for a database named 'neondb' and returns that
 * If 'neondb' doesn't exist, it returns the first available database
 * Throws an error if no databases are found
 */
export async function getDefaultDatabase(
  {
    projectId,
    branchId,
    databaseName,
  }: {
    projectId: string;
    branchId: string;
    databaseName?: string;
  },
  neonClient: Api<unknown>,
) {
  const { data } = await neonClient.listProjectBranchDatabases(
    projectId,
    branchId,
  );
  const databases = data.databases;
  if (databases.length === 0) {
    throw new NotFoundError('No databases found in your project branch');
  }

  if (databaseName) {
    const requestedDatabase = databases.find((db) => db.name === databaseName);
    if (requestedDatabase) {
      return requestedDatabase;
    }
  }

  const defaultDatabase = databases.find(
    (db) => db.name === NEON_DEFAULT_DATABASE_NAME,
  );
  return defaultDatabase || databases[0];
}

/**
 * Resolves the organization ID for API calls that require org_id parameter.
 *
 * For new users (those without billing_account), this function fetches user's organizations and auto-selects only organization managed by console. If there are multiple organizations managed by console, it throws an error asking user to specify org_id.
 *
 * For existing users (with billing_account), returns undefined to use default behavior.
 *
 * @param params - The parameters object that may contain org_id
 * @param neonClient - The Neon API client
 * @returns The organization to use, or undefined for default behavior
 */
export async function getOrgByOrgIdOrDefault(
  params: { org_id?: string },
  neonClient: Api<unknown>,
  extra: ToolHandlerExtraParams,
): Promise<Organization | undefined> {
  // 1. If org_id is provided use it
  // 2. If using Org API key, use the account id
  if (params.org_id || extra.account.isOrg) {
    const orgId = params.org_id || extra.account.id;
    const { data } = await neonClient.getOrganization(orgId);
    return data;
  }

  const { data: user } = await neonClient.getCurrentUserInfo();
  if (user.billing_account) {
    return undefined;
  }

  const { data: response } = await neonClient.getCurrentUserOrganizations();
  const organizations = response.organizations || [];

  // 1. Filter organizations by managed_by==console, if there is only one organization, return that
  const consoleOrganizations = organizations.filter(
    (org) => org.managed_by === 'console',
  );

  if (consoleOrganizations.length === 1) {
    return consoleOrganizations[0];
  }

  // 2. If there are no organizations managed by console, and if there is only one organization (unfiltered), then return that organization
  if (consoleOrganizations.length === 0 && organizations.length === 1) {
    return organizations[0];
  }

  // 3. If there are no organizations at all, then throw error saying there are no organizations
  if (organizations.length === 0) {
    throw new NotFoundError('No organizations found for this user');
  }

  // 4. If there are multiple organizations, then throw error mentioning list of all these orgs (unfiltered)
  const orgList = organizations
    .map(
      (org) => `- ${org.name} (ID: ${org.id}) [managed by: ${org.managed_by}]`,
    )
    .join('\n');
  throw new NotFoundError(
    `Multiple organizations found. Please specify the org_id parameter with one of the following organization IDs:\n${orgList}`,
  );
}

export function filterOrganizations(
  organizations: Organization[],
  search?: string,
) {
  if (!search) {
    return organizations;
  }
  const searchLower = search.toLowerCase();
  return organizations.filter(
    (org) =>
      org.name.toLowerCase().includes(searchLower) ||
      org.id.toLowerCase().includes(searchLower),
  );
}

/**
 * Checks if a string looks like a branch ID based on the neonctl format
 * Branch IDs have format like "br-small-term-683261" (br- prefix + haiku pattern)
 */
export function looksLikeBranchId(branch: string): boolean {
  const HAIKU_REGEX = /^[a-z0-9]+-[a-z0-9]+-[a-z0-9]+$/;
  return branch.startsWith('br-') && HAIKU_REGEX.test(branch.substring(3));
}

/**
 * Resolves a branch name or ID to the actual branch ID
 * If the input looks like a branch ID, returns it as-is
 * Otherwise, searches for a branch with matching name and returns its ID
 */
export async function resolveBranchId(
  branchNameOrId: string,
  projectId: string,
  neonClient: Api<unknown>,
): Promise<{ branchId: string; branches: Branch[] }> {
  // Get all branches (we'll need this data anyway)
  const branchResponse = await neonClient.listProjectBranches({
    projectId,
  });
  const branches = branchResponse.data.branches;

  if (looksLikeBranchId(branchNameOrId)) {
    // Verify the branch ID actually exists
    const branch = branches.find((b) => b.id === branchNameOrId);
    if (!branch) {
      throw new NotFoundError(
        `Branch ID "${branchNameOrId}" not found in project ${projectId}`,
      );
    }
    return { branchId: branchNameOrId, branches };
  }

  // Search by name
  const branch = branches.find((b) => b.name === branchNameOrId);
  if (!branch) {
    const availableBranches = branches.map((b) => b.name).join(', ');
    throw new NotFoundError(
      `Branch name "${branchNameOrId}" not found in project ${projectId}.\nAvailable branches: ${availableBranches}`,
    );
  }

  return { branchId: branch.id, branches };
}

```

--------------------------------------------------------------------------------
/src/tools-evaluations/prepare-database-migration.eval.ts:
--------------------------------------------------------------------------------

```typescript
import { Eval, EvalCase, Reporter, reportFailures } from 'braintrust';
import { LLMClassifierFromTemplate } from 'autoevals';

import { createApiClient } from '@neondatabase/api-client';
import { deleteNonDefaultBranches, evaluateTask } from './evalUtils';

const EVAL_INFO = {
  projectId: 'black-recipe-75251165',
  roleName: 'neondb_owner',
  databaseName: 'neondb',
  mainBranchId: 'br-cold-bird-a5icgh5h',
};

const getMainBranchDatabaseSchema = async () => {
  const neonClient = createApiClient({
    apiKey: process.env.NEON_API_KEY!,
  });

  const dbSchema = await neonClient.getProjectBranchSchema({
    projectId: EVAL_INFO.projectId,
    branchId: EVAL_INFO.mainBranchId,
    db_name: EVAL_INFO.databaseName,
  });

  return dbSchema.data.sql;
};

const factualityAnthropic = LLMClassifierFromTemplate({
  name: 'Factuality Anthropic',
  promptTemplate: `
  You are comparing a submitted answer to an expert answer on a given question. Here is the data:
[BEGIN DATA]
************
[Question]: {{{input}}}
************
[Expert]: {{{expected}}}
************
[Submission]: {{{output}}}
************
[END DATA]

Compare the factual content of the submitted answer with the expert answer. 
Implementation details like specific IDs, or exact formatting should be considered non-factual differences.

Ignore the following differences:
- Specific migration IDs or references
- Formatting or structural variations
- Order of presenting the information
- Restatements of the same request/question
- Additional confirmatory language that doesn't add new information

The submitted answer may either be:
(A) A subset missing key factual information from the expert answer
(B) A superset that FIRST agrees with the expert answer's core facts AND THEN adds additional factual information  
(C) Factually equivalent to the expert answer
(D) In factual disagreement with or takes a completely different action than the expert answer
(E) Different only in non-factual implementation details

Select the most appropriate option, prioritizing the core factual content over implementation specifics.
  `,
  choiceScores: {
    A: 0.4,
    B: 0.8,
    C: 1,
    D: 0,
    E: 1,
  },
  temperature: 0,
  useCoT: true,
  model: 'claude-3-5-sonnet-20241022',
});

const mainBranchIntegrityCheck = async (args: {
  input: string;
  output: string;
  expected: string;
  metadata?: {
    databaseSchemaBeforeRun: string;
    databaseSchemaAfterRun: string;
  };
}) => {
  const databaseSchemaBeforeRun = args.metadata?.databaseSchemaBeforeRun;
  const databaseSchemaAfterRun = args.metadata?.databaseSchemaAfterRun;
  const databaseSchemaAfterRunResponseIsComplete =
    databaseSchemaAfterRun?.includes('PostgreSQL database dump complete') ??
    false;

  // sometimes the pg_dump fails to deliver the full responses, which leads to false negatives
  // so we must eject
  if (!databaseSchemaAfterRunResponseIsComplete) {
    return null;
  }

  const isSame = databaseSchemaBeforeRun === databaseSchemaAfterRun;

  return {
    name: 'Main Branch Integrity Check',
    score: isSame ? 1 : 0,
  };
};

Eval('prepare_database_migration', {
  data: (): EvalCase<
    string,
    string,
    | {
        databaseSchemaBeforeRun: string;
        databaseSchemaAfterRun: string;
      }
    | undefined
  >[] => {
    return [
      // Add column
      {
        input: `in my ${EVAL_INFO.projectId} project, add a new column Description to the posts table`,
        expected: `
    I've verified that the Description column has been successfully added to the posts table in a temporary branch. Would you like to commit the migration to the main branch?

    Migration Details:
    - Migration ID: <migration_id>
    - Temporary Branch Name: <temporary_branch_name>
    - Temporary Branch ID: <temporary_branch_id>
    - Migration Result: <migration_result>
    `,
      },

      // Add column with different type
      {
        input: `in my ${EVAL_INFO.projectId} project, add view_count column to posts table`,
        expected: `
    I've verified that the view_count column has been successfully added to the posts table in a temporary branch. Would you like to commit the migration to the main branch?

    Migration Details:
    - Migration ID: <migration_id>
    - Temporary Branch Name: <temporary_branch_name>
    - Temporary Branch ID: <temporary_branch_id>
    - Migration Result: <migration_result>
    `,
      },

      // Rename column
      {
        input: `in my ${EVAL_INFO.projectId} project, rename the content column to body in posts table`,
        expected: `
    I've verified that the content column has been successfully renamed to body in the posts table in a temporary branch. Would you like to commit the migration to the main branch?

    Migration Details:
    - Migration ID: <migration_id>
    - Temporary Branch Name: <temporary_branch_name>
    - Temporary Branch ID: <temporary_branch_id>
    - Migration Result: <migration_result>
    `,
      },

      // Add index
      {
        input: `in my ${EVAL_INFO.projectId} project, create an index on title column in posts table`,
        expected: `
    I've verified that the index has been successfully created on the title column in the posts table in a temporary branch. Would you like to commit the migration to the main branch?

    Migration Details:
    - Migration ID: <migration_id>
    - Temporary Branch Name: <temporary_branch_name>
    - Temporary Branch ID: <temporary_branch_id>
    - Migration Result: <migration_result>
    `,
      },

      // Drop column
      {
        input: `in my ${EVAL_INFO.projectId} project, drop the content column from posts table`,
        expected: `
    I've verified that the content column has been successfully dropped from the posts table in a temporary branch. Would you like to commit the migration to the main branch?

    Migration Details:
    - Migration ID: <migration_id>
    - Temporary Branch Name: <temporary_branch_name>
    - Temporary Branch ID: <temporary_branch_id>
    - Migration Result: <migration_result>
    `,
      },

      // Alter column type
      {
        input: `in my ${EVAL_INFO.projectId} project, change the title column type to text in posts table`,
        expected: `
    I've verified that the data type of the title column has been successfully changed in the posts table in a temporary branch. Would you like to commit the migration to the main branch?

    Migration Details:
    - Migration ID: <migration_id>
    - Temporary Branch Name: <temporary_branch_name>
    - Temporary Branch ID: <temporary_branch_id>
    - Migration Result: <migration_result>
    `,
      },

      // Add boolean column
      {
        input: `in my ${EVAL_INFO.projectId} project, add is_published column to posts table`,
        expected: `
    I've verified that the is_published column has been successfully added to the posts table in a temporary branch. Would you like to commit the migration to the main branch?

    Migration Details:
    - Migration ID: <migration_id>
    - Temporary Branch Name: <temporary_branch_name>
    - Temporary Branch ID: <temporary_branch_id>
    - Migration Result: <migration_result>
    `,
      },

      // Add numeric column
      {
        input: `in my ${EVAL_INFO.projectId} project, add likes_count column to posts table`,
        expected: `
    I've verified that the likes_count column has been successfully added to the posts table in a temporary branch. Would you like to commit the migration to the main branch?

    Migration Details:
    - Migration ID: <migration_id>
    - Temporary Branch Name: <temporary_branch_name>
    - Temporary Branch ID: <temporary_branch_id>
    - Migration Result: <migration_result>
    `,
      },

      // Create index
      {
        input: `in my ${EVAL_INFO.projectId} project, create index on title column in posts table`,
        expected: `
    I've verified that the index has been successfully created on the title column in the posts table in a temporary branch. Would you like to commit the migration to the main branch?

    Migration Details:
    - Migration ID: <migration_id>
    - Temporary Branch Name: <temporary_branch_name>
    - Temporary Branch ID: <temporary_branch_id>
    - Migration Result: <migration_result>
    `,
      },
    ];
  },
  task: async (input, hooks) => {
    const databaseSchemaBeforeRun = await getMainBranchDatabaseSchema();
    hooks.metadata.databaseSchemaBeforeRun = databaseSchemaBeforeRun;

    const llmCallMessages = await evaluateTask(input);

    const databaseSchemaAfterRun = await getMainBranchDatabaseSchema();
    hooks.metadata.databaseSchemaAfterRun = databaseSchemaAfterRun;
    hooks.metadata.llmCallMessages = llmCallMessages;

    deleteNonDefaultBranches(EVAL_INFO.projectId);

    const finalMessage = llmCallMessages[llmCallMessages.length - 1];
    return finalMessage.content;
  },
  trialCount: 20,
  maxConcurrency: 2,
  scores: [factualityAnthropic, mainBranchIntegrityCheck],
});

Reporter('Prepare Database Migration Reporter', {
  reportEval: async (evaluator, result, { verbose, jsonl }) => {
    const { results, summary } = result;
    const failingResults = results.filter(
      (r: { error: unknown }) => r.error !== undefined,
    );

    if (failingResults.length > 0) {
      reportFailures(evaluator, failingResults, { verbose, jsonl });
    }

    console.log(jsonl ? JSON.stringify(summary) : summary);
    return failingResults.length === 0;
  },

  // cleanup branches after the run
  reportRun: async (evalReports) => {
    await deleteNonDefaultBranches(EVAL_INFO.projectId);

    return evalReports.every((r) => r);
  },
});

```

--------------------------------------------------------------------------------
/src/tools/toolsSchema.ts:
--------------------------------------------------------------------------------

```typescript
import {
  ListProjectsParams,
  ListSharedProjectsParams,
} from '@neondatabase/api-client';
import { z } from 'zod';
import { NEON_DEFAULT_DATABASE_NAME } from '../constants.js';

type ZodObjectParams<T> = z.ZodObject<{ [key in keyof T]: z.ZodType<T[key]> }>;

const DATABASE_NAME_DESCRIPTION = `The name of the database. If not provided, the default ${NEON_DEFAULT_DATABASE_NAME} or first available database is used.`;

export const listProjectsInputSchema = z.object({
  cursor: z
    .string()
    .optional()
    .describe(
      'Specify the cursor value from the previous response to retrieve the next batch of projects.',
    ),
  limit: z
    .number()
    .default(10)
    .describe(
      'Specify a value from 1 to 400 to limit number of projects in the response.',
    ),
  search: z
    .string()
    .optional()
    .describe(
      'Search by project name or id. You can specify partial name or id values to filter results.',
    ),
  org_id: z.string().optional().describe('Search for projects by org_id.'),
}) satisfies ZodObjectParams<ListProjectsParams>;

export const createProjectInputSchema = z.object({
  name: z
    .string()
    .optional()
    .describe('An optional name of the project to create.'),
  org_id: z
    .string()
    .optional()
    .describe('Create project in a specific organization.'),
});

export const deleteProjectInputSchema = z.object({
  projectId: z.string().describe('The ID of the project to delete'),
});

export const describeProjectInputSchema = z.object({
  projectId: z.string().describe('The ID of the project to describe'),
});

export const runSqlInputSchema = z.object({
  sql: z.string().describe('The SQL query to execute'),
  projectId: z
    .string()
    .describe('The ID of the project to execute the query against'),
  branchId: z
    .string()
    .optional()
    .describe(
      'An optional ID of the branch to execute the query against. If not provided the default branch is used.',
    ),
  databaseName: z.string().optional().describe(DATABASE_NAME_DESCRIPTION),
});

export const runSqlTransactionInputSchema = z.object({
  sqlStatements: z.array(z.string()).describe('The SQL statements to execute'),
  projectId: z
    .string()
    .describe('The ID of the project to execute the query against'),
  branchId: z
    .string()
    .optional()
    .describe(
      'An optional ID of the branch to execute the query against. If not provided the default branch is used.',
    ),
  databaseName: z.string().optional().describe(DATABASE_NAME_DESCRIPTION),
});

export const explainSqlStatementInputSchema = z.object({
  sql: z.string().describe('The SQL statement to analyze'),
  projectId: z
    .string()
    .describe('The ID of the project to execute the query against'),
  branchId: z
    .string()
    .optional()
    .describe(
      'An optional ID of the branch to execute the query against. If not provided the default branch is used.',
    ),
  databaseName: z.string().optional().describe(DATABASE_NAME_DESCRIPTION),
  analyze: z
    .boolean()
    .default(true)
    .describe('Whether to include ANALYZE in the EXPLAIN command'),
});
export const describeTableSchemaInputSchema = z.object({
  tableName: z.string().describe('The name of the table'),
  projectId: z
    .string()
    .describe('The ID of the project to execute the query against'),
  branchId: z
    .string()
    .optional()
    .describe(
      'An optional ID of the branch to execute the query against. If not provided the default branch is used.',
    ),
  databaseName: z.string().optional().describe(DATABASE_NAME_DESCRIPTION),
});

export const getDatabaseTablesInputSchema = z.object({
  projectId: z.string().describe('The ID of the project'),
  branchId: z
    .string()
    .optional()
    .describe(
      'An optional ID of the branch. If not provided the default branch is used.',
    ),
  databaseName: z.string().optional().describe(DATABASE_NAME_DESCRIPTION),
});

export const createBranchInputSchema = z.object({
  projectId: z
    .string()
    .describe('The ID of the project to create the branch in'),
  branchName: z.string().optional().describe('An optional name for the branch'),
});

export const prepareDatabaseMigrationInputSchema = z.object({
  migrationSql: z
    .string()
    .describe('The SQL to execute to create the migration'),
  projectId: z
    .string()
    .describe('The ID of the project to execute the query against'),
  databaseName: z.string().optional().describe(DATABASE_NAME_DESCRIPTION),
});

export const completeDatabaseMigrationInputSchema = z.object({
  migrationId: z.string(),
});

export const describeBranchInputSchema = z.object({
  projectId: z.string().describe('The ID of the project'),
  branchId: z.string().describe('An ID of the branch to describe'),
  databaseName: z.string().optional().describe(DATABASE_NAME_DESCRIPTION),
});

export const deleteBranchInputSchema = z.object({
  projectId: z.string().describe('The ID of the project containing the branch'),
  branchId: z.string().describe('The ID of the branch to delete'),
});

export const getConnectionStringInputSchema = z.object({
  projectId: z
    .string()
    .describe(
      'The ID of the project. If not provided, the only available project will be used.',
    ),
  branchId: z
    .string()
    .optional()
    .describe(
      'The ID or name of the branch. If not provided, the default branch will be used.',
    ),
  computeId: z
    .string()
    .optional()
    .describe(
      'The ID of the compute/endpoint. If not provided, the read-write compute associated with the branch will be used.',
    ),
  databaseName: z.string().optional().describe(DATABASE_NAME_DESCRIPTION),
  roleName: z
    .string()
    .optional()
    .describe(
      'The name of the role to connect with. If not provided, the database owner name will be used.',
    ),
});

export const provisionNeonAuthInputSchema = z.object({
  projectId: z
    .string()
    .describe('The ID of the project to provision Neon Auth for'),
  database: z.string().optional().describe(DATABASE_NAME_DESCRIPTION),
});

export const prepareQueryTuningInputSchema = z.object({
  sql: z.string().describe('The SQL statement to analyze and tune'),
  databaseName: z
    .string()
    .describe('The name of the database to execute the query against'),
  projectId: z
    .string()
    .describe('The ID of the project to execute the query against'),
  roleName: z
    .string()
    .optional()
    .describe(
      'The name of the role to connect with. If not provided, the default role (usually "neondb_owner") will be used.',
    ),
});

export const completeQueryTuningInputSchema = z.object({
  suggestedSqlStatements: z
    .array(z.string())
    .describe(
      'The SQL DDL statements to execute to improve performance. These statements are the result of the prior steps, for example creating additional indexes.',
    ),
  applyChanges: z
    .boolean()
    .default(false)
    .describe('Whether to apply the suggested changes to the main branch'),
  tuningId: z
    .string()
    .describe(
      'The ID of the tuning to complete. This is NOT the branch ID. Remember this ID from the prior step using tool prepare_query_tuning.',
    ),
  databaseName: z
    .string()
    .describe('The name of the database to execute the query against'),
  projectId: z
    .string()
    .describe('The ID of the project to execute the query against'),
  roleName: z
    .string()
    .optional()
    .describe(
      'The name of the role to connect with. If you have used a specific role in prepare_query_tuning you MUST pass the same role again to this tool. If not provided, the default role (usually "neondb_owner") will be used.',
    ),
  shouldDeleteTemporaryBranch: z
    .boolean()
    .default(true)
    .describe('Whether to delete the temporary branch after tuning'),
  temporaryBranchId: z
    .string()
    .describe(
      'The ID of the temporary branch that needs to be deleted after tuning.',
    ),
  branchId: z
    .string()
    .optional()
    .describe(
      'The ID or name of the branch that receives the changes. If not provided, the default (main) branch will be used.',
    ),
});

export const listSlowQueriesInputSchema = z.object({
  projectId: z
    .string()
    .describe('The ID of the project to list slow queries from'),
  branchId: z
    .string()
    .optional()
    .describe(
      'An optional ID of the branch. If not provided the default branch is used.',
    ),
  databaseName: z.string().optional().describe(DATABASE_NAME_DESCRIPTION),
  computeId: z
    .string()
    .optional()
    .describe(
      'The ID of the compute/endpoint. If not provided, the read-write compute associated with the branch will be used.',
    ),
  limit: z
    .number()
    .optional()
    .default(10)
    .describe('Maximum number of slow queries to return'),
  minExecutionTime: z
    .number()
    .optional()
    .default(1000)
    .describe(
      'Minimum execution time in milliseconds to consider a query as slow',
    ),
});

export const listBranchComputesInputSchema = z.object({
  projectId: z
    .string()
    .optional()
    .describe(
      'The ID of the project. If not provided, the only available project will be used.',
    ),
  branchId: z
    .string()
    .optional()
    .describe(
      'The ID of the branch. If provided, endpoints for this specific branch will be listed.',
    ),
});

export const listOrganizationsInputSchema = z.object({
  search: z
    .string()
    .optional()
    .describe(
      'Search organizations by name or ID. You can specify partial name or ID values to filter results.',
    ),
});

export const listSharedProjectsInputSchema = z.object({
  cursor: z
    .string()
    .optional()
    .describe(
      'Specify the cursor value from the previous response to retrieve the next batch of shared projects.',
    ),
  limit: z
    .number()
    .default(10)
    .describe(
      'Specify a value from 1 to 400 to limit number of shared projects in the response.',
    ),
  search: z
    .string()
    .optional()
    .describe(
      'Search by project name or id. You can specify partial name or id values to filter results.',
    ),
}) satisfies ZodObjectParams<ListSharedProjectsParams>;

export const resetFromParentInputSchema = z.object({
  projectId: z.string().describe('The ID of the project containing the branch'),
  branchIdOrName: z
    .string()
    .describe('The name or ID of the branch to reset from its parent'),
  preserveUnderName: z
    .string()
    .optional()
    .describe(
      'Optional name to preserve the current state under a new branch before resetting',
    ),
});

export const compareDatabaseSchemaInputSchema = z.object({
  projectId: z.string().describe('The ID of the project'),
  branchId: z.string().describe('The ID of the branch'),
  databaseName: z.string().describe(DATABASE_NAME_DESCRIPTION),
});

```
Page 1/2FirstPrevNextLast