Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ export const ANALYTICS_WRITE_KEY =
export const SENTRY_DSN =
process.env.SENTRY_DSN ??
'https://b3564134667aa2dfeaa3992a12d9c12f@o1373725.ingest.us.sentry.io/4509328350380033';

export const NEON_CONSOLE_HOST = NEON_API_HOST.replace(/\/api\/v2$/, '');
12 changes: 12 additions & 0 deletions src/tools/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
listOrganizationsInputSchema,
listSharedProjectsInputSchema,
resetFromParentInputSchema,
searchInputSchema,
fetchInputSchema,
} from './toolsSchema.js';

export const NEON_TOOLS = [
Expand Down Expand Up @@ -608,4 +610,14 @@ export const NEON_TOOLS = [
description: 'Lists compute endpoints for a project or specific branch',
inputSchema: listBranchComputesInputSchema,
},
{
name: 'search' as const,
description: `Searches across all user organizations, projects, and branches that match the query. Returns a list of objects with id, title, and url. This tool searches through all accessible resources and provides direct links to the Neon Console.`,
inputSchema: searchInputSchema,
},
{
name: 'fetch' as const,
description: `Fetches detailed information about a specific organization, project, or branch using the ID returned by the search tool. This tool provides comprehensive information about Neon resources for detailed analysis and management.`,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that this tool can only be used with the search tool combined?

inputSchema: fetchInputSchema,
},
];
84 changes: 84 additions & 0 deletions src/tools/handlers/connection-string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Api } from '@neondatabase/api-client';
import { ToolHandlerExtraParams } from '../types.js';
import { startSpan } from '@sentry/node';
import { getDefaultDatabase } from '../utils.js';
import { getDefaultBranch, getOnlyProject } from './utils.js';

export async function handleGetConnectionString(
{
projectId,
branchId,
computeId,
databaseName,
roleName,
}: {
projectId?: string;
branchId?: string;
computeId?: string;
databaseName?: string;
roleName?: string;
},
neonClient: Api<unknown>,
extra: ToolHandlerExtraParams,
) {
return await startSpan(
{
name: 'get_connection_string',
},
async () => {
// If projectId is not provided, get the first project but only if there is only one project
if (!projectId) {
const project = await getOnlyProject(neonClient, extra);
projectId = project.id;
}

if (!branchId) {
const defaultBranch = await getDefaultBranch(projectId, neonClient);
branchId = defaultBranch.id;
}

// If databaseName is not provided, use default `neondb` or first database
let dbObject;
if (!databaseName) {
dbObject = await getDefaultDatabase(
{
projectId,
branchId,
databaseName,
},
neonClient,
);
databaseName = dbObject.name;

if (!roleName) {
roleName = dbObject.owner_name;
}
} else if (!roleName) {
const { data } = await neonClient.getProjectBranchDatabase(
projectId,
branchId,
databaseName,
);
roleName = data.database.owner_name;
}

// Get connection URI with the provided parameters
const connectionString = await neonClient.getConnectionUri({
projectId,
role_name: roleName,
database_name: databaseName,
branch_id: branchId,
endpoint_id: computeId,
});

return {
uri: connectionString.data.uri,
projectId,
branchId,
databaseName,
roleName,
computeId,
};
},
);
}
17 changes: 17 additions & 0 deletions src/tools/handlers/decribe-project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Api } from '@neondatabase/api-client';

async function handleDescribeProject(
projectId: string,
neonClient: Api<unknown>,
) {
const { data: branchesData } = await neonClient.listProjectBranches({
projectId: projectId,
});
const { data: projectData } = await neonClient.getProject(projectId);
return {
branches: branchesData.branches,
project: projectData.project,
};
}

export { handleDescribeProject };
96 changes: 96 additions & 0 deletions src/tools/handlers/describe-branch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Api, Branch } from '@neondatabase/api-client';
import { ToolHandlerExtraParams } from '../types.js';
import { handleGetConnectionString } from './connection-string.js';
import { neon } from '@neondatabase/serverless';
import { DESCRIBE_DATABASE_STATEMENTS } from '../utils.js';
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { CONSOLE_URLS, generateConsoleUrl } from './urls.js';

const branchInfo = (branch: Branch) => {
return `Branch Details:
Name: ${branch.name}
ID: ${branch.id}
Parent Branch: ${branch.parent_id}
Default: ${branch.default}
Protected: ${branch.protected ? 'Yes' : 'No'}

${branch.created_by ? `Created By: ${branch.created_by.name}` : ''}
Created: ${new Date(branch.created_at).toLocaleDateString()}
Updated: ${new Date(branch.updated_at).toLocaleDateString()}

Compute Usage: ${branch.compute_time_seconds} seconds
Written Data: ${branch.written_data_bytes} bytes
Data Transfer: ${branch.data_transfer_bytes} bytes

Console Link: ${generateConsoleUrl(CONSOLE_URLS.PROJECT_BRANCH, {
projectId: branch.project_id,
branchId: branch.id,
})}
`;
};

export async function handleDescribeBranch(
{
projectId,
databaseName,
branchId,
}: {
projectId: string;
databaseName?: string;
branchId: string;
},
neonClient: Api<unknown>,
extra: ToolHandlerExtraParams,
): Promise<CallToolResult> {
const { data: branchData } = await neonClient.getProjectBranch(
projectId,
branchId,
);

const branch = branchData.branch;

let response: Record<string, any>[][];
try {
const connectionString = await handleGetConnectionString(
{
projectId,
branchId: branch.id,
databaseName,
},
neonClient,
extra,
);
const runQuery = neon(connectionString.uri);
response = await runQuery.transaction(
DESCRIBE_DATABASE_STATEMENTS.map((sql) => runQuery.query(sql)),
);

return {
content: [
{
type: 'text',
text: branchInfo(branch),
metadata: branch,
},
{
type: 'text',
text: ['Database Structure:', JSON.stringify(response, null, 2)].join(
'\n',
),
databasetree: response,
},
],
};
} catch {
// Ignore database connection errors
}

return {
content: [
{
type: 'text',
text: branchInfo(branch),
},
],
};
}
Loading