diff --git a/content/docs/ai/neon-mcp-server.md b/content/docs/ai/neon-mcp-server.md index ecb5394af4..10fd1d6533 100644 --- a/content/docs/ai/neon-mcp-server.md +++ b/content/docs/ai/neon-mcp-server.md @@ -58,166 +58,15 @@ The Neon MCP Server grants powerful database management capabilities through nat ## Other setup options - +### MCP Server Config Generator - +Use this generator to build valid Neon hosted MCP config snippets with supported auth modes, transport, and headers: -Connect to Neon's managed MCP server using OAuth. No API key configuration needed. - -```bash -npx add-mcp https://mcp.neon.tech/mcp -``` - -Or add this to your MCP config file: - -```json -{ - "mcpServers": { - "neon": { - "type": "http", - "url": "https://mcp.neon.tech/mcp" - } - } -} -``` - - -Add Neon MCP server to Cursor - - - -Add Neon MCP server to Kiro - - -After saving, restart your MCP client. When the OAuth window opens in your browser, review the requested permissions and click **Authorize** to complete the connection. - - - - - -Connect using API key authentication. Useful for remote agents where OAuth isn't available. - -**Requires:** [Neon API key](/docs/manage/api-keys) - -```bash -npx add-mcp https://mcp.neon.tech/mcp --header "Authorization: Bearer " -``` - -### MCP-only setup (OAuth) - -If you only want the MCP server and prefer OAuth, run: - -```bash -npx add-mcp https://mcp.neon.tech/mcp -``` - -The command adds the config to your editor; restart your editor (or enable the MCP server) for it to take effect. When you use the MCP connection, an OAuth window will open in your browser; follow the prompts to authorize. For the recommended quick setup (API key + agent skills), use `npx neonctl@latest init` instead. - - -Click the button below to install the Neon MCP server in Cursor. When prompted, click **Install** within Cursor. - -```json -{ - "mcpServers": { - "neon": { - "type": "http", - "url": "https://mcp.neon.tech/mcp", - "headers": { - "Authorization": "Bearer <$NEON_API_KEY>" - } - } - } -} -``` - - - - -Use an organization API key to limit access to organization projects only. - - -### Manual setup - -1. Go to your MCP Client's settings where you configure MCP Servers (this varies by client) -2. Register a new MCP Server. When prompted for the configuration, name the server "Neon" and add the following configuration: - - ```json - { - "mcpServers": { - "Neon": { - "type": "http", - "url": "https://mcp.neon.tech/mcp" - } - } - } - ``` - - > 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. - - - - - -Run the MCP server locally on your machine. - -**Requires:** Node.js >= v18, [Neon API key](/docs/manage/api-keys) - -```bash -npx add-mcp "npx -y @neondatabase/mcp-server-neon start " --name neon -``` - -Or add this to your MCP config file: - -```json -{ - "mcpServers": { - "neon": { - "command": "npx", - "args": ["-y", "@neondatabase/mcp-server-neon", "start", ""] - } - } -} -``` - - - -Use `cmd` or `wsl` if you encounter issues: - - - -```json -{ - "mcpServers": { - "neon": { - "command": "cmd", - "args": ["/c", "npx", "-y", "@neondatabase/mcp-server-neon", "start", ""] - } - } -} -``` - -```json -{ - "mcpServers": { - "neon": { - "command": "wsl", - "args": ["npx", "-y", "@neondatabase/mcp-server-neon", "start", ""] - } - } -} -``` - - - - - - - - + -## Troubleshooting +### Troubleshooting If your client does not use JSON for configuration of MCP servers (such as older versions of Cursor), use this command when prompted: diff --git a/content/docs/shared-content/mcp-tools.md b/content/docs/shared-content/mcp-tools.md index affa2db782..2a407b1bce 100644 --- a/content/docs/shared-content/mcp-tools.md +++ b/content/docs/shared-content/mcp-tools.md @@ -37,7 +37,7 @@ The Neon MCP Server provides the following actions, which are exposed as "tools" - `prepare_database_migration`: Initiates a database migration process. Critically, it creates a temporary branch to apply and test the migration safely before affecting the main branch. - `complete_database_migration`: Finalizes and applies a prepared database migration to the main branch. This action merges changes from the temporary migration branch and cleans up temporary resources. -**Query performance optimization:** +**SQL querying and optimization:** - `list_slow_queries`: Identifies performance bottlenecks by finding the slowest queries in a database. Requires the pg_stat_statements extension. - `explain_sql_statement`: Provides detailed execution plans for SQL queries to help identify performance bottlenecks. @@ -57,6 +57,8 @@ The Neon MCP Server provides the following actions, which are exposed as "tools" - `search`: Searches across organizations, projects, and branches matching a query. Returns IDs, titles, and direct links to the Neon Console. - `fetch`: Fetches detailed information about a specific organization, project, or branch using an ID (typically from the search tool). +In project-scoped mode, `search` and `fetch` are not available. + **Documentation and resources:** - `list_docs_resources`: Lists all available Neon documentation pages by fetching the docs index. Returns page URLs and titles that can be fetched individually using the `get_doc_resource` tool. diff --git a/src/components/pages/doc/mcp-setup-configurator/index.js b/src/components/pages/doc/mcp-setup-configurator/index.js new file mode 100644 index 0000000000..b05d5ecd46 --- /dev/null +++ b/src/components/pages/doc/mcp-setup-configurator/index.js @@ -0,0 +1,3 @@ +import McpSetupConfigurator from './mcp-setup-configurator'; + +export default McpSetupConfigurator; diff --git a/src/components/pages/doc/mcp-setup-configurator/mcp-setup-configurator.jsx b/src/components/pages/doc/mcp-setup-configurator/mcp-setup-configurator.jsx new file mode 100644 index 0000000000..da0ea71e1f --- /dev/null +++ b/src/components/pages/doc/mcp-setup-configurator/mcp-setup-configurator.jsx @@ -0,0 +1,596 @@ +'use client'; + +import clsx from 'clsx'; +import parse from 'html-react-parser'; +import PropTypes from 'prop-types'; +import { useEffect, useMemo, useState } from 'react'; + +import CodeBlockWrapper from 'components/shared/code-block-wrapper'; +import highlight from 'lib/shiki'; + +const SERVER_BASE = 'https://mcp.neon.tech'; +const MCP_PATH = '/mcp'; +const PROD_LIST_TOOLS_URL = `${SERVER_BASE}/api/list-tools`; + +const AUTH_MODES = [ + { + id: 'oauth', + label: 'OAuth', + description: 'Browser-based authorization. Best for local IDEs.', + }, + { + id: 'apiKey', + label: 'API Key', + description: 'Static bearer token. Best for headless and remote agents.', + }, +]; + +const SCOPE_CATEGORIES = [ + { id: 'projects', label: 'Projects', description: 'Create and manage projects' }, + { id: 'branches', label: 'Branches', description: 'Create, reset, delete branches' }, + { id: 'schema', label: 'Schema', description: 'Tables, columns, indexes' }, + { id: 'querying', label: 'Querying', description: 'Run SQL and explain plans' }, + { id: 'neon_auth', label: 'Neon Auth', description: 'Users and sessions' }, + { id: 'data_api', label: 'Data API', description: 'RESTful data endpoints' }, + { id: 'docs', label: 'Docs', description: 'Search and fetch docs' }, +]; +const SCOPE_IDS = SCOPE_CATEGORIES.map((scope) => scope.id); +const SCOPE_ID_SET = new Set(SCOPE_IDS); + +const CARD_CLASS = + 'rounded-xl border border-gray-new-90 bg-white/80 p-5 backdrop-blur-sm dark:border-gray-new-20 dark:bg-gray-new-10/50'; +const SECTION_LABEL_CLASS = + 'mb-3 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-gray-new-40 dark:text-gray-new-60'; +const FIELD_LABEL_CLASS = + 'mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-new-30 dark:text-gray-new-70'; +const HELPER_TEXT_CLASS = 'text-[13px] leading-relaxed text-gray-new-40 dark:text-gray-new-60'; + +function getListToolsBaseUrl() { + if (process.env.NEXT_PUBLIC_MCP_API_URL) { + return process.env.NEXT_PUBLIC_MCP_API_URL; + } + return PROD_LIST_TOOLS_URL; +} + +function buildQueryParams({ readOnly, projectId, selectedScopes }) { + const params = new URLSearchParams(); + if (readOnly) { + params.set('readonly', 'true'); + } + const trimmedProjectId = projectId.trim(); + if (trimmedProjectId) { + params.set('projectId', trimmedProjectId); + } + const validScopes = selectedScopes.filter((id) => SCOPE_ID_SET.has(id)); + if (validScopes.length > 0 && validScopes.length < SCOPE_IDS.length) { + params.set('category', validScopes.join(',')); + } + return params; +} + +function appendParams(baseUrl, params) { + const qs = params.toString(); + return qs ? `${baseUrl}?${qs}` : baseUrl; +} + +async function fetchTools({ url, timeoutMs = 12000 }) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + const response = await fetch(url, { signal: controller.signal }).finally(() => + clearTimeout(timeout) + ); + if (!response.ok) { + throw new Error(`Failed to fetch tools preview: ${response.status}`); + } + return response.json(); +} + +const SegmentedControl = ({ name, options, value, onChange }) => ( +
+ {options.map((option) => { + const selected = value === option.id; + return ( + + ); + })} +
+); + +SegmentedControl.propTypes = { + name: PropTypes.string.isRequired, + options: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + description: PropTypes.string, + }) + ).isRequired, + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, +}; + +const Toggle = ({ checked, onChange, label, description }) => ( + +); + +Toggle.propTypes = { + checked: PropTypes.bool.isRequired, + onChange: PropTypes.func.isRequired, + label: PropTypes.string.isRequired, + description: PropTypes.string, +}; + +const HighlightedCode = ({ code, language }) => { + const [html, setHtml] = useState(''); + + useEffect(() => { + let cancelled = false; + highlight(code, language).then((result) => { + if (!cancelled) setHtml(result); + }); + return () => { + cancelled = true; + }; + }, [code, language]); + + if (!html) { + return ( +
+        {code}
+      
+ ); + } + + return <>{parse(html)}; +}; + +HighlightedCode.propTypes = { + code: PropTypes.string.isRequired, + language: PropTypes.string.isRequired, +}; + +const McpSetupConfigurator = () => { + const [authMode, setAuthMode] = useState('oauth'); + const [apiKey, setApiKey] = useState(''); + const [readOnly, setReadOnly] = useState(false); + const [projectId, setProjectId] = useState(''); + const [selectedScopes, setSelectedScopes] = useState(SCOPE_IDS); + const [toolsPreview, setToolsPreview] = useState(null); + const [allTools, setAllTools] = useState(null); + const [toolsPreviewLoading, setToolsPreviewLoading] = useState(false); + const [toolsPreviewError, setToolsPreviewError] = useState(false); + const [toolsPreviewErrorMessage, setToolsPreviewErrorMessage] = useState(''); + const [lastSuccessfulToolsPreview, setLastSuccessfulToolsPreview] = useState(null); + const [lastSuccessfulAllTools, setLastSuccessfulAllTools] = useState(null); + const [toolsReloadNonce, setToolsReloadNonce] = useState(0); + + const queryParams = useMemo( + () => buildQueryParams({ readOnly, projectId, selectedScopes }), + [readOnly, projectId, selectedScopes] + ); + const queryString = queryParams.toString(); + + const baseServerUrl = `${SERVER_BASE}${MCP_PATH}`; + const generatedServerUrl = useMemo( + () => appendParams(baseServerUrl, queryParams), + [baseServerUrl, queryParams] + ); + + const generatedHeaders = useMemo(() => { + const headers = {}; + if (authMode === 'apiKey') { + headers.Authorization = `Bearer ${apiKey.trim() || ''}`; + } + return headers; + }, [apiKey, authMode]); + + const generatedConfig = useMemo(() => { + const neonEntry = { + type: 'http', + url: generatedServerUrl, + }; + if (Object.keys(generatedHeaders).length > 0) { + neonEntry.headers = generatedHeaders; + } + const config = { + mcpServers: { + Neon: neonEntry, + }, + }; + return JSON.stringify(config, null, 2); + }, [generatedHeaders, generatedServerUrl]); + + const addMcpCommand = useMemo(() => { + const urlArg = queryString ? `"${generatedServerUrl}"` : generatedServerUrl; + const commandParts = [`npx add-mcp@latest ${urlArg}`, '--name Neon']; + if (authMode === 'apiKey') { + commandParts.push( + `--header "Authorization: ${generatedHeaders.Authorization || 'Bearer '}"` + ); + } + return commandParts.join(' \\\n '); + }, [authMode, generatedHeaders.Authorization, generatedServerUrl, queryString]); + + const effectiveToolsPreview = useMemo(() => { + if (toolsPreviewError && lastSuccessfulToolsPreview) return lastSuccessfulToolsPreview; + return toolsPreview; + }, [lastSuccessfulToolsPreview, toolsPreview, toolsPreviewError]); + + const effectiveAllTools = useMemo(() => { + if (toolsPreviewError && lastSuccessfulAllTools) return lastSuccessfulAllTools; + return allTools; + }, [allTools, lastSuccessfulAllTools, toolsPreviewError]); + + const selectedTools = useMemo(() => { + if (!Array.isArray(effectiveToolsPreview?.tools)) return []; + return effectiveToolsPreview.tools; + }, [effectiveToolsPreview]); + + const notIncludedTools = useMemo(() => { + if (!Array.isArray(effectiveAllTools?.tools) || !Array.isArray(effectiveToolsPreview?.tools)) { + return []; + } + const selectedNames = new Set(effectiveToolsPreview.tools.map((tool) => tool.name)); + return effectiveAllTools.tools.filter((tool) => !selectedNames.has(tool.name)); + }, [effectiveAllTools, effectiveToolsPreview]); + + useEffect(() => { + let cancelled = false; + const listToolsBase = getListToolsBaseUrl(); + const filteredUrl = appendParams(listToolsBase, queryParams); + + setToolsPreviewLoading(true); + setToolsPreviewError(false); + setToolsPreviewErrorMessage(''); + + Promise.all([fetchTools({ url: filteredUrl }), fetchTools({ url: listToolsBase })]) + .then(([filteredPayload, allPayload]) => { + if (!cancelled) { + setToolsPreview(filteredPayload); + setAllTools(allPayload); + setLastSuccessfulToolsPreview(filteredPayload); + setLastSuccessfulAllTools(allPayload); + setToolsPreviewLoading(false); + } + }) + .catch((error) => { + if (!cancelled) { + setToolsPreviewError(true); + if (error instanceof Error) { + const message = + error.name === 'AbortError' + ? 'Request timed out while loading tools.' + : error.message; + setToolsPreviewErrorMessage(message); + } else { + setToolsPreviewErrorMessage('Unable to load tools.'); + } + setToolsPreviewLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [queryParams, toolsReloadNonce]); + + const toggleScope = (scopeId) => { + setSelectedScopes((prev) => + prev.includes(scopeId) ? prev.filter((item) => item !== scopeId) : [...prev, scopeId] + ); + }; + + const allScopesSelected = selectedScopes.length === SCOPE_IDS.length; + + return ( +
+
+

+ MCP Server Config Generator +

+

+ Generate a ready-to-use config for Neon's hosted MCP server. +

+
+ +
+
+

Configuration

+ +
+ Authentication + + {authMode === 'apiKey' && ( + + )} +
+ +
+ Access +
+ +
+ + + Scopes the agent to a single project. Hides project-wide management tools. + +
+
+
+ +
+
+ Tool categories + +
+

+ Pick which capability groups the agent can access. Unselected categories are excluded + via the{' '} + + category + {' '} + query param. +

+
+ {SCOPE_CATEGORIES.map((scope) => { + const checked = selectedScopes.includes(scope.id); + return ( + + ); + })} +
+
+
+ +
+
+

+ + Result +

+
+ +
+
+ + Tools preview + + + {selectedTools.length} enabled ยท {notIncludedTools.length} hidden + +
+ + {toolsPreviewLoading && ( +
+
+ {Array.from({ length: 12 }).map((_, idx) => ( + + ))} +
+
+ )} + + {toolsPreviewError && ( +
+

+ Could not refresh selected tools. + {toolsPreviewErrorMessage ? ` ${toolsPreviewErrorMessage}` : ''} +

+
+ + {lastSuccessfulToolsPreview && ( + + Showing last successful results. + + )} +
+
+ )} + + {!toolsPreviewLoading && !toolsPreviewError && ( + <> +
+ {selectedTools.map((tool) => ( + + {tool.title || tool.name} + + ))} + {selectedTools.length === 0 && ( + + No tools match the current configuration. + + )} +
+ {notIncludedTools.length > 0 && ( + <> +
+ + + Hidden + + +
+
+ {notIncludedTools.map((tool) => ( + + {tool.title || tool.name} + + ))} +
+ + )} + + )} +
+ +
+ add-mcp command + + + +
+ +
+ MCP JSON config + + + +
+
+
+
+ ); +}; + +export default McpSetupConfigurator; diff --git a/src/components/shared/code-block-wrapper/code-block-wrapper.jsx b/src/components/shared/code-block-wrapper/code-block-wrapper.jsx index 3048925093..9b32da88f9 100644 --- a/src/components/shared/code-block-wrapper/code-block-wrapper.jsx +++ b/src/components/shared/code-block-wrapper/code-block-wrapper.jsx @@ -46,7 +46,6 @@ const CodeBlockWrapper = ({ }) => { const { isCopied, handleCopy } = useCopyToClipboard(3000); - // copyCode bypasses extractTextFromNode, which can't traverse RSC lazy chunks in children const code = copyCode ?? extractTextFromNode(children).replace(/(\n)?__line_removed_in_code__(\n)?/g, ''); const isSingleLineCode = code.trimEnd().split('\n').length === 1; diff --git a/src/components/shared/content/content.jsx b/src/components/shared/content/content.jsx index 9098a6710d..50ec498c39 100644 --- a/src/components/shared/content/content.jsx +++ b/src/components/shared/content/content.jsx @@ -16,6 +16,7 @@ import DocsList from 'components/pages/doc/docs-list'; import IncludeBlock from 'components/pages/doc/include-block'; import InfoBlock from 'components/pages/doc/info-block'; import LinkPreview from 'components/pages/doc/link-preview'; +import McpSetupConfigurator from 'components/pages/doc/mcp-setup-configurator'; import PromptCards from 'components/pages/doc/prompt-cards'; import Steps from 'components/pages/doc/steps'; import StickyTable from 'components/pages/doc/sticky-table'; @@ -201,6 +202,7 @@ const getComponents = (withoutAnchorHeading, isReleaseNote, isPostgres, isTempla ExternalCode: (props) => , MegaLink, CopyPrompt, + McpSetupConfigurator, SqlToRestConverter, ...sharedComponents, });