Skip to content

Commit 2640e29

Browse files
Shridhadclaude
andauthored
refactor: improve error handling with centralized error classes and handlers (#109)
## Summary - Add new centralized error handling system with custom error classes (`InvalidArgumentError`, `NotFoundError`) - Replace generic `Error` throws with specific error types for better categorization - Centralize error handling logic in `handleToolError` function in `src/server/errors.ts` - Replace inappropriate `NeonDbError` usage with proper custom errors - Improve error messages for better user experience - Clean up unused imports and improve code organization ## Test plan - [x] All existing functionality should work as before - [x] Error messages should be more specific and helpful - [x] Linting and type checking passes - [x] No breaking changes to existing API 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a4e31af commit 2640e29

File tree

4 files changed

+97
-55
lines changed

4 files changed

+97
-55
lines changed

src/server/errors.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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+
}

src/server/index.ts

Lines changed: 3 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#!/usr/bin/env node
22

33
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4-
import { isAxiosError } from 'axios';
54
import { NEON_RESOURCES } from '../resources.js';
65
import {
76
NEON_HANDLERS,
@@ -11,12 +10,11 @@ import {
1110
import { logger } from '../utils/logger.js';
1211
import { createNeonClient, getPackageJson } from './api.js';
1312
import { track } from '../analytics/analytics.js';
14-
import { captureException, setHttpStatus, startSpan } from '@sentry/node';
13+
import { captureException, startSpan } from '@sentry/node';
1514
import { ServerContext } from '../types/context.js';
1615
import { setSentryTags } from '../sentry/utils.js';
1716
import { ToolHandlerExtraParams } from '../tools/types.js';
18-
import { handleNeonDbError } from '../tools/utils.js';
19-
import { NeonDbError } from '@neondatabase/serverless';
17+
import { handleToolError } from './errors.js';
2018

2119
export const createMcpServer = (context: ServerContext) => {
2220
const server = new McpServer(
@@ -75,36 +73,7 @@ export const createMcpServer = (context: ServerContext) => {
7573
span.setStatus({
7674
code: 2,
7775
});
78-
if (error instanceof NeonDbError) {
79-
return handleNeonDbError(error);
80-
} else {
81-
if (
82-
isAxiosError(error) &&
83-
error.response?.status &&
84-
error.response?.status < 500
85-
) {
86-
setHttpStatus(span, error.response.status);
87-
return {
88-
isError: true,
89-
content: [
90-
{
91-
type: 'text',
92-
text: error.response?.data.message || error.message,
93-
},
94-
],
95-
};
96-
}
97-
98-
logger.error('Tool call error:', {
99-
error:
100-
error instanceof Error ? error.message : 'Unknown error',
101-
properties,
102-
});
103-
captureException(error, {
104-
extra: properties,
105-
});
106-
throw error;
107-
}
76+
return handleToolError(error, properties);
10877
}
10978
},
11079
);

src/tools/tools.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import {
66
Organization,
77
ProjectCreateRequest,
88
} from '@neondatabase/api-client';
9-
import { neon, NeonDbError } from '@neondatabase/serverless';
9+
import { neon } from '@neondatabase/serverless';
1010
import crypto from 'crypto';
11+
import { InvalidArgumentError, NotFoundError } from '../server/errors.js';
1112

1213
import { describeTable, formatTableDescription } from '../describeUtils.js';
1314
import { handleProvisionNeonAuth } from './handlers/neon-auth.js';
@@ -331,7 +332,9 @@ async function handleGetConnectionString(
331332
if (projects.length === 1) {
332333
projectId = projects[0].id;
333334
} else {
334-
throw new Error('No projects found in your account');
335+
throw new NotFoundError(
336+
'Please provide a project ID or ensure you have only one project in your account.',
337+
);
335338
}
336339
}
337340

@@ -345,7 +348,9 @@ async function handleGetConnectionString(
345348
if (defaultBranch) {
346349
branchId = defaultBranch.id;
347350
} else {
348-
throw new Error('No default branch found in your project');
351+
throw new NotFoundError(
352+
'No default branch found in this project. Please provide a branch ID.',
353+
);
349354
}
350355
}
351356

@@ -655,7 +660,7 @@ async function handleQueryTuning(
655660
const tableNames = extractTableNamesFromPlan(executionPlan);
656661

657662
if (tableNames.length === 0) {
658-
throw new Error(
663+
throw new NotFoundError(
659664
'No tables found in execution plan. Cannot proceed with optimization.',
660665
);
661666
}
@@ -1000,7 +1005,7 @@ async function handleListSlowQueries(
10001005
const extensionExists = extensionCheck[0]?.extension_exists;
10011006

10021007
if (!extensionExists) {
1003-
throw new NeonDbError(
1008+
throw new NotFoundError(
10041009
`pg_stat_statements extension is not installed on the database. Please install it using the following command: CREATE EXTENSION pg_stat_statements;`,
10051010
);
10061011
}
@@ -1084,8 +1089,8 @@ async function handleListBranchComputes(
10841089
if (projects.length === 1) {
10851090
projectId = projects[0].id;
10861091
} else {
1087-
throw new Error(
1088-
'Please provide a project ID or ensure you have only one project.',
1092+
throw new InvalidArgumentError(
1093+
'Please provide a project ID or ensure you have only one project in your account.',
10891094
);
10901095
}
10911096
}

src/tools/utils.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { NEON_DEFAULT_DATABASE_NAME } from '../constants.js';
22
import { Api, Organization } from '@neondatabase/api-client';
33
import { ToolHandlerExtraParams } from './types.js';
4-
import { NeonDbError } from '@neondatabase/serverless';
4+
import { NotFoundError } from '../server/errors.js';
55

66
export const splitSqlStatements = (sql: string) => {
77
return sql.split(';').filter(Boolean);
@@ -125,7 +125,7 @@ export async function getDefaultDatabase(
125125
);
126126
const databases = data.databases;
127127
if (databases.length === 0) {
128-
throw new Error('No databases found in your project branch');
128+
throw new NotFoundError('No databases found in your project branch');
129129
}
130130

131131
if (databaseName) {
@@ -177,7 +177,7 @@ export async function getOrgByOrgIdOrDefault(
177177
);
178178

179179
if (consoleOrganizations.length === 0) {
180-
throw new Error('No organizations found for this user');
180+
throw new NotFoundError('No organizations found for this user');
181181
}
182182

183183
if (consoleOrganizations.length === 1) {
@@ -186,7 +186,7 @@ export async function getOrgByOrgIdOrDefault(
186186
const orgList = consoleOrganizations
187187
.map((org) => `- ${org.name} (ID: ${org.id})`)
188188
.join('\n');
189-
throw new Error(
189+
throw new NotFoundError(
190190
`Multiple organizations found. Please specify the org_id parameter with one of the following organization IDs:\n${orgList}`,
191191
);
192192
}
@@ -206,13 +206,3 @@ export function filterOrganizations(
206206
org.id.toLowerCase().includes(searchLower),
207207
);
208208
}
209-
210-
export function handleNeonDbError(error: NeonDbError) {
211-
return Promise.resolve({
212-
isError: true,
213-
content: [
214-
{ type: 'text' as const, text: error.message },
215-
{ type: 'text' as const, text: JSON.stringify(error, null, 2) },
216-
],
217-
});
218-
}

0 commit comments

Comments
 (0)