Skip to content

Commit addb87c

Browse files
authored
feat: add neon authV2 support (#151)
## PR Description ### Title: Add Neon Auth v2 Support with Prompts and Client Detection ### Summary This PR updates the Neon MCP server to support Neon Auth v2 (Better Auth-based) and introduces a new prompts system with client-specific instructions. ### Changes #### Neon Auth v2 Migration - **Migrated from Stack Auth to Better Auth**: Updated the `provision_neon_auth` tool to use the new `createNeonAuth` API with `BetterAuth` provider instead of the legacy `createNeonAuthIntegration` with `Stack` provider - **Branch-level provisioning**: Added optional `branchId` parameter, allowing Neon Auth to be provisioned on specific branches (not just the project's default branch) - **Updated response format**: Returns the Better Auth-compatible base URL instead of Stack-specific environment variables #### New Prompts System - **Added `setup-neon-auth` prompt**: Interactive guide for setting up Neon Auth in Vite+React projects, with steps for provisioning, package installation, client setup, and UI components - **Client-specific instructions**: The prompt adapts its guidance based on the detected MCP client (Cursor, Claude, or other) - **Just-in-Time Context Protocol**: Embedded documentation fetching protocol that guides AI agents to fetch external docs only when needed #### Client Application Detection - **New utility**: Added `detectClientApplication()` to identify the MCP client from the initialization handshake - **Client type injection**: The detected client type is now passed to tool handlers via `extra.clientApplication` #### Dependency Updates - Upgraded `@neondatabase/api-client` from 2.0.0 to 2.5.0 - Upgraded `axios` from 1.11.0 to 1.13.2 (with resolution override) #### Other - Minor UI fix: Updated color classes in landing page to use theme tokens (`text-muted-foreground`, `bg-muted`) - Added `Prettify` type helper ### Breaking Changes - The `provision_neon_auth` tool parameter `database` has been renamed to `databaseName` for consistency with other tools ---
1 parent 7662b8a commit addb87c

File tree

14 files changed

+275
-198
lines changed

14 files changed

+275
-198
lines changed

.github/workflows/claude-code-review.yml

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,8 @@ concurrency:
2222

2323
jobs:
2424
claude-review:
25-
# For automatic runs: only allow members/collaborators/owners
26-
# For manual runs: always allow (since only repo members can trigger workflows)
27-
if: |
28-
github.event_name == 'workflow_dispatch' ||
29-
github.event.pull_request.author_association == 'OWNER' ||
30-
github.event.pull_request.author_association == 'MEMBER' ||
31-
github.event.pull_request.author_association == 'COLLABORATOR'
25+
# Disabled: set to false to prevent workflow execution
26+
if: ${{ false }}
3227

3328
runs-on:
3429
group: neondatabase-protected-runner-group

bun.lock

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
"dependencies": {
77
"@keyv/postgres": "2.1.2",
88
"@modelcontextprotocol/sdk": "1.11.2",
9-
"@neondatabase/api-client": "2.0.0",
9+
"@neondatabase/api-client": "2.5.0",
1010
"@neondatabase/serverless": "1.0.0",
1111
"@radix-ui/react-accordion": "1.2.11",
1212
"@segment/analytics-node": "2.2.1",
1313
"@sentry/node": "9.19.0",
1414
"@tailwindcss/postcss": "4.1.10",
15-
"axios": "1.11.0",
15+
"axios": "1.13.2",
1616
"body-parser": "2.2.0",
1717
"chalk": "5.3.0",
1818
"class-variance-authority": "0.7.1",
@@ -55,6 +55,9 @@
5555
},
5656
},
5757
},
58+
"overrides": {
59+
"axios": "1.13.2",
60+
},
5861
"packages": {
5962
"@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
6063

@@ -232,7 +235,7 @@
232235

233236
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.11.2", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.3", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ=="],
234237

235-
"@neondatabase/api-client": ["@neondatabase/api-client@2.0.0", "", { "dependencies": { "axios": "^1.7.9" } }, "sha512-UwjRRy8gyShNDfbbP1N+rA235FzqVWP/WPi4cgLnkGgConqpiR+U9Dsegaxu+HxAgK678kQ/5wc4/EPexvE7tw=="],
238+
"@neondatabase/api-client": ["@neondatabase/api-client@2.5.0", "", { "dependencies": { "axios": "^1.9.0" } }, "sha512-Tg5/Aq8zg1kxT8OuaRULpl0NcxTLpOBpcbdeC8RO3U6Ubftu+YSjKUqB5NRZWITMwDy8GSJtPQYF/xHHlsqauQ=="],
236239

237240
"@neondatabase/serverless": ["@neondatabase/serverless@1.0.0", "", { "dependencies": { "@types/node": "^22.10.2", "@types/pg": "^8.8.0" } }, "sha512-XWmEeWpBXIoksZSDN74kftfTnXFEGZ3iX8jbANWBc+ag6dsiQuvuR4LgB0WdCOKMb5AQgjqgufc0TgAsZubUYw=="],
238241

@@ -550,7 +553,7 @@
550553

551554
"autoevals": ["autoevals@0.0.111", "", { "dependencies": { "@braintrust/core": "0.0.71", "ajv": "^8.13.0", "compute-cosine-similarity": "^1.1.0", "js-levenshtein": "^1.1.6", "js-yaml": "^4.1.0", "linear-sum-assignment": "^1.0.7", "mustache": "^4.2.0", "openai": "4.47.1", "zod": "^3.22.4", "zod-to-json-schema": "^3.22.5" } }, "sha512-H38avDrcWU6w7aP0CwCansc/Um/3Bm8yvjMX6g8TR4FIJeyY84poimdNK3GDRk+hhoiY1DovdZoEJILWC6OmgQ=="],
552555

553-
"axios": ["axios@1.11.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA=="],
556+
"axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="],
554557

555558
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
556559

@@ -1368,8 +1371,6 @@
13681371

13691372
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
13701373

1371-
"@neondatabase/api-client/axios": ["axios@1.9.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg=="],
1372-
13731374
"@neondatabase/serverless/@types/node": ["@types/node@22.15.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg=="],
13741375

13751376
"@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],

landing/components/Introduction.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ export const Introduction = ({ className }: { className?: string }) => (
4545
Safe for cloud environments. All transactions are read-only -
4646
perfect for querying and analyzing data without modification risks.
4747
</p>
48-
<p className="text-xs text-gray-600">
48+
<p className="text-xs text-muted-foreground">
4949
Enable read-only mode by adding the{' '}
50-
<code className="bg-gray-100 px-1 py-0.5 rounded text-xs">
50+
<code className="bg-muted px-1 py-0.5 rounded text-xs">
5151
x-read-only: true
5252
</code>{' '}
5353
header in your MCP configuration.

package-lock.json

Lines changed: 6 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@
3535
"dependencies": {
3636
"@keyv/postgres": "2.1.2",
3737
"@modelcontextprotocol/sdk": "1.11.2",
38-
"@neondatabase/api-client": "2.0.0",
38+
"@neondatabase/api-client": "2.5.0",
3939
"@neondatabase/serverless": "1.0.0",
4040
"@radix-ui/react-accordion": "1.2.11",
4141
"@segment/analytics-node": "2.2.1",
4242
"@sentry/node": "9.19.0",
4343
"@tailwindcss/postcss": "4.1.10",
44-
"axios": "1.11.0",
44+
"axios": "1.13.2",
4545
"body-parser": "2.2.0",
4646
"chalk": "5.3.0",
4747
"class-variance-authority": "0.7.1",
@@ -84,5 +84,8 @@
8484
},
8585
"engines": {
8686
"node": ">=22.0.0"
87+
},
88+
"resolutions": {
89+
"axios": "1.13.2"
8790
}
8891
}

src/prompts.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { z } from 'zod';
2+
import { fetchRawGithubContent } from './resources.js';
3+
import { ToolHandlerExtraParams } from './tools/types.js';
4+
import { ClientApplication } from './utils/client-application.js';
5+
6+
export const setupNeonAuthViteReactArgsSchema = {
7+
projectId: z
8+
.string()
9+
.optional()
10+
.describe(
11+
'Optional Neon project ID. If not provided, the guide will help discover available projects.',
12+
),
13+
branchId: z
14+
.string()
15+
.optional()
16+
.describe(
17+
'Optional branch ID. If not provided, the default branch will be used.',
18+
),
19+
databaseName: z
20+
.string()
21+
.optional()
22+
.describe(
23+
'Optional database name. If not provided, the default database (neondb) will be used.',
24+
),
25+
} as const;
26+
27+
export const NEON_PROMPTS = [
28+
{
29+
name: 'setup-neon-auth',
30+
description:
31+
'Interactive guide for setting up Neon Auth in a Vite+React project. Walks through provisioning, package installation, client setup, and UI components.',
32+
argsSchema: setupNeonAuthViteReactArgsSchema,
33+
},
34+
] as const;
35+
36+
const CLIENT_SPECIFIC_INSTRUCTIONS: Record<ClientApplication, string> = {
37+
cursor: `
38+
- **For URLs:** Use the \`@Web\` tool (or \`web_search\`) to fetch the page.
39+
- **For Files:** Use \`grep\` or \`cat\` to read local files.
40+
`,
41+
claude: `
42+
- **For URLs:** Use your \`web_fetch\` tool (or \`web_search\`) to read content.
43+
- **For Files:** Use \`grep\` or \`cat\` to read local files.
44+
`,
45+
other: `
46+
- **For URLs:** Use your web fetch tool to read content, or curl if you need to.
47+
- **For Files:** Use \`grep\` or \`cat\` to read local files.
48+
`,
49+
};
50+
51+
const COMMON_FOLLOW_INSTRUCTIONS = (clientApplication: ClientApplication) => `
52+
53+
# Just-in-Time Context Protocol
54+
55+
## 1. ASSESSMENT & TRIGGER
56+
- **Analyze Gaps:** Before generating code, ask: "Do I have the *exact* API signatures and patterns for this specific task in my context?"
57+
- **Lazy Loading:** Do NOT read external links or docs if the current context is sufficient.
58+
- **Trigger:** IF you lack specific details AND a reference link/path exists in the rules (e.g., \`[Full Setup](url)\`), you MUST fetch that resource immediately.
59+
60+
## 2. STRICT LINK OBEDIENCE
61+
- **No Guessing:** If a task relates to a linked topic (e.g., "Styling"), you are PROHIBITED from inferring patterns. You must read the linked reference.
62+
- **Method:**
63+
${CLIENT_SPECIFIC_INSTRUCTIONS[clientApplication]}
64+
65+
## 3. EXECUTION
66+
- **State Intent:** Briefly confirm: "Fetching [Topic] details from [Source]..."
67+
- **Apply & Discard:** Extract only what is needed for the current task.
68+
69+
## ENFORCEMENT
70+
⚠️ You MUST follow these rules. If you skip documentation lookup, you are breaking protocol.
71+
`;
72+
73+
export const getPromptTemplate = async (
74+
promptName: string,
75+
extra: ToolHandlerExtraParams,
76+
args?: Record<string, string>,
77+
): Promise<string> => {
78+
if (promptName === 'setup-neon-auth') {
79+
// Variables are available for future template interpolation
80+
void args?.projectId;
81+
void args?.branchId;
82+
void args?.databaseName;
83+
84+
const content = await fetchRawGithubContent(
85+
'/neondatabase-labs/ai-rules/main/mcp-prompts/neon-auth-setup.md',
86+
);
87+
88+
return `
89+
${COMMON_FOLLOW_INSTRUCTIONS(extra.clientApplication)}
90+
91+
---
92+
93+
${content}`;
94+
}
95+
96+
throw new Error(`Unknown prompt: ${promptName}`);
97+
};

src/server/index.ts

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
44
import { NEON_RESOURCES } from '../resources.js';
5+
import { NEON_PROMPTS, getPromptTemplate } from '../prompts.js';
56
import {
67
NEON_HANDLERS,
78
NEON_TOOLS,
@@ -15,6 +16,7 @@ import { ServerContext } from '../types/context.js';
1516
import { setSentryTags } from '../sentry/utils.js';
1617
import { ToolHandlerExtraParams } from '../tools/types.js';
1718
import { handleToolError } from './errors.js';
19+
import { detectClientApplication } from '../utils/client-application.js';
1820

1921
export const createMcpServer = (context: ServerContext) => {
2022
const server = new McpServer(
@@ -26,6 +28,9 @@ export const createMcpServer = (context: ServerContext) => {
2628
capabilities: {
2729
tools: {},
2830
resources: {},
31+
prompts: {
32+
listChanged: true,
33+
},
2934
},
3035
},
3136
);
@@ -59,22 +64,34 @@ export const createMcpServer = (context: ServerContext) => {
5964
},
6065
},
6166
async (span) => {
67+
// Get client info from MCP protocol
68+
const clientInfo = server.server.getClientVersion();
69+
6270
const properties = {
6371
tool_name: tool.name,
6472
readOnly: String(context.readOnly ?? false),
73+
clientName: clientInfo?.name ?? 'unknown',
6574
};
6675
logger.info('tool call:', properties);
76+
logger.info('MCP Client Info:', clientInfo);
6777
setSentryTags(context);
6878
track({
6979
userId: context.account.id,
7080
event: 'tool_call',
7181
properties,
72-
context: { client: context.client, app: context.app },
82+
context: {
83+
client: context.client,
84+
app: context.app,
85+
clientInfo,
86+
},
7387
});
88+
89+
const clientApplication = detectClientApplication(clientInfo?.name);
7490
const extraArgs: ToolHandlerExtraParams = {
7591
...extra,
7692
account: context.account,
7793
readOnly: context.readOnly,
94+
clientApplication,
7895
};
7996
try {
8097
return await toolHandler(args, neonClient, extraArgs);
@@ -121,6 +138,59 @@ export const createMcpServer = (context: ServerContext) => {
121138
);
122139
});
123140

141+
// Register prompts
142+
NEON_PROMPTS.forEach((prompt) => {
143+
server.prompt(
144+
prompt.name,
145+
prompt.description,
146+
prompt.argsSchema,
147+
async (args, extra) => {
148+
// Get client info from MCP protocol
149+
const clientInfo = server.server.getClientVersion();
150+
const clientApplication = detectClientApplication(clientInfo?.name);
151+
152+
const properties = { prompt_name: prompt.name };
153+
logger.info('prompt call:', properties);
154+
setSentryTags(context);
155+
track({
156+
userId: context.account.id,
157+
event: 'prompt_call',
158+
properties,
159+
context: { client: context.client, app: context.app },
160+
});
161+
try {
162+
const extraArgs: ToolHandlerExtraParams = {
163+
...extra,
164+
account: context.account,
165+
readOnly: context.readOnly,
166+
clientApplication,
167+
};
168+
const template = await getPromptTemplate(
169+
prompt.name,
170+
extraArgs,
171+
args,
172+
);
173+
return {
174+
messages: [
175+
{
176+
role: 'user',
177+
content: {
178+
type: 'text',
179+
text: template,
180+
},
181+
},
182+
],
183+
};
184+
} catch (error) {
185+
captureException(error, {
186+
extra: properties,
187+
});
188+
throw error;
189+
}
190+
},
191+
);
192+
});
193+
124194
server.server.onerror = (error: unknown) => {
125195
const message = error instanceof Error ? error.message : 'Unknown error';
126196
logger.error('Server error:', {

0 commit comments

Comments
 (0)