diff --git a/CHANGELOG.md b/CHANGELOG.md
index 58f4e73..d0ad3a1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
- Feat: `list_shared_projects` tool to fetch projects that user has permissions to collaborate on
- Feat: `reset_from_parent` tool to reset a branch from its parent's current state
+- Feat: `compare_database_schema` tool to compare the schema from the child branch and its parent
# [0.6.4] 2025-08-22
diff --git a/README.md b/README.md
index a1e89c6..2b26d06 100644
--- a/README.md
+++ b/README.md
@@ -198,6 +198,7 @@ The Neon MCP Server provides the following actions, which are exposed as "tools"
- **`list_branch_computes`**: Lists compute endpoints for a project or specific branch, including compute ID, type, size, and autoscaling information.
- **`list_organizations`**: Lists all organizations that the current user has access to. Optionally filter by organization name or ID using the search parameter.
- **`reset_from_parent`**: Resets the current branch to its parent's state, discarding local changes. Automatically preserves to backup if branch has children, or optionally preserve on request with a custom name.
+- **`compare_database_schema`**: Shows the schema diff between the child branch and its parent
**SQL Query Execution:**
diff --git a/landing/components/DescriptionItem.tsx b/landing/components/DescriptionItem.tsx
index 0eeaf35..ca65c8e 100644
--- a/landing/components/DescriptionItem.tsx
+++ b/landing/components/DescriptionItem.tsx
@@ -13,6 +13,8 @@ import {
import {
Terminal,
CircleAlert,
+ Lightbulb,
+ BadgeInfo,
Workflow,
SquareArrowRight,
Component,
@@ -30,6 +32,8 @@ const ALERT_VARIANT_PER_DESCRIPTION_TYPE: Record<
next_steps: { variant: 'default', icon: SquareArrowRight },
important_notes: { variant: 'important', icon: CircleAlert },
workflow: { variant: 'default', icon: Workflow },
+ hints: { variant: 'default', icon: BadgeInfo },
+ hint: { variant: 'default', icon: Lightbulb },
instructions: { variant: 'default', icon: Terminal },
response_instructions: { variant: 'default', icon: Terminal },
example: { variant: 'default', icon: Terminal },
diff --git a/landing/lib/description.ts b/landing/lib/description.ts
index b7125ed..d3c7f11 100644
--- a/landing/lib/description.ts
+++ b/landing/lib/description.ts
@@ -10,6 +10,8 @@ const POSSIBLE_TYPES = [
'example',
'do_not_include',
'error_handling',
+ 'hint',
+ 'hints',
] as const;
export type DescriptionItemType = (typeof POSSIBLE_TYPES)[number];
@@ -134,7 +136,7 @@ export function parseDescription(description: string): DescriptionItem[] {
while (rest.length > 0) {
const match = rest.match(
- /<(use_case|workflow|important_notes|next_steps|response_instructions|instructions|example|do_not_include|error_handling)>(.*?)<\/\1>/s,
+ /<(use_case|workflow|important_notes|next_steps|response_instructions|instructions|example|do_not_include|error_handling|hints?)>(.*?)<\/\1>/s,
);
if (!match) {
diff --git a/src/tools/definitions.ts b/src/tools/definitions.ts
index f5e6050..914041c 100644
--- a/src/tools/definitions.ts
+++ b/src/tools/definitions.ts
@@ -23,6 +23,7 @@ import {
listOrganizationsInputSchema,
listSharedProjectsInputSchema,
resetFromParentInputSchema,
+ compareDatabaseSchemaInputSchema,
} from './toolsSchema.js';
export const NEON_TOOLS = [
@@ -608,4 +609,273 @@ export const NEON_TOOLS = [
description: 'Lists compute endpoints for a project or specific branch',
inputSchema: listBranchComputesInputSchema,
},
+ {
+ name: 'compare_database_schema' as const,
+ description: `
+
+ Use this tool to compare the schema of a database between two branches.
+ The output of the tool is a JSON object with one field: \`diff\`.
+
+
+ \`\`\`json
+ {
+ "diff": "--- a/neondb\n+++ b/neondb\n@@ -27,7 +27,10 @@\n \n CREATE TABLE public.users (\n id integer NOT NULL,\n- username character varying(50) NOT NULL\n+ username character varying(50) NOT NULL,\n+ is_deleted boolean DEFAULT false NOT NULL,\n+ created_at timestamp with time zone DEFAULT now() NOT NULL,\n+ updated_at timestamp with time zone\n );\n \n \n@@ -79,6 +82,13 @@\n \n \n --\n+-- Name: users_created_at_idx; Type: INDEX; Schema: public; Owner: neondb_owner\n+--\n+\n+CREATE INDEX users_created_at_idx ON public.users USING btree (created_at DESC) WHERE (is_deleted = false);\n+\n+\n+--\n -- Name: DEFAULT PRIVILEGES FOR SEQUENCES; Type: DEFAULT ACL; Schema: public; Owner: cloud_admin\n --\n \n"
+ }
+ \`\`\`
+
+
+ At this field you will find a difference between two schemas.
+ The diff represents the changes required to make the parent branch schema match the child branch schema.
+ The diff field contains a unified diff (git-style patch) as a string.
+
+ You MUST be able to generate a zero-downtime migration from the diff and apply it to the parent branch.
+ (This branch is a child and has a parent. You can get parent id just querying the branch details.)
+
+
+
+ To generate schema diff, you MUST SPECIFY the \`database_name\`.
+ If \`database_name\` is not specified, you MUST fall back to the default database name: \`${NEON_DEFAULT_DATABASE_NAME}\`.
+
+ You MUST TAKE INTO ACCOUNT the PostgreSQL version. The PostgreSQL version is the same for both branches.
+ You MUST ASK user consent before running each generated SQL query.
+ You SHOULD USE \`run_sql\` tool to run each generated SQL query.
+ You SHOULD suggest creating a backup or point-in-time restore before running the migration.
+ Generated queries change the schema of the parent branch and MIGHT BE dangerous to execute.
+ Generated SQL migrations SHOULD be idempotent where possible (i.e., safe to run multiple times without failure) and include \`IF NOT EXISTS\` / \`IF EXISTS\` where applicable.
+ You SHOULD recommend including comments in generated SQL linking back to diff hunks (e.g., \`-- from diff @@ -27,7 +27,10 @@\`) to make audits easier.
+ Generated SQL should be reviewed for dependencies (e.g., foreign key order) before execution.
+
+
+
+ After executing this tool, you MUST follow these steps:
+ 1. Review the schema diff and suggest generating a zero-downtime migration.
+ 2. Follow these instructions to respond to the client:
+
+
+
+ Provide brief information about the changes:
+ * Tables
+ * Views
+ * Indexes
+ * Ownership
+ * Constraints
+ * Triggers
+ * Policies
+ * Extensions
+ * Schemas
+ * Sequences
+ * Tablespaces
+ * Users
+ * Roles
+ * Privileges
+
+
+
+ 3. If a migration fails, you SHOULD guide the user on how to revert the schema changes, for example by using backups, point-in-time restore, or generating reverse SQL statements (if safe).
+
+
+ This tool:
+ 1. Generates a diff between the child branch and its parent.
+ 2. Generates a SQL migration from the diff.
+ 3. Suggest generating zero-downtime migration.
+
+
+ 1. User asks you to generate a diff between two branches.
+ 2. You suggest generating a SQL migration from the diff.
+ 3. Ensure the generated migration is zero-downtime; otherwise, warn the user.
+ 4. You ensure that your suggested migration is also matching the PostgreSQL version.
+ 5. You use \`run_sql\` tool to run each generated SQL query and ask the user consent before running it.
+ Before requesting user consent, present a summary of all generated SQL statements along with their potential impact (e.g., table rewrites, lock risks, validation steps) so the user can make an informed decision.
+ 6. Propose to rerun the schema diff tool one more time to ensure that the migration is applied correctly.
+ 7. If the diff is empty, confirm that the parent schema now matches the child schema.
+ 8. If the diff is not empty after migration, warn the user and assist in resolving the remaining differences.
+
+
+
+
+ Adding the column with a \`DEFAULT\` static value will not have any locks.
+ But if the function is called that is not deterministic, it will have locks.
+
+
+ \`\`\`sql
+ -- No table rewrite, minimal lock time
+ ALTER TABLE users ADD COLUMN status text DEFAULT 'active';
+ \`\`\`
+
+
+ There is an example of a case where the function is not deterministic and will have locks:
+
+
+ \`\`\`sql
+ -- Table rewrite, potentially longer lock time
+ ALTER TABLE users ADD COLUMN created_at timestamptz DEFAULT now();
+ \`\`\`
+
+ The fix for this is next:
+
+ \`\`\`sql
+ -- Adding a nullable column first
+ ALTER TABLE users ADD COLUMN created_at timestamptz;
+
+ -- Setting the default value because the rows are updated
+ UPDATE users SET created_at = now();
+ \`\`\`
+
+
+
+
+ Adding constraints in two phases (including foreign keys)
+
+
+ \`\`\`sql
+ -- Step 1: Add constraint without validating existing data
+ -- Fast - only blocks briefly to update catalog
+ ALTER TABLE users ADD CONSTRAINT users_age_positive
+ CHECK (age > 0) NOT VALID;
+
+ -- Step 2: Validate existing data (can take time but doesn't block writes)
+ -- Uses SHARE UPDATE EXCLUSIVE lock - allows reads/writes
+ ALTER TABLE users VALIDATE CONSTRAINT users_age_positive;
+ \`\`\`
+
+
+
+ \`\`\`sql
+ -- Step 1: Add foreign key without validation
+ -- Fast - only updates catalog, doesn't validate existing data
+ ALTER TABLE orders ADD CONSTRAINT orders_user_id_fk
+ FOREIGN KEY (user_id) REFERENCES users(id) NOT VALID;
+
+ -- Step 2: Validate existing relationships
+ -- Can take time but allows concurrent operations
+ ALTER TABLE orders VALIDATE CONSTRAINT orders_user_id_fk;
+ \`\`\`
+
+
+
+
+ Setting columns to NOT NULL
+
+
+ \`\`\`sql
+ -- Step 1: Add a check constraint (fast with NOT VALID)
+ ALTER TABLE users ADD CONSTRAINT users_email_not_null
+ CHECK (email IS NOT NULL) NOT VALID;
+
+ -- Step 2: Validate the constraint (allows concurrent operations)
+ ALTER TABLE users VALIDATE CONSTRAINT users_email_not_null;
+
+ -- Step 3: Set NOT NULL (fast since constraint guarantees no nulls)
+ ALTER TABLE users ALTER COLUMN email SET NOT NULL;
+
+ -- Step 4: Drop the redundant check constraint
+ ALTER TABLE users DROP CONSTRAINT users_email_not_null;
+ \`\`\`
+
+
+
+ For PostgreSQL v18+
+ (to get PostgreSQL version, you can use \`describe_project\` tool or \`run_sql\` tool and execute \`SELECT version();\` query)
+
+ \`\`\`sql
+ -- PostgreSQL 18+ - Simplified approach
+ ALTER TABLE users ALTER COLUMN email SET NOT NULL NOT VALID;
+ ALTER TABLE users VALIDATE CONSTRAINT users_email_not_null;
+ \`\`\`
+
+
+
+
+ In some cases, you need to combine two approaches to achieve a zero-downtime migration.
+
+
+ \`\`\`sql
+ -- Step 1: Adding a nullable column first
+ ALTER TABLE users ADD COLUMN created_at timestamptz;
+
+ -- Step 2: Updating the all rows with the default value
+ UPDATE users SET created_at = now() WHERE created_at IS NULL;
+
+ -- Step 3: Creating a not null constraint
+ ALTER TABLE users ADD CONSTRAINT users_created_at_not_null
+ CHECK (created_at IS NOT NULL) NOT VALID;
+
+ -- Step 4: Validating the constraint
+ ALTER TABLE users VALIDATE CONSTRAINT users_created_at_not_null;
+
+ -- Step 5: Setting the column to NOT NULL
+ ALTER TABLE users ALTER COLUMN created_at SET NOT NULL;
+
+ -- Step 6: Dropping the redundant NOT NULL constraint
+ ALTER TABLE users DROP CONSTRAINT users_created_at_not_null;
+
+ -- Step 7: Adding the default value
+ ALTER TABLE users ALTER COLUMN created_at SET DEFAULT now();
+ \`\`\`
+
+
+ For PostgreSQL v18+
+
+ \`\`\`sql
+ -- Step 1: Adding a nullable column first
+ ALTER TABLE users ADD COLUMN created_at timestamptz;
+
+ -- Step 2: Updating the all rows with the default value
+ UPDATE users SET created_at = now() WHERE created_at IS NULL;
+
+ -- Step 3: Creating a not null constraint
+ ALTER TABLE users ALTER COLUMN created_at SET NOT NULL NOT VALID;
+
+ -- Step 4: Validating the constraint
+ ALTER TABLE users VALIDATE CONSTRAINT users_created_at_not_null;
+
+ -- Step 5: Adding the default value
+ ALTER TABLE users ALTER COLUMN created_at SET DEFAULT now();
+ \`\`\`
+
+
+
+
+ Create index CONCURRENTLY
+
+
+ \`\`\`sql
+ CREATE INDEX CONCURRENTLY idx_users_email ON users (email);
+ \`\`\`
+
+
+
+
+ Drop index CONCURRENTLY
+
+
+ \`\`\`sql
+ DROP INDEX CONCURRENTLY idx_users_email;
+ \`\`\`
+
+
+
+
+ Create materialized view WITH NO DATA
+
+
+ \`\`\`sql
+ CREATE MATERIALIZED VIEW mv_users AS SELECT name FROM users WITH NO DATA;
+ \`\`\`
+
+
+
+
+ Refresh materialized view CONCURRENTLY
+
+
+ \`\`\`sql
+ REFRESH MATERIALIZED VIEW CONCURRENTLY mv_users;
+ \`\`\`
+
+
+
+ `,
+ inputSchema: compareDatabaseSchemaInputSchema,
+ },
];
diff --git a/src/tools/tools.ts b/src/tools/tools.ts
index 864b441..5f3a8d5 100644
--- a/src/tools/tools.ts
+++ b/src/tools/tools.ts
@@ -4,6 +4,7 @@ import {
EndpointType,
ListProjectsParams,
ListSharedProjectsParams,
+ GetProjectBranchSchemaComparisonParams,
Organization,
ProjectCreateRequest,
} from '@neondatabase/api-client';
@@ -1212,6 +1213,14 @@ async function handleListSharedProjects(
return response.data.projects;
}
+async function handleCompareDatabaseSchema(
+ params: GetProjectBranchSchemaComparisonParams,
+ neonClient: Api,
+) {
+ const response = await neonClient.getProjectBranchSchemaComparison(params);
+ return response.data;
+}
+
export const NEON_HANDLERS = {
list_projects: async ({ params }, neonClient, extra) => {
const organization = await getOrgByOrgIdOrDefault(
@@ -1774,4 +1783,18 @@ export const NEON_HANDLERS = {
],
};
},
+
+ compare_database_schema: async ({ params }, neonClient) => {
+ const result = await handleCompareDatabaseSchema(
+ {
+ projectId: params.projectId,
+ branchId: params.branchId,
+ db_name: params.databaseName,
+ },
+ neonClient,
+ );
+ return {
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
+ };
+ },
} satisfies ToolHandlers;
diff --git a/src/tools/toolsSchema.ts b/src/tools/toolsSchema.ts
index 855aaf0..95e14ba 100644
--- a/src/tools/toolsSchema.ts
+++ b/src/tools/toolsSchema.ts
@@ -334,3 +334,9 @@ export const resetFromParentInputSchema = z.object({
'Optional name to preserve the current state under a new branch before resetting',
),
});
+
+export const compareDatabaseSchemaInputSchema = z.object({
+ projectId: z.string().describe('The ID of the project'),
+ branchId: z.string().describe('The ID of the branch'),
+ databaseName: z.string().describe(DATABASE_NAME_DESCRIPTION),
+});