This is page 1 of 3. Use http://codebase.md/neondatabase-labs/mcp-server-neon?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .bun-version
├── .dockerignore
├── .env.example
├── .github
│ └── workflows
│ ├── claude-code-review.yml
│ ├── claude.yml
│ ├── koyeb-preview.yml
│ ├── koyeb-prod.yml
│ └── pr.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .prettierignore
├── .prettierrc
├── AGENTS.md
├── bun.lock
├── CHANGELOG.md
├── CLAUDE.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
│ ├── prompts.ts
│ ├── resources.ts
│ ├── sentry
│ │ ├── instrument.ts
│ │ └── utils.ts
│ ├── server
│ │ ├── api.ts
│ │ ├── errors.ts
│ │ └── index.ts
│ ├── tools
│ │ ├── definitions.ts
│ │ ├── handlers
│ │ │ ├── connection-string.ts
│ │ │ ├── decribe-project.ts
│ │ │ ├── describe-branch.ts
│ │ │ ├── fetch.ts
│ │ │ ├── list-orgs.ts
│ │ │ ├── list-projects.ts
│ │ │ ├── neon-auth.ts
│ │ │ ├── search.ts
│ │ │ ├── urls.ts
│ │ │ └── utils.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
│ │ └── helpers.ts
│ ├── utils
│ │ ├── client-application.ts
│ │ ├── logger.ts
│ │ └── polyfills.ts
│ └── views
│ ├── approval-dialog.pug
│ └── styles.css
├── tsconfig.json
└── tsconfig.test.json
```
# Files
--------------------------------------------------------------------------------
/.bun-version:
--------------------------------------------------------------------------------
```
1 | 1.2.13
2 |
```
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
```
1 | v22.15.1
2 |
```
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
```
1 | save-exact=true
```
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
```
1 | build/
2 | landing/.next
3 | landing/out
4 |
```
--------------------------------------------------------------------------------
/mcp-client/.env.example:
--------------------------------------------------------------------------------
```
1 | ANTHROPIC_API_KEY=
2 | NEON_API_KEY=
3 |
```
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
```
1 | {
2 | "printWidth": 80,
3 | "singleQuote": true,
4 | "semi": true
5 | }
6 |
```
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
```
1 | **/node_modules
2 | **/*.log
3 | **/.env
4 | **/dist
5 | **/build
6 | # VS Code history extension
7 | **/.history
8 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | node_modules/
2 | *.log
3 | .env
4 | dist/
5 | build/
6 |
7 | # landing page generated files are placed in root /public directory,
8 | # so ignoring it in order to do not commit something unintentionally.
9 | # if something should be added in /public it can be done by force add:
10 | # git add -f public/somefile
11 | /public/
12 |
13 | # IDE stuff
14 | .history
15 | .idea
16 |
```
--------------------------------------------------------------------------------
/landing/.gitignore:
--------------------------------------------------------------------------------
```
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # env files (can opt-in for committing if needed)
34 | .env*
35 |
36 | # vercel
37 | .vercel
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
43 | # part of building process
44 | tools.json
45 |
```
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
```
1 | ## BrainTrust
2 | BRAINTRUST_API_KEY=
3 |
4 | ## Neon BaaS team org api key
5 | NEON_API_KEY=
6 |
7 | ## Anthropic api key to run the evals
8 | ANTHROPIC_API_KEY=
9 |
10 | ## Neon API
11 | NEON_API_HOST=https://api.neon.tech/api/v2
12 | ## OAuth upstream oauth host
13 | UPSTREAM_OAUTH_HOST='https://oauth2.neon.tech';
14 |
15 | ## OAuth client id
16 | CLIENT_ID=
17 | ## OAuth client secret
18 | CLIENT_SECRET=
19 |
20 | ## Redirect URI for OIDC callback
21 | REDIRECT_URI=http://localhost:3001/callback
22 |
23 | ## A connection string to postgres database for client and token persistence
24 | ## Optional while running in MCP in stdio
25 | OAUTH_DATABASE_URL=
26 |
27 | ## A secret key to sign and verify the cookies
28 | ## Optional while running MCP in stdio
29 | COOKIE_SECRET=
30 |
31 | ## Optional Analytics
32 | ANALYTICS_WRITE_KEY=
33 |
34 | ## Optional Sentry
35 | SENTRY_DSN=
```
--------------------------------------------------------------------------------
/mcp-client/README.md:
--------------------------------------------------------------------------------
```markdown
1 | <picture>
2 | <source media="(prefers-color-scheme: dark)" srcset="https://neon.com/brand/neon-logo-dark-color.svg">
3 | <source media="(prefers-color-scheme: light)" srcset="https://neon.com/brand/neon-logo-light-color.svg">
4 | <img width="250px" alt="Neon Logo fallback" src="https://neon.com/brand/neon-logo-dark-color.svg">
5 | </picture>
6 |
7 | ## MCP Client CLI
8 |
9 | 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).
10 |
11 | ## Requirements
12 |
13 | - ANTHROPIC_API_KEY - Get one from [Anthropic](https://console.anthropic.com/)
14 | - Node.js >= v18.0.0
15 |
16 | ## How to use
17 |
18 | ```bash
19 | export ANTHROPIC_API_KEY=your_key_here
20 | npx @neondatabase/mcp-client-cli --server-command="npx" --server-args="-y @neondatabase/mcp-server-neon start <neon-api-key>"
21 | ```
22 |
23 | ## How to develop
24 |
25 | 1. Clone the repository
26 | 2. Setup a `.env` file based on the `.env.example` file
27 | 3. Run `npm install`
28 | 4. Run `npm run start:mcp-server-neon`
29 |
```
--------------------------------------------------------------------------------
/landing/README.md:
--------------------------------------------------------------------------------
```markdown
1 | 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).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | 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.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | 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.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
37 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | <picture>
2 | <source media="(prefers-color-scheme: dark)" srcset="https://neon.com/brand/neon-logo-dark-color.svg">
3 | <source media="(prefers-color-scheme: light)" srcset="https://neon.com/brand/neon-logo-light-color.svg">
4 | <img width="250px" alt="Neon Logo fallback" src="https://neon.com/brand/neon-logo-dark-color.svg">
5 | </picture>
6 |
7 | # Neon MCP Server
8 |
9 | [](https://cursor.com/install-mcp?name=Neon&config=eyJ1cmwiOiJodHRwczovL21jcC5uZW9uLnRlY2gvbWNwIn0%3D)
10 |
11 | **Neon MCP Server** is an open-source tool that lets you interact with your Neon Postgres databases in **natural language**.
12 |
13 | [](https://www.npmjs.com/package/@neondatabase/mcp-server-neon)
14 | [](https://www.npmjs.com/package/@neondatabase/mcp-server-neon)
15 | [](https://opensource.org/licenses/MIT)
16 |
17 | 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).
18 |
19 | 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.
20 |
21 | Some of the key features of the Neon MCP server include:
22 |
23 | - **Natural language interaction:** Manage Neon databases using intuitive, conversational commands.
24 | - **Simplified database management:** Perform complex actions without writing SQL or directly using the Neon API.
25 | - **Accessibility for non-developers:** Empower users with varying technical backgrounds to interact with Neon databases.
26 | - **Database migration support:** Leverage Neon's branching capabilities for database schema changes initiated via natural language.
27 |
28 | For example, in Claude Desktop, or any MCP Client, you can use natural language to accomplish things with Neon, such as:
29 |
30 | - `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.`
31 | - `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".`
32 | - `Can you give me a summary of all of my Neon projects and what data is in each one?`
33 |
34 | > [!WARNING]
35 | > **Neon MCP Server Security Considerations**
36 | > 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.
37 | >
38 | > 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.
39 | >
40 | > For more information, see [MCP security guidance →](https://neon.tech/docs/ai/neon-mcp-server#mcp-security-guidance).
41 |
42 | ## Setting up Neon MCP Server
43 |
44 | You have two options for connecting your MCP client to Neon:
45 |
46 | 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.
47 |
48 | 2. **Local MCP Server:** Run the Neon MCP server locally on your machine, authenticating with a Neon API key.
49 |
50 | ## Prerequisites
51 |
52 | - An MCP Client application.
53 | - A [Neon account](https://console.neon.tech/signup).
54 | - **Node.js (>= v18.0.0) and npm:** Download from [nodejs.org](https://nodejs.org).
55 |
56 | 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.
57 |
58 | ### Option 1. Remote Hosted MCP Server (Preview)
59 |
60 | 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.
61 |
62 | - Add the following "Neon" entry to your client's MCP server configuration file (e.g., `mcp.json`, `mcp_config.json`):
63 |
64 | ```json
65 | {
66 | "mcpServers": {
67 | "Neon": {
68 | "command": "npx",
69 | "args": ["-y", "mcp-remote", "https://mcp.neon.tech/mcp"]
70 | }
71 | }
72 | }
73 | ```
74 |
75 | - Save the configuration file.
76 | - Restart or refresh your MCP client.
77 | - An OAuth window will open in your browser. Follow the prompts to authorize your MCP client to access your Neon account.
78 |
79 | > 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.
80 |
81 | Remote MCP Server also supports authentication using an API key in the `Authorization` header if your client supports it.
82 |
83 | #### Quick Setup with API Key
84 |
85 | **Don't want to manually create an API key?**
86 |
87 | Run [`neonctl@latest init`](https://neon.com/docs/reference/cli-init) to automatically configure Neon's MCP Server with one command:
88 |
89 | ```bash
90 | npx neonctl@latest init
91 | ```
92 |
93 | This works with Cursor, VS Code (GitHub Copilot), and Claude Code. It will authenticate via OAuth, create a Neon API key for you, and configure your editor automatically.
94 |
95 | #### Manual API Key Setup
96 |
97 | For other IDEs or manual configuration, [create a Neon API key](https://console.neon.tech/app/settings?modal=create_api_key) in the Neon Console and use this configuration:
98 |
99 | ```json
100 | {
101 | "mcpServers": {
102 | "Neon": {
103 | "url": "https://mcp.neon.tech/mcp",
104 | "headers": {
105 | "Authorization": "Bearer <$NEON_API_KEY>"
106 | }
107 | }
108 | }
109 | }
110 | ```
111 |
112 | > Provider organization's API key to limit access to projects under the organization only.
113 |
114 | **Read-Only Mode:** To prevent accidental modifications, enable read-only mode by adding the `x-read-only` header. This restricts the MCP server to only safe, non-destructive operations:
115 |
116 | ```json
117 | {
118 | "mcpServers": {
119 | "Neon": {
120 | "url": "https://mcp.neon.tech/mcp",
121 | "headers": {
122 | "Authorization": "Bearer <$NEON_API_KEY>",
123 | "x-read-only": "true"
124 | }
125 | }
126 | }
127 | }
128 | ```
129 |
130 | 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.
131 |
132 | ### Option 2. Local MCP Server
133 |
134 | 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.
135 |
136 | 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:
137 |
138 | ```json
139 | {
140 | "mcpServers": {
141 | "neon": {
142 | "command": "npx",
143 | "args": [
144 | "-y",
145 | "@neondatabase/mcp-server-neon",
146 | "start",
147 | "<YOUR_NEON_API_KEY>"
148 | ]
149 | }
150 | }
151 | }
152 | ```
153 |
154 | ### Troubleshooting
155 |
156 | 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:
157 |
158 | ```bash
159 | npx -y @neondatabase/mcp-server-neon start <YOUR_NEON_API_KEY>
160 | ```
161 |
162 | #### Troubleshooting on Windows
163 |
164 | 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:
165 |
166 | ```json
167 | {
168 | "mcpServers": {
169 | "neon": {
170 | "command": "cmd",
171 | "args": [
172 | "/c",
173 | "npx",
174 | "-y",
175 | "@neondatabase/mcp-server-neon",
176 | "start",
177 | "<YOUR_NEON_API_KEY>"
178 | ]
179 | }
180 | }
181 | }
182 | ```
183 |
184 | ```json
185 | {
186 | "mcpServers": {
187 | "neon": {
188 | "command": "wsl",
189 | "args": [
190 | "npx",
191 | "-y",
192 | "@neondatabase/mcp-server-neon",
193 | "start",
194 | "<YOUR_NEON_API_KEY>"
195 | ]
196 | }
197 | }
198 | }
199 | ```
200 |
201 | ## Guides
202 |
203 | - [Neon MCP Server Guide](https://neon.tech/docs/ai/neon-mcp-server)
204 | - [Connect MCP Clients to Neon](https://neon.tech/docs/ai/connect-mcp-clients-to-neon)
205 | - [Cursor with Neon MCP Server](https://neon.tech/guides/cursor-mcp-neon)
206 | - [Claude Desktop with Neon MCP Server](https://neon.tech/guides/neon-mcp-server)
207 | - [Cline with Neon MCP Server](https://neon.tech/guides/cline-mcp-neon)
208 | - [Windsurf with Neon MCP Server](https://neon.tech/guides/windsurf-mcp-neon)
209 | - [Zed with Neon MCP Server](https://neon.tech/guides/zed-mcp-neon)
210 |
211 | # Features
212 |
213 | ## Supported Tools
214 |
215 | 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.
216 |
217 | **Project Management:**
218 |
219 | - **`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.
220 | - **`list_shared_projects`**: Lists Neon projects shared with the current user. Supports a search parameter and limiting the number of projects returned (default: 10).
221 | - **`describe_project`**: Fetches detailed information about a specific Neon project, including its ID, name, and associated branches and databases.
222 | - **`create_project`**: Creates a new Neon project in your Neon account. A project acts as a container for branches, databases, roles, and computes.
223 | - **`delete_project`**: Deletes an existing Neon project and all its associated resources.
224 | - **`list_organizations`**: Lists all organizations that the current user has access to. Optionally filter by organization name or ID using the search parameter.
225 |
226 | **Branch Management:**
227 |
228 | - **`create_branch`**: Creates a new branch within a specified Neon project. Leverages [Neon's branching](/docs/introduction/branching) feature for development, testing, or migrations.
229 | - **`delete_branch`**: Deletes an existing branch from a Neon project.
230 | - **`describe_branch`**: Retrieves details about a specific branch, such as its name, ID, and parent branch.
231 | - **`list_branch_computes`**: Lists compute endpoints for a project or specific branch, including compute ID, type, size, last active time, and autoscaling information.
232 | - **`compare_database_schema`**: Shows the schema diff between the child branch and its parent
233 | - **`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.
234 |
235 | **SQL Query Execution:**
236 |
237 | - **`get_connection_string`**: Returns your database connection string.
238 | - **`run_sql`**: Executes a single SQL query against a specified Neon database. Supports both read and write operations.
239 | - **`run_sql_transaction`**: Executes a series of SQL queries within a single transaction against a Neon database.
240 | - **`get_database_tables`**: Lists all tables within a specified Neon database.
241 | - **`describe_table_schema`**: Retrieves the schema definition of a specific table, detailing columns, data types, and constraints.
242 |
243 | **Database Migrations (Schema Changes):**
244 |
245 | - **`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.
246 | - **`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.
247 |
248 | **Query Performance Optimization:**
249 |
250 | - **`list_slow_queries`**: Identifies performance bottlenecks by finding the slowest queries in a database. Requires the pg_stat_statements extension.
251 | - **`explain_sql_statement`**: Provides detailed execution plans for SQL queries to help identify performance bottlenecks.
252 | - **`prepare_query_tuning`**: Analyzes query performance and suggests optimizations, like index creation. Creates a temporary branch for safely testing these optimizations.
253 | - **`complete_query_tuning`**: Finalizes query tuning by either applying optimizations to the main branch or discarding them. Cleans up the temporary tuning branch.
254 |
255 | **Neon Auth:**
256 |
257 | - **`provision_neon_auth`**: Provisions Neon Auth for a Neon project. It allows developers to easily set up authentication infrastructure by creating an integration with an Auth provider.
258 |
259 | **Search and Discovery:**
260 |
261 | - **`search`**: Searches across organizations, projects, and branches matching a query. Returns IDs, titles, and direct links to the Neon Console.
262 | - **`fetch`**: Fetches detailed information about a specific organization, project, or branch using an ID (typically from the search tool).
263 |
264 | **Documentation and Resources:**
265 |
266 | - **`load_resource`**: Loads comprehensive Neon documentation and usage guidelines, including the "neon-get-started" guide for setup, configuration, and best practices.
267 |
268 | ## Migrations
269 |
270 | 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.
271 |
272 | 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.
273 |
274 | # Development
275 |
276 | ## Development with MCP CLI Client
277 |
278 | The easiest way to iterate on the MCP Server is using the `mcp-client/`. Learn more in `mcp-client/README.md`.
279 |
280 | ```bash
281 | npm install
282 | npm run build
283 | npm run watch # You can keep this open.
284 | cd mcp-client/ && NEON_API_KEY=... npm run start:mcp-server-neon
285 | ```
286 |
287 | ## Development with Claude Desktop (Local MCP Server)
288 |
289 | ```bash
290 | npm install
291 | npm run build
292 | npm run watch # You can keep this open.
293 | node dist/index.js init $NEON_API_KEY
294 | ```
295 |
296 | Then, **restart Claude** each time you want to test changes.
297 |
298 | # Testing
299 |
300 | To run the tests you need to setup the `.env` file according to the `.env.example` file.
301 |
302 | ```bash
303 | npm run test
304 | ```
305 |
```
--------------------------------------------------------------------------------
/AGENTS.md:
--------------------------------------------------------------------------------
```markdown
1 | ## Agents.md
2 |
3 | Use [claude.md](CLAUDE.md) to guide the agents when working with code in this repository.
4 |
```
--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
```markdown
1 | # CLAUDE.md
2 |
3 | This file provides guidance to AI agents when working with code in this repository.
4 |
5 | ## Project Overview
6 |
7 | This is the **Neon MCP Server** - a Model Context Protocol server that bridges natural language requests to the Neon API, enabling LLMs to manage Neon Postgres databases through conversational commands. The project implements both local (stdio) and remote (SSE/Streamable HTTP) MCP server transports with OAuth authentication support.
8 |
9 | ## Development Commands
10 |
11 | ### Building and Running
12 |
13 | ```bash
14 | # Install dependencies
15 | npm install
16 |
17 | # Build the project (compiles TypeScript and builds landing page)
18 | npm run build
19 |
20 | # Watch mode for development (auto-recompiles on changes)
21 | npm run watch
22 |
23 | # Type checking without emitting files
24 | npm run typecheck
25 |
26 | # Start local MCP server with API key
27 | node dist/index.js start <NEON_API_KEY>
28 |
29 | # Start SSE transport server
30 | node dist/index.js start:sse
31 | ```
32 |
33 | ### Development with MCP CLI Client
34 |
35 | The fastest way to iterate on the MCP Server is using the `mcp-client/` CLI:
36 |
37 | ```bash
38 | npm install
39 | npm run build
40 | npm run watch # Keep this running in one terminal
41 | cd mcp-client/ && NEON_API_KEY=<your-key> npm run start:mcp-server-neon
42 | ```
43 |
44 | This provides an interactive terminal to test MCP tools without restarting Claude Desktop.
45 |
46 | ### Testing
47 |
48 | ```bash
49 | # Run Braintrust evaluations
50 | npm run test
51 |
52 | # You must configure .env file with:
53 | # - BRAINTRUST_API_KEY
54 | # - NEON_API_KEY
55 | # - ANTHROPIC_API_KEY
56 | ```
57 |
58 | ### Linting and Formatting
59 |
60 | ```bash
61 | # Run linting and formatting checks
62 | npm run lint
63 |
64 | # Auto-fix linting and formatting issues
65 | npm run lint:fix
66 |
67 | # Format code
68 | npm run format
69 | ```
70 |
71 | ### Single Test Development
72 |
73 | To develop and test a single tool without running full test suite, modify the test file in `src/tools-evaluations/` and run:
74 |
75 | ```bash
76 | npm run test
77 | ```
78 |
79 | ## Architecture
80 |
81 | ### Core Components
82 |
83 | 1. **MCP Server (`src/server/index.ts`)**
84 |
85 | - Creates and configures the MCP server instance
86 | - Registers all tools and resources from centralized definitions
87 | - Implements error handling and observability (Sentry, analytics)
88 | - Each tool call is tracked and wrapped in error handling
89 |
90 | 2. **Tools System (`src/tools/`)**
91 |
92 | - `definitions.ts`: Exports `NEON_TOOLS` array defining all available tools with their schemas
93 | - `tools.ts`: Exports `NEON_HANDLERS` object mapping tool names to handler functions
94 | - `toolsSchema.ts`: Zod schemas for tool input validation
95 | - `handlers/`: Individual tool handler implementations organized by feature
96 |
97 | 3. **Transport Layers (`src/transports/`)**
98 |
99 | - `stdio.ts`: Standard input/output transport for local MCP clients (Claude Desktop, Cursor)
100 | - `sse-express.ts`: Server-Sent Events transport for remote MCP server (deprecated)
101 | - `stream.ts`: Streamable HTTP transport for remote MCP server (recommended)
102 |
103 | 4. **OAuth System (`src/oauth/`)**
104 |
105 | - OAuth 2.0 server implementation for remote MCP authentication
106 | - Integrates with Neon's OAuth provider (UPSTREAM_OAUTH_HOST)
107 | - Token persistence using Keyv with Postgres backend
108 | - Cookie-based client approval tracking
109 |
110 | 5. **Resources (`src/resources.ts`)**
111 | - MCP resources that provide read-only context (like "getting started" guides)
112 | - Registered alongside tools but don't execute operations
113 |
114 | ### Key Architectural Patterns
115 |
116 | - **Tool Registration Pattern**: All tools are defined in `NEON_TOOLS` array and handlers in `NEON_HANDLERS` object. The server iterates through tools and registers them with their corresponding handlers.
117 |
118 | - **Error Handling**: Tools throw errors which are caught by the server wrapper, logged to Sentry, and returned as structured error messages to the LLM.
119 |
120 | - **State Management**: Some tools (migrations, query tuning) create temporary branches and maintain state across multiple tool calls. The LLM is prompted to remember branch IDs from previous calls.
121 |
122 | - **Analytics & Observability**: Every tool call, resource access, and error is tracked through Segment analytics and Sentry error reporting.
123 |
124 | ## Adding New Tools
125 |
126 | 1. Define the tool schema in `src/tools/toolsSchema.ts`:
127 |
128 | ```typescript
129 | export const myNewToolInputSchema = z.object({
130 | project_id: z.string().describe('The Neon project ID'),
131 | // ... other fields
132 | });
133 | ```
134 |
135 | 2. Add the tool definition to `NEON_TOOLS` array in `src/tools/definitions.ts`:
136 |
137 | ```typescript
138 | {
139 | name: 'my_new_tool' as const,
140 | description: 'Description of what this tool does',
141 | inputSchema: myNewToolInputSchema,
142 | }
143 | ```
144 |
145 | 3. Create a handler in `src/tools/handlers/my-new-tool.ts`:
146 |
147 | ```typescript
148 | import { ToolHandler } from '../types.js';
149 | import { myNewToolInputSchema } from '../toolsSchema.js';
150 |
151 | export const myNewToolHandler: ToolHandler<'my_new_tool'> = async (
152 | args,
153 | neonClient,
154 | extra,
155 | ) => {
156 | // Implementation
157 | return {
158 | content: [
159 | {
160 | type: 'text',
161 | text: 'Result message',
162 | },
163 | ],
164 | };
165 | };
166 | ```
167 |
168 | 4. Register the handler in `src/tools/tools.ts`:
169 |
170 | ```typescript
171 | import { myNewToolHandler } from './handlers/my-new-tool.js';
172 |
173 | export const NEON_HANDLERS = {
174 | // ... existing handlers
175 | my_new_tool: myNewToolHandler,
176 | };
177 | ```
178 |
179 | 5. Add evaluations in `src/tools-evaluations/` to test your tool.
180 |
181 | ## Environment Configuration
182 |
183 | See `.env.example` for all configuration options. Key variables:
184 |
185 | - `NEON_API_KEY`: Required for local development and testing
186 | - `BRAINTRUST_API_KEY`: Required for running evaluations
187 | - `ANTHROPIC_API_KEY`: Required for running evaluations
188 | - `OAUTH_DATABASE_URL`: Required for remote MCP server with OAuth
189 | - `COOKIE_SECRET`: Required for remote MCP server OAuth flow
190 | - `CLIENT_ID` / `CLIENT_SECRET`: OAuth client credentials
191 |
192 | ## Project Structure
193 |
194 | ```
195 | src/
196 | ├── index.ts # Entry point, command parser, transport selection
197 | ├── server/
198 | │ ├── index.ts # MCP server creation and tool/resource registration
199 | │ └── api.ts # Neon API client factory
200 | ├── tools/
201 | │ ├── definitions.ts # Tool definitions (NEON_TOOLS)
202 | │ ├── tools.ts # Tool handlers mapping (NEON_HANDLERS)
203 | │ ├── toolsSchema.ts # Zod schemas for tool inputs
204 | │ └── handlers/ # Individual tool implementations
205 | ├── transports/
206 | │ ├── stdio.ts # Local MCP transport
207 | │ ├── sse-express.ts # Remote SSE transport
208 | │ └── stream.ts # Remote Streamable HTTP transport
209 | ├── oauth/ # OAuth 2.0 implementation
210 | ├── analytics/ # Segment analytics integration
211 | ├── sentry/ # Sentry error tracking
212 | └── utils/ # Shared utilities
213 |
214 | mcp-client/ # CLI client for testing
215 | landing/ # Next.js landing page
216 | ```
217 |
218 | ## Important Notes
219 |
220 | - **TypeScript Configuration**: Uses ES2022 with Node16 module resolution. All imports must use `.js` extensions (not `.ts`) due to ESM requirements.
221 |
222 | - **Building**: The build process includes chmod operations to make `dist/index.js` executable, exports tool definitions to `landing/tools.json`, and builds the landing page.
223 |
224 | - **Logger Behavior**: In stdio mode, the logger is silenced to prevent stderr pollution. In SSE mode, logging is active.
225 |
226 | - **Migration Pattern**: Tools like `prepare_database_migration` and `prepare_query_tuning` create temporary branches. The LLM must remember these branch IDs to pass to subsequent `complete_*` tools.
227 |
228 | - **Neon API Client**: Created using `@neondatabase/api-client` package. All tool handlers receive a pre-configured `neonClient` instance.
229 |
230 | ## Claude Code Review Workflow
231 |
232 | This repository uses an enhanced Claude Code Review workflow that provides inline feedback on pull requests.
233 |
234 | ### What Gets Reviewed
235 |
236 | - Architecture and design patterns (tool registration, handler typing)
237 | - Security vulnerabilities (SQL injection, secrets, input validation)
238 | - Logic bugs (error handling, state management, edge cases)
239 | - Performance issues (N+1 queries, inefficient API usage)
240 | - Testing gaps (missing evaluations, uncovered scenarios)
241 | - MCP-specific patterns (analytics tracking, error handling, Sentry capture)
242 |
243 | ### What's Automated (Not Reviewed by Claude)
244 |
245 | - Linting: `bun run lint` (checked by pr.yml)
246 | - Building: `bun run build` (checked by pr.yml)
247 | - Formatting: Automated formatting checks
248 |
249 | ### Review Process
250 |
251 | 1. Workflow triggers automatically on PR open
252 | 2. Claude analyzes changes with full project context
253 | 3. Inline comments posted on significant issues
254 | 4. Summary comment provides overview and statistics
255 |
256 | ### Inline Comment Format
257 |
258 | - **Severity**: 🔴 Critical | 🟡 Important | 🔵 Consider
259 | - **Category**: [Security/Logic/Performance/Architecture/Testing/MCP]
260 | - **Description**: Clear explanation with context
261 | - **Fix**: Actionable code example or reference
262 |
263 | Example:
264 |
265 | ```
266 | 🔴 **[Security]**: SQL injection vulnerability - user input concatenated directly into SQL.
267 |
268 | **Fix:** Use parameterized queries:
269 | const result = await query('SELECT * FROM users WHERE name = $1', [userName]);
270 | ```
271 |
272 | ### Triggering Reviews
273 |
274 | - **Automatic**: Opens when PR is created
275 | - **Manual**: Run workflow via GitHub Actions with PR number
276 | - **Security**: Only OWNER/MEMBER/COLLABORATOR PRs (blocks external)
277 |
278 | ## Testing Strategy
279 |
280 | Tests use Braintrust for LLM-based evaluations. Each test:
281 |
282 | 1. Defines a task/prompt
283 | 2. Executes it against the MCP server
284 | 3. Evaluates the result using Braintrust scoring functions
285 |
286 | This validates that tools work correctly with realistic LLM interactions.
287 |
```
--------------------------------------------------------------------------------
/src/types/helpers.ts:
--------------------------------------------------------------------------------
```typescript
1 | export type Prettify<T> = {
2 | [K in keyof T]: T[K];
3 | } & {};
4 |
```
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["src/tools-evaluations/**/*"]
4 | }
5 |
```
--------------------------------------------------------------------------------
/landing/postcss.config.mjs:
--------------------------------------------------------------------------------
```
1 | const config = {
2 | plugins: ['@tailwindcss/postcss'],
3 | };
4 |
5 | export default config;
6 |
```
--------------------------------------------------------------------------------
/landing/next.config.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { NextConfig } from 'next';
2 |
3 | const nextConfig: NextConfig = {
4 | output: 'export',
5 | };
6 |
7 | export default nextConfig;
8 |
```
--------------------------------------------------------------------------------
/src/tools/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | export { NEON_TOOLS } from './definitions.js';
2 | export { NEON_HANDLERS } from './tools.js';
3 | export { ToolHandlers, ToolHandlerExtended } from './types.js';
4 |
```
--------------------------------------------------------------------------------
/landing/lib/utils.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { clsx, type ClassValue } from 'clsx';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
```
--------------------------------------------------------------------------------
/PUBLISH.md:
--------------------------------------------------------------------------------
```markdown
1 | ## Publish
2 |
3 | ### New release
4 |
5 | ```bash
6 | npm run build
7 | npm version patch|minor|major
8 | npm publish
9 | ```
10 |
11 | ### New Beta Release
12 |
13 | ```bash
14 | npm run build
15 | npm version prerelease --preid=beta
16 | npm publish --tag beta
17 | ```
18 |
19 | ### Promote beta to release
20 |
21 | ```bash
22 | npm version patch
23 | npm publish
24 | ```
25 |
```
--------------------------------------------------------------------------------
/mcp-client/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "rootDir": "./src",
7 | "strict": true,
8 | "esModuleInterop": true,
9 | "skipLibCheck": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "outDir": "./dist"
12 | },
13 | "include": ["src/**/*"],
14 | "exclude": ["node_modules"]
15 | }
16 |
```
--------------------------------------------------------------------------------
/mcp-client/src/neon-cli-client.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { MCPClientCLI } from './cli-client.js';
2 | import path from 'path';
3 | import dotenv from 'dotenv';
4 |
5 | dotenv.config({
6 | path: path.resolve(__dirname, '../.env'),
7 | });
8 | const cli = new MCPClientCLI({
9 | command: path.resolve(__dirname, '../../dist/index.js'), // Use __dirname for relative path
10 | args: ['start', process.env.NEON_API_KEY!],
11 | });
12 |
13 | cli.start();
14 |
```
--------------------------------------------------------------------------------
/src/types/express.d.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { AuthContext } from './auth.js';
2 |
3 | // to make the file a module and avoid the TypeScript error
4 | export {};
5 |
6 | // Extends the Express Request interface to add the auth context
7 | declare global {
8 | namespace Express {
9 | // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
10 | export interface Request {
11 | auth?: AuthContext;
12 | }
13 | }
14 | }
15 |
```
--------------------------------------------------------------------------------
/src/types/auth.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
2 |
3 | export type AuthContext = {
4 | extra: {
5 | readOnly?: boolean;
6 | account: {
7 | id: string;
8 | name: string;
9 | email?: string;
10 | isOrg?: boolean; // For STDIO mode with org API key
11 | };
12 | client?: {
13 | id: string;
14 | name: string;
15 | };
16 | [key: string]: unknown;
17 | };
18 | } & AuthInfo;
19 |
```
--------------------------------------------------------------------------------
/src/sentry/instrument.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { init } from '@sentry/node';
2 | import { SENTRY_DSN } from '../constants.js';
3 | import { getPackageJson } from '../server/api.js';
4 |
5 | init({
6 | dsn: SENTRY_DSN,
7 | environment: process.env.NODE_ENV,
8 | release: getPackageJson().version,
9 | tracesSampleRate: 1.0,
10 |
11 | // Setting this option to true will send default PII data to Sentry.
12 | // For example, automatic IP address collection on events
13 | sendDefaultPii: true,
14 | });
15 |
```
--------------------------------------------------------------------------------
/landing/components/ExternalLink.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import { ReactNode } from 'react';
2 |
3 | import { ExternalIcon } from '@/components/ExternalIcon';
4 |
5 | type ExternalLinkProps = { href: string; children?: ReactNode };
6 |
7 | export const ExternalLink = ({ href, children }: ExternalLinkProps) => (
8 | <a
9 | className="inline-flex items-center gap-1 w-fit external-link"
10 | href={href}
11 | target="_blank"
12 | rel="noopener noreferrer"
13 | >
14 | {children}
15 | <ExternalIcon />
16 | </a>
17 | );
18 |
```
--------------------------------------------------------------------------------
/landing/components.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "",
8 | "css": "app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
22 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "rootDir": "./src",
7 | "strict": true,
8 | "esModuleInterop": true,
9 | "skipLibCheck": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "outDir": "./dist",
12 | "typeRoots": ["./node_modules/@types", "./src/types"]
13 | },
14 | "include": ["src/**/*"],
15 | "exclude": ["node_modules", "src/tools-evaluations/**/*"]
16 | }
17 |
```
--------------------------------------------------------------------------------
/src/tools/handlers/decribe-project.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Api } from '@neondatabase/api-client';
2 |
3 | async function handleDescribeProject(
4 | projectId: string,
5 | neonClient: Api<unknown>,
6 | ) {
7 | const { data: branchesData } = await neonClient.listProjectBranches({
8 | projectId: projectId,
9 | });
10 | const { data: projectData } = await neonClient.getProject(projectId);
11 | return {
12 | branches: branchesData.branches,
13 | project: projectData.project,
14 | };
15 | }
16 |
17 | export { handleDescribeProject };
18 |
```
--------------------------------------------------------------------------------
/src/transports/stdio.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5 | /**
6 | * Start the server using stdio transport.
7 | * This allows the server to communicate via standard input/output streams.
8 | */
9 | export const startStdio = async (server: McpServer) => {
10 | const transport = new StdioServerTransport();
11 | await server.connect(transport);
12 | };
13 |
```
--------------------------------------------------------------------------------
/src/types/context.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Environment } from '../constants.js';
2 | import { AuthContext } from './auth.js';
3 |
4 | export type AppContext = {
5 | name: string;
6 | transport: 'sse' | 'stdio' | 'stream';
7 | environment: Environment;
8 | version: string;
9 | };
10 |
11 | export type ServerContext = {
12 | apiKey: string;
13 | client?: AuthContext['extra']['client'];
14 | account: AuthContext['extra']['account'];
15 | app: AppContext;
16 | readOnly?: AuthContext['extra']['readOnly'];
17 | userAgent?: string;
18 | };
19 |
```
--------------------------------------------------------------------------------
/src/utils/polyfills.ts:
--------------------------------------------------------------------------------
```typescript
1 | import nodeFetch, {
2 | Headers as NodeHeaders,
3 | Request as NodeRequest,
4 | Response as NodeResponse,
5 | } from 'node-fetch';
6 |
7 | // Use different names to avoid conflicts
8 | declare global {
9 | function fetch(
10 | url: string | Request | URL,
11 | init?: RequestInit,
12 | ): Promise<Response>;
13 | }
14 |
15 | if (!global.fetch) {
16 | global.fetch = nodeFetch as any;
17 | global.Headers = NodeHeaders as any;
18 | global.Request = NodeRequest as any;
19 | global.Response = NodeResponse as any;
20 | }
21 |
```
--------------------------------------------------------------------------------
/landing/eslint.config.mjs:
--------------------------------------------------------------------------------
```
1 | import { dirname } from 'path';
2 | import { fileURLToPath } from 'url';
3 | import { FlatCompat } from '@eslint/eslintrc';
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [
13 | ...compat.extends('next/core-web-vitals', 'next/typescript'),
14 | {
15 | rules: {
16 | 'react/no-unescaped-entities': 'off',
17 | },
18 | },
19 | ];
20 |
21 | export default eslintConfig;
22 |
```
--------------------------------------------------------------------------------
/landing/components/ExternalIcon.tsx:
--------------------------------------------------------------------------------
```typescript
1 | export const ExternalIcon = () => (
2 | <svg
3 | xmlns="http://www.w3.org/2000/svg"
4 | width="12"
5 | height="12"
6 | fill="none"
7 | viewBox="0 0 12 12"
8 | className="-mb-px shrink-0"
9 | >
10 | <rect
11 | width="12"
12 | height="12"
13 | fill="currentColor"
14 | opacity="0.2"
15 | rx="2"
16 | ></rect>
17 | <path
18 | stroke="currentColor"
19 | strokeLinecap="round"
20 | strokeLinejoin="round"
21 | d="M8.499 7.616v-4.12h-4.12M8.25 3.75 3.5 8.5"
22 | ></path>
23 | </svg>
24 | );
25 |
```
--------------------------------------------------------------------------------
/src/sentry/utils.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { setTags, setUser } from '@sentry/node';
2 | import { ServerContext } from '../types/context.js';
3 |
4 | export const setSentryTags = (context: ServerContext) => {
5 | setUser({
6 | id: context.account.id,
7 | });
8 | setTags({
9 | 'app.name': context.app.name,
10 | 'app.version': context.app.version,
11 | 'app.transport': context.app.transport,
12 | 'app.environment': context.app.environment,
13 | });
14 | if (context.client) {
15 | setTags({
16 | 'client.id': context.client.id,
17 | 'client.name': context.client.name,
18 | });
19 | }
20 | };
21 |
```
--------------------------------------------------------------------------------
/src/server/api.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { createApiClient } from '@neondatabase/api-client';
2 | import path from 'node:path';
3 | import { fileURLToPath } from 'node:url';
4 | import fs from 'node:fs';
5 | import { NEON_API_HOST } from '../constants.js';
6 |
7 | export const getPackageJson = () => {
8 | const __dirname = path.dirname(fileURLToPath(import.meta.url));
9 | return JSON.parse(
10 | fs.readFileSync(path.join(__dirname, '../..', 'package.json'), 'utf8'),
11 | );
12 | };
13 |
14 | export const createNeonClient = (apiKey: string) =>
15 | createApiClient({
16 | apiKey,
17 | baseURL: NEON_API_HOST,
18 | headers: {
19 | 'User-Agent': `mcp-server-neon/${getPackageJson().version}`,
20 | },
21 | });
22 |
```
--------------------------------------------------------------------------------
/landing/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2018",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": [
26 | "**/*.ts",
27 | "**/*.tsx",
28 | ".next/types/**/*.ts",
29 | "next-env.d.ts",
30 | "../public/types/**/*.ts"
31 | ],
32 | "exclude": ["node_modules"]
33 | }
34 |
```
--------------------------------------------------------------------------------
/src/tools/handlers/list-orgs.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Api, Organization } from '@neondatabase/api-client';
2 | import { ToolHandlerExtraParams } from '../types.js';
3 | import { filterOrganizations } from '../utils.js';
4 |
5 | export async function handleListOrganizations(
6 | neonClient: Api<unknown>,
7 | account: ToolHandlerExtraParams['account'],
8 | search?: string,
9 | ): Promise<Organization[]> {
10 | if (account.isOrg) {
11 | const orgId = account.id;
12 | const { data } = await neonClient.getOrganization(orgId);
13 | return filterOrganizations([data], search);
14 | }
15 |
16 | const { data: response } = await neonClient.getCurrentUserOrganizations();
17 | const organizations = response.organizations || [];
18 | return filterOrganizations(organizations, search);
19 | }
20 |
```
--------------------------------------------------------------------------------
/.github/workflows/pr.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Lint and Build
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | lint-and-build:
10 | runs-on:
11 | group: neondatabase-protected-runner-group
12 | labels: linux-ubuntu-latest
13 | steps:
14 | - name: Checkout code
15 | uses: actions/checkout@v4
16 | - name: Setup Node
17 | uses: actions/setup-node@v4
18 | with:
19 | node-version-file: .nvmrc
20 | - name: Setup Bun
21 | uses: oven-sh/setup-bun@v2
22 | with:
23 | bun-version-file: .bun-version
24 | - name: Install root dependencies
25 | run: bun install --frozen-lockfile
26 | - name: Install landing dependencies
27 | working-directory: landing
28 | run: bun install --frozen-lockfile
29 | - name: Lint
30 | run: bun run lint
31 | - name: Build
32 | run: bun run build
33 |
```
--------------------------------------------------------------------------------
/landing/icons/github.svg:
--------------------------------------------------------------------------------
```
1 | <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
1 | import chalk from 'chalk';
2 |
3 | type LoggingMode = 'verbose' | 'error' | 'none';
4 |
5 | export type LoggerOptions = {
6 | mode: LoggingMode;
7 | };
8 |
9 | export const consoleStyles = {
10 | prompt: chalk.green('You: '),
11 | assistant: chalk.blue('Claude: '),
12 | tool: {
13 | name: chalk.cyan.bold,
14 | args: chalk.yellow,
15 | bracket: chalk.dim,
16 | },
17 | error: chalk.red,
18 | info: chalk.blue,
19 | success: chalk.green,
20 | warning: chalk.yellow,
21 | separator: chalk.gray('─'.repeat(50)),
22 | default: chalk,
23 | };
24 |
25 | export class Logger {
26 | private mode: LoggingMode = 'verbose';
27 |
28 | constructor({ mode }: LoggerOptions) {
29 | this.mode = mode;
30 | }
31 |
32 | log(
33 | message: string,
34 | options?: { type?: 'info' | 'error' | 'success' | 'warning' },
35 | ) {
36 | if (this.mode === 'none') return;
37 | if (this.mode === 'error' && options?.type !== 'error') return;
38 |
39 | process.stdout.write(consoleStyles[options?.type ?? 'default'](message));
40 | }
41 | }
42 |
```
--------------------------------------------------------------------------------
/landing/app/layout.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import type { ReactNode } from 'react';
2 | import type { Metadata } from 'next';
3 | import Head from 'next/head';
4 | import { Geist, Geist_Mono } from 'next/font/google';
5 |
6 | import { ThemeProvider } from '@/components/ThemeProvider';
7 |
8 | import './globals.css';
9 |
10 | const geistSans = Geist({
11 | variable: '--font-geist-sans',
12 | subsets: ['latin'],
13 | });
14 |
15 | const geistMono = Geist_Mono({
16 | variable: '--font-geist-mono',
17 | subsets: ['latin'],
18 | });
19 |
20 | export const metadata: Metadata = {
21 | title: 'Neon MCP',
22 | description: 'Learn how to use Neon MCP',
23 | };
24 |
25 | export default function RootLayout({ children }: { children: ReactNode }) {
26 | return (
27 | <html lang="en">
28 | <Head>
29 | <link rel="icon" href="/favicon.ico" type="image/x-icon" />
30 | </Head>
31 | <body
32 | className={`${geistSans.variable} ${geistMono.variable} antialiased`}
33 | >
34 | <ThemeProvider>{children}</ThemeProvider>
35 | </body>
36 | </html>
37 | );
38 | }
39 |
```
--------------------------------------------------------------------------------
/src/utils/client-application.ts:
--------------------------------------------------------------------------------
```typescript
1 | type KnownClientApplication =
2 | | 'cursor'
3 | | 'claude-code'
4 | | 'claude-desktop'
5 | | 'v0'
6 | | 'vscode';
7 |
8 | export type ClientApplication = KnownClientApplication | 'unknown';
9 |
10 | /**
11 | * Detects the client application type from the MCP client name or User-Agent.
12 | * @param clientName - The name of the MCP client
13 | * @returns The detected client application type
14 | */
15 | export function detectClientApplication(
16 | clientName?: string,
17 | ): ClientApplication {
18 | if (!clientName) return 'unknown';
19 |
20 | const normalized = clientName.toLowerCase();
21 |
22 | // Known clients
23 | if (normalized.includes('cursor')) return 'cursor';
24 | if (normalized.includes('claude-code')) return 'claude-code';
25 | if (
26 | normalized.includes('claude-user') ||
27 | normalized.includes('claude desktop')
28 | )
29 | return 'claude-desktop';
30 | if (normalized.includes('v0bot')) return 'v0';
31 | if (normalized.includes('visual studio code')) return 'vscode';
32 |
33 | return 'unknown';
34 | }
35 |
```
--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------
```typescript
1 | import winston from 'winston';
2 | import morgan from 'morgan';
3 | import { Request, Response, NextFunction } from 'express';
4 |
5 | const loggerFormat = winston.format.combine(
6 | winston.format.timestamp(),
7 | winston.format.simple(),
8 | winston.format.errors({ stack: true }),
9 | winston.format.align(),
10 | winston.format.colorize(),
11 | );
12 | // Configure Winston logger
13 | export const logger = winston.createLogger({
14 | level: 'info',
15 | format: loggerFormat,
16 | transports: [
17 | new winston.transports.Console({
18 | format: loggerFormat,
19 | }),
20 | ],
21 | });
22 |
23 | // Configure Morgan for HTTP request logging
24 | export const morganConfig = morgan('combined', {
25 | stream: {
26 | write: (message: string) => logger.info(message.trim()),
27 | },
28 | });
29 |
30 | // Configure error handling middleware
31 | export const errorHandler = (
32 | err: Error,
33 | req: Request,
34 | res: Response,
35 | next: NextFunction,
36 | ) => {
37 | logger.error('Error:', { error: err.message, stack: err.stack });
38 | next(err);
39 | };
40 |
```
--------------------------------------------------------------------------------
/landing/components/CopyableUrl.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import { useState } from 'react';
4 |
5 | export const CopyableUrl = ({ url }: { url: string }) => {
6 | const [copied, setCopied] = useState(false);
7 |
8 | const handleCopy = async () => {
9 | try {
10 | await navigator.clipboard.writeText(url);
11 | setCopied(true);
12 | setTimeout(() => setCopied(false), 2000);
13 | } catch (err) {
14 | console.error('Failed to copy:', err);
15 | }
16 | };
17 |
18 | return (
19 | <div className="my-2 relative">
20 | <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">
21 | <span className="text-sm">{url}</span>
22 | <button
23 | onClick={handleCopy}
24 | 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"
25 | title="Copy to clipboard"
26 | >
27 | {copied ? 'Copied!' : 'Copy'}
28 | </button>
29 | </div>
30 | </div>
31 | );
32 | };
33 |
```
--------------------------------------------------------------------------------
/landing/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "neon-mcp-landing",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --turbopack",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@radix-ui/react-accordion": "^1.2.10",
13 | "@radix-ui/react-slot": "^1.2.2",
14 | "class-variance-authority": "^0.7.1",
15 | "clsx": "^2.1.1",
16 | "lodash": "^4.17.21",
17 | "lucide-react": "^0.511.0",
18 | "next": "15.3.2",
19 | "react": "^19.0.0",
20 | "react-dom": "^19.0.0",
21 | "react-syntax-highlighter": "^15.6.1",
22 | "tailwind-merge": "^3.3.0"
23 | },
24 | "devDependencies": {
25 | "@eslint/eslintrc": "^3",
26 | "@tailwindcss/postcss": "^4",
27 | "@types/lodash": "^4.17.16",
28 | "@types/node": "^20",
29 | "@types/react": "19.1.8",
30 | "@types/react-dom": "^19",
31 | "@types/react-syntax-highlighter": "^15.5.13",
32 | "eslint": "^9",
33 | "eslint-config-next": "15.3.2",
34 | "tailwindcss": "^4",
35 | "tw-animate-css": "^1.3.0",
36 | "typescript": "^5"
37 | }
38 | }
39 |
```
--------------------------------------------------------------------------------
/landing/components/CodeSnippet.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import { Suspense } from 'react';
4 | import dynamic from 'next/dynamic';
5 | import {
6 | docco,
7 | stackoverflowDark,
8 | } from 'react-syntax-highlighter/dist/esm/styles/hljs';
9 | import { useTheme } from '@/components/ThemeProvider';
10 |
11 | const SyntaxHighlighter = dynamic(
12 | () => import('react-syntax-highlighter').then((module) => module.default),
13 | {
14 | ssr: false,
15 | },
16 | );
17 |
18 | type Props = {
19 | type?: string;
20 | children: string;
21 | };
22 |
23 | export const CodeSnippet = ({ type, children }: Props) => {
24 | const theme = useTheme();
25 |
26 | return (
27 | <div className="my-2">
28 | <Suspense
29 | fallback={
30 | <div className="monospaced whitespace-pre-wrap bg-secondary px-2 py-[0.5em] border-l-4">
31 | {children}
32 | </div>
33 | }
34 | >
35 | <SyntaxHighlighter
36 | language={type}
37 | wrapLongLines
38 | style={theme === 'light' ? docco : stackoverflowDark}
39 | >
40 | {children}
41 | </SyntaxHighlighter>
42 | </Suspense>
43 | </div>
44 | );
45 | };
46 |
```
--------------------------------------------------------------------------------
/src/tools/handlers/urls.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { NEON_CONSOLE_HOST } from '../../constants.js';
2 | import { NotFoundError } from '../../server/errors.js';
3 |
4 | export enum CONSOLE_URLS {
5 | ORGANIZATION = '/app/:orgId/projects',
6 | PROJECT = '/app/projects/:projectId',
7 | PROJECT_BRANCH = '/app/projects/:projectId/branches/:branchId',
8 | }
9 |
10 | type ExtractPathParams<T extends string> =
11 | T extends `${string}:${infer Param}/${infer Rest}`
12 | ? { [k in Param | keyof ExtractPathParams<`/${Rest}`>]: string | number }
13 | : T extends `${string}:${infer Param}`
14 | ? Record<Param, string | number>
15 | : Record<string, never>;
16 |
17 | export function generateConsoleUrl<T extends CONSOLE_URLS>(
18 | url: T,
19 | params: ExtractPathParams<T>,
20 | ): string {
21 | const link = url.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
22 | if ((params as any)[key] === undefined) {
23 | throw new NotFoundError(`Missing parameter '${key}' for url '${url}'`);
24 | }
25 | return encodeURIComponent(String((params as any)[key]));
26 | });
27 | return new URL(link, NEON_CONSOLE_HOST).toString();
28 | }
29 |
```
--------------------------------------------------------------------------------
/mcp-client/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@neondatabase/mcp-client-cli",
3 | "version": "0.1.1",
4 | "description": "MCP client CLI for interacting with a MCP server",
5 | "license": "MIT",
6 | "author": "Neon, Inc. (https://neon.tech/)",
7 | "homepage": "https://github.com/neondatabase/mcp-server-neon/",
8 | "bugs": "https://github.com/neondatabase/mcp-server-neon/issues",
9 | "type": "module",
10 | "access": "public",
11 | "bin": {
12 | "mcp-client": "./dist/bin.js"
13 | },
14 | "files": [
15 | "dist"
16 | ],
17 | "scripts": {
18 | "start:mcp-server-neon": "cd .. && bun run build && cd - && bun ./src/neon-cli-client.ts",
19 | "build": "tsc && node -e \"require('fs').chmodSync('dist/index.js', '755')\"",
20 | "prepare": "npm run build",
21 | "watch": "tsc-watch --onSuccess \"chmod 755 dist/index.js\"",
22 | "format": "prettier --write ."
23 | },
24 | "dependencies": {
25 | "@anthropic-ai/sdk": "^0.32.1",
26 | "@modelcontextprotocol/sdk": "^1.0.3",
27 | "chalk": "^5.3.0",
28 | "dotenv": "16.4.7",
29 | "zod": "^3.24.1"
30 | },
31 | "devDependencies": {
32 | "@types/node": "^22.10.2",
33 | "bun": "^1.1.38",
34 | "prettier": "^3.4.1",
35 | "tsc-watch": "^6.2.1",
36 | "typescript": "^5.7.2"
37 | }
38 | }
39 |
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
2 | # Use the imbios/bun-node image as the base image with Node and Bun
3 | # Keep bun and node version in sync with package.json
4 | FROM imbios/bun-node:1.1.38-18-alpine AS builder
5 |
6 | # Set the working directory in the container
7 | WORKDIR /app
8 |
9 | # Copy package.json and package-lock.json
10 | COPY package.json package-lock.json ./
11 |
12 | # Copy the entire project to the working directory
13 | COPY . .
14 |
15 | # Install the dependencies and devDependencies
16 | RUN npm install
17 |
18 | # Build the project
19 | RUN npm run build
20 |
21 | # Use a smaller base image for the final image
22 | FROM node:18-alpine AS release
23 |
24 | # Set the working directory
25 | WORKDIR /app
26 |
27 | # Copy only the necessary files from the builder stage
28 | COPY --from=builder /app/dist /app/dist
29 | COPY --from=builder /app/package.json /app/package.json
30 | COPY --from=builder /app/package-lock.json /app/package-lock.json
31 |
32 | # Install only production dependencies
33 | RUN npm ci --omit=dev
34 |
35 | # Define environment variables
36 | ENV NODE_ENV=production
37 |
38 | # Specify the command to run the MCP server
39 | ENTRYPOINT ["node", "dist/index.js", "start", "$NEON_API_KEY"]
40 |
```
--------------------------------------------------------------------------------
/src/tools-evaluations/evalUtils.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { createApiClient } from '@neondatabase/api-client';
2 | import path from 'path';
3 | import { MCPClient } from '../../mcp-client/src/index.js';
4 |
5 | export async function deleteNonDefaultBranches(projectId: string) {
6 | const neonClient = createApiClient({
7 | apiKey: process.env.NEON_API_KEY!,
8 | });
9 |
10 | try {
11 | const allBranches = await neonClient.listProjectBranches({
12 | projectId: projectId,
13 | });
14 |
15 | const branchesToDelete = allBranches.data.branches.filter(
16 | (b) => !b.default,
17 | );
18 |
19 | await Promise.all(
20 | branchesToDelete.map((b) =>
21 | neonClient.deleteProjectBranch(b.project_id, b.id),
22 | ),
23 | );
24 | } catch (e) {
25 | console.error(e);
26 | }
27 | }
28 |
29 | export async function evaluateTask(input: string) {
30 | const client = new MCPClient({
31 | command: path.resolve(__dirname, '../../dist/index.js'),
32 | args: ['start', process.env.NEON_API_KEY!],
33 | loggerOptions: {
34 | mode: 'error',
35 | },
36 | });
37 |
38 | await client.start();
39 | const response = await client.processQuery(input);
40 | await client.stop();
41 |
42 | if (!response) {
43 | throw new Error('No response from MCP Client');
44 | }
45 |
46 | return response;
47 | }
48 |
```
--------------------------------------------------------------------------------
/mcp-client/src/bin.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import { parseArgs } from 'node:util';
4 | import { MCPClientCLI } from './cli-client.js';
5 |
6 | function checkRequiredEnvVars() {
7 | if (!process.env.ANTHROPIC_API_KEY) {
8 | console.error(
9 | '\x1b[31mError: ANTHROPIC_API_KEY environment variable is required\x1b[0m',
10 | );
11 | console.error('Please set it before running the CLI:');
12 | console.error(' export ANTHROPIC_API_KEY=your_key_here');
13 | process.exit(1);
14 | }
15 | }
16 |
17 | async function main() {
18 | try {
19 | checkRequiredEnvVars();
20 |
21 | const args = parseArgs({
22 | options: {
23 | 'server-command': { type: 'string' },
24 | 'server-args': { type: 'string' },
25 | },
26 | allowPositionals: true,
27 | });
28 |
29 | const serverCommand = args.values['server-command'];
30 | const serverArgs = args.values['server-args']?.split(' ') || [];
31 |
32 | if (!serverCommand) {
33 | console.error('Error: --server-command is required');
34 | process.exit(1);
35 | }
36 |
37 | const cli = new MCPClientCLI({
38 | command: serverCommand,
39 | args: serverArgs,
40 | });
41 |
42 | await cli.start();
43 | } catch (error) {
44 | console.error('Failed to start CLI:', error);
45 | process.exit(1);
46 | }
47 | }
48 |
49 | main();
50 |
```
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { config } from 'dotenv';
2 |
3 | config();
4 |
5 | export type Environment = 'development' | 'production' | 'preview';
6 | export const NEON_DEFAULT_DATABASE_NAME = 'neondb';
7 |
8 | export const NODE_ENV = (process.env.NODE_ENV ?? 'production') as Environment;
9 | export const IS_DEV = NODE_ENV === 'development';
10 | export const SERVER_PORT = 3001;
11 | export const SERVER_HOST =
12 | process.env.SERVER_HOST ?? `http://localhost:${SERVER_PORT}`;
13 | export const CLIENT_ID = process.env.CLIENT_ID ?? '';
14 | export const CLIENT_SECRET = process.env.CLIENT_SECRET ?? '';
15 | export const UPSTREAM_OAUTH_HOST =
16 | process.env.UPSTREAM_OAUTH_HOST ?? 'https://oauth2.neon.tech';
17 | export const REDIRECT_URI = `${SERVER_HOST}/callback`;
18 | export const NEON_API_HOST =
19 | process.env.NEON_API_HOST ?? 'https://console.neon.tech/api/v2';
20 | export const COOKIE_SECRET = process.env.COOKIE_SECRET ?? '';
21 | export const ANALYTICS_WRITE_KEY =
22 | process.env.ANALYTICS_WRITE_KEY ?? 'gFVzt8ozOp6AZRXoD0g0Lv6UQ6aaoS7O';
23 | export const SENTRY_DSN =
24 | process.env.SENTRY_DSN ??
25 | 'https://[email protected]/4509328350380033';
26 |
27 | export const NEON_CONSOLE_HOST = NEON_API_HOST.replace(/\/api\/v2$/, '');
28 |
```
--------------------------------------------------------------------------------
/landing/components/ThemeProvider.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import {
4 | createContext,
5 | ReactNode,
6 | useContext,
7 | useLayoutEffect,
8 | useState,
9 | } from 'react';
10 |
11 | export type Theme = 'light' | 'dark';
12 |
13 | type ThemeProviderState = {
14 | theme: Theme;
15 | };
16 |
17 | const ThemeContext = createContext<ThemeProviderState>({
18 | theme: 'light',
19 | });
20 |
21 | export const ThemeProvider = ({ children }: { children?: ReactNode }) => {
22 | const [themeState, setThemeState] = useState<ThemeProviderState>({
23 | theme: 'light',
24 | });
25 |
26 | useLayoutEffect(() => {
27 | const match = window.matchMedia('(prefers-color-scheme:dark)');
28 |
29 | function onChange(event: { matches: boolean }) {
30 | setThemeState((themeState) => {
31 | const targetTheme = event.matches ? 'dark' : 'light';
32 |
33 | if (themeState.theme === targetTheme) {
34 | return themeState;
35 | }
36 |
37 | return {
38 | ...themeState,
39 | theme: targetTheme,
40 | };
41 | });
42 | }
43 |
44 | onChange(match);
45 |
46 | match.addEventListener('change', onChange);
47 | return () => {
48 | match.removeEventListener('change', onChange);
49 | };
50 | }, []);
51 |
52 | return <ThemeContext value={themeState}>{children}</ThemeContext>;
53 | };
54 |
55 | export function useTheme(): Theme {
56 | return useContext(ThemeContext).theme;
57 | }
58 |
```
--------------------------------------------------------------------------------
/src/tools/state.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Branch } from '@neondatabase/api-client';
2 |
3 | type MigrationId = string;
4 | export type MigrationDetails = {
5 | migrationSql: string;
6 | databaseName: string;
7 | appliedBranch: Branch;
8 | roleName?: string;
9 | };
10 |
11 | type TuningId = string;
12 | export type TuningDetails = {
13 | sql: string;
14 | databaseName: string;
15 | tuningBranch: Branch;
16 | roleName?: string;
17 | originalPlan?: any;
18 | suggestedChanges?: string[];
19 | improvedPlan?: any;
20 | };
21 |
22 | const migrationsState = new Map<MigrationId, MigrationDetails>();
23 | const tuningState = new Map<TuningId, TuningDetails>();
24 |
25 | export function getMigrationFromMemory(migrationId: string) {
26 | return migrationsState.get(migrationId);
27 | }
28 |
29 | export function persistMigrationToMemory(
30 | migrationId: string,
31 | migrationDetails: MigrationDetails,
32 | ) {
33 | migrationsState.set(migrationId, migrationDetails);
34 | }
35 |
36 | export function getTuningFromMemory(tuningId: string) {
37 | return tuningState.get(tuningId);
38 | }
39 |
40 | export function persistTuningToMemory(
41 | tuningId: string,
42 | tuningDetails: TuningDetails,
43 | ) {
44 | tuningState.set(tuningId, tuningDetails);
45 | }
46 |
47 | export function updateTuningInMemory(
48 | tuningId: string,
49 | updates: Partial<TuningDetails>,
50 | ) {
51 | const existing = tuningState.get(tuningId);
52 | if (existing) {
53 | tuningState.set(tuningId, { ...existing, ...updates });
54 | }
55 | }
56 |
```
--------------------------------------------------------------------------------
/src/tools/types.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
2 | import { Api } from '@neondatabase/api-client';
3 |
4 | import { NEON_TOOLS } from './definitions.js';
5 | import { AuthContext } from '../types/auth.js';
6 | import { ClientApplication } from '../utils/client-application.js';
7 |
8 | // Extract the tool names as a union type
9 | type NeonToolName = (typeof NEON_TOOLS)[number]['name'];
10 | export type ToolParams<T extends NeonToolName> = Extract<
11 | (typeof NEON_TOOLS)[number],
12 | { name: T }
13 | >['inputSchema'];
14 |
15 | export type ToolHandler<T extends NeonToolName> = ToolCallback<{
16 | params: ToolParams<T>;
17 | }>;
18 |
19 | export type ToolHandlerExtraParams = Parameters<
20 | ToolHandler<NeonToolName>
21 | >['1'] & {
22 | account: AuthContext['extra']['account'];
23 | readOnly?: AuthContext['extra']['readOnly'];
24 | /** Detected client application type (e.g., 'cursor', 'claude', 'other') */
25 | clientApplication: ClientApplication;
26 | };
27 |
28 | export type ToolHandlerExtended<T extends NeonToolName> = (
29 | ...args: [
30 | args: Parameters<ToolHandler<T>>['0'],
31 | neonClient: Api<unknown>,
32 | extra: ToolHandlerExtraParams,
33 | ]
34 | ) => ReturnType<ToolHandler<T>>;
35 |
36 | // Create a type for the tool handlers that directly maps each tool to its appropriate input schema
37 | export type ToolHandlers = {
38 | [K in NeonToolName]: ToolHandlerExtended<K>;
39 | };
40 |
```
--------------------------------------------------------------------------------
/scripts/before-publish.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import fs from 'fs';
4 | import path from 'path';
5 | import { execSync } from 'child_process';
6 |
7 | function checkMainBranch(version) {
8 | // Skip main branch check for beta versions
9 | if (version.includes('beta')) {
10 | return;
11 | }
12 |
13 | try {
14 | const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
15 | encoding: 'utf8',
16 | }).trim();
17 |
18 | if (currentBranch !== 'main') {
19 | console.error(
20 | '\x1b[31mError: Publishing stable versions is only allowed from the main branch\x1b[0m',
21 | );
22 | console.error(`Current branch: ${currentBranch}`);
23 | process.exit(1);
24 | }
25 | } catch (error) {
26 | console.error('Error: Git repository not found');
27 | process.exit(1);
28 | }
29 | }
30 |
31 | function checkChangelog() {
32 | const changelogPath = path.join(__dirname, '../CHANGELOG.md');
33 | const packagePath = path.join(__dirname, '../package.json');
34 |
35 | const { version } = require(packagePath);
36 |
37 | try {
38 | const changelog = fs.readFileSync(changelogPath, 'utf8');
39 | if (!changelog.includes(version)) {
40 | console.error(
41 | `\x1b[31mError: Version ${version} not found in CHANGELOG.md\x1b[0m`,
42 | );
43 | console.error('Please update the changelog before publishing');
44 | process.exit(1);
45 | }
46 | return version;
47 | } catch (err) {
48 | console.error('\x1b[31mError: CHANGELOG.md not found\x1b[0m');
49 | process.exit(1);
50 | }
51 | }
52 |
53 | function beforePublish() {
54 | const version = checkChangelog();
55 | checkMainBranch(version);
56 | }
57 |
58 | beforePublish();
59 |
```
--------------------------------------------------------------------------------
/remote.Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 |
2 | # Use the imbios/bun-node image as the base image with Node and Bun
3 | # Keep bun and node version in sync with package.json
4 | ARG NODE_VERSION=22.0.0
5 | ARG BUN_VERSION=1.2.13
6 | FROM imbios/bun-node:1.2.13-22-slim AS base
7 |
8 | # Set the working directory in the container
9 | WORKDIR /app
10 |
11 | # Set production environment
12 | ENV NODE_ENV="production"
13 |
14 | # Throw-away build stage to reduce size of final image
15 | FROM base As builder
16 |
17 | # Install packages needed to build node modules
18 | RUN apt-get update -qq && \
19 | apt-get install --no-install-recommends -y build-essential node-gyp pkg-config python-is-python3
20 |
21 | # Copy package.json and package-lock.json
22 | COPY package.json package-lock.json ./
23 |
24 | # Install the root dependencies and devDependencies
25 | RUN npm ci --include=dev
26 |
27 | # Copy landing's package.json and package-lock.json
28 | COPY landing/package.json landing/package-lock.json ./landing/
29 |
30 | # Install the landing dependencies and devDependencies
31 | RUN cd landing/ && npm ci --include=dev
32 |
33 | # Copy the entire project to the working directory
34 | COPY . .
35 |
36 | # Build the project
37 | RUN npm run build
38 |
39 | # Remove development dependencies
40 | RUN npm prune --omit=dev
41 |
42 | # We don't need Next.js dependencies since landing is statically exported during build step.
43 | RUN rm -rf landing/node_modules
44 |
45 | # Final stage for app image
46 | FROM base
47 |
48 | # Copy built application
49 | COPY --from=builder /app /app
50 |
51 |
52 | # Define environment variables
53 | ENV NODE_ENV=production
54 |
55 | EXPOSE 3001
56 | # Specify the command to run the MCP server
57 | CMD ["node", "dist/index.js", "start:sse"]
58 |
```
--------------------------------------------------------------------------------
/.github/workflows/koyeb-prod.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Build and deploy backend to production
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches:
7 | - 'main'
8 |
9 | jobs:
10 | deploy:
11 | concurrency:
12 | group: '${{ github.ref_name }}'
13 | cancel-in-progress: true
14 | runs-on:
15 | group: neondatabase-protected-runner-group
16 | labels: linux-ubuntu-latest
17 | # Only main branch is allowed to deploy to production
18 | if: github.ref == 'refs/heads/main'
19 | steps:
20 | - name: Checkout
21 | uses: actions/checkout@v4
22 | - name: Install and configure the Koyeb CLI
23 | uses: koyeb-community/koyeb-actions@v2
24 | with:
25 | api_token: '${{ secrets.KOYEB_PROD_TOKEN }}'
26 | - name: Build and deploy to Koyeb production
27 | run: |
28 | koyeb deploy . platform-${{ github.ref_name }}/main \
29 | --instance-type medium \
30 | --region was \
31 | --archive-builder docker \
32 | --archive-docker-dockerfile remote.Dockerfile \
33 | --privileged \
34 | --type web \
35 | --port 3001:http \
36 | --route /:3001 \
37 | --wait \
38 | --env CLIENT_ID=${{secrets.PROD_CLIENT_ID}} \
39 | --env CLIENT_SECRET=${{secrets.PROD_CLIENT_SECRET}} \
40 | --env OAUTH_DATABASE_URL=${{secrets.PROD_OAUTH_DATABASE_URL}} \
41 | --env SERVER_HOST=${{vars.PROD_SERVER_HOST}} \
42 | --env NEON_API_HOST=${{vars.PROD_NEON_API_HOST}} \
43 | --env UPSTREAM_OAUTH_HOST=${{vars.PROD_OAUTH_HOST}} \
44 | --env COOKIE_SECRET=${{secrets.COOKIE_SECRET}} \
45 |
```
--------------------------------------------------------------------------------
/landing/components/ui/alert.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import * as React from 'react';
2 | import { cva, type VariantProps } from 'class-variance-authority';
3 |
4 | import { cn } from '@/lib/utils';
5 |
6 | const alertVariants = cva(
7 | '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',
8 | {
9 | variants: {
10 | variant: {
11 | default: 'bg-background text-foreground',
12 | important:
13 | 'border-important-notes/50 text-important-notes dark:border-important-notes [&>svg]:text-important-notes',
14 | destructive:
15 | 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
16 | },
17 | },
18 | defaultVariants: {
19 | variant: 'default',
20 | },
21 | },
22 | );
23 |
24 | export type AlertVariant = VariantProps<typeof alertVariants>['variant'];
25 |
26 | export function Alert({
27 | className,
28 | variant,
29 | ...props
30 | }: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
31 | return (
32 | <div
33 | role="alert"
34 | className={cn(alertVariants({ variant }), className)}
35 | {...props}
36 | />
37 | );
38 | }
39 |
40 | export function AlertTitle({
41 | className,
42 | ...props
43 | }: React.ComponentProps<'div'>) {
44 | return (
45 | <div
46 | className={cn(
47 | 'py-1 mb-1 font-medium leading-none tracking-tight',
48 | className,
49 | )}
50 | {...props}
51 | />
52 | );
53 | }
54 |
55 | export function AlertDescription({
56 | className,
57 | ...props
58 | }: React.ComponentProps<'div'>) {
59 | return (
60 | <div
61 | className={cn('text-sm [&_p]:leading-relaxed', className)}
62 | {...props}
63 | />
64 | );
65 | }
66 |
```
--------------------------------------------------------------------------------
/landing/components/Header.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import Image from 'next/image';
2 |
3 | import { Button } from '@/components/ui/button';
4 | import githubSvg from '@/icons/github.svg';
5 | import neonSvg from '@/icons/neon.svg';
6 |
7 | type HeaderProps = {
8 | packageVersion: number;
9 | };
10 |
11 | export const Header = ({ packageVersion }: HeaderProps) => (
12 | <header className="flex items-center justify-between gap-2">
13 | <div className="flex items-center gap-3">
14 | <Image src={neonSvg} width={30} height={30} alt="Neon logo" />
15 | <div className="flex items-baseline gap-2">
16 | <h1 className="text-3xl font-bold whitespace-nowrap">Neon MCP</h1>{' '}
17 | version: {packageVersion}
18 | </div>
19 | </div>
20 | <div className="flex items-center gap-2">
21 | <a
22 | href="https://cursor.com/en-US/install-mcp?name=Neon%20MCP%20Server&config=eyJ1cmwiOiJodHRwOi8vbWNwLm5lb24udGVjaC9tY3AifQ%3D%3D"
23 | target="_blank"
24 | rel="noopener noreferrer"
25 | >
26 | <Image
27 | alt="Add to Cursor"
28 | src="https://cursor.com/deeplink/mcp-install-light.svg"
29 | className="invert dark:invert-0"
30 | width={126}
31 | height={32}
32 | />
33 | </a>
34 |
35 | <Button asChild size="xs">
36 | <a
37 | href="https://github.com/neondatabase-labs/mcp-server-neon?tab=readme-ov-file"
38 | target="_blank"
39 | rel="noopener noreferrer"
40 | >
41 | <Image
42 | alt=""
43 | src={githubSvg}
44 | className="invert dark:invert-0"
45 | width={16}
46 | height={16}
47 | />{' '}
48 | Github
49 | </a>
50 | </Button>
51 | </div>
52 | </header>
53 | );
54 |
```
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
```javascript
1 | import js from '@eslint/js';
2 | import ts from 'typescript-eslint';
3 | import prettierConfig from 'eslint-config-prettier';
4 |
5 | // @ts-check
6 | export default ts.config(
7 | {
8 | files: ['**/*.ts', '**/*.cts', '**.*.mts'],
9 | ignores: [
10 | '**/*.js',
11 | '**/*.gen.ts',
12 | // see https://eslint.org/docs/latest/use/configure/configuration-files#globally-ignoring-files-with-ignores
13 | 'src/tools-evaluations/**/*',
14 | 'landing/**/*',
15 | ],
16 | rules: {
17 | 'no-console': 'off',
18 | '@typescript-eslint/consistent-type-definitions': ['error', 'type'],
19 | '@typescript-eslint/no-explicit-any': 'off',
20 | '@typescript-eslint/no-unsafe-member-access': 'off',
21 | '@typescript-eslint/no-unsafe-argument': 'off',
22 | '@typescript-eslint/no-unsafe-assignment': 'off',
23 | '@typescript-eslint/no-unsafe-return': 'off',
24 | '@typescript-eslint/no-unsafe-call': 'off',
25 | '@typescript-eslint/non-nullable-type-assertion-style': 'off',
26 | '@typescript-eslint/prefer-nullish-coalescing': 'off',
27 | '@typescript-eslint/no-unnecessary-condition': 'off',
28 | '@typescript-eslint/restrict-template-expressions': [
29 | 'error',
30 | {
31 | allowAny: true,
32 | allowBoolean: true,
33 | allowNullish: true,
34 | allowNumber: true,
35 | allowRegExp: true,
36 | },
37 | ],
38 | },
39 | languageOptions: {
40 | parserOptions: {
41 | project: true,
42 | tsconfigRootDir: import.meta.dirname,
43 | },
44 | },
45 | extends: [
46 | js.configs.recommended,
47 | ...ts.configs.strictTypeChecked,
48 | ...ts.configs.stylisticTypeChecked,
49 | ],
50 | },
51 | prettierConfig,
52 | );
53 |
```
--------------------------------------------------------------------------------
/src/tools/handlers/list-projects.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Api, ListProjectsParams } from '@neondatabase/api-client';
2 | import { ToolHandlerExtraParams } from '../types.js';
3 | import { getOrgByOrgIdOrDefault } from '../utils.js';
4 | import { handleListOrganizations } from './list-orgs.js';
5 |
6 | export async function handleListProjects(
7 | params: ListProjectsParams,
8 | neonClient: Api<unknown>,
9 | extra: ToolHandlerExtraParams,
10 | ) {
11 | const organization = await getOrgByOrgIdOrDefault(params, neonClient, extra);
12 |
13 | const response = await neonClient.listProjects({
14 | ...params,
15 | org_id: organization?.id,
16 | });
17 | if (response.status !== 200) {
18 | throw new Error(`Failed to list projects: ${response.statusText}`);
19 | }
20 |
21 | let projects = response.data.projects;
22 |
23 | // If search is provided and no org_id specified, and no projects found in personal account,
24 | // search across all user organizations
25 | if (params.search && !params.org_id && projects.length === 0) {
26 | const organizations = await handleListOrganizations(
27 | neonClient,
28 | extra.account,
29 | );
30 |
31 | // Search projects across all organizations
32 | const allProjects = [];
33 | for (const org of organizations) {
34 | // Skip the default organization
35 | if (organization?.id === org.id) {
36 | continue;
37 | }
38 |
39 | const orgResponse = await neonClient.listProjects({
40 | ...params,
41 | org_id: org.id,
42 | });
43 | if (orgResponse.status === 200) {
44 | allProjects.push(...orgResponse.data.projects);
45 | }
46 | }
47 |
48 | // If we found projects in other organizations, return them
49 | if (allProjects.length > 0) {
50 | projects = allProjects;
51 | }
52 | }
53 |
54 | return projects;
55 | }
56 |
```
--------------------------------------------------------------------------------
/src/tools/handlers/utils.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Api } from '@neondatabase/api-client';
2 | import { handleListProjects } from './list-projects.js';
3 | import { ToolHandlerExtraParams } from '../types.js';
4 | import { NotFoundError } from '../../server/errors.js';
5 | import { looksLikeBranchId, looksLikeProjectId } from '../utils.js';
6 |
7 | type MCPOrgId = `org:${string}`;
8 | type MCPProjectId = `project:${string}`;
9 | type MCPBranchId = `branch:${string}/${string}`; // projectId/branchId
10 | export type SearchResultId = MCPOrgId | MCPProjectId | MCPBranchId;
11 |
12 | export const isOrgId = (id: string): id is MCPOrgId => {
13 | return id.startsWith('org:') || id.startsWith('org-');
14 | };
15 |
16 | export const isProjectId = (id: string): id is MCPProjectId => {
17 | return id.startsWith('project:') || looksLikeProjectId(id);
18 | };
19 |
20 | export const isBranchId = (id: string): id is MCPProjectId => {
21 | return id.startsWith('branch:') || looksLikeBranchId(id);
22 | };
23 |
24 | export async function getOnlyProject(
25 | neonClient: Api<unknown>,
26 | extra: ToolHandlerExtraParams,
27 | ) {
28 | const projects = await handleListProjects({}, neonClient, extra);
29 | if (projects.length === 1) {
30 | return projects[0];
31 | }
32 | throw new NotFoundError(
33 | 'Please provide a project ID or ensure you have only one project in your account.',
34 | );
35 | }
36 |
37 | export const getDefaultBranch = async (
38 | projectId: string,
39 | neonClient: Api<unknown>,
40 | ) => {
41 | const branches = await neonClient.listProjectBranches({
42 | projectId,
43 | });
44 | const defaultBranch = branches.data.branches.find((branch) => branch.default);
45 | if (defaultBranch) {
46 | return defaultBranch;
47 | }
48 | throw new NotFoundError(
49 | 'No default branch found in this project. Please provide a branch ID.',
50 | );
51 | };
52 |
```
--------------------------------------------------------------------------------
/mcp-client/src/cli-client.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { StdioServerParameters } from '@modelcontextprotocol/sdk/client/stdio.js';
2 | import readline from 'readline/promises';
3 | import { MCPClient } from './index.js';
4 | import { consoleStyles, Logger } from './logger.js';
5 |
6 | const EXIT_COMMAND = 'exit';
7 |
8 | export class MCPClientCLI {
9 | private rl: readline.Interface;
10 | private client: MCPClient;
11 | private logger: Logger;
12 |
13 | constructor(serverConfig: StdioServerParameters) {
14 | this.client = new MCPClient(serverConfig);
15 | this.logger = new Logger({ mode: 'verbose' });
16 |
17 | this.rl = readline.createInterface({
18 | input: process.stdin,
19 | output: process.stdout,
20 | });
21 | }
22 |
23 | async start() {
24 | try {
25 | this.logger.log(consoleStyles.separator + '\n', { type: 'info' });
26 | this.logger.log('🤖 Interactive Claude CLI\n', { type: 'info' });
27 | this.logger.log(`Type your queries or "${EXIT_COMMAND}" to exit\n`, {
28 | type: 'info',
29 | });
30 | this.logger.log(consoleStyles.separator + '\n', { type: 'info' });
31 | this.client.start();
32 |
33 | await this.chat_loop();
34 | } catch (error) {
35 | this.logger.log('Failed to initialize tools: ' + error + '\n', {
36 | type: 'error',
37 | });
38 | process.exit(1);
39 | } finally {
40 | this.rl.close();
41 | process.exit(0);
42 | }
43 | }
44 |
45 | private async chat_loop() {
46 | while (true) {
47 | try {
48 | const query = (await this.rl.question(consoleStyles.prompt)).trim();
49 | if (query.toLowerCase() === EXIT_COMMAND) {
50 | this.logger.log('\nGoodbye! 👋\n', { type: 'warning' });
51 | break;
52 | }
53 |
54 | await this.client.processQuery(query);
55 | this.logger.log('\n' + consoleStyles.separator + '\n');
56 | } catch (error) {
57 | this.logger.log('\nError: ' + error + '\n', { type: 'error' });
58 | }
59 | }
60 | }
61 | }
62 |
```
--------------------------------------------------------------------------------
/src/server/errors.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { isAxiosError } from 'axios';
2 | import { NeonDbError } from '@neondatabase/serverless';
3 | import { logger } from '../utils/logger.js';
4 | import { captureException } from '@sentry/node';
5 |
6 | export class InvalidArgumentError extends Error {
7 | constructor(message: string) {
8 | super(message);
9 | this.name = 'InvalidArgumentError';
10 | }
11 | }
12 |
13 | export class NotFoundError extends Error {
14 | constructor(message: string) {
15 | super(message);
16 | this.name = 'NotFoundError';
17 | }
18 | }
19 |
20 | export function isClientError(
21 | error: unknown,
22 | ): error is InvalidArgumentError | NotFoundError {
23 | return (
24 | error instanceof InvalidArgumentError || error instanceof NotFoundError
25 | );
26 | }
27 |
28 | export function errorResponse(error: unknown) {
29 | return {
30 | isError: true,
31 | content: [
32 | {
33 | type: 'text' as const,
34 | text:
35 | error instanceof Error
36 | ? `${error.name}: ${error.message}`
37 | : 'Unknown error',
38 | },
39 | ],
40 | };
41 | }
42 |
43 | export function handleToolError(
44 | error: unknown,
45 | properties: Record<string, string>,
46 | ) {
47 | if (error instanceof NeonDbError || isClientError(error)) {
48 | return errorResponse(error);
49 | } else if (
50 | isAxiosError(error) &&
51 | error.response?.status &&
52 | error.response?.status < 500
53 | ) {
54 | return {
55 | isError: true,
56 | content: [
57 | {
58 | type: 'text' as const,
59 | text: error.response.data.message,
60 | },
61 | {
62 | type: 'text' as const,
63 | text: `[${error.response.statusText}] ${error.message}`,
64 | },
65 | ],
66 | };
67 | } else {
68 | logger.error('Tool call error:', {
69 | error:
70 | error instanceof Error
71 | ? `${error.name}: ${error.message}`
72 | : 'Unknown error',
73 | properties,
74 | });
75 | captureException(error, { extra: properties });
76 | return errorResponse(error);
77 | }
78 | }
79 |
```
--------------------------------------------------------------------------------
/src/analytics/analytics.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Analytics } from '@segment/analytics-node';
2 | import { ANALYTICS_WRITE_KEY } from '../constants.js';
3 | import { Api, AuthDetailsResponse } from '@neondatabase/api-client';
4 | import { AuthContext } from '../types/auth.js';
5 |
6 | let analytics: Analytics | undefined;
7 | type Account = AuthContext['extra']['account'];
8 | export const initAnalytics = () => {
9 | if (ANALYTICS_WRITE_KEY) {
10 | analytics = new Analytics({
11 | writeKey: ANALYTICS_WRITE_KEY,
12 | host: 'https://track.neon.tech',
13 | });
14 | }
15 | };
16 |
17 | export const identify = (
18 | account: Account | null,
19 | params: Omit<Parameters<Analytics['identify']>[0], 'userId' | 'anonymousId'>,
20 | ) => {
21 | if (account) {
22 | analytics?.identify({
23 | ...params,
24 | userId: account.id,
25 | traits: {
26 | name: account.name,
27 | email: account.email,
28 | isOrg: account.isOrg,
29 | },
30 | });
31 | } else {
32 | analytics?.identify({
33 | ...params,
34 | anonymousId: 'anonymous',
35 | });
36 | }
37 | };
38 |
39 | export const track = (params: Parameters<Analytics['track']>[0]) => {
40 | analytics?.track(params);
41 | };
42 |
43 | /**
44 | * 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.
45 | */
46 | export const identifyApiKey = async (
47 | auth: AuthDetailsResponse,
48 | neonClient: Api<unknown>,
49 | params: Omit<Parameters<Analytics['identify']>[0], 'userId' | 'anonymousId'>,
50 | ) => {
51 | if (auth.auth_method === 'api_key_org') {
52 | const { data: org } = await neonClient.getOrganization(auth.account_id);
53 | const account = {
54 | id: auth.account_id,
55 | name: org.name,
56 | isOrg: true,
57 | };
58 | identify(account, params);
59 | return account;
60 | }
61 | const { data: user } = await neonClient.getCurrentUserInfo();
62 | const account = {
63 | id: user.id,
64 | name: user.name,
65 | email: user.email,
66 | isOrg: false,
67 | };
68 | identify(account, params);
69 | return account;
70 | };
71 |
```
--------------------------------------------------------------------------------
/src/oauth/kv-store.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { KeyvPostgres, KeyvPostgresOptions } from '@keyv/postgres';
2 | import { logger } from '../utils/logger.js';
3 | import { AuthorizationCode, Client, Token } from 'oauth2-server';
4 | import Keyv from 'keyv';
5 | import { AuthContext } from '../types/auth.js';
6 | import { AuthDetailsResponse } from '@neondatabase/api-client';
7 |
8 | const SCHEMA = 'mcpauth';
9 |
10 | const createKeyv = <T>(options: KeyvPostgresOptions) =>
11 | new Keyv<T>({ store: new KeyvPostgres(options) });
12 |
13 | export const clients = createKeyv<Client>({
14 | connectionString: process.env.OAUTH_DATABASE_URL,
15 | schema: SCHEMA,
16 | table: 'clients',
17 | });
18 |
19 | clients.on('error', (err) => {
20 | logger.error('Clients keyv error:', { err });
21 | });
22 |
23 | export const tokens = createKeyv<Token>({
24 | connectionString: process.env.OAUTH_DATABASE_URL,
25 | schema: SCHEMA,
26 | table: 'tokens',
27 | });
28 |
29 | tokens.on('error', (err) => {
30 | logger.error('Tokens keyv error:', { err });
31 | });
32 |
33 | export type RefreshToken = {
34 | refreshToken: string;
35 | refreshTokenExpiresAt?: Date | undefined;
36 | accessToken: string;
37 | };
38 |
39 | export const refreshTokens = createKeyv<RefreshToken>({
40 | connectionString: process.env.OAUTH_DATABASE_URL,
41 | schema: SCHEMA,
42 | table: 'refresh_tokens',
43 | });
44 |
45 | refreshTokens.on('error', (err) => {
46 | logger.error('Refresh tokens keyv error:', { err });
47 | });
48 |
49 | export const authorizationCodes = createKeyv<AuthorizationCode>({
50 | connectionString: process.env.OAUTH_DATABASE_URL,
51 | schema: SCHEMA,
52 | table: 'authorization_codes',
53 | });
54 |
55 | authorizationCodes.on('error', (err) => {
56 | logger.error('Authorization codes keyv error:', { err });
57 | });
58 |
59 | export type ApiKeyRecord = {
60 | apiKey: string;
61 | authMethod: AuthDetailsResponse['auth_method'];
62 | account: AuthContext['extra']['account'];
63 | };
64 |
65 | export const apiKeys = createKeyv<ApiKeyRecord>({
66 | connectionString: process.env.OAUTH_DATABASE_URL,
67 | schema: SCHEMA,
68 | table: 'api_keys',
69 | });
70 |
71 | apiKeys.on('error', (err) => {
72 | logger.error('API keys keyv error:', { err });
73 | });
74 |
```
--------------------------------------------------------------------------------
/src/views/approval-dialog.pug:
--------------------------------------------------------------------------------
```
1 | - var clientName = client.client_name || 'A new MCP Client'
2 | - var logo = client.logo || client.logo_url || 'https://placehold.co/100x100/EEE/31343C?font=montserrat&text=MCP Client'
3 | - var website = client.client_uri || client.website
4 | - var redirectUris = client.redirect_uris
5 | - var serverName = 'Neon MCP Server'
6 |
7 | html(lang='en')
8 | head
9 | meta(charset='utf-8')
10 | meta(name='viewport', content='width=device-width, initial-scale=1')
11 | style
12 | include styles.css
13 | title #{clientName} | Authorization Request
14 | body
15 | div(class='container')
16 | div(class='precard')
17 | a(class="header", href='/', target='_blank')
18 | img(src='/logo.png', alt="Neon MCP", class="logo")
19 | div(class="card")
20 | h2(class="alert")
21 | strong MCP Client Authorization Request
22 | div(class="client-info")
23 | div(class='client-detail')
24 | div(class='detail-label') Name:
25 | div(class='detail-value') #{clientName}
26 | if website
27 | div(class='client-detail')
28 | div(class='detail-label') Website:
29 | div(class='detail-value small')
30 | a(href=website, target='_blank' rel='noopener noreferrer') #{website}
31 | if redirectUris
32 | div(class='client-detail')
33 | div(class='detail-label') Redirect URIs:
34 | div(class='detail-value small')
35 | each uri in redirectUris
36 | div #{uri}
37 | p(class="description") This MCP client is requesting to be authorized
38 | | on #{serverName}. If you approve, you will be redirected to complete the authentication.
39 |
40 | form(method='POST', action='/authorize')
41 | input(type='hidden', name='state', value=state)
42 |
43 | div(class='actions')
44 | button(type='button', class='button button-secondary' onclick='window.history.back()') Cancel
45 | button(type='submit', class='button button-primary') Approve
46 |
```
--------------------------------------------------------------------------------
/src/oauth/client.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Request } from 'express';
2 | import {
3 | discovery,
4 | buildAuthorizationUrl,
5 | authorizationCodeGrant,
6 | ClientSecretPost,
7 | refreshTokenGrant,
8 | } from 'openid-client';
9 | import {
10 | CLIENT_ID,
11 | CLIENT_SECRET,
12 | UPSTREAM_OAUTH_HOST,
13 | REDIRECT_URI,
14 | SERVER_HOST,
15 | } from '../constants.js';
16 | import { logger } from '../utils/logger.js';
17 |
18 | const NEON_MCP_SCOPES = [
19 | 'openid',
20 | 'offline',
21 | 'offline_access',
22 | 'urn:neoncloud:projects:create',
23 | 'urn:neoncloud:projects:read',
24 | 'urn:neoncloud:projects:update',
25 | 'urn:neoncloud:projects:delete',
26 | 'urn:neoncloud:orgs:create',
27 | 'urn:neoncloud:orgs:read',
28 | 'urn:neoncloud:orgs:update',
29 | 'urn:neoncloud:orgs:delete',
30 | 'urn:neoncloud:orgs:permission',
31 | ] as const;
32 |
33 | const getUpstreamConfig = async () => {
34 | const url = new URL(UPSTREAM_OAUTH_HOST);
35 | const config = await discovery(
36 | url,
37 | CLIENT_ID,
38 | {
39 | client_secret: CLIENT_SECRET,
40 | },
41 | ClientSecretPost(CLIENT_SECRET),
42 | {},
43 | );
44 |
45 | return config;
46 | };
47 |
48 | export const upstreamAuth = async (state: string) => {
49 | const config = await getUpstreamConfig();
50 | return buildAuthorizationUrl(config, {
51 | redirect_uri: REDIRECT_URI,
52 | token_endpoint_auth_method: 'client_secret_post',
53 | scope: NEON_MCP_SCOPES.join(' '),
54 | response_type: 'code',
55 | state,
56 | });
57 | };
58 |
59 | export const exchangeCode = async (req: Request) => {
60 | try {
61 | const config = await getUpstreamConfig();
62 | const currentUrl = new URL(req.originalUrl, SERVER_HOST);
63 | return await authorizationCodeGrant(config, currentUrl, {
64 | expectedState: req.query.state as string,
65 | idTokenExpected: true,
66 | });
67 | } catch (error: unknown) {
68 | logger.error('failed to exchange code:', {
69 | message: error instanceof Error ? error.message : 'Unknown error',
70 | error,
71 | });
72 | throw error;
73 | }
74 | };
75 |
76 | export const exchangeRefreshToken = async (token: string) => {
77 | const config = await getUpstreamConfig();
78 | return refreshTokenGrant(config, token);
79 | };
80 |
```
--------------------------------------------------------------------------------
/landing/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as AccordionPrimitive from '@radix-ui/react-accordion';
5 | import { ChevronDownIcon } from 'lucide-react';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | function Accordion({
10 | ...props
11 | }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
12 | return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
13 | }
14 |
15 | function AccordionItem({
16 | className,
17 | ...props
18 | }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
19 | return (
20 | <AccordionPrimitive.Item
21 | data-slot="accordion-item"
22 | className={cn('border-b last:border-b-0', className)}
23 | {...props}
24 | />
25 | );
26 | }
27 |
28 | function AccordionTrigger({
29 | className,
30 | children,
31 | ...props
32 | }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
33 | return (
34 | <AccordionPrimitive.Header className="flex">
35 | <AccordionPrimitive.Trigger
36 | data-slot="accordion-trigger"
37 | className={cn(
38 | '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',
39 | className,
40 | )}
41 | {...props}
42 | >
43 | {children}
44 | <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
45 | </AccordionPrimitive.Trigger>
46 | </AccordionPrimitive.Header>
47 | );
48 | }
49 |
50 | function AccordionContent({
51 | className,
52 | children,
53 | ...props
54 | }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
55 | return (
56 | <AccordionPrimitive.Content
57 | data-slot="accordion-content"
58 | className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
59 | {...props}
60 | >
61 | <div className={cn('pt-0 pb-4', className)}>{children}</div>
62 | </AccordionPrimitive.Content>
63 | );
64 | }
65 |
66 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
67 |
```
--------------------------------------------------------------------------------
/src/tools/handlers/connection-string.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Api } from '@neondatabase/api-client';
2 | import { ToolHandlerExtraParams } from '../types.js';
3 | import { startSpan } from '@sentry/node';
4 | import { getDefaultDatabase } from '../utils.js';
5 | import { getDefaultBranch, getOnlyProject } from './utils.js';
6 |
7 | export async function handleGetConnectionString(
8 | {
9 | projectId,
10 | branchId,
11 | computeId,
12 | databaseName,
13 | roleName,
14 | }: {
15 | projectId?: string;
16 | branchId?: string;
17 | computeId?: string;
18 | databaseName?: string;
19 | roleName?: string;
20 | },
21 | neonClient: Api<unknown>,
22 | extra: ToolHandlerExtraParams,
23 | ) {
24 | return await startSpan(
25 | {
26 | name: 'get_connection_string',
27 | },
28 | async () => {
29 | // If projectId is not provided, get the first project but only if there is only one project
30 | if (!projectId) {
31 | const project = await getOnlyProject(neonClient, extra);
32 | projectId = project.id;
33 | }
34 |
35 | if (!branchId) {
36 | const defaultBranch = await getDefaultBranch(projectId, neonClient);
37 | branchId = defaultBranch.id;
38 | }
39 |
40 | // If databaseName is not provided, use default `neondb` or first database
41 | let dbObject;
42 | if (!databaseName) {
43 | dbObject = await getDefaultDatabase(
44 | {
45 | projectId,
46 | branchId,
47 | databaseName,
48 | },
49 | neonClient,
50 | );
51 | databaseName = dbObject.name;
52 |
53 | if (!roleName) {
54 | roleName = dbObject.owner_name;
55 | }
56 | } else if (!roleName) {
57 | const { data } = await neonClient.getProjectBranchDatabase(
58 | projectId,
59 | branchId,
60 | databaseName,
61 | );
62 | roleName = data.database.owner_name;
63 | }
64 |
65 | // Get connection URI with the provided parameters
66 | const connectionString = await neonClient.getConnectionUri({
67 | projectId,
68 | role_name: roleName,
69 | database_name: databaseName,
70 | branch_id: branchId,
71 | endpoint_id: computeId,
72 | });
73 |
74 | return {
75 | uri: connectionString.data.uri,
76 | projectId,
77 | branchId,
78 | databaseName,
79 | roleName,
80 | computeId,
81 | };
82 | },
83 | );
84 | }
85 |
```
--------------------------------------------------------------------------------
/landing/components/ui/button.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import * as React from 'react';
2 | import { Slot } from '@radix-ui/react-slot';
3 | import { cva, type VariantProps } from 'class-variance-authority';
4 |
5 | import { cn } from '@/lib/utils';
6 |
7 | const buttonVariants = cva(
8 | "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",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
14 | destructive:
15 | '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',
16 | outline:
17 | 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
18 | secondary:
19 | 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
20 | ghost:
21 | 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
22 | link: 'text-primary underline-offset-4 hover:underline',
23 | },
24 | size: {
25 | default: 'h-9 px-4 py-2 has-[>svg]:px-3',
26 | xs: 'h-7 px-2.5 has-[>svg]:px-2',
27 | sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
28 | lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
29 | icon: 'size-9',
30 | },
31 | },
32 | defaultVariants: {
33 | variant: 'default',
34 | size: 'default',
35 | },
36 | },
37 | );
38 |
39 | function Button({
40 | className,
41 | variant,
42 | size,
43 | asChild = false,
44 | ...props
45 | }: React.ComponentProps<'button'> &
46 | VariantProps<typeof buttonVariants> & {
47 | asChild?: boolean;
48 | }) {
49 | const Comp = asChild ? Slot : 'button';
50 |
51 | return (
52 | <Comp
53 | data-slot="button"
54 | className={cn(buttonVariants({ variant, size, className }))}
55 | {...props}
56 | />
57 | );
58 | }
59 |
60 | export { Button, buttonVariants };
61 |
```
--------------------------------------------------------------------------------
/src/oauth/model.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {
2 | AuthorizationCode,
3 | AuthorizationCodeModel,
4 | Client,
5 | Token,
6 | User,
7 | } from 'oauth2-server';
8 | import {
9 | clients,
10 | tokens,
11 | refreshTokens,
12 | authorizationCodes,
13 | RefreshToken,
14 | } from './kv-store.js';
15 |
16 | class Model implements AuthorizationCodeModel {
17 | getClient: (
18 | clientId: string,
19 | clientSecret: string,
20 | ) => Promise<Client | undefined> = async (clientId) => {
21 | return clients.get(clientId);
22 | };
23 | saveClient: (client: Client) => Promise<Client> = async (client) => {
24 | await clients.set(client.id, client);
25 | return client;
26 | };
27 | saveToken: (token: Token) => Promise<Token> = async (token) => {
28 | await tokens.set(token.accessToken, token);
29 | return token;
30 | };
31 | deleteToken: (token: Token) => Promise<boolean> = async (token) => {
32 | return tokens.delete(token.accessToken);
33 | };
34 | saveRefreshToken: (token: RefreshToken) => Promise<RefreshToken> = async (
35 | token,
36 | ) => {
37 | await refreshTokens.set(token.refreshToken, token);
38 | return token;
39 | };
40 | deleteRefreshToken: (token: RefreshToken) => Promise<boolean> = async (
41 | token,
42 | ) => {
43 | return refreshTokens.delete(token.refreshToken);
44 | };
45 |
46 | validateScope: (
47 | user: User,
48 | client: Client,
49 | scope: string,
50 | ) => Promise<string> = (user, client, scope) => {
51 | // For demo purposes, accept all scopes
52 | return Promise.resolve(scope);
53 | };
54 | verifyScope: (token: Token, scope: string) => Promise<boolean> = () => {
55 | // For demo purposes, accept all scopes
56 | return Promise.resolve(true);
57 | };
58 | getAccessToken: (accessToken: string) => Promise<Token | undefined> = async (
59 | accessToken,
60 | ) => {
61 | const token = await tokens.get(accessToken);
62 | return token;
63 | };
64 | getRefreshToken: (refreshToken: string) => Promise<RefreshToken | undefined> =
65 | async (refreshToken) => {
66 | return refreshTokens.get(refreshToken);
67 | };
68 | saveAuthorizationCode: (
69 | code: AuthorizationCode,
70 | ) => Promise<AuthorizationCode> = async (code) => {
71 | await authorizationCodes.set(code.authorizationCode, code);
72 | return code;
73 | };
74 | getAuthorizationCode: (
75 | code: string,
76 | ) => Promise<AuthorizationCode | undefined> = async (code) => {
77 | return authorizationCodes.get(code);
78 | };
79 | revokeAuthorizationCode: (code: AuthorizationCode) => Promise<boolean> =
80 | async (code) => {
81 | return authorizationCodes.delete(code.authorizationCode);
82 | };
83 | }
84 |
85 | export const model = new Model();
86 |
```
--------------------------------------------------------------------------------
/src/tools/handlers/describe-branch.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Api, Branch } from '@neondatabase/api-client';
2 | import { ToolHandlerExtraParams } from '../types.js';
3 | import { handleGetConnectionString } from './connection-string.js';
4 | import { neon } from '@neondatabase/serverless';
5 | import { DESCRIBE_DATABASE_STATEMENTS } from '../utils.js';
6 | import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
7 | import { CONSOLE_URLS, generateConsoleUrl } from './urls.js';
8 |
9 | const branchInfo = (branch: Branch) => {
10 | return `Branch Details:
11 | Name: ${branch.name}
12 | ID: ${branch.id}
13 | Parent Branch: ${branch.parent_id}
14 | Default: ${branch.default}
15 | Protected: ${branch.protected ? 'Yes' : 'No'}
16 |
17 | ${branch.created_by ? `Created By: ${branch.created_by.name}` : ''}
18 | Created: ${new Date(branch.created_at).toLocaleDateString()}
19 | Updated: ${new Date(branch.updated_at).toLocaleDateString()}
20 |
21 | Compute Usage: ${branch.compute_time_seconds} seconds
22 | Written Data: ${branch.written_data_bytes} bytes
23 | Data Transfer: ${branch.data_transfer_bytes} bytes
24 |
25 | Console Link: ${generateConsoleUrl(CONSOLE_URLS.PROJECT_BRANCH, {
26 | projectId: branch.project_id,
27 | branchId: branch.id,
28 | })}
29 | `;
30 | };
31 |
32 | export async function handleDescribeBranch(
33 | {
34 | projectId,
35 | databaseName,
36 | branchId,
37 | }: {
38 | projectId: string;
39 | databaseName?: string;
40 | branchId: string;
41 | },
42 | neonClient: Api<unknown>,
43 | extra: ToolHandlerExtraParams,
44 | ): Promise<CallToolResult> {
45 | const { data: branchData } = await neonClient.getProjectBranch(
46 | projectId,
47 | branchId,
48 | );
49 |
50 | const branch = branchData.branch;
51 |
52 | let response: Record<string, any>[][];
53 | try {
54 | const connectionString = await handleGetConnectionString(
55 | {
56 | projectId,
57 | branchId: branch.id,
58 | databaseName,
59 | },
60 | neonClient,
61 | extra,
62 | );
63 | const runQuery = neon(connectionString.uri);
64 | response = await runQuery.transaction(
65 | DESCRIBE_DATABASE_STATEMENTS.map((sql) => runQuery.query(sql)),
66 | );
67 |
68 | return {
69 | content: [
70 | {
71 | type: 'text',
72 | text: branchInfo(branch),
73 | metadata: branch,
74 | },
75 | {
76 | type: 'text',
77 | text: ['Database Structure:', JSON.stringify(response, null, 2)].join(
78 | '\n',
79 | ),
80 | databasetree: response,
81 | },
82 | ],
83 | };
84 | } catch {
85 | // Ignore database connection errors
86 | }
87 |
88 | return {
89 | content: [
90 | {
91 | type: 'text',
92 | text: branchInfo(branch),
93 | },
94 | ],
95 | };
96 | }
97 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import { identifyApiKey, initAnalytics, track } from './analytics/analytics.js';
4 | import { NODE_ENV } from './constants.js';
5 | import { handleInit, parseArgs } from './initConfig.js';
6 | import { createNeonClient, getPackageJson } from './server/api.js';
7 | import { createMcpServer } from './server/index.js';
8 | import { createSseTransport } from './transports/sse-express.js';
9 | import { startStdio } from './transports/stdio.js';
10 | import { logger } from './utils/logger.js';
11 | import { AppContext } from './types/context.js';
12 | import { NEON_TOOLS } from './tools/index.js';
13 | import './utils/polyfills.js';
14 |
15 | const args = parseArgs();
16 | const appVersion = getPackageJson().version;
17 | const appName = getPackageJson().name;
18 |
19 | if (args.command === 'export-tools') {
20 | console.log(
21 | JSON.stringify(
22 | NEON_TOOLS.map((item) => ({ ...item, inputSchema: undefined })),
23 | null,
24 | 2,
25 | ),
26 | );
27 | process.exit(0);
28 | }
29 |
30 | const appContext: AppContext = {
31 | environment: NODE_ENV,
32 | name: appName,
33 | version: appVersion,
34 | transport: 'stdio',
35 | };
36 |
37 | if (args.analytics) {
38 | initAnalytics();
39 | }
40 |
41 | if (args.command === 'start:sse') {
42 | createSseTransport({
43 | ...appContext,
44 | transport: 'sse',
45 | });
46 | } else {
47 | // Turn off logger in stdio mode to avoid capturing stderr in wrong format by host application (Claude Desktop)
48 | logger.silent = true;
49 |
50 | try {
51 | const neonClient = createNeonClient(args.neonApiKey);
52 | const { data } = await neonClient.getAuthDetails();
53 | const accountId = data.account_id;
54 |
55 | const account = await identifyApiKey(data, neonClient, {
56 | context: appContext,
57 | });
58 |
59 | if (args.command === 'init') {
60 | track({
61 | userId: accountId,
62 | event: 'init_stdio',
63 | context: appContext,
64 | });
65 | handleInit({
66 | executablePath: args.executablePath,
67 | neonApiKey: args.neonApiKey,
68 | analytics: args.analytics,
69 | });
70 | process.exit(0);
71 | }
72 |
73 | if (args.command === 'start') {
74 | track({
75 | userId: accountId,
76 | event: 'start_stdio',
77 | context: appContext,
78 | });
79 | const server = createMcpServer({
80 | apiKey: args.neonApiKey,
81 | account,
82 | app: appContext,
83 | });
84 | await startStdio(server);
85 | }
86 | } catch (error) {
87 | console.error('Server error:', error);
88 | track({
89 | anonymousId: 'anonymous',
90 | event: 'server_error',
91 | properties: { error },
92 | context: appContext,
93 | });
94 | process.exit(1);
95 | }
96 | }
97 |
```
--------------------------------------------------------------------------------
/src/transports/stream.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Request, Response, Router } from 'express';
2 | import { AppContext } from '../types/context.js';
3 | import { createMcpServer } from '../server/index.js';
4 | import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
5 | import { logger } from '../utils/logger.js';
6 | import { track } from '../analytics/analytics.js';
7 | import { requiresAuth } from '../oauth/utils.js';
8 |
9 | export const createStreamTransport = (appContext: AppContext) => {
10 | const router = Router();
11 |
12 | router.post('/', requiresAuth(), async (req: Request, res: Response) => {
13 | const auth = req.auth;
14 | if (!auth) {
15 | res.status(401).send('Unauthorized');
16 | return;
17 | }
18 |
19 | try {
20 | const server = createMcpServer({
21 | apiKey: auth.token,
22 | client: auth.extra.client,
23 | account: auth.extra.account,
24 | app: appContext,
25 | readOnly: auth.extra.readOnly,
26 | userAgent: req.headers['user-agent'],
27 | });
28 |
29 | const transport = new StreamableHTTPServerTransport({
30 | sessionIdGenerator: undefined,
31 | });
32 | res.on('close', () => {
33 | void transport.close();
34 | void server.close();
35 | });
36 | await server.connect(transport);
37 | await transport.handleRequest(req, res, req.body);
38 | } catch (error: unknown) {
39 | logger.error('Failed to connect to MCP server:', {
40 | message: error instanceof Error ? error.message : 'Unknown error',
41 | error,
42 | });
43 | track({
44 | userId: auth.extra.account.id,
45 | event: 'stream_connection_errored',
46 | properties: { error },
47 | context: {
48 | app: appContext,
49 | client: auth.extra.client,
50 | },
51 | });
52 | if (!res.headersSent) {
53 | res.status(500).json({
54 | jsonrpc: '2.0',
55 | error: {
56 | code: -32603,
57 | message: 'Internal server error',
58 | },
59 | id: null,
60 | });
61 | }
62 | }
63 | });
64 |
65 | router.get('/', requiresAuth(), (req: Request, res: Response) => {
66 | logger.info('Received GET MCP request');
67 | res.writeHead(405).end(
68 | JSON.stringify({
69 | jsonrpc: '2.0',
70 | error: {
71 | code: -32000,
72 | message: 'Method not allowed.',
73 | },
74 | id: null,
75 | }),
76 | );
77 | });
78 |
79 | router.delete('/', requiresAuth(), (req: Request, res: Response) => {
80 | logger.info('Received DELETE MCP request');
81 | res.writeHead(405).end(
82 | JSON.stringify({
83 | jsonrpc: '2.0',
84 | error: {
85 | code: -32000,
86 | message: 'Method not allowed.',
87 | },
88 | id: null,
89 | }),
90 | );
91 | });
92 |
93 | return router;
94 | };
95 |
```
--------------------------------------------------------------------------------
/src/resources.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ReadResourceCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
2 | import { Resource } from '@modelcontextprotocol/sdk/types.js';
3 |
4 | export async function fetchRawGithubContent(rawPath: string) {
5 | const path = rawPath.replace('/blob', '');
6 |
7 | const response = await fetch(`https://raw.githubusercontent.com${path}`);
8 | if (!response.ok) {
9 | throw new Error(
10 | `Failed to fetch GitHub content: ${response.status} ${response.statusText}`,
11 | );
12 | }
13 | return response.text();
14 | }
15 |
16 | export const NEON_RESOURCES = [
17 | {
18 | name: 'neon-auth',
19 | uri: 'https://github.com/neondatabase-labs/ai-rules/blob/main/neon-auth.mdc',
20 | mimeType: 'text/plain',
21 | description: 'Neon Auth usage instructions',
22 | handler: async (url) => {
23 | const uri = url.host;
24 | const rawPath = url.pathname;
25 | const content = await fetchRawGithubContent(rawPath);
26 | return {
27 | contents: [
28 | {
29 | uri: uri,
30 | mimeType: 'text/plain',
31 | text: content,
32 | },
33 | ],
34 | };
35 | },
36 | },
37 | {
38 | name: 'neon-serverless',
39 | uri: 'https://github.com/neondatabase-labs/ai-rules/blob/main/neon-serverless.mdc',
40 | mimeType: 'text/plain',
41 | description: 'Neon Serverless usage instructions',
42 | handler: async (url) => {
43 | const uri = url.host;
44 | const rawPath = url.pathname;
45 | const content = await fetchRawGithubContent(rawPath);
46 | return {
47 | contents: [
48 | {
49 | uri,
50 | mimeType: 'text/plain',
51 | text: content,
52 | },
53 | ],
54 | };
55 | },
56 | },
57 | {
58 | name: 'neon-drizzle',
59 | uri: 'https://github.com/neondatabase-labs/ai-rules/blob/main/neon-drizzle.mdc',
60 | mimeType: 'text/plain',
61 | description: 'Neon Drizzle usage instructions',
62 | handler: async (url) => {
63 | const uri = url.host;
64 | const rawPath = url.pathname;
65 | const content = await fetchRawGithubContent(rawPath);
66 | return {
67 | contents: [
68 | {
69 | uri,
70 | mimeType: 'text/plain',
71 | text: content,
72 | },
73 | ],
74 | };
75 | },
76 | },
77 | {
78 | name: 'neon-get-started',
79 | uri: 'https://github.com/neondatabase-labs/ai-rules/blob/main/neon-get-started.mdc',
80 | mimeType: 'text/markdown',
81 | description: 'Neon getting started guide',
82 | handler: async (url) => {
83 | const uri = url.host;
84 | const rawPath = url.pathname;
85 | const content = await fetchRawGithubContent(rawPath);
86 | return {
87 | contents: [
88 | {
89 | uri,
90 | mimeType: 'text/markdown',
91 | text: content,
92 | },
93 | ],
94 | };
95 | },
96 | },
97 | ] satisfies (Resource & { handler: ReadResourceCallback })[];
98 |
```
--------------------------------------------------------------------------------
/src/tools/handlers/neon-auth.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2 | import { Api, NeonAuthSupportedAuthProvider } from '@neondatabase/api-client';
3 | import { provisionNeonAuthInputSchema } from '../toolsSchema.js';
4 | import { z } from 'zod';
5 | import { getDefaultDatabase } from '../utils.js';
6 | import { getDefaultBranch } from './utils.js';
7 | import { ToolHandlerExtraParams } from '../types.js';
8 |
9 | type Props = z.infer<typeof provisionNeonAuthInputSchema>;
10 | export async function handleProvisionNeonAuth(
11 | { projectId, branchId, databaseName }: Props,
12 | neonClient: Api<unknown>,
13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
14 | _extra: ToolHandlerExtraParams,
15 | ): Promise<CallToolResult> {
16 | // If branchId is not provided, use the default branch
17 | let resolvedBranchId = branchId;
18 | if (!resolvedBranchId) {
19 | const defaultBranch = await getDefaultBranch(projectId, neonClient);
20 | resolvedBranchId = defaultBranch.id;
21 | }
22 |
23 | const defaultDatabase = await getDefaultDatabase(
24 | {
25 | projectId,
26 | branchId: resolvedBranchId,
27 | databaseName,
28 | },
29 | neonClient,
30 | );
31 |
32 | if (!defaultDatabase) {
33 | return {
34 | isError: true,
35 | content: [
36 | {
37 | type: 'text',
38 | text: databaseName
39 | ? `The branch has no database named '${databaseName}'.`
40 | : 'The branch has no databases.',
41 | },
42 | ],
43 | };
44 | }
45 |
46 | const response = await neonClient.createNeonAuth(
47 | projectId,
48 | resolvedBranchId,
49 | {
50 | auth_provider: NeonAuthSupportedAuthProvider.BetterAuth,
51 | database_name: defaultDatabase.name,
52 | },
53 | );
54 |
55 | // In case of 409, it means that the integration already exists
56 | // We should not return an error, but a message that the integration already exists and fetch the existing integration
57 | if (response.status === 409) {
58 | return {
59 | content: [
60 | {
61 | type: 'text',
62 | text: 'Neon Auth already provisioned.',
63 | },
64 | ],
65 | };
66 | }
67 |
68 | if (response.status !== 201) {
69 | return {
70 | isError: true,
71 | content: [
72 | {
73 | type: 'text',
74 | text: `Failed to provision Neon Auth. Error: ${response.statusText}`,
75 | },
76 | ],
77 | };
78 | }
79 |
80 | return {
81 | content: [
82 | {
83 | type: 'text',
84 | text: `Authentication has been successfully provisioned for your Neon project and branch.
85 | \`\`\`
86 | Use this URL to access the Neon Auth through your better auth compatible client: ${response.data.base_url}
87 | \`\`\`
88 | `,
89 | },
90 | {
91 | type: 'text',
92 | text: `
93 | Use Following JWKS URL to retrieve the public key to verify the JSON Web Tokens (JWT) issued by authentication provider:
94 | \`\`\`
95 | ${response.data.jwks_url}
96 | \`\`\`
97 | `,
98 | },
99 | ],
100 | };
101 | }
102 |
```
--------------------------------------------------------------------------------
/landing/icons/neon.svg:
--------------------------------------------------------------------------------
```
1 | <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
1 | /* Modern, responsive styling with system fonts */
2 | :root {
3 | --primary-color: #0070f3;
4 | --error-color: #f44336;
5 | --border-color: #e5e7eb;
6 | --text-color: #dedede;
7 | --text-color-secondary: #949494;
8 | --background-color: #1c1c1c;
9 | --border-color: #2a2929;
10 | --card-shadow: 0 0px 12px 0px rgb(0 230 153 / 0.3);
11 | --link-color: rgb(0 230 153 / 1);
12 | }
13 |
14 | body {
15 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
16 | Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
17 | line-height: 1.6;
18 | color: var(--text-color);
19 | background-color: var(--background-color);
20 | margin: 0;
21 | padding: 0;
22 | }
23 |
24 | .container {
25 | max-width: 600px;
26 |
27 | margin: 2rem auto;
28 | padding: 1rem;
29 | }
30 |
31 | .precard {
32 | padding: 2rem;
33 | text-align: center;
34 | }
35 |
36 | .card {
37 | background-color: #0a0c09e6;
38 | border-radius: 8px;
39 | box-shadow: var(--card-shadow);
40 | padding: 2rem 2rem 0.5rem;
41 | }
42 |
43 | .header {
44 | display: flex;
45 | align-items: center;
46 | justify-content: center;
47 | margin-bottom: 1.5rem;
48 | color: var(--text-color);
49 | text-decoration: none;
50 | }
51 |
52 | .logo {
53 | width: 48px;
54 | height: 48px;
55 | margin-right: 1rem;
56 | border-radius: 8px;
57 | object-fit: contain;
58 | }
59 |
60 | .title {
61 | margin: 0;
62 | font-size: 1.3rem;
63 | font-weight: 400;
64 | }
65 |
66 | .alert {
67 | margin: 0;
68 | font-size: 1.5rem;
69 | font-weight: 400;
70 | margin: 1rem 0;
71 | text-align: center;
72 | }
73 |
74 | .description {
75 | color: var(--text-color-secondary);
76 | }
77 |
78 | .client-info {
79 | border: 1px solid var(--border-color);
80 | border-radius: 6px;
81 | padding: 1rem 1rem 0.5rem;
82 | margin-bottom: 1.5rem;
83 | }
84 |
85 | .client-name {
86 | font-weight: 600;
87 | font-size: 1.2rem;
88 | margin: 0 0 0.5rem 0;
89 | }
90 |
91 | .client-detail {
92 | display: flex;
93 | margin-bottom: 0.5rem;
94 | align-items: baseline;
95 | }
96 |
97 | .detail-label {
98 | font-weight: 500;
99 | min-width: 120px;
100 | }
101 |
102 | .detail-value {
103 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
104 | 'Courier New', monospace;
105 | word-break: break-all;
106 | }
107 |
108 | .detail-value a {
109 | color: inherit;
110 | text-decoration: underline;
111 | }
112 |
113 | .detail-value.small {
114 | font-size: 0.8em;
115 | }
116 |
117 | .external-link-icon {
118 | font-size: 0.75em;
119 | margin-left: 0.25rem;
120 | vertical-align: super;
121 | }
122 |
123 | .actions {
124 | display: flex;
125 | justify-content: flex-end;
126 | gap: 1rem;
127 | margin-top: 2rem;
128 | }
129 |
130 | .button {
131 | padding: 0.65rem 1rem;
132 | border-radius: 6px;
133 | font-weight: 500;
134 | cursor: pointer;
135 | border: none;
136 | font-size: 1rem;
137 | }
138 |
139 | .button-primary {
140 | background-color: rgb(0 229 153 / 1);
141 | color: rgb(26 26 26 / 1);
142 | }
143 |
144 | .button-secondary {
145 | background-color: transparent;
146 | border: 1px solid rgb(73 75 80 / 1);
147 | color: var(--text-color);
148 | }
149 |
150 | /* Responsive adjustments */
151 | @media (max-width: 640px) {
152 | .container {
153 | margin: 1rem auto;
154 | padding: 0.5rem;
155 | }
156 |
157 | .card {
158 | padding: 1.5rem;
159 | }
160 |
161 | .client-detail {
162 | flex-direction: column;
163 | }
164 |
165 | .detail-label {
166 | min-width: unset;
167 | margin-bottom: 0.25rem;
168 | }
169 |
170 | .actions {
171 | flex-direction: column;
172 | }
173 |
174 | .button {
175 | width: 100%;
176 | }
177 | }
178 |
```
--------------------------------------------------------------------------------
/landing/components/Introduction.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import { cn } from '@/lib/utils';
2 | import { ExternalLink } from '@/components/ExternalLink';
3 | import { CopyableUrl } from '@/components/CopyableUrl';
4 |
5 | export const Introduction = ({ className }: { className?: string }) => (
6 | <div className={cn('flex flex-col gap-2', className)}>
7 | <desc className="text-xl mb-2">
8 | Manage your Neon Postgres databases with natural language.
9 | </desc>
10 |
11 | <CopyableUrl url="https://mcp.neon.tech/mcp" />
12 |
13 | <div>
14 | The <strong className="font-semibold">Neon MCP Server</strong> lets AI
15 | agents and dev tools like Cursor interact with Neon by translating plain
16 | English into{' '}
17 | <ExternalLink href="https://api-docs.neon.tech/reference/getting-started-with-neon-api">
18 | Neon API
19 | </ExternalLink>{' '}
20 | calls—no code required. You can create databases, run queries, and make
21 | schema changes just by typing commands like "Create a database named
22 | 'my-new-database'" or "List all my Neon projects".
23 | </div>
24 | <div>
25 | Built on the{' '}
26 | <ExternalLink href="https://modelcontextprotocol.org/">
27 | Model Context Protocol (MCP)
28 | </ExternalLink>
29 | , the server bridges natural language and the Neon API to support actions
30 | like creating projects, managing branches, running queries, and handling
31 | migrations.
32 | <br />
33 | <ExternalLink href="https://neon.tech/docs/ai/neon-mcp-server">
34 | Learn more in the docs
35 | </ExternalLink>
36 | </div>
37 |
38 | <div className="bg-gradient-to-r from-emerald-50 to-teal-50 border border-emerald-200 rounded-lg p-4 my-2">
39 | <p className="text-sm">
40 | <strong className="font-semibold">Quick setup:</strong> Run{' '}
41 | <code className="bg-white px-2 py-0.5 rounded text-sm border border-emerald-200">
42 | npx neonctl@latest init
43 | </code>{' '}
44 | to authenticate via OAuth, automatically create a Neon API key, and
45 | configure Cursor, VS Code, or Claude Code CLI to connect to the Neon MCP
46 | Server. Then ask your AI assistant "Get started with Neon".{' '}
47 | <ExternalLink href="https://neon.tech/docs/reference/cli-init">
48 | Learn more in the docs
49 | </ExternalLink>
50 | </p>
51 | </div>
52 |
53 | <div className="mt-4">
54 | <h3 className="text-lg font-semibold mb-2">Read-Only Version</h3>
55 | <div className="flex flex-col gap-3">
56 | <div>
57 | <p className="text-sm mb-2">
58 | Safe for cloud environments. All transactions are read-only -
59 | perfect for querying and analyzing data without modification risks.
60 | </p>
61 | <p className="text-xs text-muted-foreground">
62 | Enable read-only mode by adding the{' '}
63 | <code className="bg-muted px-1 py-0.5 rounded text-xs">
64 | x-read-only: true
65 | </code>{' '}
66 | header in your MCP configuration.
67 | </p>
68 | </div>
69 | </div>
70 | </div>
71 | </div>
72 | );
73 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@neondatabase/mcp-server-neon",
3 | "version": "0.6.4",
4 | "description": "MCP server for interacting with Neon Management API and databases",
5 | "license": "MIT",
6 | "author": "Neon, Inc. (https://neon.tech/)",
7 | "homepage": "https://github.com/neondatabase/mcp-server-neon/",
8 | "bugs": "https://github.com/neondatabase/mcp-server-neon/issues",
9 | "type": "module",
10 | "access": "public",
11 | "bin": {
12 | "mcp-server-neon": "./dist/index.js"
13 | },
14 | "files": [
15 | "dist",
16 | "CHANGELOG.md"
17 | ],
18 | "scripts": {
19 | "typecheck": "tsc --noEmit",
20 | "build": "tsc && npm run build:chmod && npm run export-tools && npm run build:landing",
21 | "build:chmod": "node -e \"require('fs').chmodSync('dist/index.js', '755')\"",
22 | "build:landing": "cd landing/ && npm run build && cp -r out/* ../public",
23 | "watch": "tsc-watch --onSuccess \"chmod 755 dist/index.js\"",
24 | "inspector": "npx @modelcontextprotocol/inspector dist/index.js",
25 | "format": "prettier --write .",
26 | "lint": "npm run typecheck && eslint src && prettier --check .",
27 | "lint:fix": "npm run typecheck && eslint src --fix && prettier --w .",
28 | "prerelease": "npm run build",
29 | "prepublishOnly": "bun scripts/before-publish.ts",
30 | "test": "npx braintrust eval src/tools-evaluations",
31 | "start": "node dist/index.js start",
32 | "start:sse": "node dist/index.js start:sse",
33 | "export-tools": "node dist/index.js export-tools > landing/tools.json"
34 | },
35 | "dependencies": {
36 | "@keyv/postgres": "2.1.2",
37 | "@modelcontextprotocol/sdk": "1.11.2",
38 | "@neondatabase/api-client": "2.5.0",
39 | "@neondatabase/serverless": "1.0.0",
40 | "@radix-ui/react-accordion": "1.2.11",
41 | "@segment/analytics-node": "2.2.1",
42 | "@sentry/node": "9.19.0",
43 | "@tailwindcss/postcss": "4.1.10",
44 | "axios": "1.13.2",
45 | "body-parser": "2.2.0",
46 | "chalk": "5.3.0",
47 | "class-variance-authority": "0.7.1",
48 | "cookie-parser": "1.4.7",
49 | "cors": "2.8.5",
50 | "dotenv": "16.4.7",
51 | "express": "5.0.1",
52 | "keyv": "5.3.2",
53 | "lucide-react": "0.515.0",
54 | "morgan": "1.10.0",
55 | "next": "15.3.3",
56 | "node-fetch": "2.7.0",
57 | "oauth2-server": "3.1.1",
58 | "openid-client": "6.3.4",
59 | "pug": "3.0.3",
60 | "react-syntax-highlighter": "15.6.1",
61 | "tailwind-merge": "3.3.1",
62 | "winston": "3.17.0",
63 | "zod": "3.24.1"
64 | },
65 | "devDependencies": {
66 | "@eslint/js": "9.21.0",
67 | "@types/cookie-parser": "1.4.8",
68 | "@types/cors": "2.8.17",
69 | "@types/express": "5.0.1",
70 | "@types/morgan": "1.9.9",
71 | "@types/node": "20.17.9",
72 | "@types/node-fetch": "2.6.12",
73 | "@types/oauth2-server": "3.0.18",
74 | "@types/react": "19.1.8",
75 | "autoevals": "0.0.111",
76 | "braintrust": "0.0.177",
77 | "bun": "1.1.40",
78 | "eslint": "9.21.0",
79 | "eslint-config-prettier": "10.0.2",
80 | "prettier": "3.4.1",
81 | "tsc-watch": "6.2.1",
82 | "typescript": "5.7.2",
83 | "typescript-eslint": "v8.25.0"
84 | },
85 | "engines": {
86 | "node": ">=22.0.0"
87 | },
88 | "resolutions": {
89 | "axios": "1.13.2"
90 | }
91 | }
92 |
```
--------------------------------------------------------------------------------
/.github/workflows/claude.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Claude Code
2 |
3 | on:
4 | issue_comment:
5 | types: [created]
6 | pull_request_review_comment:
7 | types: [created]
8 | issues:
9 | types: [opened, assigned]
10 | pull_request_review:
11 | types: [submitted]
12 |
13 | concurrency:
14 | group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }}-${{ github.actor }}
15 | cancel-in-progress: true
16 |
17 | jobs:
18 | claude:
19 | if: |
20 | (
21 | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') &&
22 | (github.event.comment.author_association == 'OWNER' ||
23 | github.event.comment.author_association == 'MEMBER' ||
24 | github.event.comment.author_association == 'COLLABORATOR')) ||
25 | (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') &&
26 | (github.event.comment.author_association == 'OWNER' ||
27 | github.event.comment.author_association == 'MEMBER' ||
28 | github.event.comment.author_association == 'COLLABORATOR')) ||
29 | (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') &&
30 | (github.event.review.author_association == 'OWNER' ||
31 | github.event.review.author_association == 'MEMBER' ||
32 | github.event.review.author_association == 'COLLABORATOR')) ||
33 | (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) &&
34 | (github.event.issue.author_association == 'OWNER' ||
35 | github.event.issue.author_association == 'MEMBER' ||
36 | github.event.issue.author_association == 'COLLABORATOR'))
37 | )
38 | runs-on:
39 | group: neondatabase-protected-runner-group
40 | labels: linux-ubuntu-latest
41 | permissions:
42 | contents: read
43 | pull-requests: read
44 | issues: read
45 | id-token: write
46 | actions: read # Required for Claude to read CI results on PRs
47 | steps:
48 | - name: Checkout repository
49 | uses: actions/checkout@v4
50 | with:
51 | fetch-depth: 1
52 |
53 | - name: Run Claude Code
54 | id: claude
55 | uses: anthropics/claude-code-action@v1
56 | with:
57 | anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
58 |
59 | # This is an optional setting that allows Claude to read CI results on PRs
60 | additional_permissions: |
61 | actions: read
62 |
63 | # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
64 | # prompt: 'Update the pull request description to include a summary of changes.'
65 |
66 | # Optional: Add claude_args to customize behavior and configuration
67 | # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
68 | # or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
69 | claude_args: '--allowed-tools "Bash(gh issue:*),Bash(gh pr:*),Bash(gh search:*)"'
70 |
```
--------------------------------------------------------------------------------
/.github/workflows/koyeb-preview.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Build and deploy backend to preview
2 |
3 | on:
4 | workflow_dispatch:
5 | pull_request:
6 | types: [synchronize, labeled]
7 |
8 | jobs:
9 | deploy:
10 | concurrency:
11 | group: '${{ github.ref_name }}'
12 | cancel-in-progress: true
13 | runs-on:
14 | group: neondatabase-protected-runner-group
15 | labels: linux-ubuntu-latest
16 | if: contains(github.event.pull_request.labels.*.name, 'deploy-preview')
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v4
20 | - name: Install and configure the Koyeb CLI
21 | uses: koyeb-community/koyeb-actions@v2
22 | with:
23 | api_token: '${{ secrets.KOYEB_PREVIEW_TOKEN }}'
24 | - name: Build and deploy to Koyeb preview
25 | run: |
26 | koyeb deploy . platform-koyeb-preview/main \
27 | --instance-type nano \
28 | --region was \
29 | --archive-builder docker \
30 | --archive-docker-dockerfile remote.Dockerfile \
31 | --privileged \
32 | --type web \
33 | --port 3001:http \
34 | --route /:3001 \
35 | --wait \
36 | --env CLIENT_ID=${{secrets.CLIENT_ID}} \
37 | --env CLIENT_SECRET=${{secrets.CLIENT_SECRET}} \
38 | --env OAUTH_DATABASE_URL=${{secrets.PREVIEW_OAUTH_DATABASE_URL}} \
39 | --env SERVER_HOST=${{vars.KOYEB_PREVIEW_SERVER_HOST}} \
40 | --env NEON_API_HOST=${{vars.NEON_API_HOST_STAGING}} \
41 | --env UPSTREAM_OAUTH_HOST=${{vars.OAUTH_HOST_STAGING}} \
42 | --env COOKIE_SECRET=${{secrets.COOKIE_SECRET}} \
43 |
44 | - name: Comment on PR with deployment URL
45 | if: ${{ github.event_name == 'pull_request' && success() }}
46 | uses: actions/github-script@v7
47 | with:
48 | github-token: ${{ secrets.GITHUB_TOKEN }}
49 | script: |
50 | // GitHub bot id taken from (https://api.github.com/users/github-actions[bot])
51 | const githubActionsBotId = 41898282
52 |
53 | const ownerRepoParams = {
54 | owner: context.repo.owner,
55 | repo: context.repo.repo,
56 | }
57 |
58 | // Hidden start marker for the comment
59 | const startMarker = '<!-- Preview Deployment Comment-->';
60 | const body = `${startMarker}
61 | 🚀 Preview deployment ready: [https://preview-mcp.neon.tech](https://preview-mcp.neon.tech)`;
62 |
63 | const comments = await github.paginate(github.rest.issues.listComments, {
64 | ...ownerRepoParams,
65 | issue_number: context.issue.number,
66 | });
67 |
68 | // Delete previous comments regarding preview deployments.
69 | for (comment of comments.filter(comment => comment.user.id === githubActionsBotId && comment.body.startsWith(startMarker))) {
70 | await github.rest.issues.deleteComment({
71 | comment_id: comment.id,
72 | ...ownerRepoParams,
73 | })
74 | }
75 |
76 | await github.rest.issues.createComment({
77 | ...ownerRepoParams,
78 | issue_number: context.issue.number,
79 | body
80 | });
81 |
```
--------------------------------------------------------------------------------
/landing/components/DescriptionItem.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import {
2 | DescriptionItem,
3 | DescriptionItemType,
4 | TextBlock,
5 | } from '@/lib/description';
6 | import { CodeSnippet } from '@/components/CodeSnippet';
7 | import {
8 | Alert,
9 | AlertDescription,
10 | AlertTitle,
11 | AlertVariant,
12 | } from '@/components/ui/alert';
13 | import {
14 | Terminal,
15 | CircleAlert,
16 | Lightbulb,
17 | BadgeInfo,
18 | Workflow,
19 | SquareArrowRight,
20 | Component,
21 | BookOpenCheck,
22 | } from 'lucide-react';
23 |
24 | const ALERT_VARIANT_PER_DESCRIPTION_TYPE: Record<
25 | DescriptionItemType,
26 | {
27 | variant: AlertVariant;
28 | icon: typeof Component;
29 | }
30 | > = {
31 | use_case: { variant: 'default', icon: BookOpenCheck },
32 | next_steps: { variant: 'default', icon: SquareArrowRight },
33 | important_notes: { variant: 'important', icon: CircleAlert },
34 | workflow: { variant: 'default', icon: Workflow },
35 | hints: { variant: 'default', icon: BadgeInfo },
36 | hint: { variant: 'default', icon: Lightbulb },
37 | instructions: { variant: 'default', icon: Terminal },
38 | response_instructions: { variant: 'default', icon: Terminal },
39 | example: { variant: 'default', icon: Terminal },
40 | do_not_include: { variant: 'destructive', icon: CircleAlert },
41 | error_handling: { variant: 'destructive', icon: CircleAlert },
42 | };
43 |
44 | export const TextBlockUi = (block: TextBlock) => {
45 | if (block.type === 'text') {
46 | return (
47 | <div className="text-sm/[24px]">
48 | {block.content.map((item, index) =>
49 | item.type === 'text' ? (
50 | item.content
51 | ) : (
52 | <span key={index} className="monospaced bg-secondary p-1 py-0.25">
53 | {item.content}
54 | </span>
55 | ),
56 | )}
57 | </div>
58 | );
59 | }
60 |
61 | return <CodeSnippet type={block.syntax}>{block.content}</CodeSnippet>;
62 | };
63 |
64 | export const DescriptionItemUi = (item: DescriptionItem) => {
65 | if (item.type === 'text') {
66 | return (
67 | <div className="whitespace-pre-line">
68 | {item.content.map((childItem, index) => (
69 | <TextBlockUi key={index} {...childItem} />
70 | ))}
71 | </div>
72 | );
73 | }
74 |
75 | // If an example section contains only code snippet then render snippet
76 | // without a section wrapper. An extra wrapper makes the code less readable.
77 | if (
78 | item.type === 'example' &&
79 | item.content.length === 1 &&
80 | item.content[0].type === 'text' &&
81 | item.content[0].content.length === 1 &&
82 | item.content[0].content[0].type === 'code'
83 | ) {
84 | const snippet = item.content[0].content[0];
85 |
86 | return <CodeSnippet type={snippet.syntax}>{snippet.content}</CodeSnippet>;
87 | }
88 |
89 | const { variant, icon: IconComp } =
90 | ALERT_VARIANT_PER_DESCRIPTION_TYPE[item.type];
91 |
92 | return (
93 | <Alert variant={variant} className="my-2">
94 | <IconComp className="w-4 h-4" />
95 | <AlertTitle className="first-letter:capitalize font-semibold">
96 | {item.type.replaceAll('_', ' ')}
97 | </AlertTitle>
98 | <AlertDescription className="whitespace-pre-line">
99 | <DescriptionItemsUi description={item.content} />
100 | </AlertDescription>
101 | </Alert>
102 | );
103 | };
104 |
105 | export const DescriptionItemsUi = ({
106 | description,
107 | }: {
108 | description: DescriptionItem[];
109 | }) => (
110 | <div className="flex flex-col">
111 | {description.map((item, index) => (
112 | <DescriptionItemUi key={index} {...item} />
113 | ))}
114 | </div>
115 | );
116 |
```
--------------------------------------------------------------------------------
/landing/app/page.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import fs from 'fs/promises';
2 |
3 | import {
4 | Accordion,
5 | AccordionContent,
6 | AccordionItem,
7 | AccordionTrigger,
8 | } from '@/components/ui/accordion';
9 | import { parseDescription } from '@/lib/description';
10 | import { DescriptionItemsUi } from '@/components/DescriptionItem';
11 | import { Introduction } from '@/components/Introduction';
12 | import { Header } from '@/components/Header';
13 |
14 | type ToolDescription = {
15 | name: string;
16 | description: string;
17 | };
18 |
19 | export default async function Home() {
20 | const packageJson = await fs.readFile('../package.json', 'utf-8');
21 | const packageVersion = JSON.parse(packageJson).version as number;
22 |
23 | const toolsJson = await fs.readFile('./tools.json', 'utf-8');
24 | const rawTools = JSON.parse(toolsJson) as ToolDescription[];
25 |
26 | const tools = rawTools.map(({ description, ...data }) => ({
27 | ...data,
28 | description: parseDescription(description),
29 | }));
30 |
31 | return (
32 | <div className="flex flex-col items-center min-h-screen p-4 pb-0 sm:p-8 sm:pb-0">
33 | <main className="w-full max-w-3xl">
34 | <article className="flex flex-col gap-10">
35 | <Header packageVersion={packageVersion} />
36 | <Introduction />
37 | <div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-6">
38 | <div className="flex">
39 | <div className="ml-3">
40 | <p className="text-sm text-yellow-700">
41 | <strong>Security Notice:</strong> The Neon MCP Server grants
42 | powerful database management capabilities through natural
43 | language requests. Please review our{' '}
44 | <a
45 | href="https://neon.tech/docs/ai/neon-mcp-server#mcp-security-guidance"
46 | className="underline hover:text-yellow-800"
47 | target="_blank"
48 | rel="noopener noreferrer"
49 | >
50 | MCP security guidance
51 | </a>{' '}
52 | before using.
53 | </p>
54 | </div>
55 | </div>
56 | </div>
57 | <section id="tools">
58 | <h2 className="text-2xl font-bold mb-2 border-b-3 border-b-emerald-600">
59 | Available Tools
60 | </h2>
61 | {tools === undefined ? (
62 | <div>tools.json is not found</div>
63 | ) : (
64 | <Accordion type="multiple" asChild>
65 | <ul>
66 | {tools.map(({ name, description }) => (
67 | <AccordionItem key={name} value={name} asChild>
68 | <li key={name}>
69 | <AccordionTrigger className="flex items-center">
70 | <h3 className="monospaced text-xl font-semibold">
71 | {name}
72 | </h3>
73 | </AccordionTrigger>
74 | <AccordionContent>
75 | <DescriptionItemsUi description={description} />
76 | </AccordionContent>
77 | </li>
78 | </AccordionItem>
79 | ))}
80 | </ul>
81 | </Accordion>
82 | )}
83 | </section>
84 | </article>
85 | </main>
86 | <footer className="text-center w-full p-4 mt-10">Neon Inc. 2025</footer>
87 | </div>
88 | );
89 | }
90 |
```
--------------------------------------------------------------------------------
/src/prompts.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from 'zod';
2 | import { fetchRawGithubContent } from './resources.js';
3 | import { ToolHandlerExtraParams } from './tools/types.js';
4 | import { ClientApplication } from './utils/client-application.js';
5 |
6 | export const setupNeonAuthViteReactArgsSchema = {
7 | projectId: z
8 | .string()
9 | .optional()
10 | .describe(
11 | 'Optional Neon project ID. If not provided, the guide will help discover available projects.',
12 | ),
13 | branchId: z
14 | .string()
15 | .optional()
16 | .describe(
17 | 'Optional branch ID. If not provided, the default branch will be used.',
18 | ),
19 | databaseName: z
20 | .string()
21 | .optional()
22 | .describe(
23 | 'Optional database name. If not provided, the default database (neondb) will be used.',
24 | ),
25 | } as const;
26 |
27 | export const NEON_PROMPTS = [
28 | {
29 | name: 'setup-neon-auth',
30 | description:
31 | 'Interactive guide for setting up Neon Auth in a Vite+React project. Walks through provisioning, package installation, client setup, and UI components.',
32 | argsSchema: setupNeonAuthViteReactArgsSchema,
33 | },
34 | ] as const;
35 |
36 | function getClientSpecificInstructions(clientApplication: ClientApplication) {
37 | switch (clientApplication) {
38 | case 'cursor':
39 | return `
40 | - **For URLs:** Use the \`@Web\` tool (or \`web_search\`) to fetch the page.
41 | - **For Files:** Use \`grep\` or \`cat\` to read local files.
42 | `;
43 | case 'claude-code':
44 | case 'claude-desktop':
45 | return `
46 | - **For URLs:** Use your \`web_fetch\` tool (or \`web_search\`) to read content.
47 | - **For Files:** Use \`grep\` or \`cat\` to read local files.
48 | `;
49 | default:
50 | return `
51 | - **For URLs:** Use your web fetch tool to read content, or curl if you need to.
52 | - **For Files:** Use \`grep\` or \`cat\` to read local files.
53 | `;
54 | }
55 | }
56 |
57 | const COMMON_FOLLOW_INSTRUCTIONS = (clientApplication: ClientApplication) => `
58 |
59 | # Just-in-Time Context Protocol
60 |
61 | ## 1. ASSESSMENT & TRIGGER
62 | - **Analyze Gaps:** Before generating code, ask: "Do I have the *exact* API signatures and patterns for this specific task in my context?"
63 | - **Lazy Loading:** Do NOT read external links or docs if the current context is sufficient.
64 | - **Trigger:** IF you lack specific details AND a reference link/path exists in the rules (e.g., \`[Full Setup](url)\`), you MUST fetch that resource immediately.
65 |
66 | ## 2. STRICT LINK OBEDIENCE
67 | - **No Guessing:** If a task relates to a linked topic (e.g., "Styling"), you are PROHIBITED from inferring patterns. You must read the linked reference.
68 | - **Method:**
69 | ${getClientSpecificInstructions(clientApplication)}
70 |
71 | ## 3. EXECUTION
72 | - **State Intent:** Briefly confirm: "Fetching [Topic] details from [Source]..."
73 | - **Apply & Discard:** Extract only what is needed for the current task.
74 |
75 | ## ENFORCEMENT
76 | ⚠️ You MUST follow these rules. If you skip documentation lookup, you are breaking protocol.
77 | `;
78 |
79 | export const getPromptTemplate = async (
80 | promptName: string,
81 | extra: ToolHandlerExtraParams,
82 | args?: Record<string, string>,
83 | ): Promise<string> => {
84 | if (promptName === 'setup-neon-auth') {
85 | // Variables are available for future template interpolation
86 | void args?.projectId;
87 | void args?.branchId;
88 | void args?.databaseName;
89 |
90 | const content = await fetchRawGithubContent(
91 | '/neondatabase-labs/ai-rules/main/mcp-prompts/neon-auth-setup.md',
92 | );
93 |
94 | return `
95 | ${COMMON_FOLLOW_INSTRUCTIONS(extra.clientApplication)}
96 |
97 | ---
98 |
99 | ${content}`;
100 | }
101 |
102 | throw new Error(`Unknown prompt: ${promptName}`);
103 | };
104 |
```
--------------------------------------------------------------------------------
/landing/lib/description.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { min } from 'lodash';
2 |
3 | const POSSIBLE_TYPES = [
4 | 'use_case',
5 | 'workflow',
6 | 'important_notes',
7 | 'next_steps',
8 | 'response_instructions',
9 | 'instructions',
10 | 'example',
11 | 'do_not_include',
12 | 'error_handling',
13 | 'hint',
14 | 'hints',
15 | ] as const;
16 |
17 | export type DescriptionItemType = (typeof POSSIBLE_TYPES)[number];
18 |
19 | export type DescriptionItem =
20 | | {
21 | type: 'text';
22 | content: TextBlock[];
23 | }
24 | | {
25 | type: DescriptionItemType;
26 | content: DescriptionItem[];
27 | };
28 |
29 | export type TextBlock =
30 | | {
31 | type: 'text';
32 | content: TextSpan[];
33 | }
34 | | {
35 | type: 'code';
36 | syntax?: string;
37 | content: string;
38 | };
39 |
40 | export type TextSpan =
41 | | {
42 | type: 'text';
43 | content: string;
44 | }
45 | | {
46 | type: 'code';
47 | content: string;
48 | };
49 |
50 | function isValidType(string: string): string is DescriptionItemType {
51 | return POSSIBLE_TYPES.includes(string as DescriptionItemType);
52 | }
53 |
54 | function highlightCodeSpans(text: string): TextSpan[] {
55 | const items: TextSpan[] = [];
56 | let rest = text.trim();
57 |
58 | while (rest.length > 0) {
59 | const match = rest.match(/`([^`]*)?`/);
60 |
61 | if (!match) {
62 | items.push({
63 | type: 'text',
64 | content: rest,
65 | });
66 | break;
67 | }
68 |
69 | if ((match.index ?? 0) !== 0) {
70 | items.push({
71 | type: 'text',
72 | content: rest.substring(0, match.index),
73 | });
74 | }
75 |
76 | items.push({
77 | type: 'code',
78 | content: match[1].trim(),
79 | });
80 |
81 | rest = rest.substring((match.index ?? 0) + match[0].length);
82 | }
83 |
84 | return items;
85 | }
86 |
87 | function removeRedundantIndentation(text: string): string {
88 | const lines = text.split('\n');
89 | const minIndent = min(
90 | lines.map((line) => line.match(/^\s+/)?.[0].length ?? 0),
91 | );
92 | if (!minIndent) {
93 | return text;
94 | }
95 |
96 | return lines.map((line) => line.substring(minIndent)).join('\n');
97 | }
98 |
99 | function highlightCodeBlocks(description: string): TextBlock[] {
100 | const parts: TextBlock[] = [];
101 | let rest = description.trim();
102 |
103 | while (rest.length > 0) {
104 | const match = rest.match(/```([^\n]*?)\n(.*?)\n\s*?```/s);
105 |
106 | if (!match) {
107 | parts.push({
108 | type: 'text',
109 | content: highlightCodeSpans(rest),
110 | });
111 | break;
112 | }
113 |
114 | if ((match.index ?? 0) > 0) {
115 | parts.push({
116 | type: 'text',
117 | content: highlightCodeSpans(rest.slice(0, match.index).trim()),
118 | });
119 | }
120 |
121 | parts.push({
122 | type: 'code',
123 | syntax: match[1].trim() || undefined,
124 | content: removeRedundantIndentation(match[2]),
125 | });
126 |
127 | rest = rest.substring((match.index ?? 0) + match[0].length).trim();
128 | }
129 |
130 | return parts;
131 | }
132 |
133 | export function parseDescription(description: string): DescriptionItem[] {
134 | const parts: DescriptionItem[] = [];
135 | let rest = description.trim();
136 |
137 | while (rest.length > 0) {
138 | const match = rest.match(
139 | /<(use_case|workflow|important_notes|next_steps|response_instructions|instructions|example|do_not_include|error_handling|hints?)>(.*?)<\/\1>/s,
140 | );
141 |
142 | if (!match) {
143 | parts.push({
144 | type: 'text',
145 | content: highlightCodeBlocks(rest),
146 | });
147 | break;
148 | }
149 |
150 | const type = match[1];
151 |
152 | if (!isValidType(type)) {
153 | throw new Error('Invalid type');
154 | }
155 |
156 | if ((match.index ?? 0) > 0) {
157 | parts.push({
158 | type: 'text',
159 | content: highlightCodeBlocks(rest.slice(0, match.index).trim()),
160 | });
161 | }
162 |
163 | parts.push({
164 | type,
165 | content: parseDescription(match[2].trim()),
166 | });
167 |
168 | rest = rest.substring((match.index ?? 0) + match[0].length).trim();
169 | }
170 |
171 | return parts;
172 | }
173 |
```
--------------------------------------------------------------------------------
/src/initConfig.ts:
--------------------------------------------------------------------------------
```typescript
1 | import path from 'node:path';
2 | import os from 'node:os';
3 | import fs from 'node:fs';
4 | import chalk from 'chalk';
5 | import { fileURLToPath } from 'url';
6 | import { logger } from './utils/logger.js';
7 |
8 | const __dirname = path.dirname(fileURLToPath(import.meta.url));
9 | const packageJson = JSON.parse(
10 | fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'),
11 | );
12 | // Determine Claude config path based on OS platform
13 | let claudeConfigPath: string;
14 | const platform = os.platform();
15 |
16 | if (platform === 'win32') {
17 | // Windows path - using %APPDATA%
18 | // For Node.js, we access %APPDATA% via process.env.APPDATA
19 | claudeConfigPath = path.join(
20 | process.env.APPDATA || '',
21 | 'Claude',
22 | 'claude_desktop_config.json',
23 | );
24 | } else {
25 | // macOS and Linux path (according to official docs)
26 | claudeConfigPath = path.join(
27 | os.homedir(),
28 | 'Library',
29 | 'Application Support',
30 | 'Claude',
31 | 'claude_desktop_config.json',
32 | );
33 | }
34 |
35 | const MCP_NEON_SERVER = 'neon';
36 |
37 | type Args =
38 | | {
39 | command: 'start:sse';
40 | analytics: boolean;
41 | }
42 | | {
43 | command: 'start';
44 | neonApiKey: string;
45 | analytics: boolean;
46 | }
47 | | {
48 | command: 'init';
49 | executablePath: string;
50 | neonApiKey: string;
51 | analytics: boolean;
52 | }
53 | | {
54 | command: 'export-tools';
55 | };
56 |
57 | const commands = ['init', 'start', 'start:sse', 'export-tools'] as const;
58 |
59 | export const parseArgs = (): Args => {
60 | const args = process.argv;
61 |
62 | if (args.length < 3) {
63 | logger.error('Invalid number of arguments');
64 | process.exit(1);
65 | }
66 |
67 | if (args.length === 3 && args[2] === 'start:sse') {
68 | return {
69 | command: 'start:sse',
70 | analytics: true,
71 | };
72 | }
73 |
74 | if (args.length === 3 && args[2] === 'export-tools') {
75 | return {
76 | command: 'export-tools',
77 | };
78 | }
79 |
80 | const command = args[2];
81 |
82 | if (!commands.includes(command as (typeof commands)[number])) {
83 | logger.error(`Invalid command: ${command}`);
84 | process.exit(1);
85 | }
86 |
87 | if (command === 'export-tools') {
88 | return {
89 | command: 'export-tools',
90 | };
91 | }
92 |
93 | if (args.length < 4) {
94 | logger.error(
95 | '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',
96 | );
97 | process.exit(1);
98 | }
99 |
100 | return {
101 | executablePath: args[1],
102 | command: args[2] as 'start' | 'init',
103 | neonApiKey: args[3],
104 | analytics: !args[4]?.includes('no-analytics'),
105 | };
106 | };
107 |
108 | export function handleInit({
109 | executablePath,
110 | neonApiKey,
111 | analytics,
112 | }: {
113 | executablePath: string;
114 | neonApiKey: string;
115 | analytics: boolean;
116 | }) {
117 | // If the executable path is a local path to the dist/index.js file, use it directly
118 | // Otherwise, use the name of the package to always load the latest version from remote
119 | const serverPath = executablePath.includes('dist/index.js')
120 | ? executablePath
121 | : packageJson.name;
122 |
123 | const neonConfig = {
124 | command: 'npx',
125 | args: [
126 | '-y',
127 | serverPath,
128 | 'start',
129 | neonApiKey,
130 | analytics ? '' : '--no-analytics',
131 | ],
132 | };
133 |
134 | const configDir = path.dirname(claudeConfigPath);
135 | if (!fs.existsSync(configDir)) {
136 | console.log(chalk.blue('Creating Claude config directory...'));
137 | fs.mkdirSync(configDir, { recursive: true });
138 | }
139 |
140 | const existingConfig = fs.existsSync(claudeConfigPath)
141 | ? JSON.parse(fs.readFileSync(claudeConfigPath, 'utf8'))
142 | : { mcpServers: {} };
143 |
144 | if (MCP_NEON_SERVER in (existingConfig?.mcpServers || {})) {
145 | console.log(chalk.yellow('Replacing existing Neon MCP config...'));
146 | }
147 |
148 | const newConfig = {
149 | ...existingConfig,
150 | mcpServers: {
151 | ...existingConfig.mcpServers,
152 | [MCP_NEON_SERVER]: neonConfig,
153 | },
154 | };
155 |
156 | fs.writeFileSync(claudeConfigPath, JSON.stringify(newConfig, null, 2));
157 | console.log(chalk.green(`Config written to: ${claudeConfigPath}`));
158 | console.log(
159 | chalk.blue(
160 | 'The Neon MCP server will start automatically the next time you open Claude.',
161 | ),
162 | );
163 | }
164 |
```