This is page 1 of 2. 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
│ ├── koyeb-preview.yml
│ ├── koyeb-prod.yml
│ └── pr.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .prettierignore
├── .prettierrc
├── bun.lock
├── CHANGELOG.md
├── Dockerfile
├── eslint.config.js
├── landing
│ ├── .gitignore
│ ├── app
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── components
│ │ ├── CodeSnippet.tsx
│ │ ├── CopyableUrl.tsx
│ │ ├── DescriptionItem.tsx
│ │ ├── ExternalIcon.tsx
│ │ ├── ExternalLink.tsx
│ │ ├── Header.tsx
│ │ ├── Introduction.tsx
│ │ ├── ThemeProvider.tsx
│ │ └── ui
│ │ ├── accordion.tsx
│ │ ├── alert.tsx
│ │ └── button.tsx
│ ├── components.json
│ ├── eslint.config.mjs
│ ├── icons
│ │ ├── github.svg
│ │ └── neon.svg
│ ├── lib
│ │ ├── description.ts
│ │ └── utils.ts
│ ├── next.config.ts
│ ├── package-lock.json
│ ├── package.json
│ ├── postcss.config.mjs
│ ├── public
│ │ └── favicon.ico
│ ├── README.md
│ └── tsconfig.json
├── LICENSE
├── mcp-client
│ ├── .env.example
│ ├── package-lock.json
│ ├── package.json
│ ├── README.md
│ ├── src
│ │ ├── bin.ts
│ │ ├── cli-client.ts
│ │ ├── index.ts
│ │ ├── logger.ts
│ │ └── neon-cli-client.ts
│ └── tsconfig.json
├── package-lock.json
├── package.json
├── public
│ └── logo.png
├── PUBLISH.md
├── README.md
├── remote.Dockerfile
├── scripts
│ └── before-publish.ts
├── src
│ ├── analytics
│ │ └── analytics.ts
│ ├── constants.ts
│ ├── describeUtils.ts
│ ├── index.ts
│ ├── initConfig.ts
│ ├── oauth
│ │ ├── client.ts
│ │ ├── cookies.ts
│ │ ├── kv-store.ts
│ │ ├── model.ts
│ │ ├── server.ts
│ │ └── utils.ts
│ ├── resources.ts
│ ├── sentry
│ │ ├── instrument.ts
│ │ └── utils.ts
│ ├── server
│ │ ├── api.ts
│ │ ├── errors.ts
│ │ └── index.ts
│ ├── tools
│ │ ├── definitions.ts
│ │ ├── handlers
│ │ │ └── neon-auth.ts
│ │ ├── index.ts
│ │ ├── state.ts
│ │ ├── tools.ts
│ │ ├── toolsSchema.ts
│ │ ├── types.ts
│ │ └── utils.ts
│ ├── tools-evaluations
│ │ ├── evalUtils.ts
│ │ └── prepare-database-migration.eval.ts
│ ├── transports
│ │ ├── sse-express.ts
│ │ ├── stdio.ts
│ │ └── stream.ts
│ ├── types
│ │ ├── auth.ts
│ │ ├── context.ts
│ │ └── express.d.ts
│ ├── utils
│ │ ├── logger.ts
│ │ └── polyfills.ts
│ └── views
│ ├── approval-dialog.pug
│ └── styles.css
├── tsconfig.json
└── tsconfig.test.json
```
# Files
--------------------------------------------------------------------------------
/.bun-version:
--------------------------------------------------------------------------------
```
1 | 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 API key in the `Authorization` header if your client supports it
82 |
83 | ```json
84 | {
85 | "mcpServers": {
86 | "Neon": {
87 | "url": "https://mcp.neon.tech/mcp",
88 | "headers": {
89 | "Authorization": "Bearer <$NEON_API_KEY>"
90 | }
91 | }
92 | }
93 | }
94 | ```
95 |
96 | > Provider organization's API key to limit access to projects under the organization only.
97 |
98 | 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.
99 |
100 | ### Option 2. Local MCP Server
101 |
102 | 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.
103 |
104 | 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:
105 |
106 | ```json
107 | {
108 | "mcpServers": {
109 | "neon": {
110 | "command": "npx",
111 | "args": [
112 | "-y",
113 | "@neondatabase/mcp-server-neon",
114 | "start",
115 | "<YOUR_NEON_API_KEY>"
116 | ]
117 | }
118 | }
119 | }
120 | ```
121 |
122 | ### Troubleshooting
123 |
124 | 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:
125 |
126 | ```bash
127 | npx -y @neondatabase/mcp-server-neon start <YOUR_NEON_API_KEY>
128 | ```
129 |
130 | #### Troubleshooting on Windows
131 |
132 | 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:
133 |
134 | ```json
135 | {
136 | "mcpServers": {
137 | "neon": {
138 | "command": "cmd",
139 | "args": [
140 | "/c",
141 | "npx",
142 | "-y",
143 | "@neondatabase/mcp-server-neon",
144 | "start",
145 | "<YOUR_NEON_API_KEY>"
146 | ]
147 | }
148 | }
149 | }
150 | ```
151 |
152 | ```json
153 | {
154 | "mcpServers": {
155 | "neon": {
156 | "command": "wsl",
157 | "args": [
158 | "npx",
159 | "-y",
160 | "@neondatabase/mcp-server-neon",
161 | "start",
162 | "<YOUR_NEON_API_KEY>"
163 | ]
164 | }
165 | }
166 | }
167 | ```
168 |
169 | ## Guides
170 |
171 | - [Neon MCP Server Guide](https://neon.tech/docs/ai/neon-mcp-server)
172 | - [Connect MCP Clients to Neon](https://neon.tech/docs/ai/connect-mcp-clients-to-neon)
173 | - [Cursor with Neon MCP Server](https://neon.tech/guides/cursor-mcp-neon)
174 | - [Claude Desktop with Neon MCP Server](https://neon.tech/guides/neon-mcp-server)
175 | - [Cline with Neon MCP Server](https://neon.tech/guides/cline-mcp-neon)
176 | - [Windsurf with Neon MCP Server](https://neon.tech/guides/windsurf-mcp-neon)
177 | - [Zed with Neon MCP Server](https://neon.tech/guides/zed-mcp-neon)
178 |
179 | # Features
180 |
181 | ## Supported Tools
182 |
183 | 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.
184 |
185 | **Project Management:**
186 |
187 | - **`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.
188 | - **`list_shared_projects`**: Lists Neon projects shared with the current user. Supports a search parameter and limiting the number of projects returned (default: 10).
189 | - **`describe_project`**: Fetches detailed information about a specific Neon project, including its ID, name, and associated branches and databases.
190 | - **`create_project`**: Creates a new Neon project in your Neon account. A project acts as a container for branches, databases, roles, and computes.
191 | - **`delete_project`**: Deletes an existing Neon project and all its associated resources.
192 |
193 | **Branch Management:**
194 |
195 | - **`create_branch`**: Creates a new branch within a specified Neon project. Leverages [Neon's branching](/docs/introduction/branching) feature for development, testing, or migrations.
196 | - **`delete_branch`**: Deletes an existing branch from a Neon project.
197 | - **`describe_branch`**: Retrieves details about a specific branch, such as its name, ID, and parent branch.
198 | - **`list_branch_computes`**: Lists compute endpoints for a project or specific branch, including compute ID, type, size, and autoscaling information.
199 | - **`list_organizations`**: Lists all organizations that the current user has access to. Optionally filter by organization name or ID using the search parameter.
200 | - **`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.
201 | - **`compare_database_schema`**: Shows the schema diff between the child branch and its parent
202 |
203 | **SQL Query Execution:**
204 |
205 | - **`get_connection_string`**: Returns your database connection string.
206 | - **`run_sql`**: Executes a single SQL query against a specified Neon database. Supports both read and write operations.
207 | - **`run_sql_transaction`**: Executes a series of SQL queries within a single transaction against a Neon database.
208 | - **`get_database_tables`**: Lists all tables within a specified Neon database.
209 | - **`describe_table_schema`**: Retrieves the schema definition of a specific table, detailing columns, data types, and constraints.
210 | - **`list_slow_queries`**: Identifies performance bottlenecks by finding the slowest queries in a database. Requires the pg_stat_statements extension.
211 |
212 | **Database Migrations (Schema Changes):**
213 |
214 | - **`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.
215 | - **`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.
216 |
217 | **Query Performance Optimization:**
218 |
219 | - **`explain_sql_statement`**: Provides detailed execution plans for SQL queries to help identify performance bottlenecks.
220 | - **`prepare_query_tuning`**: Analyzes query performance and suggests optimizations like index creation. Creates a temporary branch for safely testing these optimizations.
221 | - **`complete_query_tuning`**: Applies or discards query optimizations after testing. Can merge changes from the temporary branch to the main branch.
222 | - **`list_slow_queries`**: Identifies and analyzes slow-performing queries in your database. Requires the `pg_stat_statements` extension.
223 |
224 | **Compute Management:**
225 |
226 | - **`list_branch_computes`**: Lists compute endpoints for a project or specific branch, showing details like compute ID, type, size, and last active time.
227 |
228 | **Neon Auth:**
229 |
230 | - **`provision_neon_auth`**: Provisions Neon Auth for a Neon project. It allows developers to easily set up authentication infrastructure by creating an integration with Stack Auth (`@stackframe/stack`).
231 |
232 | **Query Performance Tuning:**
233 |
234 | - **`explain_sql_statement`**: Analyzes a SQL query and returns detailed execution plan information to help understand query performance.
235 | - **`prepare_query_tuning`**: Identifies potential performance issues in a SQL query and suggests optimizations. Creates a temporary branch for testing improvements.
236 | - **`complete_query_tuning`**: Finalizes and applies query optimizations after testing. Merges changes from the temporary tuning branch to the main branch.
237 |
238 | ## Migrations
239 |
240 | 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.
241 |
242 | 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.
243 |
244 | # Development
245 |
246 | ## Development with MCP CLI Client
247 |
248 | The easiest way to iterate on the MCP Server is using the `mcp-client/`. Learn more in `mcp-client/README.md`.
249 |
250 | ```bash
251 | npm install
252 | npm run build
253 | npm run watch # You can keep this open.
254 | cd mcp-client/ && NEON_API_KEY=... npm run start:mcp-server-neon
255 | ```
256 |
257 | ## Development with Claude Desktop (Local MCP Server)
258 |
259 | ```bash
260 | npm install
261 | npm run build
262 | npm run watch # You can keep this open.
263 | node dist/index.js init $NEON_API_KEY
264 | ```
265 |
266 | Then, **restart Claude** each time you want to test changes.
267 |
268 | # Testing
269 |
270 | To run the tests you need to setup the `.env` file according to the `.env.example` file.
271 |
272 | ```bash
273 | npm run test
274 | ```
275 |
```
--------------------------------------------------------------------------------
/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/auth.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
2 |
3 | export type AuthContext = {
4 | extra: {
5 | account: {
6 | id: string;
7 | name: string;
8 | email?: string;
9 | isOrg?: boolean; // For STDIO mode with org API key
10 | };
11 | client?: {
12 | id: string;
13 | name: string;
14 | };
15 | [key: string]: unknown;
16 | };
17 | } & AuthInfo;
18 |
```
--------------------------------------------------------------------------------
/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/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 | };
17 |
```
--------------------------------------------------------------------------------
/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/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/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 |
```
--------------------------------------------------------------------------------
/.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: ubuntu-latest
11 | steps:
12 | - name: Checkout code
13 | uses: actions/checkout@v4
14 | - name: Setup Node
15 | uses: actions/setup-node@v4
16 | with:
17 | node-version-file: .nvmrc
18 | - name: Setup Bun
19 | uses: oven-sh/setup-bun@v2
20 | with:
21 | bun-version-file: .bun-version
22 | - name: Install root dependencies
23 | run: bun install --frozen-lockfile
24 | - name: Install landing dependencies
25 | working-directory: landing
26 | run: bun install --frozen-lockfile
27 | - name: Lint
28 | run: bun run lint
29 | - name: Build
30 | run: bun run build
31 |
```
--------------------------------------------------------------------------------
/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/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/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 |
7 | // Extract the tool names as a union type
8 | type NeonToolName = (typeof NEON_TOOLS)[number]['name'];
9 | export type ToolParams<T extends NeonToolName> = Extract<
10 | (typeof NEON_TOOLS)[number],
11 | { name: T }
12 | >['inputSchema'];
13 |
14 | export type ToolHandler<T extends NeonToolName> = ToolCallback<{
15 | params: ToolParams<T>;
16 | }>;
17 |
18 | export type ToolHandlerExtraParams = Parameters<
19 | ToolHandler<NeonToolName>
20 | >['1'] & { account: AuthContext['extra']['account'] };
21 |
22 | export type ToolHandlerExtended<T extends NeonToolName> = (
23 | ...args: [
24 | args: Parameters<ToolHandler<T>>['0'],
25 | neonClient: Api<unknown>,
26 | extra: ToolHandlerExtraParams,
27 | ]
28 | ) => ReturnType<ToolHandler<T>>;
29 |
30 | // Create a type for the tool handlers that directly maps each tool to its appropriate input schema
31 | export type ToolHandlers = {
32 | [K in NeonToolName]: ToolHandlerExtended<K>;
33 | };
34 |
```
--------------------------------------------------------------------------------
/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 |
```
--------------------------------------------------------------------------------
/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 |
```
--------------------------------------------------------------------------------
/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 |
```
--------------------------------------------------------------------------------
/.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: ubuntu-latest
15 | # Only main branch is allowed to deploy to production
16 | if: github.ref == 'refs/heads/main'
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_PROD_TOKEN }}'
24 | - name: Build and deploy to Koyeb production
25 | run: |
26 | koyeb deploy . platform-${{ github.ref_name }}/main \
27 | --instance-type medium \
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.PROD_CLIENT_ID}} \
37 | --env CLIENT_SECRET=${{secrets.PROD_CLIENT_SECRET}} \
38 | --env OAUTH_DATABASE_URL=${{secrets.PROD_OAUTH_DATABASE_URL}} \
39 | --env SERVER_HOST=${{vars.PROD_SERVER_HOST}} \
40 | --env NEON_API_HOST=${{vars.PROD_NEON_API_HOST}} \
41 | --env UPSTREAM_OAUTH_HOST=${{vars.PROD_OAUTH_HOST}} \
42 | --env COOKIE_SECRET=${{secrets.COOKIE_SECRET}} \
43 |
```
--------------------------------------------------------------------------------
/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 |
```
--------------------------------------------------------------------------------
/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 | </div>
38 | );
39 |
```
--------------------------------------------------------------------------------
/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 |
```
--------------------------------------------------------------------------------
/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/install-mcp?name=Neon&config=eyJ1cmwiOiJodHRwczovL21jcC5uZW9uLnRlY2gvbWNwIn0%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 |
```
--------------------------------------------------------------------------------
/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 |
```
--------------------------------------------------------------------------------
/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 |
```
--------------------------------------------------------------------------------
/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 |
```
--------------------------------------------------------------------------------
/src/resources.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ReadResourceCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
2 | import { Resource } from '@modelcontextprotocol/sdk/types.js';
3 |
4 | async function fetchRawGithubContent(rawPath: string) {
5 | const path = rawPath.replace('/blob', '');
6 |
7 | return fetch(`https://raw.githubusercontent.com${path}`).then((res) =>
8 | res.text(),
9 | );
10 | }
11 |
12 | export const NEON_RESOURCES = [
13 | {
14 | name: 'neon-auth',
15 | uri: 'https://github.com/neondatabase-labs/ai-rules/blob/main/neon-auth.mdc',
16 | mimeType: 'text/plain',
17 | description: 'Neon Auth usage instructions',
18 | handler: async (url) => {
19 | const uri = url.host;
20 | const rawPath = url.pathname;
21 | const content = await fetchRawGithubContent(rawPath);
22 | return {
23 | contents: [
24 | {
25 | uri: uri,
26 | mimeType: 'text/plain',
27 | text: content,
28 | },
29 | ],
30 | };
31 | },
32 | },
33 | {
34 | name: 'neon-serverless',
35 | uri: 'https://github.com/neondatabase-labs/ai-rules/blob/main/neon-serverless.mdc',
36 | mimeType: 'text/plain',
37 | description: 'Neon Serverless usage instructions',
38 | handler: async (url) => {
39 | const uri = url.host;
40 | const rawPath = url.pathname;
41 | const content = await fetchRawGithubContent(rawPath);
42 | return {
43 | contents: [
44 | {
45 | uri,
46 | mimeType: 'text/plain',
47 | text: content,
48 | },
49 | ],
50 | };
51 | },
52 | },
53 | {
54 | name: 'neon-drizzle',
55 | uri: 'https://github.com/neondatabase-labs/ai-rules/blob/main/neon-drizzle.mdc',
56 | mimeType: 'text/plain',
57 | description: 'Neon Drizzle usage instructions',
58 | handler: async (url) => {
59 | const uri = url.host;
60 | const rawPath = url.pathname;
61 | const content = await fetchRawGithubContent(rawPath);
62 | return {
63 | contents: [
64 | {
65 | uri,
66 | mimeType: 'text/plain',
67 | text: content,
68 | },
69 | ],
70 | };
71 | },
72 | },
73 | ] satisfies (Resource & { handler: ReadResourceCallback })[];
74 |
```
--------------------------------------------------------------------------------
/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 |
```
--------------------------------------------------------------------------------
/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/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 | });
26 |
27 | const transport = new StreamableHTTPServerTransport({
28 | sessionIdGenerator: undefined,
29 | });
30 | res.on('close', () => {
31 | void transport.close();
32 | void server.close();
33 | });
34 | await server.connect(transport);
35 | await transport.handleRequest(req, res, req.body);
36 | } catch (error: unknown) {
37 | logger.error('Failed to connect to MCP server:', {
38 | message: error instanceof Error ? error.message : 'Unknown error',
39 | error,
40 | });
41 | track({
42 | userId: auth.extra.account.id,
43 | event: 'stream_connection_errored',
44 | properties: { error },
45 | context: {
46 | app: appContext,
47 | client: auth.extra.client,
48 | },
49 | });
50 | if (!res.headersSent) {
51 | res.status(500).json({
52 | jsonrpc: '2.0',
53 | error: {
54 | code: -32603,
55 | message: 'Internal server error',
56 | },
57 | id: null,
58 | });
59 | }
60 | }
61 | });
62 |
63 | router.get('/', requiresAuth(), (req: Request, res: Response) => {
64 | logger.info('Received GET MCP request');
65 | res.writeHead(405).end(
66 | JSON.stringify({
67 | jsonrpc: '2.0',
68 | error: {
69 | code: -32000,
70 | message: 'Method not allowed.',
71 | },
72 | id: null,
73 | }),
74 | );
75 | });
76 |
77 | router.delete('/', requiresAuth(), (req: Request, res: Response) => {
78 | logger.info('Received DELETE MCP request');
79 | res.writeHead(405).end(
80 | JSON.stringify({
81 | jsonrpc: '2.0',
82 | error: {
83 | code: -32000,
84 | message: 'Method not allowed.',
85 | },
86 | id: null,
87 | }),
88 | );
89 | });
90 |
91 | return router;
92 | };
93 |
```
--------------------------------------------------------------------------------
/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 |
```
--------------------------------------------------------------------------------
/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.0.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.11.0",
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 | }
89 |
```
--------------------------------------------------------------------------------
/.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: ubuntu-latest
14 | if: contains(github.event.pull_request.labels.*.name, 'deploy-preview')
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v4
18 | - name: Install and configure the Koyeb CLI
19 | uses: koyeb-community/koyeb-actions@v2
20 | with:
21 | api_token: '${{ secrets.KOYEB_PREVIEW_TOKEN }}'
22 | - name: Build and deploy to Koyeb preview
23 | run: |
24 | koyeb deploy . platform-koyeb-preview/main \
25 | --instance-type nano \
26 | --region was \
27 | --archive-builder docker \
28 | --archive-docker-dockerfile remote.Dockerfile \
29 | --privileged \
30 | --type web \
31 | --port 3001:http \
32 | --route /:3001 \
33 | --wait \
34 | --env CLIENT_ID=${{secrets.CLIENT_ID}} \
35 | --env CLIENT_SECRET=${{secrets.CLIENT_SECRET}} \
36 | --env OAUTH_DATABASE_URL=${{secrets.PREVIEW_OAUTH_DATABASE_URL}} \
37 | --env SERVER_HOST=${{vars.KOYEB_PREVIEW_SERVER_HOST}} \
38 | --env NEON_API_HOST=${{vars.NEON_API_HOST_STAGING}} \
39 | --env UPSTREAM_OAUTH_HOST=${{vars.OAUTH_HOST_STAGING}} \
40 | --env COOKIE_SECRET=${{secrets.COOKIE_SECRET}} \
41 |
42 | - name: Comment on PR with deployment URL
43 | if: ${{ github.event_name == 'pull_request' && success() }}
44 | uses: actions/github-script@v7
45 | with:
46 | github-token: ${{ secrets.GITHUB_TOKEN }}
47 | script: |
48 | // GitHub bot id taken from (https://api.github.com/users/github-actions[bot])
49 | const githubActionsBotId = 41898282
50 |
51 | const ownerRepoParams = {
52 | owner: context.repo.owner,
53 | repo: context.repo.repo,
54 | }
55 |
56 | // Hidden start marker for the comment
57 | const startMarker = '<!-- Preview Deployment Comment-->';
58 | const body = `${startMarker}
59 | 🚀 Preview deployment ready: [https://preview-mcp.neon.tech](https://preview-mcp.neon.tech)`;
60 |
61 | const comments = await github.paginate(github.rest.issues.listComments, {
62 | ...ownerRepoParams,
63 | issue_number: context.issue.number,
64 | });
65 |
66 | // Delete previous comments regarding preview deployments.
67 | for (comment of comments.filter(comment => comment.user.id === githubActionsBotId && comment.body.startsWith(startMarker))) {
68 | await github.rest.issues.deleteComment({
69 | comment_id: comment.id,
70 | ...ownerRepoParams,
71 | })
72 | }
73 |
74 | await github.rest.issues.createComment({
75 | ...ownerRepoParams,
76 | issue_number: context.issue.number,
77 | body
78 | });
79 |
```
--------------------------------------------------------------------------------
/src/tools/handlers/neon-auth.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2 |
3 | import { Api, NeonAuthSupportedAuthProvider } from '@neondatabase/api-client';
4 | import { provisionNeonAuthInputSchema } from '../toolsSchema.js';
5 | import { z } from 'zod';
6 | import { getDefaultDatabase } from '../utils.js';
7 |
8 | type Props = z.infer<typeof provisionNeonAuthInputSchema>;
9 | export async function handleProvisionNeonAuth(
10 | { projectId, database }: Props,
11 | neonClient: Api<unknown>,
12 | ): Promise<CallToolResult> {
13 | const {
14 | data: { branches },
15 | } = await neonClient.listProjectBranches({
16 | projectId,
17 | });
18 | const defaultBranch =
19 | branches.find((branch) => branch.default) ?? branches[0];
20 | if (!defaultBranch) {
21 | return {
22 | isError: true,
23 | content: [
24 | {
25 | type: 'text',
26 | text: 'The project has no default branch. Neon Auth can only be provisioned with a default branch.',
27 | },
28 | ],
29 | };
30 | }
31 | const defaultDatabase = await getDefaultDatabase(
32 | {
33 | projectId,
34 | branchId: defaultBranch.id,
35 | databaseName: database,
36 | },
37 | neonClient,
38 | );
39 |
40 | if (!defaultDatabase) {
41 | return {
42 | isError: true,
43 | content: [
44 | {
45 | type: 'text',
46 | text: `The project has no database named '${database}'.`,
47 | },
48 | ],
49 | };
50 | }
51 |
52 | const response = await neonClient.createNeonAuthIntegration({
53 | auth_provider: NeonAuthSupportedAuthProvider.Stack,
54 | project_id: projectId,
55 | branch_id: defaultBranch.id,
56 | database_name: defaultDatabase.name,
57 | role_name: defaultDatabase.owner_name,
58 | });
59 |
60 | // In case of 409, it means that the integration already exists
61 | // We should not return an error, but a message that the integration already exists and fetch the existing integration
62 | if (response.status === 409) {
63 | return {
64 | content: [
65 | {
66 | type: 'text',
67 | text: 'Neon Auth already provisioned.',
68 | },
69 | ],
70 | };
71 | }
72 |
73 | if (response.status !== 201) {
74 | return {
75 | isError: true,
76 | content: [
77 | {
78 | type: 'text',
79 | text: `Failed to provision Neon Auth. Error: ${response.statusText}`,
80 | },
81 | ],
82 | };
83 | }
84 |
85 | return {
86 | content: [
87 | {
88 | type: 'text',
89 | text: `Authentication has been successfully provisioned for your Neon project. Following are the environment variables you need to set in your project:
90 | \`\`\`
91 | NEXT_PUBLIC_STACK_PROJECT_ID='${response.data.auth_provider_project_id}'
92 | NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY='${response.data.pub_client_key}'
93 | STACK_SECRET_SERVER_KEY='${response.data.secret_server_key}'
94 | \`\`\`
95 |
96 | Copy the above environment variables and place them in your \`.env.local\` file for Next.js project. Note that variables with \`NEXT_PUBLIC_\` prefix will be available in the client side.
97 | `,
98 | },
99 | {
100 | type: 'text',
101 | text: `
102 | Use Following JWKS URL to retrieve the public key to verify the JSON Web Tokens (JWT) issued by authentication provider:
103 | \`\`\`
104 | ${response.data.jwks_url}
105 | \`\`\`
106 | `,
107 | },
108 | ],
109 | };
110 | }
111 |
```
--------------------------------------------------------------------------------
/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 |
```
--------------------------------------------------------------------------------
/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/server/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4 | import { NEON_RESOURCES } from '../resources.js';
5 | import {
6 | NEON_HANDLERS,
7 | NEON_TOOLS,
8 | ToolHandlerExtended,
9 | } from '../tools/index.js';
10 | import { logger } from '../utils/logger.js';
11 | import { createNeonClient, getPackageJson } from './api.js';
12 | import { track } from '../analytics/analytics.js';
13 | import { captureException, startSpan } from '@sentry/node';
14 | import { ServerContext } from '../types/context.js';
15 | import { setSentryTags } from '../sentry/utils.js';
16 | import { ToolHandlerExtraParams } from '../tools/types.js';
17 | import { handleToolError } from './errors.js';
18 |
19 | export const createMcpServer = (context: ServerContext) => {
20 | const server = new McpServer(
21 | {
22 | name: 'mcp-server-neon',
23 | version: getPackageJson().version,
24 | },
25 | {
26 | capabilities: {
27 | tools: {},
28 | resources: {},
29 | },
30 | },
31 | );
32 |
33 | const neonClient = createNeonClient(context.apiKey);
34 |
35 | // Register tools
36 | NEON_TOOLS.forEach((tool) => {
37 | const handler = NEON_HANDLERS[tool.name];
38 | if (!handler) {
39 | throw new Error(`Handler for tool ${tool.name} not found`);
40 | }
41 |
42 | const toolHandler = handler as ToolHandlerExtended<typeof tool.name>;
43 |
44 | server.tool(
45 | tool.name,
46 | tool.description,
47 | { params: tool.inputSchema },
48 | async (args, extra) => {
49 | return await startSpan(
50 | {
51 | name: 'tool_call',
52 | attributes: {
53 | tool_name: tool.name,
54 | },
55 | },
56 | async (span) => {
57 | const properties = { tool_name: tool.name };
58 | logger.info('tool call:', properties);
59 | setSentryTags(context);
60 | track({
61 | userId: context.account.id,
62 | event: 'tool_call',
63 | properties,
64 | context: { client: context.client, app: context.app },
65 | });
66 | const extraArgs: ToolHandlerExtraParams = {
67 | ...extra,
68 | account: context.account,
69 | };
70 | try {
71 | return await toolHandler(args, neonClient, extraArgs);
72 | } catch (error) {
73 | span.setStatus({
74 | code: 2,
75 | });
76 | return handleToolError(error, properties);
77 | }
78 | },
79 | );
80 | },
81 | );
82 | });
83 |
84 | // Register resources
85 | NEON_RESOURCES.forEach((resource) => {
86 | server.resource(
87 | resource.name,
88 | resource.uri,
89 | {
90 | description: resource.description,
91 | mimeType: resource.mimeType,
92 | },
93 | async (url) => {
94 | const properties = { resource_name: resource.name };
95 | logger.info('resource call:', properties);
96 | setSentryTags(context);
97 | track({
98 | userId: context.account.id,
99 | event: 'resource_call',
100 | properties,
101 | context: { client: context.client, app: context.app },
102 | });
103 | try {
104 | return await resource.handler(url);
105 | } catch (error) {
106 | captureException(error, {
107 | extra: properties,
108 | });
109 | throw error;
110 | }
111 | },
112 | );
113 | });
114 |
115 | server.server.onerror = (error: unknown) => {
116 | const message = error instanceof Error ? error.message : 'Unknown error';
117 | logger.error('Server error:', {
118 | message,
119 | error,
120 | });
121 | const contexts = { app: context.app, client: context.client };
122 | const eventId = captureException(error, {
123 | user: { id: context.account.id },
124 | contexts: contexts,
125 | });
126 | track({
127 | userId: context.account.id,
128 | event: 'server_error',
129 | properties: { message, error, eventId },
130 | context: contexts,
131 | });
132 | };
133 |
134 | return server;
135 | };
136 |
```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
1 | # Changelog
2 |
3 | # [Unreleased]
4 |
5 | - Feat: `list_shared_projects` tool to fetch projects that user has permissions to collaborate on
6 | - Feat: `reset_from_parent` tool to reset a branch from its parent's current state
7 | - Feat: `compare_database_schema` tool to compare the schema from the child branch and its parent
8 |
9 | # [0.6.4] 2025-08-22
10 |
11 | - Fix: Do not log user sensitive information on errors
12 | - Fix: Return non-500 errors as valid response with `isError=true` without logging
13 | - Improvement: Custom error handling user generated erorrs
14 | - Improvement: Extend org-only users search to support orgs not managed by console.
15 |
16 | # [0.6.3] 2025-08-04
17 |
18 | - Feat: A new tool to list authenitcated user's organizations - `list_organizations`
19 | - Docs: Switch configs to use streamable HTTP by default
20 | - Impr: While searching for project in `list_projects` tool, extend the search to all organizations.
21 |
22 | ## [0.6.2] 2025-07-17
23 |
24 | - Add warnings on security risks involved in MCP tools in production environments
25 | - Migrate the deployment to Koyeb
26 | - Mark `param` as required argument for all tools
27 |
28 | ## [0.6.1] 2025-06-19
29 |
30 | - Documentation: Updated README with new tools and features
31 | - Support API key authentication for remote server
32 |
33 | ## [0.6.0] 2025-06-16
34 |
35 | - Fix: Issue with ORG API keys in local mode
36 | - Refc: Tools into smaller manageable modules
37 | - Feat: New landing page with details of supported tools
38 | - Feat: Streamable HTTP support
39 |
40 | ## [0.5.0] 2025-05-28
41 |
42 | - Tracking tool calls and errors with Segment
43 | - Capture exections with Sentry
44 | - Add tracing with sentry
45 | - Support new org-only accounts
46 |
47 | ## [0.4.1] - 2025-05-08
48 |
49 | - fix the `npx start` command to start server in stdio transport mode
50 | - fix issue with unexpected tokens in stdio transport mode
51 |
52 | ## [0.4.0] - 2025-05-08
53 |
54 | - Feature: Support for remote MCP with OAuth flow.
55 | - Remove `__node_version` tool
56 | - Feature: Add `list_slow_queries` tool for monitoring database performance
57 | - Add `list_branch_computes` tool to list compute endpoints for a project or specific branch
58 |
59 | ## [0.3.7] - 2025-04-23
60 |
61 | - Fixes Neon Auth instructions to install latest version of the SDK
62 |
63 | ## [0.3.6] - 2025-04-20
64 |
65 | - Bumps the Neon serverless driver to 1.0.0
66 |
67 | ## [0.3.5] - 2025-04-19
68 |
69 | - Fix default database name or role name assumptions.
70 | - Adds better error message for project creations.
71 |
72 | ## [0.3.4] - 2025-03-26
73 |
74 | - Add `neon-auth`, `neon-serverless`, and `neon-drizzle` resources
75 | - Fix initialization on Windows by implementing correct platform-specific paths for Claude configuration
76 |
77 | ## [0.3.3] - 2025-03-19
78 |
79 | - Fix the API Host
80 |
81 | ## [0.3.2] - 2025-03-19
82 |
83 | - Add User-Agent to api calls from mcp server
84 |
85 | ## [0.3.1] - 2025-03-19
86 |
87 | - Add User-Agent to api calls from mcp server
88 |
89 | ## [0.3.0] - 2025-03-14
90 |
91 | - Add `provision_neon_auth` tool
92 |
93 | ## [0.2.3] - 2025-03-06
94 |
95 | - Adds `get_connection_string` tool
96 | - Hints the LLM to call the `create_project` tool to create new databases
97 |
98 | ## [0.2.2] - 2025-02-26
99 |
100 | - Fixed a bug in the `list_projects` tool when passing no params
101 | - Added a `params` property to all the tools input schemas
102 |
103 | ## [0.2.1] - 2025-02-25
104 |
105 | - Fixes a bug in the `list_projects` tool
106 | - Update the `@modelcontextprotocol/sdk` to the latest version
107 | - Use `zod` to validate tool input schemas
108 |
109 | ## [0.2.0] - 2025-02-24
110 |
111 | - Add [Smithery](https://smithery.ai/server/neon) deployment config
112 |
113 | ## [0.1.9] - 2025-01-06
114 |
115 | - Setups tests to the `prepare_database_migration` tool
116 | - Updates the `prepare_database_migration` tool to be more deterministic
117 | - Removes logging from the MCP server, following the [docs](https://modelcontextprotocol.io/docs/tools/debugging#implementing-logging)
118 |
119 | ## [0.1.8] - 2024-12-25
120 |
121 | - Added `beforePublish` script so make sure the changelog is updated before publishing
122 | - Makes the descriptions/prompts for the prepare_database_migration and complete_database_migration tools much better
123 |
124 | ## [0.1.7-beta.1] - 2024-12-19
125 |
126 | - Added support for `prepare_database_migration` and `complete_database_migration` tools
127 |
```
--------------------------------------------------------------------------------
/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 |
```
--------------------------------------------------------------------------------
/src/transports/sse-express.ts:
--------------------------------------------------------------------------------
```typescript
1 | import '../sentry/instrument.js';
2 | import { setupExpressErrorHandler } from '@sentry/node';
3 | import express, { Request, Response, RequestHandler } from 'express';
4 | import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
5 | import { createMcpServer } from '../server/index.js';
6 | import { logger, morganConfig, errorHandler } from '../utils/logger.js';
7 | import { authRouter } from '../oauth/server.js';
8 | import { SERVER_PORT, SERVER_HOST } from '../constants.js';
9 | import { ensureCorsHeaders, requiresAuth } from '../oauth/utils.js';
10 | import bodyParser from 'body-parser';
11 | import cookieParser from 'cookie-parser';
12 | import { track } from '../analytics/analytics.js';
13 | import { AppContext } from '../types/context.js';
14 | import { createStreamTransport } from './stream.js';
15 |
16 | export const createSseTransport = (appContext: AppContext) => {
17 | const app = express();
18 |
19 | app.use(morganConfig);
20 | app.use(errorHandler);
21 | app.use(cookieParser());
22 | app.use(ensureCorsHeaders());
23 | app.use(express.static('public'));
24 | app.set('view engine', 'pug');
25 | app.set('views', 'src/views');
26 | const streamHandler = createStreamTransport({
27 | ...appContext,
28 | transport: 'stream',
29 | });
30 | app.use('/mcp', streamHandler);
31 | app.use('/', authRouter);
32 |
33 | // to support multiple simultaneous connections we have a lookup object from
34 | // sessionId to transport
35 | const transports = new Map<string, SSEServerTransport>();
36 |
37 | app.get(
38 | '/sse',
39 | bodyParser.raw(),
40 | requiresAuth(),
41 | async (req: Request, res: Response) => {
42 | const auth = req.auth;
43 | if (!auth) {
44 | res.status(401).send('Unauthorized');
45 | return;
46 | }
47 | const transport = new SSEServerTransport('/messages', res);
48 | transports.set(transport.sessionId, transport);
49 | logger.info('new sse connection', {
50 | sessionId: transport.sessionId,
51 | });
52 |
53 | res.on('close', () => {
54 | logger.info('SSE connection closed', {
55 | sessionId: transport.sessionId,
56 | });
57 | transports.delete(transport.sessionId);
58 | });
59 |
60 | try {
61 | const server = createMcpServer({
62 | apiKey: auth.token,
63 | client: auth.extra.client,
64 | account: auth.extra.account,
65 | app: appContext,
66 | });
67 | await server.connect(transport);
68 | } catch (error: unknown) {
69 | logger.error('Failed to connect to MCP server:', {
70 | message: error instanceof Error ? error.message : 'Unknown error',
71 | error,
72 | });
73 | track({
74 | userId: auth.extra.account.id,
75 | event: 'sse_connection_errored',
76 | properties: { error },
77 | context: {
78 | app: appContext,
79 | client: auth.extra.client,
80 | },
81 | });
82 | }
83 | },
84 | );
85 |
86 | app.post('/messages', bodyParser.raw(), requiresAuth(), (async (
87 | request: Request,
88 | response: Response,
89 | ) => {
90 | const auth = request.auth;
91 | if (!auth) {
92 | response.status(401).send('Unauthorized');
93 | return;
94 | }
95 | const sessionId = request.query.sessionId as string;
96 | const transport = transports.get(sessionId);
97 | logger.info('transport message received', {
98 | sessionId,
99 | hasTransport: Boolean(transport),
100 | });
101 |
102 | try {
103 | if (transport) {
104 | await transport.handlePostMessage(request, response);
105 | } else {
106 | logger.warn('No transport found for sessionId', { sessionId });
107 | response.status(400).send('No transport found for sessionId');
108 | }
109 | } catch (error: unknown) {
110 | logger.error('Failed to handle post message:', {
111 | message: error instanceof Error ? error.message : 'Unknown error',
112 | error,
113 | });
114 | track({
115 | userId: auth.extra.account.id,
116 | event: 'transport_message_errored',
117 | properties: { error },
118 | context: { app: appContext, client: auth.extra.client },
119 | });
120 | }
121 | }) as RequestHandler);
122 |
123 | setupExpressErrorHandler(app);
124 |
125 | try {
126 | app.listen({ port: SERVER_PORT });
127 | logger.info(`Server started on ${SERVER_HOST}`);
128 | } catch (err: unknown) {
129 | logger.error('Failed to start server:', {
130 | error: err instanceof Error ? err.message : 'Unknown error',
131 | });
132 | process.exit(1);
133 | }
134 | };
135 |
```
--------------------------------------------------------------------------------
/src/describeUtils.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * This module is derived from @neondatabase/psql-describe
3 | * Original source: https://github.com/neondatabase/psql-describe
4 | */
5 |
6 | import { neon } from '@neondatabase/serverless';
7 |
8 | export type TableDescription = {
9 | columns: ColumnDescription[];
10 | indexes: IndexDescription[];
11 | constraints: ConstraintDescription[];
12 | tableSize: string;
13 | indexSize: string;
14 | totalSize: string;
15 | };
16 |
17 | export type ColumnDescription = {
18 | name: string;
19 | type: string;
20 | nullable: boolean;
21 | default: string | null;
22 | description: string | null;
23 | };
24 |
25 | export type IndexDescription = {
26 | name: string;
27 | definition: string;
28 | size: string;
29 | };
30 |
31 | export type ConstraintDescription = {
32 | name: string;
33 | type: string;
34 | definition: string;
35 | };
36 |
37 | export const DESCRIBE_TABLE_STATEMENTS = [
38 | // Get column information
39 | `
40 | SELECT
41 | c.column_name as name,
42 | c.data_type as type,
43 | c.is_nullable = 'YES' as nullable,
44 | c.column_default as default,
45 | pd.description
46 | FROM information_schema.columns c
47 | LEFT JOIN pg_catalog.pg_statio_all_tables st ON c.table_schema = st.schemaname AND c.table_name = st.relname
48 | LEFT JOIN pg_catalog.pg_description pd ON pd.objoid = st.relid AND pd.objsubid = c.ordinal_position
49 | WHERE c.table_schema = 'public' AND c.table_name = $1
50 | ORDER BY c.ordinal_position;
51 | `,
52 |
53 | // Get index information
54 | `
55 | SELECT
56 | i.relname as name,
57 | pg_get_indexdef(i.oid) as definition,
58 | pg_size_pretty(pg_relation_size(i.oid)) as size
59 | FROM pg_class t
60 | JOIN pg_index ix ON t.oid = ix.indrelid
61 | JOIN pg_class i ON i.oid = ix.indexrelid
62 | WHERE t.relname = $1 AND t.relkind = 'r';
63 | `,
64 |
65 | // Get constraint information
66 | `
67 | SELECT
68 | tc.constraint_name as name,
69 | tc.constraint_type as type,
70 | pg_get_constraintdef(cc.oid) as definition
71 | FROM information_schema.table_constraints tc
72 | JOIN pg_catalog.pg_constraint cc ON tc.constraint_name = cc.conname
73 | WHERE tc.table_schema = 'public' AND tc.table_name = $1;
74 | `,
75 |
76 | // Get table size information
77 | `
78 | SELECT
79 | pg_size_pretty(pg_total_relation_size($1)) as total_size,
80 | pg_size_pretty(pg_relation_size($1)) as table_size,
81 | pg_size_pretty(pg_total_relation_size($1) - pg_relation_size($1)) as index_size;
82 | `,
83 | ];
84 |
85 | export async function describeTable(
86 | connectionString: string,
87 | tableName: string,
88 | ): Promise<TableDescription> {
89 | const sql = neon(connectionString);
90 |
91 | // Execute all queries in parallel
92 | const [columns, indexes, constraints, sizes] = await Promise.all([
93 | sql.query(DESCRIBE_TABLE_STATEMENTS[0], [tableName]),
94 | sql.query(DESCRIBE_TABLE_STATEMENTS[1], [tableName]),
95 | sql.query(DESCRIBE_TABLE_STATEMENTS[2], [tableName]),
96 | sql.query(DESCRIBE_TABLE_STATEMENTS[3], [tableName]),
97 | ]);
98 |
99 | return {
100 | columns: columns.map((col) => ({
101 | name: col.name,
102 | type: col.type,
103 | nullable: col.nullable,
104 | default: col.default,
105 | description: col.description,
106 | })),
107 | indexes: indexes.map((idx) => ({
108 | name: idx.name,
109 | definition: idx.definition,
110 | size: idx.size,
111 | })),
112 | constraints: constraints.map((con) => ({
113 | name: con.name,
114 | type: con.type,
115 | definition: con.definition,
116 | })),
117 | tableSize: sizes[0].table_size,
118 | indexSize: sizes[0].index_size,
119 | totalSize: sizes[0].total_size,
120 | };
121 | }
122 |
123 | export function formatTableDescription(desc: TableDescription): string {
124 | const lines: string[] = [];
125 |
126 | // Add table size information
127 | lines.push(`Table size: ${desc.tableSize}`);
128 | lines.push(`Index size: ${desc.indexSize}`);
129 | lines.push(`Total size: ${desc.totalSize}`);
130 | lines.push('');
131 |
132 | // Add columns
133 | lines.push('Columns:');
134 | desc.columns.forEach((col) => {
135 | const nullable = col.nullable ? 'NULL' : 'NOT NULL';
136 | const defaultStr = col.default ? ` DEFAULT ${col.default}` : '';
137 | const descStr = col.description ? `\n ${col.description}` : '';
138 | lines.push(` ${col.name} ${col.type} ${nullable}${defaultStr}${descStr}`);
139 | });
140 | lines.push('');
141 |
142 | // Add indexes
143 | if (desc.indexes.length > 0) {
144 | lines.push('Indexes:');
145 | desc.indexes.forEach((idx) => {
146 | lines.push(` ${idx.name} (${idx.size})`);
147 | lines.push(` ${idx.definition}`);
148 | });
149 | lines.push('');
150 | }
151 |
152 | // Add constraints
153 | if (desc.constraints.length > 0) {
154 | lines.push('Constraints:');
155 | desc.constraints.forEach((con) => {
156 | lines.push(` ${con.name} (${con.type})`);
157 | lines.push(` ${con.definition}`);
158 | });
159 | }
160 |
161 | return lines.join('\n');
162 | }
163 |
```
--------------------------------------------------------------------------------
/src/oauth/cookies.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {
2 | Request as ExpressRequest,
3 | Response as ExpressResponse,
4 | } from 'express';
5 |
6 | const COOKIE_NAME = 'approved-mcp-clients';
7 | const ONE_YEAR_IN_SECONDS = 365 * 24 * 60 * 60 * 1000; // 365 days
8 |
9 | /**
10 | * Imports a secret key string for HMAC-SHA256 signing.
11 | * @param secret - The raw secret key string.
12 | * @returns A promise resolving to the CryptoKey object.
13 | */
14 | const importKey = async (secret: string): Promise<CryptoKey> => {
15 | const enc = new TextEncoder();
16 | return crypto.subtle.importKey(
17 | 'raw',
18 | enc.encode(secret),
19 | { name: 'HMAC', hash: 'SHA-256' },
20 | false,
21 | ['sign', 'verify'],
22 | );
23 | };
24 |
25 | /**
26 | * Signs data using HMAC-SHA256.
27 | * @param key - The CryptoKey for signing.
28 | * @param data - The string data to sign.
29 | * @returns A promise resolving to the signature as a hex string.
30 | */
31 | const signData = async (key: CryptoKey, data: string): Promise<string> => {
32 | const enc = new TextEncoder();
33 | const signatureBuffer = await crypto.subtle.sign(
34 | 'HMAC',
35 | key,
36 | enc.encode(data),
37 | );
38 | // Convert ArrayBuffer to hex string
39 | return Array.from(new Uint8Array(signatureBuffer))
40 | .map((b) => b.toString(16).padStart(2, '0'))
41 | .join('');
42 | };
43 |
44 | /**
45 | * Verifies an HMAC-SHA256 signature.
46 | * @param key - The CryptoKey for verification.
47 | * @param signatureHex - The signature to verify (hex string).
48 | * @param data - The original data that was signed.
49 | * @returns A promise resolving to true if the signature is valid, false otherwise.
50 | */
51 | const verifySignature = async (
52 | key: CryptoKey,
53 | signatureHex: string,
54 | data: string,
55 | ): Promise<boolean> => {
56 | try {
57 | // Convert hex signature back to ArrayBuffer
58 | const enc = new TextEncoder();
59 | const signatureBytes = new Uint8Array(
60 | signatureHex.match(/.{1,2}/g)?.map((byte) => parseInt(byte, 16)) ?? [],
61 | );
62 |
63 | return await crypto.subtle.verify(
64 | 'HMAC',
65 | key,
66 | signatureBytes.buffer,
67 | enc.encode(data),
68 | );
69 | } catch (e) {
70 | // Handle errors during hex parsing or verification
71 | console.error('Error verifying signature:', e);
72 | return false;
73 | }
74 | };
75 |
76 | /**
77 | * Parses the signed cookie and verifies its integrity.
78 | * @param cookieHeader - The value of the Cookie header from the request.
79 | * @param secret - The secret key used for signing.
80 | * @returns A promise resolving to the list of approved client IDs if the cookie is valid, otherwise null.
81 | */
82 | const getApprovedClientsFromCookie = async (
83 | cookie: string,
84 | secret: string,
85 | ): Promise<string[]> => {
86 | if (!cookie) return [];
87 |
88 | try {
89 | const [signatureHex, base64Payload] = cookie.split('.');
90 | if (!signatureHex || !base64Payload) return [];
91 |
92 | const payload = atob(base64Payload);
93 | const key = await importKey(secret);
94 | const isValid = await verifySignature(key, signatureHex, payload);
95 | if (!isValid) return [];
96 |
97 | const clients = JSON.parse(payload);
98 | return Array.isArray(clients) ? clients : [];
99 | } catch {
100 | return [];
101 | }
102 | };
103 |
104 | /**
105 | * Checks if a given client has already been approved by the user,
106 | * based on a signed cookie.
107 | *
108 | * @param request - The incoming Request object to read cookies from.
109 | * @param clientId - The OAuth client ID to check approval for.
110 | * @param cookieSecret - The secret key used to sign/verify the approval cookie.
111 | * @returns A promise resolving to true if the client ID is in the list of approved clients in a valid cookie, false otherwise.
112 | */
113 | export const isClientAlreadyApproved = async (
114 | req: ExpressRequest,
115 | clientId: string,
116 | cookieSecret: string,
117 | ) => {
118 | const approvedClients = await getApprovedClientsFromCookie(
119 | req.cookies[COOKIE_NAME] ?? '',
120 | cookieSecret,
121 | );
122 | return approvedClients.includes(clientId);
123 | };
124 |
125 | /**
126 | * Updates the approved clients cookie with a new client ID.
127 | * The cookie is signed using HMAC-SHA256 for integrity.
128 | *
129 | * @param request - Express request containing existing cookie
130 | * @param clientId - Client ID to add to approved list
131 | * @param cookieSecret - Secret key for signing cookie
132 | * @returns Cookie string with updated approved clients list
133 | */
134 | export const updateApprovedClientsCookie = async (
135 | req: ExpressRequest,
136 | res: ExpressResponse,
137 | clientId: string,
138 | cookieSecret: string,
139 | ) => {
140 | const approvedClients = await getApprovedClientsFromCookie(
141 | req.cookies[COOKIE_NAME] ?? '',
142 | cookieSecret,
143 | );
144 | const newApprovedClients = JSON.stringify(
145 | Array.from(new Set([...approvedClients, clientId])),
146 | );
147 | const key = await importKey(cookieSecret);
148 | const signature = await signData(key, newApprovedClients);
149 | res.cookie(COOKIE_NAME, `${signature}.${btoa(newApprovedClients)}`, {
150 | httpOnly: true,
151 | secure: true,
152 | sameSite: 'lax',
153 | maxAge: ONE_YEAR_IN_SECONDS,
154 | path: '/',
155 | });
156 | };
157 |
```
--------------------------------------------------------------------------------
/landing/app/globals.css:
--------------------------------------------------------------------------------
```css
1 | @import 'tailwindcss';
2 | @import 'tw-animate-css';
3 |
4 | /* @custom-variant dark (&:is(.dark *)); */
5 |
6 | @theme inline {
7 | --color-background: var(--background);
8 | --color-foreground: var(--foreground);
9 | --color-sidebar-ring: var(--sidebar-ring);
10 | --color-sidebar-border: var(--sidebar-border);
11 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
12 | --color-sidebar-accent: var(--sidebar-accent);
13 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
14 | --color-sidebar-primary: var(--sidebar-primary);
15 | --color-sidebar-foreground: var(--sidebar-foreground);
16 | --color-sidebar: var(--sidebar);
17 | --color-chart-5: var(--chart-5);
18 | --color-chart-4: var(--chart-4);
19 | --color-chart-3: var(--chart-3);
20 | --color-chart-2: var(--chart-2);
21 | --color-chart-1: var(--chart-1);
22 | --color-ring: var(--ring);
23 | --color-input: var(--input);
24 | --color-border: var(--border);
25 | --color-destructive: var(--destructive);
26 | --color-accent-foreground: var(--accent-foreground);
27 | --color-accent: var(--accent);
28 | --color-muted-foreground: var(--muted-foreground);
29 | --color-muted: var(--muted);
30 | --color-secondary-foreground: var(--secondary-foreground);
31 | --color-secondary: var(--secondary);
32 | --color-primary-foreground: var(--primary-foreground);
33 | --color-primary: var(--primary);
34 | --color-popover-foreground: var(--popover-foreground);
35 | --color-popover: var(--popover);
36 | --color-card-foreground: var(--card-foreground);
37 | --color-card: var(--card);
38 | --radius-sm: calc(var(--radius) - 4px);
39 | --radius-md: calc(var(--radius) - 2px);
40 | --radius-lg: var(--radius);
41 | --radius-xl: calc(var(--radius) + 4px);
42 | --font-sans: var(--font-geist-sans);
43 | --font-mono: var(--font-geist-mono);
44 |
45 | /* user defined */
46 | --color-important-notes: var(--important-notes);
47 | --color-link: var(--link);
48 | }
49 |
50 | :root {
51 | --radius: 0.625rem;
52 | --background: oklch(1 0 0);
53 | --foreground: oklch(0.129 0.042 264.695);
54 | --card: oklch(1 0 0);
55 | --card-foreground: oklch(0.129 0.042 264.695);
56 | --popover: oklch(1 0 0);
57 | --popover-foreground: oklch(0.129 0.042 264.695);
58 | --primary: oklch(0.208 0.042 265.755);
59 | --primary-foreground: oklch(0.984 0.003 247.858);
60 | --secondary: oklch(0.968 0.007 247.896);
61 | --secondary-foreground: oklch(0.208 0.042 265.755);
62 | --muted: oklch(0.968 0.007 247.896);
63 | --muted-foreground: oklch(0.554 0.046 257.417);
64 | --accent: oklch(0.968 0.007 247.896);
65 | --accent-foreground: oklch(0.208 0.042 265.755);
66 | --destructive: oklch(0.577 0.245 27.325);
67 | --border: oklch(0.929 0.013 255.508);
68 | --input: oklch(0.929 0.013 255.508);
69 | --ring: oklch(0.704 0.04 256.788);
70 | --chart-1: oklch(0.646 0.222 41.116);
71 | --chart-2: oklch(0.6 0.118 184.704);
72 | --chart-3: oklch(0.398 0.07 227.392);
73 | --chart-4: oklch(0.828 0.189 84.429);
74 | --chart-5: oklch(0.769 0.188 70.08);
75 | --sidebar: oklch(0.984 0.003 247.858);
76 | --sidebar-foreground: oklch(0.129 0.042 264.695);
77 | --sidebar-primary: oklch(0.208 0.042 265.755);
78 | --sidebar-primary-foreground: oklch(0.984 0.003 247.858);
79 | --sidebar-accent: oklch(0.968 0.007 247.896);
80 | --sidebar-accent-foreground: oklch(0.208 0.042 265.755);
81 | --sidebar-border: oklch(0.929 0.013 255.508);
82 | --sidebar-ring: oklch(0.704 0.04 256.788);
83 |
84 | /* user defined */
85 | --important-notes: var(--color-orange-800);
86 | --link: oklch(0.64 0.14 160.38);
87 | }
88 |
89 | @variant dark {
90 | :root {
91 | --background: oklch(0.129 0.042 264.695);
92 | --foreground: oklch(0.984 0.003 247.858);
93 | --card: oklch(0.208 0.042 265.755);
94 | --card-foreground: oklch(0.984 0.003 247.858);
95 | --popover: oklch(0.208 0.042 265.755);
96 | --popover-foreground: oklch(0.984 0.003 247.858);
97 | --primary: oklch(0.929 0.013 255.508);
98 | --primary-foreground: oklch(0.208 0.042 265.755);
99 | --secondary: oklch(0.279 0.041 260.031);
100 | --secondary-foreground: oklch(0.984 0.003 247.858);
101 | --muted: oklch(0.279 0.041 260.031);
102 | --muted-foreground: oklch(0.704 0.04 256.788);
103 | --accent: oklch(0.279 0.041 260.031);
104 | --accent-foreground: oklch(0.984 0.003 247.858);
105 | --destructive: oklch(0.704 0.191 22.216);
106 | --border: oklch(1 0 0 / 10%);
107 | --input: oklch(1 0 0 / 15%);
108 | --ring: oklch(0.551 0.027 264.364);
109 | --chart-1: oklch(0.488 0.243 264.376);
110 | --chart-2: oklch(0.696 0.17 162.48);
111 | --chart-3: oklch(0.769 0.188 70.08);
112 | --chart-4: oklch(0.627 0.265 303.9);
113 | --chart-5: oklch(0.645 0.246 16.439);
114 | --sidebar: oklch(0.208 0.042 265.755);
115 | --sidebar-foreground: oklch(0.984 0.003 247.858);
116 | --sidebar-primary: oklch(0.488 0.243 264.376);
117 | --sidebar-primary-foreground: oklch(0.984 0.003 247.858);
118 | --sidebar-accent: oklch(0.279 0.041 260.031);
119 | --sidebar-accent-foreground: oklch(0.984 0.003 247.858);
120 | --sidebar-border: oklch(1 0 0 / 10%);
121 | --sidebar-ring: oklch(0.551 0.027 264.364);
122 |
123 | /* user defined */
124 | --important-notes: var(--color-orange-100);
125 | --link: oklch(0.81 0.18 160.37);
126 | }
127 | }
128 |
129 | @utility monospaced {
130 | @apply font-[family-name:var(--font-geist-mono)];
131 | }
132 |
133 | @layer base {
134 | * {
135 | @apply border-border outline-ring/50;
136 | }
137 |
138 | body {
139 | @apply bg-background text-foreground;
140 | /*font-family: Arial, Helvetica, sans-serif;*/
141 | }
142 |
143 | button:not(:disabled),
144 | [role='button']:not(:disabled) {
145 | cursor: pointer;
146 | }
147 | }
148 |
149 | @layer page {
150 | .external-link {
151 | @apply text-link;
152 | @apply font-semibold;
153 | @apply border-b;
154 | @apply border-transparent;
155 | @apply hover:border-current;
156 | }
157 | }
158 |
```