Skip to content

Commit 1c75803

Browse files
committed
fix(web): clean up failed Ask MCP connections
1 parent ea6f3f2 commit 1c75803

2 files changed

Lines changed: 86 additions & 49 deletions

File tree

packages/web/src/app/api/(server)/ee/askmcp/connect/route.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ function createPrismaMock() {
9191
},
9292
userMcpServer: {
9393
upsert: vi.fn().mockResolvedValue({ userId: 'user-1', serverId: 'server-1' }),
94+
deleteMany: vi.fn().mockResolvedValue({ count: 1 }),
9495
},
9596
};
9697
}
@@ -306,5 +307,15 @@ describe('POST /api/ee/askmcp/connect', () => {
306307
authMode: 'dynamic',
307308
failureReason: 'invalid_client',
308309
});
310+
expect(prisma.userMcpServer.deleteMany).toHaveBeenCalledWith({
311+
where: {
312+
userId: 'user-1',
313+
serverId: 'server-1',
314+
tokens: null,
315+
tokensExpiresAt: null,
316+
codeVerifier: null,
317+
state: null,
318+
},
319+
});
309320
});
310321
});

packages/web/src/app/api/(server)/ee/askmcp/connect/route.ts

Lines changed: 75 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -113,60 +113,86 @@ export const POST = apiHandler(async (request: NextRequest) => {
113113
update: {},
114114
});
115115

116-
const connectResult = await __unsafePrisma.$transaction(async (tx) => {
117-
const lockedRows = await tx.$queryRaw<{ id: string }[]>`
118-
SELECT id
119-
FROM "McpServer"
120-
WHERE id = ${mcpServer.id} AND "orgId" = ${org.id}
121-
FOR UPDATE
122-
`;
123-
124-
if (lockedRows.length === 0) {
125-
throw new ServiceErrorException(notFound('Connector not found'));
126-
}
127-
128-
const provider = new PrismaOAuthClientProvider({
129-
prisma: tx,
130-
clientInvalidationPrisma: tx,
131-
serverId: mcpServer.id,
132-
orgId: org.id,
133-
userId: user.id,
134-
callbackUrl: getMcpOAuthCallbackUrl(),
135-
callbackReturnTo,
136-
allowClientRegistration: true,
137-
});
116+
let connectResult: {
117+
authResult: Awaited<ReturnType<typeof mcpAuth>>;
118+
authorizationUrl: string | null;
119+
};
138120

139-
let authResult: Awaited<ReturnType<typeof mcpAuth>>;
140-
try {
141-
authResult = await mcpAuth(provider, {
142-
serverUrl: new URL(mcpServer.serverUrl),
143-
fetchFn: createTimeoutFetch(MCP_AUTH_FETCH_TIMEOUT_MS),
144-
});
145-
} catch (error) {
146-
logger.warn('Failed to start connector authorization.', {
121+
try {
122+
connectResult = await __unsafePrisma.$transaction(async (tx) => {
123+
const lockedRows = await tx.$queryRaw<{ id: string }[]>`
124+
SELECT id
125+
FROM "McpServer"
126+
WHERE id = ${mcpServer.id} AND "orgId" = ${org.id}
127+
FOR UPDATE
128+
`;
129+
130+
if (lockedRows.length === 0) {
131+
throw new ServiceErrorException(notFound('Connector not found'));
132+
}
133+
134+
const provider = new PrismaOAuthClientProvider({
135+
prisma: tx,
136+
clientInvalidationPrisma: tx,
147137
serverId: mcpServer.id,
148138
orgId: org.id,
149-
error: getExternalMcpErrorLogFields(error),
150-
});
151-
void captureEvent('ask_mcp_connector_connection_failed', {
152-
...eventProperties,
153-
failureReason: getMcpConnectorFailureReason(error),
139+
userId: user.id,
140+
callbackUrl: getMcpOAuthCallbackUrl(),
141+
callbackReturnTo,
142+
allowClientRegistration: true,
154143
});
155-
throw new ServiceErrorException({
156-
statusCode: StatusCodes.BAD_GATEWAY,
157-
errorCode: ErrorCode.UNEXPECTED_ERROR,
158-
message: 'Could not start connector authorization.',
144+
145+
let authResult: Awaited<ReturnType<typeof mcpAuth>>;
146+
try {
147+
authResult = await mcpAuth(provider, {
148+
serverUrl: new URL(mcpServer.serverUrl),
149+
fetchFn: createTimeoutFetch(MCP_AUTH_FETCH_TIMEOUT_MS),
150+
});
151+
} catch (error) {
152+
logger.warn('Failed to start connector authorization.', {
153+
serverId: mcpServer.id,
154+
orgId: org.id,
155+
error: getExternalMcpErrorLogFields(error),
156+
});
157+
void captureEvent('ask_mcp_connector_connection_failed', {
158+
...eventProperties,
159+
failureReason: getMcpConnectorFailureReason(error),
160+
});
161+
throw new ServiceErrorException({
162+
statusCode: StatusCodes.BAD_GATEWAY,
163+
errorCode: ErrorCode.UNEXPECTED_ERROR,
164+
message: 'Could not start connector authorization.',
165+
});
166+
}
167+
168+
return {
169+
authResult,
170+
authorizationUrl: provider.authorizationUrl ?? null,
171+
};
172+
}, {
173+
maxWait: MCP_AUTH_TRANSACTION_MAX_WAIT_MS,
174+
timeout: MCP_AUTH_TRANSACTION_TIMEOUT_MS,
175+
});
176+
} catch (error) {
177+
await prisma.userMcpServer.deleteMany({
178+
where: {
179+
userId: user.id,
180+
serverId: mcpServer.id,
181+
tokens: null,
182+
tokensExpiresAt: null,
183+
codeVerifier: null,
184+
state: null,
185+
},
186+
}).catch((cleanupError) => {
187+
logger.warn('Failed to clean up empty MCP connector connection.', {
188+
serverId: mcpServer.id,
189+
orgId: org.id,
190+
userId: user.id,
191+
error: getExternalMcpErrorLogFields(cleanupError),
159192
});
160-
}
161-
162-
return {
163-
authResult,
164-
authorizationUrl: provider.authorizationUrl ?? null,
165-
};
166-
}, {
167-
maxWait: MCP_AUTH_TRANSACTION_MAX_WAIT_MS,
168-
timeout: MCP_AUTH_TRANSACTION_TIMEOUT_MS,
169-
});
193+
});
194+
throw error;
195+
}
170196

171197
if (connectResult.authResult === 'AUTHORIZED') {
172198
// Already has valid tokens (e.g., refreshed)

0 commit comments

Comments
 (0)